@serkanalgur/opencode-nexus 1.0.7 → 1.2.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.
Files changed (4) hide show
  1. package/README.md +296 -248
  2. package/dist/index.js +2060 -297
  3. package/dist/tui.js +269 -19
  4. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,3 +1,6 @@
1
+ import { createRequire } from "node:module";
2
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
3
+
1
4
  // node_modules/@opencode/plugin/dist/promise/plugin.js
2
5
  function define(plugin) {
3
6
  return plugin;
@@ -3457,7 +3460,7 @@ function hasCheck(checks, id) {
3457
3460
  }
3458
3461
  var number2 = /* @__PURE__ */ new Number4;
3459
3462
 
3460
- class Boolean extends Base2 {
3463
+ class Boolean2 extends Base2 {
3461
3464
  _tag = "Boolean";
3462
3465
  getParser() {
3463
3466
  return fromRefinement(this, isBoolean);
@@ -3466,7 +3469,7 @@ class Boolean extends Base2 {
3466
3469
  return "boolean";
3467
3470
  }
3468
3471
  }
3469
- var boolean = /* @__PURE__ */ new Boolean;
3472
+ var boolean = /* @__PURE__ */ new Boolean2;
3470
3473
  class Arrays extends Base2 {
3471
3474
  _tag = "Arrays";
3472
3475
  isMutable;
@@ -5372,7 +5375,7 @@ var Null2 = /* @__PURE__ */ make9(null_);
5372
5375
  var Undefined2 = /* @__PURE__ */ make9(undefined_2);
5373
5376
  var String5 = /* @__PURE__ */ make9(string2);
5374
5377
  var Number5 = /* @__PURE__ */ make9(number2);
5375
- var Boolean2 = /* @__PURE__ */ make9(boolean);
5378
+ var Boolean3 = /* @__PURE__ */ make9(boolean);
5376
5379
  function makeStruct(ast, fields) {
5377
5380
  return make9(ast, {
5378
5381
  fields,
@@ -6406,14 +6409,14 @@ var Option = Struct({
6406
6409
  var When = Struct({
6407
6410
  key: String5,
6408
6411
  op: Literals(["eq", "neq"]),
6409
- value: Union2([String5, Number5, Boolean2])
6412
+ value: Union2([String5, Number5, Boolean3])
6410
6413
  }).annotate({ identifier: "Form.When" });
6411
6414
  var FieldBase = {
6412
6415
  key: String5,
6413
6416
  title: String5.pipe(optional3),
6414
6417
  description: String5.pipe(optional3),
6415
- required: Boolean2.pipe(optional3),
6416
- hidden: Boolean2.pipe(optional3).annotate({
6418
+ required: Boolean3.pipe(optional3),
6419
+ hidden: Boolean3.pipe(optional3).annotate({
6417
6420
  description: "Skip the interactive authentication prompt and use the default unless an answer is supplied"
6418
6421
  }),
6419
6422
  when: ArraySchema(When).pipe(optional3)
@@ -6428,7 +6431,7 @@ var StringField = Struct({
6428
6431
  placeholder: String5.pipe(optional3),
6429
6432
  default: String5.pipe(optional3),
6430
6433
  options: ArraySchema(Option).pipe(optional3),
6431
- custom: Boolean2.pipe(optional3)
6434
+ custom: Boolean3.pipe(optional3)
6432
6435
  }).annotate({ identifier: "Form.StringField" });
6433
6436
  var NumberField = Struct({
6434
6437
  ...FieldBase,
@@ -6447,7 +6450,7 @@ var IntegerField = Struct({
6447
6450
  var BooleanField = Struct({
6448
6451
  ...FieldBase,
6449
6452
  type: Literal2("boolean"),
6450
- default: Boolean2.pipe(optional3)
6453
+ default: Boolean3.pipe(optional3)
6451
6454
  }).annotate({ identifier: "Form.BooleanField" });
6452
6455
  var MultiselectField = Struct({
6453
6456
  ...FieldBase,
@@ -6455,7 +6458,7 @@ var MultiselectField = Struct({
6455
6458
  options: ArraySchema(Option),
6456
6459
  minItems: NonNegativeInt.pipe(optional3),
6457
6460
  maxItems: NonNegativeInt.pipe(optional3),
6458
- custom: Boolean2.pipe(optional3),
6461
+ custom: Boolean3.pipe(optional3),
6459
6462
  default: ArraySchema(String5).pipe(optional3)
6460
6463
  }).annotate({ identifier: "Form.MultiselectField" });
6461
6464
  var ExternalField = Struct({
@@ -6484,7 +6487,7 @@ var Info2 = Struct({
6484
6487
  ...InfoBase,
6485
6488
  fields: Fields
6486
6489
  }).annotate({ identifier: "Form.Info" });
6487
- var Value = Union2([String5, Number5, Boolean2, ArraySchema(String5)]).annotate({
6490
+ var Value = Union2([String5, Number5, Boolean3, ArraySchema(String5)]).annotate({
6488
6491
  identifier: "Form.Value"
6489
6492
  });
6490
6493
  var Answer = Record(String5, Value).annotate({ identifier: "Form.Answer" });
@@ -6720,14 +6723,14 @@ var Overlays2 = {
6720
6723
  };
6721
6724
  var Compatibility = Struct({
6722
6725
  reasoningField: ReasoningField.pipe(optional3),
6723
- requireReasoning: Boolean2.pipe(optional3),
6726
+ requireReasoning: Boolean3.pipe(optional3),
6724
6727
  maxTokensField: MaxTokensField.pipe(optional3),
6725
- requireFinishReason: Boolean2.pipe(optional3),
6726
- requireAssistantAfterTool: Boolean2.pipe(optional3),
6727
- supportsPromptCacheKey: Boolean2.pipe(optional3)
6728
+ requireFinishReason: Boolean3.pipe(optional3),
6729
+ requireAssistantAfterTool: Boolean3.pipe(optional3),
6730
+ supportsPromptCacheKey: Boolean3.pipe(optional3)
6728
6731
  }).annotate({ identifier: "Model.Compatibility" });
6729
6732
  var Capabilities = Struct({
6730
- tools: Boolean2,
6733
+ tools: Boolean3,
6731
6734
  input: ArraySchema(String5),
6732
6735
  output: ArraySchema(String5)
6733
6736
  }).annotate({ identifier: "Model.Capabilities" }).pipe(statics(() => ({
@@ -6766,7 +6769,7 @@ var Info6 = Struct({
6766
6769
  }),
6767
6770
  cost: ArraySchema(Cost),
6768
6771
  status: Literals(["alpha", "beta", "deprecated", "active"]),
6769
- enabled: Boolean2,
6772
+ enabled: Boolean3,
6770
6773
  limit: Struct({
6771
6774
  context: Int,
6772
6775
  input: Int.pipe(optional3),
@@ -6850,7 +6853,7 @@ var Info7 = Struct({
6850
6853
  system: String5.pipe(optional3),
6851
6854
  description: String5.pipe(optional3),
6852
6855
  mode: Literals(["subagent", "primary", "all"]),
6853
- hidden: Boolean2,
6856
+ hidden: Boolean3,
6854
6857
  color: Color.pipe(optional3),
6855
6858
  steps: PositiveInt.pipe(optional3),
6856
6859
  permissions: Ruleset
@@ -6909,8 +6912,8 @@ class LocalConfig extends Class3("Mcp.LocalConfig")({
6909
6912
  description: "Working directory for the MCP server process. Relative paths resolve from the workspace directory."
6910
6913
  }),
6911
6914
  environment: Record(String5, String5).pipe(optional3),
6912
- disabled: Boolean2.pipe(optional3),
6913
- codemode: Boolean2.pipe(optional3).annotate({
6915
+ disabled: Boolean3.pipe(optional3),
6916
+ codemode: Boolean3.pipe(optional3).annotate({
6914
6917
  description: "Expose this server's tools through Code Mode. Defaults to true."
6915
6918
  }),
6916
6919
  timeout: TimeoutConfig.pipe(optional3),
@@ -6935,8 +6938,8 @@ class RemoteConfig extends Class3("Mcp.RemoteConfig")({
6935
6938
  url: String5,
6936
6939
  headers: Record(String5, String5).pipe(optional3),
6937
6940
  oauth: Union2([OAuthConfig, Literal2(false)]).pipe(optional3),
6938
- disabled: Boolean2.pipe(optional3),
6939
- codemode: Boolean2.pipe(optional3).annotate({
6941
+ disabled: Boolean3.pipe(optional3),
6942
+ codemode: Boolean3.pipe(optional3).annotate({
6940
6943
  description: "Expose this server's tools through Code Mode. Defaults to true."
6941
6944
  }),
6942
6945
  timeout: TimeoutConfig.pipe(optional3),
@@ -7118,7 +7121,7 @@ var Info11 = Struct({
7118
7121
  id: ID11,
7119
7122
  name: Name2,
7120
7123
  description: String5.pipe(optional3),
7121
- autoinvoke: Boolean2.pipe(optional3),
7124
+ autoinvoke: Boolean3.pipe(optional3),
7122
7125
  path: AbsolutePath,
7123
7126
  content: String5
7124
7127
  }).annotate({ identifier: "Skill.Info" });
@@ -7261,7 +7264,7 @@ var Output = Struct({
7261
7264
  output: String5,
7262
7265
  cursor: NonNegativeInt,
7263
7266
  size: NonNegativeInt,
7264
- truncated: Boolean2
7267
+ truncated: Boolean3
7265
7268
  });
7266
7269
  // node_modules/@opencode/schema/dist/session-error.js
7267
7270
  var Error5 = Struct({
@@ -7386,7 +7389,7 @@ var AssistantTool = Struct({
7386
7389
  type: tag("tool"),
7387
7390
  id: String5,
7388
7391
  name: String5,
7389
- executed: Boolean2.pipe(optional3),
7392
+ executed: Boolean3.pipe(optional3),
7390
7393
  providerState: ProviderState.pipe(optional3),
7391
7394
  providerResultState: ProviderState.pipe(optional3),
7392
7395
  state: ToolState,
@@ -7583,7 +7586,7 @@ var Info17 = Struct({
7583
7586
  }).annotate({ identifier: "InstructionEntry.Info" });
7584
7587
  var Snapshot = ArraySchema(Struct({
7585
7588
  ...Info17.fields,
7586
- removed: Boolean2
7589
+ removed: Boolean3
7587
7590
  })).annotate({ identifier: "InstructionEntry.Snapshot" });
7588
7591
  var MaxValueBytes = 256 * 1024;
7589
7592
 
@@ -8028,7 +8031,7 @@ var Tool;
8028
8031
  schema: {
8029
8032
  ...ToolBase,
8030
8033
  input: Record(String5, Unknown2),
8031
- executed: Boolean2,
8034
+ executed: Boolean3,
8032
8035
  state: ProviderState.pipe(optional3)
8033
8036
  }
8034
8037
  });
@@ -8049,7 +8052,7 @@ var Tool;
8049
8052
  ...ToolBase,
8050
8053
  content: NonEmptyArray(Content),
8051
8054
  metadata: Record(String5, Json2).pipe(optional3),
8052
- executed: Boolean2,
8055
+ executed: Boolean3,
8053
8056
  resultState: ProviderState.pipe(optional3)
8054
8057
  }
8055
8058
  });
@@ -8064,7 +8067,7 @@ var Tool;
8064
8067
  error: Error5,
8065
8068
  content: NonEmptyArray(Content).pipe(optional3),
8066
8069
  metadata: Record(String5, Json2).pipe(optional3),
8067
- executed: Boolean2,
8070
+ executed: Boolean3,
8068
8071
  resultState: ProviderState.pipe(optional3)
8069
8072
  }
8070
8073
  });
@@ -8241,21 +8244,21 @@ var LocalSource = Struct({
8241
8244
  type: Literal2("local"),
8242
8245
  path: AbsolutePath,
8243
8246
  description: String5.pipe(optional3),
8244
- hidden: Boolean2.pipe(optional3)
8247
+ hidden: Boolean3.pipe(optional3)
8245
8248
  }).annotate({ identifier: "Reference.LocalSource" });
8246
8249
  var GitSource = Struct({
8247
8250
  type: Literal2("git"),
8248
8251
  repository: String5,
8249
8252
  branch: String5.pipe(optional3),
8250
8253
  description: String5.pipe(optional3),
8251
- hidden: Boolean2.pipe(optional3)
8254
+ hidden: Boolean3.pipe(optional3)
8252
8255
  }).annotate({ identifier: "Reference.GitSource" });
8253
8256
  var Source4 = Union2([LocalSource, GitSource]).pipe(toTaggedUnion("type")).annotate({ identifier: "Reference.Source" });
8254
8257
  var Info21 = Struct({
8255
8258
  name: String5,
8256
8259
  path: AbsolutePath,
8257
8260
  description: String5.pipe(optional3),
8258
- hidden: Boolean2.pipe(optional3),
8261
+ hidden: Boolean3.pipe(optional3),
8259
8262
  source: Source4
8260
8263
  }).annotate({ identifier: "Reference.Info" });
8261
8264
  // node_modules/@opencode/schema/dist/vcs.js
@@ -8301,7 +8304,7 @@ var Result2 = Struct({
8301
8304
  })
8302
8305
  }).annotate({ identifier: "WebSearch.Result" });
8303
8306
 
8304
- class Response extends Class3("WebSearch.Response")({
8307
+ class Response2 extends Class3("WebSearch.Response")({
8305
8308
  providerID: ID16,
8306
8309
  results: ArraySchema(Result2)
8307
8310
  }) {
@@ -8325,7 +8328,7 @@ var CreateInput4 = Struct({
8325
8328
  var RemoveInput = Struct({
8326
8329
  projectID: ID10,
8327
8330
  directory: AbsolutePath,
8328
- force: Boolean2
8331
+ force: Boolean3
8329
8332
  }).annotate({ identifier: "Worktree.RemoveInput" });
8330
8333
  var Info23 = Struct({
8331
8334
  directory: AbsolutePath
@@ -8341,7 +8344,7 @@ var ListEntry = Struct({
8341
8344
 
8342
8345
  class OperationError extends TaggedError3()("Worktree.OperationError", {
8343
8346
  message: String5,
8344
- forceRequired: optional3(Boolean2)
8347
+ forceRequired: optional3(Boolean3)
8345
8348
  }) {
8346
8349
  }
8347
8350
  var List = ArraySchema(Directory).annotate({ identifier: "Worktree.List" });
@@ -8361,6 +8364,64 @@ var Resolved = durable({
8361
8364
  });
8362
8365
  var Event16 = { Updated: Updated12, Resolved, Definitions: inventory(Updated12, Resolved) };
8363
8366
  // src/config.ts
8367
+ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
8368
+ import { homedir } from "node:os";
8369
+ import { join, dirname } from "node:path";
8370
+ function stripJsonComments(jsonc) {
8371
+ const result = [];
8372
+ let i = 0;
8373
+ const len = jsonc.length;
8374
+ while (i < len) {
8375
+ const ch = jsonc[i];
8376
+ if (ch === '"') {
8377
+ result.push(ch);
8378
+ i++;
8379
+ while (i < len && jsonc[i] !== '"') {
8380
+ if (jsonc[i] === "\\") {
8381
+ result.push(jsonc[i], jsonc[i + 1] ?? "");
8382
+ i += 2;
8383
+ } else {
8384
+ result.push(jsonc[i]);
8385
+ i++;
8386
+ }
8387
+ }
8388
+ if (i < len) {
8389
+ result.push(jsonc[i]);
8390
+ i++;
8391
+ }
8392
+ continue;
8393
+ }
8394
+ if (ch === "/" && jsonc[i + 1] === "/") {
8395
+ while (i < len && jsonc[i] !== `
8396
+ `)
8397
+ i++;
8398
+ continue;
8399
+ }
8400
+ if (ch === "/" && jsonc[i + 1] === "*") {
8401
+ i += 2;
8402
+ while (i < len && !(jsonc[i] === "*" && jsonc[i + 1] === "/"))
8403
+ i++;
8404
+ i += 2;
8405
+ continue;
8406
+ }
8407
+ result.push(ch);
8408
+ i++;
8409
+ }
8410
+ return result.join("");
8411
+ }
8412
+ function readJsoncFile(filePath) {
8413
+ try {
8414
+ const raw = readFileSync(filePath, "utf-8");
8415
+ const stripped = stripJsonComments(raw);
8416
+ const parsed = JSON.parse(stripped);
8417
+ return parsed;
8418
+ } catch (err) {
8419
+ if (err.code !== "ENOENT") {
8420
+ console.warn(`[nexus] Failed to load config from ${filePath}: ${err.message}`);
8421
+ }
8422
+ return null;
8423
+ }
8424
+ }
8364
8425
  var DEFAULT_CONFIG = {
8365
8426
  models: {
8366
8427
  architect: "anthropic/claude-sonnet-4-6",
@@ -8388,12 +8449,18 @@ class NexusConfigManager {
8388
8449
  globalConfig = null;
8389
8450
  storageConfig = null;
8390
8451
  constructor() {
8391
- this.loadConfigs();
8392
- }
8393
- loadConfigs() {
8394
8452
  this.projectConfig = null;
8395
8453
  this.globalConfig = null;
8396
8454
  }
8455
+ loadConfigs(basePath) {
8456
+ const projectPath = join(basePath, ".opencode", "nexus.jsonc");
8457
+ this.projectConfig = readJsoncFile(projectPath);
8458
+ const globalPath = join(homedir(), ".config", "opencode", "nexus.jsonc");
8459
+ this.globalConfig = readJsoncFile(globalPath);
8460
+ }
8461
+ loadFromPath(basePath) {
8462
+ this.loadConfigs(basePath);
8463
+ }
8397
8464
  getConfig() {
8398
8465
  return {
8399
8466
  models: {
@@ -8451,24 +8518,108 @@ class NexusConfigManager {
8451
8518
  }
8452
8519
  getRoleDisplayName(role) {
8453
8520
  const names = {
8454
- architect: "\uD83C\uDFD7️ Architect",
8455
- coder: "\uD83D\uDCBB Coder",
8456
- reviewer: "\uD83D\uDD0D Reviewer",
8457
- tester: "\uD83E\uDDEA Tester",
8458
- explorer: "\uD83D\uDD2C Explorer",
8459
- documenter: "\uD83D\uDCDD Documenter"
8521
+ architect: "Architect",
8522
+ coder: "Coder",
8523
+ reviewer: "Reviewer",
8524
+ tester: "Tester",
8525
+ explorer: "Explorer",
8526
+ documenter: "Documenter"
8460
8527
  };
8461
8528
  return names[role] || role;
8462
8529
  }
8530
+ getRoleEmoji(role) {
8531
+ const emojis = {
8532
+ architect: "\uD83C\uDFD7️",
8533
+ coder: "\uD83D\uDCBB",
8534
+ reviewer: "\uD83D\uDD0D",
8535
+ tester: "\uD83E\uDDEA",
8536
+ explorer: "\uD83D\uDD2C",
8537
+ documenter: "\uD83D\uDCDD"
8538
+ };
8539
+ return emojis[role] || "\uD83E\uDD16";
8540
+ }
8463
8541
  exportConfig() {
8464
8542
  return this.getConfig();
8465
8543
  }
8466
8544
  importConfig(config) {
8467
8545
  this.storageConfig = { ...config };
8468
8546
  }
8547
+ writeJsoncFile(filePath, config) {
8548
+ const dir = dirname(filePath);
8549
+ mkdirSync(dir, { recursive: true });
8550
+ const jsonc = [
8551
+ "// Nexus Configuration — https://github.com/serkanalgur/opencode-nexus",
8552
+ "// Precedence: this file > global config > TUI settings > defaults",
8553
+ "",
8554
+ JSON.stringify(config, null, 2)
8555
+ ].join(`
8556
+ `);
8557
+ writeFileSync(filePath, jsonc + `
8558
+ `, "utf-8");
8559
+ }
8560
+ saveProjectConfig(basePath) {
8561
+ const projectPath = join(basePath, ".opencode", "nexus.jsonc");
8562
+ const config = this.getNonDefaultConfig();
8563
+ this.writeJsoncFile(projectPath, config);
8564
+ }
8565
+ saveGlobalConfig() {
8566
+ const globalPath = join(homedir(), ".config", "opencode", "nexus.jsonc");
8567
+ const config = this.getNonDefaultConfig();
8568
+ this.writeJsoncFile(globalPath, config);
8569
+ }
8570
+ saveConfig(level, basePath) {
8571
+ if (level === "project") {
8572
+ this.saveProjectConfig(basePath || process.cwd());
8573
+ } else {
8574
+ this.saveGlobalConfig();
8575
+ }
8576
+ }
8577
+ getNonDefaultConfig() {
8578
+ const current = this.getConfig();
8579
+ const result = {};
8580
+ const models = {};
8581
+ for (const role of this.getRoles()) {
8582
+ if (current.models[role] !== DEFAULT_CONFIG.models[role]) {
8583
+ models[role] = current.models[role];
8584
+ }
8585
+ }
8586
+ if (Object.keys(models).length > 0) {
8587
+ result.models = models;
8588
+ }
8589
+ const budget = {};
8590
+ const budgetKeys = ["maxTotalCost", "maxCostPerTask", "maxCostPerAgent", "alertThreshold"];
8591
+ for (const key of budgetKeys) {
8592
+ if (current.budget[key] !== DEFAULT_CONFIG.budget[key]) {
8593
+ budget[key] = current.budget[key];
8594
+ }
8595
+ }
8596
+ if (Object.keys(budget).length > 0) {
8597
+ result.budget = budget;
8598
+ }
8599
+ const selfHealing = {};
8600
+ const shKeys = ["enabled", "maxRetries", "contextTransfer"];
8601
+ for (const key of shKeys) {
8602
+ if (current.selfHealing[key] !== DEFAULT_CONFIG.selfHealing[key]) {
8603
+ selfHealing[key] = current.selfHealing[key];
8604
+ }
8605
+ }
8606
+ if (Object.keys(selfHealing).length > 0) {
8607
+ result.selfHealing = selfHealing;
8608
+ }
8609
+ return result;
8610
+ }
8469
8611
  resetToDefaults() {
8470
8612
  this.storageConfig = null;
8471
8613
  }
8614
+ applyPreset(name) {
8615
+ const preset = PRESETS[name];
8616
+ if (!preset)
8617
+ throw new Error(`Unknown preset: ${name}. Available: ${Object.keys(PRESETS).join(", ")}`);
8618
+ this.storageConfig = { ...this.storageConfig, ...preset.config };
8619
+ }
8620
+ listPresets() {
8621
+ return Object.keys(PRESETS);
8622
+ }
8472
8623
  getSummary() {
8473
8624
  const config = this.getConfig();
8474
8625
  const lines = [
@@ -8496,210 +8647,1369 @@ class NexusConfigManager {
8496
8647
  `);
8497
8648
  }
8498
8649
  }
8499
-
8500
- // src/orchestrator.ts
8501
- class NexusOrchestrator {
8502
- agents = new Map;
8503
- tasks = new Map;
8504
- dag = null;
8505
- config;
8506
- budget;
8507
- running = false;
8508
- paused = false;
8509
- totalSpent = 0;
8510
- costByAgent = new Map;
8511
- costByModel = new Map;
8512
- messageQueue = [];
8513
- subscribers = new Map;
8514
- memory = new Map;
8515
- eventHandlers = new Map;
8516
- configManager;
8517
- ctx = null;
8518
- onStateChange = null;
8519
- constructor(config) {
8520
- this.config = this.mergeConfig(config);
8521
- this.budget = this.config.budget;
8522
- this.configManager = new NexusConfigManager;
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
- }
8564
- mergeConfig(partial) {
8565
- const defaults = {
8566
- maxConcurrency: 5,
8567
- schedulerInterval: 1000,
8568
- defaultTimeout: 300000,
8650
+ var PRESETS = {
8651
+ minimal: {
8652
+ name: "Minimal",
8653
+ description: "Low-cost setup with free/cheap models, minimal budget",
8654
+ config: {
8655
+ models: {
8656
+ architect: "google/gemini-2.5-flash",
8657
+ coder: "google/gemini-2.5-flash",
8658
+ reviewer: "google/gemini-2.5-flash",
8659
+ tester: "google/gemini-2.5-flash",
8660
+ explorer: "google/gemini-2.5-flash",
8661
+ documenter: "google/gemini-2.5-flash"
8662
+ },
8663
+ budget: {
8664
+ maxTotalCost: 1,
8665
+ maxCostPerTask: 0.1,
8666
+ maxCostPerAgent: 0.5,
8667
+ alertThreshold: 0.5
8668
+ },
8669
+ selfHealing: {
8670
+ enabled: false,
8671
+ maxRetries: 1,
8672
+ contextTransfer: false
8673
+ }
8674
+ }
8675
+ },
8676
+ balanced: {
8677
+ name: "Balanced",
8678
+ description: "Good quality models with moderate budget",
8679
+ config: {
8680
+ models: {
8681
+ architect: "anthropic/claude-sonnet-4-6",
8682
+ coder: "anthropic/claude-sonnet-4-6",
8683
+ reviewer: "openai/gpt-5-mini",
8684
+ tester: "anthropic/claude-haiku-4-5",
8685
+ explorer: "google/gemini-2.5-flash",
8686
+ documenter: "anthropic/claude-haiku-4-5"
8687
+ },
8569
8688
  budget: {
8570
8689
  maxTotalCost: 10,
8571
8690
  maxCostPerTask: 1,
8572
8691
  maxCostPerAgent: 2,
8573
- alertThreshold: 0.2,
8574
- hardLimit: false
8575
- },
8576
- agents: {
8577
- defaultRole: "coder",
8578
- spawnDelay: 100,
8579
- healthCheckInterval: 30000
8692
+ alertThreshold: 0.2
8580
8693
  },
8581
8694
  selfHealing: {
8582
8695
  enabled: true,
8583
8696
  maxRetries: 3,
8584
- retryDelay: 1000,
8585
- backoffMultiplier: 2,
8586
8697
  contextTransfer: true
8698
+ }
8699
+ }
8700
+ },
8701
+ enterprise: {
8702
+ name: "Enterprise",
8703
+ description: "High-quality models with generous budget and full self-healing",
8704
+ config: {
8705
+ models: {
8706
+ architect: "anthropic/claude-sonnet-4-6",
8707
+ coder: "anthropic/claude-sonnet-4-6",
8708
+ reviewer: "openai/gpt-5",
8709
+ tester: "anthropic/claude-sonnet-4-6",
8710
+ explorer: "anthropic/claude-sonnet-4-6",
8711
+ documenter: "anthropic/claude-haiku-4-5"
8587
8712
  },
8588
- communication: {
8589
- mode: "pubsub",
8590
- maxQueueSize: 100,
8591
- messageTTL: 60000,
8592
- persistence: false
8593
- },
8594
- memory: {
8595
- enabled: true,
8596
- storage: "memory",
8597
- maxEntriesPerScope: 1000,
8598
- syncInterval: 5000
8713
+ budget: {
8714
+ maxTotalCost: 50,
8715
+ maxCostPerTask: 5,
8716
+ maxCostPerAgent: 10,
8717
+ alertThreshold: 0.1
8599
8718
  },
8600
- dashboard: {
8719
+ selfHealing: {
8601
8720
  enabled: true,
8602
- port: 4747,
8603
- host: "127.0.0.1"
8721
+ maxRetries: 5,
8722
+ contextTransfer: true
8723
+ }
8724
+ }
8725
+ },
8726
+ "cost-optimized": {
8727
+ name: "Cost-Optimized",
8728
+ description: "Minimize costs while maintaining reasonable quality",
8729
+ config: {
8730
+ models: {
8731
+ architect: "anthropic/claude-haiku-4-5",
8732
+ coder: "google/gemini-2.5-flash",
8733
+ reviewer: "openai/gpt-5-mini",
8734
+ tester: "google/gemini-2.5-flash",
8735
+ explorer: "google/gemini-2.5-flash",
8736
+ documenter: "google/gemini-2.5-flash"
8604
8737
  },
8605
- security: {
8606
- sastEnabled: true,
8607
- secretsScanning: true,
8608
- scopeEnforcement: true
8738
+ budget: {
8739
+ maxTotalCost: 3,
8740
+ maxCostPerTask: 0.3,
8741
+ maxCostPerAgent: 1,
8742
+ alertThreshold: 0.3
8609
8743
  },
8610
- learning: {
8744
+ selfHealing: {
8611
8745
  enabled: true,
8612
- patternStorage: "memory",
8613
- minConfidence: 0.7
8746
+ maxRetries: 2,
8747
+ contextTransfer: false
8614
8748
  }
8615
- };
8616
- return {
8617
- ...defaults,
8618
- ...partial,
8619
- budget: { ...defaults.budget, ...partial?.budget },
8620
- agents: { ...defaults.agents, ...partial?.agents },
8621
- selfHealing: { ...defaults.selfHealing, ...partial?.selfHealing },
8622
- communication: { ...defaults.communication, ...partial?.communication },
8623
- memory: { ...defaults.memory, ...partial?.memory },
8624
- dashboard: { ...defaults.dashboard, ...partial?.dashboard },
8625
- security: { ...defaults.security, ...partial?.security },
8626
- learning: { ...defaults.learning, ...partial?.learning }
8627
- };
8749
+ }
8628
8750
  }
8629
- notifyStateChange() {
8630
- this.onStateChange?.();
8751
+ };
8752
+
8753
+ // src/broadcast.ts
8754
+ class StateBroadcaster {
8755
+ clients = new Set;
8756
+ orchestrator;
8757
+ broadcastTimer = null;
8758
+ throttleMs;
8759
+ unsubscribers = [];
8760
+ constructor(orchestrator, opts) {
8761
+ this.orchestrator = orchestrator;
8762
+ this.throttleMs = opts?.throttleMs ?? 1000;
8763
+ this.setupEventListeners();
8764
+ }
8765
+ addClient(ws) {
8766
+ this.clients.add(ws);
8767
+ this.sendTo(ws, {
8768
+ type: "orchestrator:state",
8769
+ data: this.orchestrator.getState(),
8770
+ timestamp: new Date().toISOString()
8771
+ });
8631
8772
  }
8632
- async execute(request) {
8633
- if (this.running) {
8634
- throw new Error("Orchestrator is already running");
8773
+ removeClient(ws) {
8774
+ this.clients.delete(ws);
8775
+ }
8776
+ setupEventListeners() {
8777
+ const orch = this.orchestrator;
8778
+ const eventNames = [
8779
+ "agent:spawned",
8780
+ "agent:terminated",
8781
+ "budget:alert",
8782
+ "budget:exceeded"
8783
+ ];
8784
+ for (const eventName of eventNames) {
8785
+ const unsub = orch.on?.(eventName, (data) => {
8786
+ this.broadcast(eventName, data);
8787
+ });
8788
+ if (unsub)
8789
+ this.unsubscribers.push(unsub);
8635
8790
  }
8636
- if (!this.ctx) {
8637
- throw new Error("Orchestrator not initialized - call initialize(ctx) first");
8791
+ }
8792
+ broadcastState() {
8793
+ if (this.broadcastTimer)
8794
+ return;
8795
+ this.broadcastTimer = setTimeout(() => {
8796
+ this.broadcastTimer = null;
8797
+ this.broadcast("orchestrator:state", this.orchestrator.getState());
8798
+ }, this.throttleMs);
8799
+ }
8800
+ broadcast(event, data) {
8801
+ const message = JSON.stringify({
8802
+ type: event,
8803
+ data,
8804
+ timestamp: new Date().toISOString()
8805
+ });
8806
+ for (const client of this.clients) {
8807
+ try {
8808
+ client.send(message);
8809
+ } catch {
8810
+ this.clients.delete(client);
8811
+ }
8638
8812
  }
8639
- this.running = true;
8640
- const startTime = Date.now();
8641
- this.notifyStateChange();
8813
+ }
8814
+ sendTo(ws, data) {
8642
8815
  try {
8643
- this.dag = this.buildDAG(request.tasks);
8644
- if (request.budget) {
8645
- this.budget = request.budget;
8646
- }
8647
- await this.executeDAG();
8648
- const results = this.collectResults();
8649
- const totalDuration = Date.now() - startTime;
8650
- return {
8651
- success: true,
8652
- tasks: results,
8653
- totalCost: this.totalSpent,
8654
- totalDuration,
8655
- agentsUsed: this.agents.size
8656
- };
8657
- } catch (error) {
8658
- return {
8659
- success: false,
8660
- tasks: [],
8661
- totalCost: this.totalSpent,
8662
- totalDuration: Date.now() - startTime,
8663
- agentsUsed: this.agents.size
8664
- };
8665
- } finally {
8666
- this.running = false;
8667
- this.notifyStateChange();
8816
+ ws.send(JSON.stringify(data));
8817
+ } catch {
8818
+ this.clients.delete(ws);
8668
8819
  }
8669
8820
  }
8670
- buildDAG(tasks) {
8671
- const dag = {
8672
- nodes: new Map,
8673
- addNode: (node) => {
8674
- dag.nodes.set(node.id, node);
8675
- },
8676
- addDependency: (nodeId, dependsOn) => {
8677
- const node = dag.nodes.get(nodeId);
8678
- if (node) {
8679
- node.dependencies.push(dependsOn);
8680
- }
8681
- },
8682
- removeNode: (nodeId) => {
8683
- dag.nodes.delete(nodeId);
8684
- },
8685
- getReadyNodes: () => {
8686
- const ready = [];
8687
- dag.nodes.forEach((node) => {
8688
- if (node.status === "pending") {
8689
- const depsComplete = node.dependencies.every((dep) => {
8690
- const depNode = dag.nodes.get(dep);
8691
- return depNode?.status === "completed";
8692
- });
8693
- if (depsComplete) {
8694
- ready.push(node);
8695
- }
8696
- }
8697
- });
8698
- return ready;
8699
- },
8700
- markComplete: (nodeId, result) => {
8701
- const node = dag.nodes.get(nodeId);
8702
- if (node) {
8821
+ getClientCount() {
8822
+ return this.clients.size;
8823
+ }
8824
+ destroy() {
8825
+ if (this.broadcastTimer) {
8826
+ clearTimeout(this.broadcastTimer);
8827
+ this.broadcastTimer = null;
8828
+ }
8829
+ for (const unsub of this.unsubscribers) {
8830
+ unsub();
8831
+ }
8832
+ this.unsubscribers = [];
8833
+ this.clients.clear();
8834
+ }
8835
+ }
8836
+
8837
+ // src/dashboard.ts
8838
+ var CORS_HEADERS = {
8839
+ "Access-Control-Allow-Origin": "*",
8840
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
8841
+ "Access-Control-Allow-Headers": "Content-Type"
8842
+ };
8843
+ function jsonResponse(data, status = 200) {
8844
+ return new Response(JSON.stringify(data), {
8845
+ status,
8846
+ headers: {
8847
+ "Content-Type": "application/json",
8848
+ ...CORS_HEADERS
8849
+ }
8850
+ });
8851
+ }
8852
+
8853
+ class DashboardModule {
8854
+ server = null;
8855
+ clients = new Set;
8856
+ orchestrator;
8857
+ constructor(orchestrator) {
8858
+ this.orchestrator = orchestrator;
8859
+ }
8860
+ start(port, host) {
8861
+ const self = this;
8862
+ try {
8863
+ this.server = Bun.serve({
8864
+ port,
8865
+ hostname: host,
8866
+ fetch(req, server) {
8867
+ const url = new URL(req.url);
8868
+ if (req.method === "OPTIONS") {
8869
+ return new Response(null, { headers: CORS_HEADERS });
8870
+ }
8871
+ if (url.pathname === "/ws/events") {
8872
+ if (server.upgrade(req, { data: {} }))
8873
+ return new Response(null);
8874
+ return new Response("WebSocket upgrade failed", { status: 500 });
8875
+ }
8876
+ if (url.pathname === "/api/state") {
8877
+ return jsonResponse(self.orchestrator.getState());
8878
+ }
8879
+ if (url.pathname === "/api/config") {
8880
+ return jsonResponse(self.orchestrator.configManager.exportConfig());
8881
+ }
8882
+ if (url.pathname === "/api/agents") {
8883
+ const state = self.orchestrator.getState();
8884
+ return jsonResponse(state.agents);
8885
+ }
8886
+ if (url.pathname === "/api/costs") {
8887
+ return jsonResponse(self.orchestrator.getCostReport());
8888
+ }
8889
+ if (url.pathname === "/api/health") {
8890
+ return jsonResponse({ ok: true, uptime: process.uptime() });
8891
+ }
8892
+ return new Response("Nexus Dashboard API", {
8893
+ headers: {
8894
+ "Content-Type": "text/plain",
8895
+ ...CORS_HEADERS
8896
+ }
8897
+ });
8898
+ },
8899
+ websocket: {
8900
+ open(ws) {
8901
+ self.clients.add(ws);
8902
+ ws.send(JSON.stringify({
8903
+ type: "orchestrator:state",
8904
+ data: self.orchestrator.getState(),
8905
+ timestamp: new Date().toISOString()
8906
+ }));
8907
+ },
8908
+ message(ws, message) {
8909
+ try {
8910
+ const msg = JSON.parse(typeof message === "string" ? message : message.toString());
8911
+ if (msg.type === "ping") {
8912
+ ws.send(JSON.stringify({
8913
+ type: "pong",
8914
+ timestamp: new Date().toISOString()
8915
+ }));
8916
+ }
8917
+ } catch {}
8918
+ },
8919
+ close(ws) {
8920
+ self.clients.delete(ws);
8921
+ }
8922
+ }
8923
+ });
8924
+ console.log(`[nexus] Dashboard server running at http://${host}:${port}`);
8925
+ } catch (err) {
8926
+ this.server = null;
8927
+ console.error(`[nexus] Failed to start dashboard on ${host}:${port}: ${err.message}`);
8928
+ throw new Error(`Dashboard failed to start on port ${port}: ${err.message}`);
8929
+ }
8930
+ }
8931
+ broadcast(event, data) {
8932
+ const message = JSON.stringify({
8933
+ type: event,
8934
+ data,
8935
+ timestamp: new Date().toISOString()
8936
+ });
8937
+ for (const client of this.clients) {
8938
+ try {
8939
+ client.send(message);
8940
+ } catch {
8941
+ this.clients.delete(client);
8942
+ }
8943
+ }
8944
+ }
8945
+ stop() {
8946
+ if (this.server) {
8947
+ this.server.stop();
8948
+ this.server = null;
8949
+ }
8950
+ this.clients.clear();
8951
+ }
8952
+ getClientCount() {
8953
+ return this.clients.size;
8954
+ }
8955
+ isRunning() {
8956
+ return this.server !== null;
8957
+ }
8958
+ }
8959
+
8960
+ // src/dag.ts
8961
+ function detectCycles(nodes) {
8962
+ const cycles = [];
8963
+ const visited = new Set;
8964
+ const inStack = new Set;
8965
+ const path = [];
8966
+ function dfs(nodeId) {
8967
+ if (inStack.has(nodeId)) {
8968
+ const cycleStart = path.indexOf(nodeId);
8969
+ cycles.push([...path.slice(cycleStart), nodeId]);
8970
+ return;
8971
+ }
8972
+ if (visited.has(nodeId))
8973
+ return;
8974
+ visited.add(nodeId);
8975
+ inStack.add(nodeId);
8976
+ path.push(nodeId);
8977
+ const node = nodes.find((n) => n.id === nodeId);
8978
+ if (node) {
8979
+ for (const depId of node.dependencies) {
8980
+ dfs(depId);
8981
+ }
8982
+ }
8983
+ path.pop();
8984
+ inStack.delete(nodeId);
8985
+ }
8986
+ for (const node of nodes) {
8987
+ dfs(node.id);
8988
+ }
8989
+ return cycles;
8990
+ }
8991
+
8992
+ // src/message-store.ts
8993
+ import { appendFileSync, readFileSync as readFileSync2, existsSync, mkdirSync as mkdirSync2 } from "node:fs";
8994
+ import { join as join2, dirname as dirname2 } from "node:path";
8995
+ var DEFAULT_CONFIG2 = {
8996
+ storagePath: join2(process.env.HOME || "~", ".local", "share", "opencode-nexus", "messages.jsonl"),
8997
+ maxMessages: 1e4,
8998
+ rotationSize: 1000
8999
+ };
9000
+
9001
+ class MessageStore {
9002
+ messages = [];
9003
+ config;
9004
+ writeCount = 0;
9005
+ constructor(config) {
9006
+ this.config = { ...DEFAULT_CONFIG2, ...config };
9007
+ this.loadFromDisk();
9008
+ }
9009
+ loadFromDisk() {
9010
+ try {
9011
+ if (existsSync(this.config.storagePath)) {
9012
+ const raw = readFileSync2(this.config.storagePath, "utf-8");
9013
+ const lines = raw.split(`
9014
+ `).filter((l) => l.trim());
9015
+ this.messages = lines.map((l) => JSON.parse(l)).slice(-this.config.maxMessages);
9016
+ }
9017
+ } catch {
9018
+ this.messages = [];
9019
+ }
9020
+ }
9021
+ ensureDir() {
9022
+ try {
9023
+ mkdirSync2(dirname2(this.config.storagePath), { recursive: true });
9024
+ } catch {}
9025
+ }
9026
+ add(message) {
9027
+ this.messages.push(message);
9028
+ if (this.messages.length > this.config.maxMessages) {
9029
+ this.messages = this.messages.slice(-this.config.maxMessages);
9030
+ }
9031
+ try {
9032
+ this.ensureDir();
9033
+ appendFileSync(this.config.storagePath, JSON.stringify(message) + `
9034
+ `);
9035
+ this.writeCount++;
9036
+ if (this.writeCount % this.config.rotationSize === 0) {
9037
+ this.rotate();
9038
+ }
9039
+ } catch {}
9040
+ }
9041
+ rotate() {
9042
+ try {
9043
+ const content = this.messages.map((m) => JSON.stringify(m)).join(`
9044
+ `) + `
9045
+ `;
9046
+ const { writeFileSync } = __require("node:fs");
9047
+ writeFileSync(this.config.storagePath, content);
9048
+ } catch {}
9049
+ }
9050
+ getAll() {
9051
+ return [...this.messages];
9052
+ }
9053
+ getByTopic(topic) {
9054
+ return this.messages.filter((m) => m.topic === topic);
9055
+ }
9056
+ getByAgent(agentId) {
9057
+ return this.messages.filter((m) => m.from === agentId || m.to === agentId);
9058
+ }
9059
+ getRecent(count) {
9060
+ return this.messages.slice(-count);
9061
+ }
9062
+ clear() {
9063
+ this.messages = [];
9064
+ this.writeCount = 0;
9065
+ }
9066
+ getStats() {
9067
+ let fileSize = 0;
9068
+ try {
9069
+ if (existsSync(this.config.storagePath)) {
9070
+ const { statSync } = __require("node:fs");
9071
+ fileSize = statSync(this.config.storagePath).size;
9072
+ }
9073
+ } catch {}
9074
+ return {
9075
+ total: this.messages.length,
9076
+ writeCount: this.writeCount,
9077
+ fileSize
9078
+ };
9079
+ }
9080
+ }
9081
+
9082
+ // src/memory-store.ts
9083
+ import { Database } from "bun:sqlite";
9084
+ import { join as join3, dirname as dirname3 } from "node:path";
9085
+ import { mkdirSync as mkdirSync3 } from "node:fs";
9086
+ var DEFAULT_CONFIG3 = {
9087
+ dbPath: join3(process.env.HOME || "~", ".local", "share", "opencode-nexus", "memory.db"),
9088
+ defaultTTL: 0,
9089
+ maxEntries: 1e4
9090
+ };
9091
+
9092
+ class PersistentMemoryStore {
9093
+ db;
9094
+ config;
9095
+ constructor(config) {
9096
+ this.config = { ...DEFAULT_CONFIG3, ...config };
9097
+ try {
9098
+ mkdirSync3(dirname3(this.config.dbPath), { recursive: true });
9099
+ } catch {}
9100
+ this.db = new Database(this.config.dbPath);
9101
+ this.init();
9102
+ }
9103
+ init() {
9104
+ this.db.exec(`
9105
+ CREATE TABLE IF NOT EXISTS memory (
9106
+ id TEXT PRIMARY KEY,
9107
+ key TEXT NOT NULL,
9108
+ value TEXT NOT NULL,
9109
+ scope TEXT NOT NULL,
9110
+ author TEXT NOT NULL,
9111
+ timestamp INTEGER NOT NULL,
9112
+ confidence REAL DEFAULT 1.0,
9113
+ tags TEXT DEFAULT '[]',
9114
+ ttl INTEGER DEFAULT 0,
9115
+ expires_at INTEGER
9116
+ );
9117
+ CREATE INDEX IF NOT EXISTS idx_memory_scope ON memory(scope);
9118
+ CREATE INDEX IF NOT EXISTS idx_memory_key ON memory(key);
9119
+ CREATE INDEX IF NOT EXISTS idx_memory_author ON memory(author);
9120
+ `);
9121
+ }
9122
+ set(entry) {
9123
+ const id = `mem-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
9124
+ const timestamp = Date.now();
9125
+ const expiresAt = entry.ttl ? timestamp + entry.ttl : null;
9126
+ const stmt = this.db.prepare(`
9127
+ INSERT OR REPLACE INTO memory (id, key, value, scope, author, timestamp, confidence, tags, ttl, expires_at)
9128
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
9129
+ `);
9130
+ stmt.run(id, entry.key, JSON.stringify(entry.value), entry.scope, entry.author, timestamp, entry.confidence, JSON.stringify(entry.tags || []), entry.ttl || 0, expiresAt);
9131
+ return { ...entry, id, timestamp: new Date(timestamp) };
9132
+ }
9133
+ get(key, scope) {
9134
+ const entries = this.getByKey(key, scope);
9135
+ return entries[entries.length - 1] || null;
9136
+ }
9137
+ getByKey(key, scope) {
9138
+ let query = "SELECT * FROM memory WHERE key = ?";
9139
+ const params = [key];
9140
+ if (scope) {
9141
+ query += " AND scope = ?";
9142
+ params.push(scope);
9143
+ }
9144
+ query += " AND (expires_at IS NULL OR expires_at > ?)";
9145
+ params.push(Date.now());
9146
+ const rows = this.db.prepare(query).all(...params);
9147
+ return rows.map(this.rowToEntry);
9148
+ }
9149
+ getByScope(scope) {
9150
+ const rows = this.db.prepare("SELECT * FROM memory WHERE scope = ? AND (expires_at IS NULL OR expires_at > ?) ORDER BY timestamp DESC").all(scope, Date.now());
9151
+ return rows.map(this.rowToEntry);
9152
+ }
9153
+ search(query) {
9154
+ const rows = this.db.prepare(`SELECT * FROM memory WHERE (key LIKE ? OR value LIKE ?) AND (expires_at IS NULL OR expires_at > ?) ORDER BY timestamp DESC`).all(`%${query}%`, `%${query}%`, Date.now());
9155
+ return rows.map(this.rowToEntry);
9156
+ }
9157
+ getRecent(count) {
9158
+ const rows = this.db.prepare("SELECT * FROM memory WHERE (expires_at IS NULL OR expires_at > ?) ORDER BY timestamp DESC LIMIT ?").all(Date.now(), count);
9159
+ return rows.map(this.rowToEntry);
9160
+ }
9161
+ getByAuthor(author) {
9162
+ const rows = this.db.prepare("SELECT * FROM memory WHERE author = ? AND (expires_at IS NULL OR expires_at > ?) ORDER BY timestamp DESC").all(author, Date.now());
9163
+ return rows.map(this.rowToEntry);
9164
+ }
9165
+ delete(key, scope) {
9166
+ let query = "DELETE FROM memory WHERE key = ?";
9167
+ const params = [key];
9168
+ if (scope) {
9169
+ query += " AND scope = ?";
9170
+ params.push(scope);
9171
+ }
9172
+ const result = this.db.prepare(query).run(...params);
9173
+ return result.changes > 0;
9174
+ }
9175
+ clear(scope) {
9176
+ let query = "DELETE FROM memory";
9177
+ const params = [];
9178
+ if (scope) {
9179
+ query += " WHERE scope = ?";
9180
+ params.push(scope);
9181
+ }
9182
+ const result = this.db.prepare(query).run(...params);
9183
+ return result.changes;
9184
+ }
9185
+ getStats() {
9186
+ const total = this.db.prepare("SELECT COUNT(*) as count FROM memory").get().count;
9187
+ const byScope = {};
9188
+ const scopes = this.db.prepare("SELECT DISTINCT scope FROM memory").all();
9189
+ for (const { scope } of scopes) {
9190
+ byScope[scope] = this.db.prepare("SELECT COUNT(*) as count FROM memory WHERE scope = ?").get(scope).count;
9191
+ }
9192
+ const expired = this.db.prepare("SELECT COUNT(*) as count FROM memory WHERE expires_at IS NOT NULL AND expires_at <= ?").get(Date.now()).count;
9193
+ return { total, byScope, expired };
9194
+ }
9195
+ rowToEntry(row) {
9196
+ let value;
9197
+ try {
9198
+ value = JSON.parse(row.value);
9199
+ } catch {
9200
+ value = row.value;
9201
+ }
9202
+ let tags = [];
9203
+ try {
9204
+ tags = JSON.parse(row.tags || "[]");
9205
+ } catch {
9206
+ tags = [];
9207
+ }
9208
+ return {
9209
+ id: row.id,
9210
+ key: row.key,
9211
+ value,
9212
+ scope: row.scope,
9213
+ author: row.author,
9214
+ timestamp: new Date(row.timestamp),
9215
+ confidence: row.confidence,
9216
+ tags,
9217
+ ttl: row.ttl || undefined
9218
+ };
9219
+ }
9220
+ close() {
9221
+ this.db.close();
9222
+ }
9223
+ }
9224
+
9225
+ // src/health.ts
9226
+ var DEFAULT_HEALTH_CONFIG = {
9227
+ checkInterval: 30000,
9228
+ unhealthyThreshold: 0.5,
9229
+ degradedThreshold: 1e4
9230
+ };
9231
+
9232
+ class HealthMonitor {
9233
+ checks = new Map;
9234
+ interval = null;
9235
+ config;
9236
+ constructor(config) {
9237
+ this.config = { ...DEFAULT_HEALTH_CONFIG, ...config };
9238
+ }
9239
+ start(getAgents) {
9240
+ this.stop();
9241
+ this.interval = setInterval(() => {
9242
+ const agents = getAgents();
9243
+ for (const agent of agents) {
9244
+ if (agent.status === "working" || agent.status === "idle") {
9245
+ this.checkAgent(agent);
9246
+ }
9247
+ }
9248
+ }, this.config.checkInterval);
9249
+ }
9250
+ stop() {
9251
+ if (this.interval) {
9252
+ clearInterval(this.interval);
9253
+ this.interval = null;
9254
+ }
9255
+ }
9256
+ checkAgent(agent) {
9257
+ const errorRate = agent.metrics.tasksFailed / Math.max(1, agent.metrics.tasksCompleted + agent.metrics.tasksFailed);
9258
+ const avgResponseTime = agent.metrics.averageResponseTime || 0;
9259
+ let status = "healthy";
9260
+ if (errorRate > this.config.unhealthyThreshold)
9261
+ status = "unhealthy";
9262
+ else if (avgResponseTime > this.config.degradedThreshold)
9263
+ status = "degraded";
9264
+ const check = {
9265
+ agentId: agent.id,
9266
+ timestamp: new Date,
9267
+ responseTime: avgResponseTime,
9268
+ errorRate,
9269
+ tokensPerSecond: 0,
9270
+ memoryUsage: 0,
9271
+ status
9272
+ };
9273
+ const history = this.checks.get(agent.id) || [];
9274
+ history.push(check);
9275
+ if (history.length > 100)
9276
+ history.shift();
9277
+ this.checks.set(agent.id, history);
9278
+ return check;
9279
+ }
9280
+ getHealth(agentId) {
9281
+ const history = this.checks.get(agentId);
9282
+ return history?.[history.length - 1] || null;
9283
+ }
9284
+ getHistory(agentId) {
9285
+ return this.checks.get(agentId) || [];
9286
+ }
9287
+ getUnhealthyAgents() {
9288
+ const unhealthy = [];
9289
+ for (const [agentId, history] of this.checks) {
9290
+ const latest = history[history.length - 1];
9291
+ if (latest?.status === "unhealthy")
9292
+ unhealthy.push(agentId);
9293
+ }
9294
+ return unhealthy;
9295
+ }
9296
+ getConfig() {
9297
+ return { ...this.config };
9298
+ }
9299
+ isActive() {
9300
+ return this.interval !== null;
9301
+ }
9302
+ }
9303
+
9304
+ // src/fanout.ts
9305
+ class MessageRouter {
9306
+ routes = new Map;
9307
+ wildcardSubscribers = new Set;
9308
+ messageHandlers = new Map;
9309
+ subscribe(subscriberId, topic, handler) {
9310
+ this.messageHandlers.set(subscriberId, handler);
9311
+ if (topic === "*") {
9312
+ this.wildcardSubscribers.add(subscriberId);
9313
+ } else {
9314
+ const subs = this.routes.get(topic) || new Set;
9315
+ subs.add(subscriberId);
9316
+ this.routes.set(topic, subs);
9317
+ }
9318
+ }
9319
+ unsubscribe(subscriberId) {
9320
+ this.messageHandlers.delete(subscriberId);
9321
+ this.wildcardSubscribers.delete(subscriberId);
9322
+ for (const subs of this.routes.values()) {
9323
+ subs.delete(subscriberId);
9324
+ }
9325
+ }
9326
+ route(message) {
9327
+ const deliveredTo = [];
9328
+ const exactSubs = this.routes.get(message.topic);
9329
+ if (exactSubs) {
9330
+ for (const subId of exactSubs) {
9331
+ const handler = this.messageHandlers.get(subId);
9332
+ if (handler) {
9333
+ handler(message);
9334
+ deliveredTo.push(subId);
9335
+ }
9336
+ }
9337
+ }
9338
+ for (const subId of this.wildcardSubscribers) {
9339
+ if (!deliveredTo.includes(subId)) {
9340
+ const handler = this.messageHandlers.get(subId);
9341
+ if (handler) {
9342
+ handler(message);
9343
+ deliveredTo.push(subId);
9344
+ }
9345
+ }
9346
+ }
9347
+ return deliveredTo;
9348
+ }
9349
+ broadcast(message) {
9350
+ for (const handler of this.messageHandlers.values()) {
9351
+ handler(message);
9352
+ }
9353
+ }
9354
+ getSubscribers(topic) {
9355
+ const subs = this.routes.get(topic);
9356
+ return subs ? [...subs] : [];
9357
+ }
9358
+ getWildcardSubscribers() {
9359
+ return [...this.wildcardSubscribers];
9360
+ }
9361
+ getStats() {
9362
+ return {
9363
+ topics: this.routes.size,
9364
+ totalSubscribers: this.messageHandlers.size,
9365
+ wildcardSubscribers: this.wildcardSubscribers.size
9366
+ };
9367
+ }
9368
+ }
9369
+
9370
+ // src/notifications.ts
9371
+ import { spawn } from "node:child_process";
9372
+
9373
+ class NotificationManager {
9374
+ enabled;
9375
+ platform;
9376
+ constructor(enabled = true) {
9377
+ this.enabled = enabled;
9378
+ this.platform = process.platform;
9379
+ }
9380
+ async notify(options) {
9381
+ if (!this.enabled)
9382
+ return false;
9383
+ try {
9384
+ if (this.platform === "darwin") {
9385
+ return await this.notifyMacOS(options);
9386
+ } else if (this.platform === "linux") {
9387
+ return await this.notifyLinux(options);
9388
+ } else if (this.platform === "win32") {
9389
+ return await this.notifyWindows(options);
9390
+ }
9391
+ } catch {}
9392
+ return false;
9393
+ }
9394
+ async notifyMacOS(options) {
9395
+ const soundFlag = options.silent ? "" : options.sound !== false ? 'sound name "Glass"' : "";
9396
+ const script = `display notification "${options.body}" with title "${options.title}" ${soundFlag}`;
9397
+ return new Promise((resolve) => {
9398
+ const proc = spawn("osascript", ["-e", script]);
9399
+ proc.on("close", (code) => resolve(code === 0));
9400
+ proc.on("error", () => resolve(false));
9401
+ setTimeout(() => {
9402
+ proc.kill();
9403
+ resolve(false);
9404
+ }, 3000);
9405
+ });
9406
+ }
9407
+ async notifyLinux(options) {
9408
+ return new Promise((resolve) => {
9409
+ const proc = spawn("notify-send", [options.title, options.body]);
9410
+ proc.on("close", (code) => resolve(code === 0));
9411
+ proc.on("error", () => resolve(false));
9412
+ setTimeout(() => {
9413
+ proc.kill();
9414
+ resolve(false);
9415
+ }, 3000);
9416
+ });
9417
+ }
9418
+ async notifyWindows(options) {
9419
+ return new Promise((resolve) => {
9420
+ const proc = spawn("powershell", ["-Command", `New-BurntToastNotification -Text '${options.title}','${options.body}'`]);
9421
+ proc.on("close", (code) => resolve(code === 0));
9422
+ proc.on("error", () => resolve(false));
9423
+ setTimeout(() => {
9424
+ proc.kill();
9425
+ resolve(false);
9426
+ }, 3000);
9427
+ });
9428
+ }
9429
+ setEnabled(enabled) {
9430
+ this.enabled = enabled;
9431
+ }
9432
+ isEnabled() {
9433
+ return this.enabled;
9434
+ }
9435
+ }
9436
+
9437
+ // src/learning.ts
9438
+ class LearningModule {
9439
+ entries = new Map;
9440
+ minConfidence;
9441
+ constructor(minConfidence = 0.5) {
9442
+ this.minConfidence = minConfidence;
9443
+ }
9444
+ recordFailure(pattern, solution, context, tags = []) {
9445
+ const existing = this.findByPattern(pattern);
9446
+ if (existing) {
9447
+ existing.failCount++;
9448
+ existing.lastSeen = new Date;
9449
+ existing.confidence = Math.max(0, existing.confidence - 0.1);
9450
+ return existing;
9451
+ }
9452
+ const entry = {
9453
+ id: `learn-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
9454
+ pattern,
9455
+ solution,
9456
+ context,
9457
+ confidence: 0.3,
9458
+ successCount: 0,
9459
+ failCount: 1,
9460
+ lastSeen: new Date,
9461
+ tags
9462
+ };
9463
+ this.entries.set(entry.id, entry);
9464
+ return entry;
9465
+ }
9466
+ recordSuccess(entryId) {
9467
+ const entry = this.entries.get(entryId);
9468
+ if (entry) {
9469
+ entry.successCount++;
9470
+ entry.confidence = Math.min(1, entry.confidence + 0.15);
9471
+ entry.lastSeen = new Date;
9472
+ }
9473
+ }
9474
+ findSolutions(errorPattern) {
9475
+ const matches = [];
9476
+ for (const entry of this.entries.values()) {
9477
+ if (entry.confidence < this.minConfidence)
9478
+ continue;
9479
+ const similarity = this.calculateSimilarity(errorPattern, entry.pattern);
9480
+ if (similarity > 0.3) {
9481
+ matches.push({ entry, similarity });
9482
+ }
9483
+ }
9484
+ return matches.sort((a, b) => b.similarity - a.similarity);
9485
+ }
9486
+ getReliable() {
9487
+ return [...this.entries.values()].filter((e) => e.confidence >= this.minConfidence && e.successCount > 0).sort((a, b) => b.confidence - a.confidence);
9488
+ }
9489
+ getStats() {
9490
+ const all = [...this.entries.values()];
9491
+ const reliable = all.filter((e) => e.confidence >= this.minConfidence && e.successCount > 0);
9492
+ const avgConfidence = all.length > 0 ? all.reduce((sum, e) => sum + e.confidence, 0) / all.length : 0;
9493
+ return { total: all.length, reliable: reliable.length, avgConfidence };
9494
+ }
9495
+ getEntries() {
9496
+ return [...this.entries.values()];
9497
+ }
9498
+ findByPattern(pattern) {
9499
+ for (const entry of this.entries.values()) {
9500
+ if (entry.pattern === pattern)
9501
+ return entry;
9502
+ }
9503
+ return;
9504
+ }
9505
+ calculateSimilarity(a, b) {
9506
+ const wordsA = new Set(a.toLowerCase().split(/\s+/));
9507
+ const wordsB = new Set(b.toLowerCase().split(/\s+/));
9508
+ const intersection = new Set([...wordsA].filter((w) => wordsB.has(w)));
9509
+ const union = new Set([...wordsA, ...wordsB]);
9510
+ return union.size > 0 ? intersection.size / union.size : 0;
9511
+ }
9512
+ clear() {
9513
+ this.entries.clear();
9514
+ }
9515
+ }
9516
+
9517
+ // src/modules.ts
9518
+ class ModuleRegistry {
9519
+ modules = new Map;
9520
+ contexts = new Map;
9521
+ register(module) {
9522
+ if (this.modules.has(module.name)) {
9523
+ throw new Error(`Module '${module.name}' is already registered`);
9524
+ }
9525
+ this.modules.set(module.name, module);
9526
+ }
9527
+ async setupAll(ctx) {
9528
+ for (const [name, module] of this.modules) {
9529
+ try {
9530
+ if (module.setup) {
9531
+ await module.setup(ctx);
9532
+ this.contexts.set(name, ctx);
9533
+ }
9534
+ if (module.hooks) {
9535
+ for (const hook of module.hooks) {
9536
+ ctx.on(hook.event, hook.handler);
9537
+ }
9538
+ }
9539
+ } catch (err) {
9540
+ console.error(`[nexus] Failed to setup module '${name}':`, err);
9541
+ }
9542
+ }
9543
+ }
9544
+ async teardownAll() {
9545
+ for (const [name, module] of this.modules) {
9546
+ try {
9547
+ if (module.teardown) {
9548
+ await module.teardown();
9549
+ }
9550
+ } catch (err) {
9551
+ console.error(`[nexus] Failed to teardown module '${name}':`, err);
9552
+ }
9553
+ }
9554
+ this.contexts.clear();
9555
+ }
9556
+ get(name) {
9557
+ return this.modules.get(name);
9558
+ }
9559
+ list() {
9560
+ return [...this.modules.values()];
9561
+ }
9562
+ getTools() {
9563
+ const tools = [];
9564
+ for (const module of this.modules.values()) {
9565
+ if (module.tools) {
9566
+ tools.push(...module.tools);
9567
+ }
9568
+ }
9569
+ return tools;
9570
+ }
9571
+ isRegistered(name) {
9572
+ return this.modules.has(name);
9573
+ }
9574
+ unregister(name) {
9575
+ this.contexts.delete(name);
9576
+ return this.modules.delete(name);
9577
+ }
9578
+ }
9579
+
9580
+ // src/security.ts
9581
+ var DEFAULT_SECURITY_CONFIG = {
9582
+ enabled: true,
9583
+ scanSecrets: true,
9584
+ scanPatterns: true,
9585
+ customPatterns: [],
9586
+ excludeFiles: ["node_modules", ".git", "dist", "*.test.ts", "*.spec.ts"]
9587
+ };
9588
+ var SECRET_PATTERNS = [
9589
+ { pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*['"][^'"]+['"]/gi, category: "secrets", message: "Possible API key found" },
9590
+ { pattern: /(?:secret|password|passwd|pwd)\s*[:=]\s*['"][^'"]+['"]/gi, category: "secrets", message: "Possible password/secret found" },
9591
+ { pattern: /(?:token|access_token|auth_token)\s*[:=]\s*['"][^'"]+['"]/gi, category: "secrets", message: "Possible token found" },
9592
+ { pattern: /(?:private[_-]?key)\s*[:=]\s*['"][^'"]+['"]/gi, category: "secrets", message: "Possible private key found" },
9593
+ { pattern: /-----BEGIN (?:RSA |EC )?PRIVATE KEY-----/g, category: "secrets", message: "Private key block found" },
9594
+ { pattern: /(?:AWS|aws)(?:_ACCESS_KEY_ID|_SECRET_ACCESS_KEY)\s*[:=]\s*['"][^'"]+['"]/gi, category: "secrets", message: "AWS credentials found" }
9595
+ ];
9596
+ var DANGEROUS_PATTERNS = [
9597
+ { pattern: /eval\s*\(/g, category: "injection", message: "eval() usage detected", suggestion: "Use safer alternatives like JSON.parse() or Function constructor" },
9598
+ { pattern: /exec\s*\(/g, category: "injection", message: "exec() usage detected", suggestion: "Use execFile() or spawn() with argument arrays" },
9599
+ { pattern: /child_process/g, category: "injection", message: "child_process usage detected", suggestion: "Ensure user input is sanitized before shell execution" },
9600
+ { pattern: /innerHTML\s*=/g, category: "xss", message: "innerHTML assignment detected", suggestion: "Use textContent or sanitize input before setting innerHTML" },
9601
+ { pattern: /document\.write\s*\(/g, category: "xss", message: "document.write() usage detected", suggestion: "Use DOM manipulation methods instead" },
9602
+ { pattern: /new Function\s*\(/g, category: "injection", message: "Function constructor usage detected", suggestion: "Avoid dynamic code execution" },
9603
+ { pattern: /__proto__\s*=/g, category: "prototype-pollution", message: "__proto__ assignment detected", suggestion: "Use Object.create() or Object.assign() instead" },
9604
+ { pattern: /process\.env\[?[\"'][^\"']+[\"']\]?(?!\.replace)/g, category: "data-exposure", message: "Direct process.env access", suggestion: "Use a config module with validation" }
9605
+ ];
9606
+
9607
+ class SecurityScanner {
9608
+ config;
9609
+ issues = [];
9610
+ constructor(config) {
9611
+ this.config = { ...DEFAULT_SECURITY_CONFIG, ...config };
9612
+ }
9613
+ scanContent(content, filename) {
9614
+ if (!this.config.enabled)
9615
+ return [];
9616
+ if (this.isExcluded(filename))
9617
+ return [];
9618
+ const issues = [];
9619
+ if (this.config.scanSecrets) {
9620
+ for (const { pattern, category, message } of SECRET_PATTERNS) {
9621
+ const matches = content.matchAll(new RegExp(pattern.source, pattern.flags));
9622
+ for (const match of matches) {
9623
+ const line = this.getLineNumber(content, match.index || 0);
9624
+ issues.push({
9625
+ id: `sec-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`,
9626
+ severity: "critical",
9627
+ category,
9628
+ message,
9629
+ file: filename,
9630
+ line,
9631
+ suggestion: "Remove hardcoded secrets and use environment variables or a secrets manager"
9632
+ });
9633
+ }
9634
+ }
9635
+ }
9636
+ if (this.config.scanPatterns) {
9637
+ for (const { pattern, category, message, suggestion } of DANGEROUS_PATTERNS) {
9638
+ const matches = content.matchAll(new RegExp(pattern.source, pattern.flags));
9639
+ for (const match of matches) {
9640
+ const line = this.getLineNumber(content, match.index || 0);
9641
+ issues.push({
9642
+ id: `sec-${Date.now()}-${Math.random().toString(36).substr(6, 6)}`,
9643
+ severity: "medium",
9644
+ category,
9645
+ message,
9646
+ file: filename,
9647
+ line,
9648
+ suggestion
9649
+ });
9650
+ }
9651
+ }
9652
+ }
9653
+ for (const pattern of this.config.customPatterns) {
9654
+ const matches = content.matchAll(new RegExp(pattern.source, pattern.flags));
9655
+ for (const match of matches) {
9656
+ const line = this.getLineNumber(content, match.index || 0);
9657
+ issues.push({
9658
+ id: `sec-${Date.now()}-${Math.random().toString(36).substr(12, 6)}`,
9659
+ severity: "medium",
9660
+ category: "custom",
9661
+ message: `Custom pattern match in ${filename}`,
9662
+ file: filename,
9663
+ line
9664
+ });
9665
+ }
9666
+ }
9667
+ this.issues.push(...issues);
9668
+ return issues;
9669
+ }
9670
+ getResult() {
9671
+ const severityScores = { critical: 0, high: 10, medium: 25, low: 50 };
9672
+ let deductions = 0;
9673
+ for (const issue of this.issues) {
9674
+ deductions += severityScores[issue.severity] || 25;
9675
+ }
9676
+ const score = Math.max(0, 100 - deductions);
9677
+ return {
9678
+ issues: [...this.issues],
9679
+ score,
9680
+ scannedAt: new Date,
9681
+ filesScanned: new Set(this.issues.map((i) => i.file).filter(Boolean)).size || 0
9682
+ };
9683
+ }
9684
+ getBySeverity(severity) {
9685
+ return this.issues.filter((i) => i.severity === severity);
9686
+ }
9687
+ getByCategory(category) {
9688
+ return this.issues.filter((i) => i.category === category);
9689
+ }
9690
+ isExcluded(filename) {
9691
+ return this.config.excludeFiles.some((exclusion) => {
9692
+ if (exclusion.startsWith("*")) {
9693
+ return filename.endsWith(exclusion.slice(1));
9694
+ }
9695
+ return filename.includes(exclusion);
9696
+ });
9697
+ }
9698
+ getLineNumber(content, index) {
9699
+ return content.substring(0, index).split(`
9700
+ `).length;
9701
+ }
9702
+ clear() {
9703
+ this.issues = [];
9704
+ }
9705
+ getStats() {
9706
+ return {
9707
+ total: this.issues.length,
9708
+ critical: this.getBySeverity("critical").length,
9709
+ high: this.getBySeverity("high").length,
9710
+ medium: this.getBySeverity("medium").length,
9711
+ low: this.getBySeverity("low").length
9712
+ };
9713
+ }
9714
+ }
9715
+
9716
+ // src/orchestrator.ts
9717
+ var DEFAULT_ESCALATION = {
9718
+ maxRetries: 3,
9719
+ retryDelay: 1000,
9720
+ enableRespawn: true,
9721
+ fallbackModels: ["google/gemini-2.5-flash", "anthropic/claude-haiku-4-5"],
9722
+ alertOnFailure: true
9723
+ };
9724
+
9725
+ class NexusOrchestrator {
9726
+ agents = new Map;
9727
+ tasks = new Map;
9728
+ dag = null;
9729
+ config;
9730
+ budget;
9731
+ running = false;
9732
+ paused = false;
9733
+ budgetExceeded = false;
9734
+ totalSpent = 0;
9735
+ costByAgent = new Map;
9736
+ costByModel = new Map;
9737
+ messageQueue = [];
9738
+ subscribers = new Map;
9739
+ messageStore;
9740
+ memoryStore;
9741
+ messageRouter;
9742
+ eventHandlers = new Map;
9743
+ configManager;
9744
+ ctx = null;
9745
+ broadcaster = null;
9746
+ dashboard = null;
9747
+ _healthMonitor = null;
9748
+ learning;
9749
+ escalationPolicy;
9750
+ nodeRetryCounts = new Map;
9751
+ moduleRegistry;
9752
+ securityScanner;
9753
+ onStateChange = null;
9754
+ stateChangeTimer = null;
9755
+ costHistory = [];
9756
+ cleanupInterval = null;
9757
+ get healthMonitor() {
9758
+ return this._healthMonitor;
9759
+ }
9760
+ notifications = null;
9761
+ constructor(config, messageStoreConfig, memoryStoreConfig) {
9762
+ this.config = this.mergeConfig(config);
9763
+ this.budget = this.config.budget;
9764
+ this.configManager = new NexusConfigManager;
9765
+ this.moduleRegistry = new ModuleRegistry;
9766
+ this.messageStore = new MessageStore(messageStoreConfig);
9767
+ this.memoryStore = new PersistentMemoryStore(memoryStoreConfig);
9768
+ this.messageRouter = new MessageRouter;
9769
+ this.escalationPolicy = {
9770
+ ...DEFAULT_ESCALATION,
9771
+ maxRetries: this.config.selfHealing.maxRetries,
9772
+ retryDelay: this.config.selfHealing.retryDelay,
9773
+ enableRespawn: this.config.selfHealing.contextTransfer
9774
+ };
9775
+ this.learning = new LearningModule(this.config.learning.minConfidence);
9776
+ this.securityScanner = new SecurityScanner;
9777
+ }
9778
+ async initialize(ctx, onStateChange) {
9779
+ this.ctx = ctx;
9780
+ this.onStateChange = onStateChange ?? null;
9781
+ this.configManager.loadFromPath(process.cwd());
9782
+ this.cleanupInterval = setInterval(() => this.cleanupStaleData(), 300000);
9783
+ this._healthMonitor = new HealthMonitor({
9784
+ checkInterval: this.config.agents.healthCheckInterval
9785
+ });
9786
+ this.notifications = new NotificationManager(true);
9787
+ const moduleCtx = {
9788
+ orchestrator: this,
9789
+ config: this.config,
9790
+ emit: (event, data) => this.emit(event, data),
9791
+ on: (event, handler) => {
9792
+ this.on(event, handler);
9793
+ }
9794
+ };
9795
+ await this.moduleRegistry.setupAll(moduleCtx);
9796
+ }
9797
+ initBroadcaster(opts) {
9798
+ this.broadcaster = new StateBroadcaster(this, opts);
9799
+ const previousOnStateChange = this.onStateChange;
9800
+ this.onStateChange = () => {
9801
+ previousOnStateChange?.();
9802
+ this.broadcaster?.broadcastState();
9803
+ };
9804
+ }
9805
+ startDashboard(port, host) {
9806
+ const dashPort = port || this.config.dashboard.port;
9807
+ const dashHost = host || this.config.dashboard.host;
9808
+ this.dashboard = new DashboardModule(this);
9809
+ this.dashboard.start(dashPort, dashHost);
9810
+ this.on("agent:spawned", (agent) => this.dashboard?.broadcast("agent:spawned", agent));
9811
+ this.on("agent:terminated", (agent) => this.dashboard?.broadcast("agent:terminated", agent));
9812
+ this.on("budget:alert", (data) => this.dashboard?.broadcast("budget:alert", data));
9813
+ this.on("budget:exceeded", (data) => this.dashboard?.broadcast("budget:exceeded", data));
9814
+ }
9815
+ stopDashboard() {
9816
+ if (this.dashboard) {
9817
+ this.dashboard.stop();
9818
+ this.dashboard = null;
9819
+ }
9820
+ }
9821
+ getState() {
9822
+ const agents = Array.from(this.agents.values()).map((a) => ({
9823
+ id: a.id,
9824
+ name: a.name,
9825
+ role: a.role,
9826
+ status: a.status,
9827
+ model: `${a.model.provider}/${a.model.model}`,
9828
+ sessionID: a.sessionID,
9829
+ spawnedAt: a.spawnedAt.toISOString(),
9830
+ tasksCompleted: a.metrics.tasksCompleted,
9831
+ tasksFailed: a.metrics.tasksFailed,
9832
+ totalCost: a.metrics.totalCost
9833
+ }));
9834
+ const tasks = Array.from(this.tasks.values()).map((t) => ({
9835
+ id: t.id,
9836
+ name: t.name,
9837
+ role: t.requiredRole,
9838
+ priority: t.priority || "normal",
9839
+ status: t.status,
9840
+ assignedAgent: t.assignedAgent,
9841
+ result: t.result ? {
9842
+ success: t.result.success,
9843
+ output: t.result.output?.slice(0, 500),
9844
+ error: t.result.error,
9845
+ duration: t.result.duration
9846
+ } : undefined
9847
+ }));
9848
+ return {
9849
+ running: this.running,
9850
+ paused: this.paused,
9851
+ agents,
9852
+ tasks,
9853
+ totalSpent: this.totalSpent,
9854
+ budgetRemaining: this.budget.maxTotalCost - this.totalSpent,
9855
+ lastUpdated: new Date().toISOString()
9856
+ };
9857
+ }
9858
+ mergeConfig(partial) {
9859
+ const defaults = {
9860
+ maxConcurrency: 5,
9861
+ schedulerInterval: 1000,
9862
+ defaultTimeout: 300000,
9863
+ budget: {
9864
+ maxTotalCost: 10,
9865
+ maxCostPerTask: 1,
9866
+ maxCostPerAgent: 2,
9867
+ alertThreshold: 0.2,
9868
+ hardLimit: false
9869
+ },
9870
+ agents: {
9871
+ defaultRole: "coder",
9872
+ spawnDelay: 100,
9873
+ healthCheckInterval: 30000
9874
+ },
9875
+ selfHealing: {
9876
+ enabled: true,
9877
+ maxRetries: 3,
9878
+ retryDelay: 1000,
9879
+ backoffMultiplier: 2,
9880
+ contextTransfer: true
9881
+ },
9882
+ communication: {
9883
+ mode: "pubsub",
9884
+ maxQueueSize: 100,
9885
+ messageTTL: 60000,
9886
+ persistence: false
9887
+ },
9888
+ memory: {
9889
+ enabled: true,
9890
+ storage: "memory",
9891
+ maxEntriesPerScope: 1000,
9892
+ syncInterval: 5000
9893
+ },
9894
+ dashboard: {
9895
+ enabled: true,
9896
+ port: 4747,
9897
+ host: "127.0.0.1"
9898
+ },
9899
+ security: {
9900
+ sastEnabled: true,
9901
+ secretsScanning: true,
9902
+ scopeEnforcement: true
9903
+ },
9904
+ learning: {
9905
+ enabled: true,
9906
+ patternStorage: "memory",
9907
+ minConfidence: 0.7
9908
+ }
9909
+ };
9910
+ return {
9911
+ ...defaults,
9912
+ ...partial,
9913
+ budget: { ...defaults.budget, ...partial?.budget },
9914
+ agents: { ...defaults.agents, ...partial?.agents },
9915
+ selfHealing: { ...defaults.selfHealing, ...partial?.selfHealing },
9916
+ communication: { ...defaults.communication, ...partial?.communication },
9917
+ memory: { ...defaults.memory, ...partial?.memory },
9918
+ dashboard: { ...defaults.dashboard, ...partial?.dashboard },
9919
+ security: { ...defaults.security, ...partial?.security },
9920
+ learning: { ...defaults.learning, ...partial?.learning }
9921
+ };
9922
+ }
9923
+ notifyStateChange() {
9924
+ if (this.stateChangeTimer)
9925
+ return;
9926
+ this.stateChangeTimer = setTimeout(() => {
9927
+ this.stateChangeTimer = null;
9928
+ this.onStateChange?.();
9929
+ }, 100);
9930
+ }
9931
+ async execute(request) {
9932
+ if (this.running) {
9933
+ throw new Error("Orchestrator is already running");
9934
+ }
9935
+ if (!this.ctx) {
9936
+ throw new Error("Orchestrator not initialized - call initialize(ctx) first");
9937
+ }
9938
+ this.running = true;
9939
+ const startTime = Date.now();
9940
+ this.notifyStateChange();
9941
+ try {
9942
+ this.dag = this.buildDAG(request.tasks);
9943
+ const dagNodes = Array.from(this.dag.nodes.values());
9944
+ const cycles = detectCycles(dagNodes);
9945
+ if (cycles.length > 0) {
9946
+ const cycleDescriptions = cycles.map((c) => c.join(" → ")).join(", ");
9947
+ throw new Error(`Circular dependency detected: ${cycleDescriptions}`);
9948
+ }
9949
+ if (request.budget) {
9950
+ this.budget = request.budget;
9951
+ }
9952
+ await this.executeDAG();
9953
+ const results = this.collectResults();
9954
+ const totalDuration = Date.now() - startTime;
9955
+ return {
9956
+ success: true,
9957
+ tasks: results,
9958
+ totalCost: this.totalSpent,
9959
+ totalDuration,
9960
+ agentsUsed: this.agents.size
9961
+ };
9962
+ } catch (error) {
9963
+ return {
9964
+ success: false,
9965
+ tasks: [],
9966
+ totalCost: this.totalSpent,
9967
+ totalDuration: Date.now() - startTime,
9968
+ agentsUsed: this.agents.size
9969
+ };
9970
+ } finally {
9971
+ this.running = false;
9972
+ this.notifyStateChange();
9973
+ }
9974
+ }
9975
+ buildDAG(tasks) {
9976
+ const dag = {
9977
+ nodes: new Map,
9978
+ addNode: (node) => {
9979
+ dag.nodes.set(node.id, node);
9980
+ },
9981
+ addDependency: (nodeId, dependsOn) => {
9982
+ const node = dag.nodes.get(nodeId);
9983
+ if (node) {
9984
+ node.dependencies.push(dependsOn);
9985
+ }
9986
+ },
9987
+ removeNode: (nodeId) => {
9988
+ dag.nodes.delete(nodeId);
9989
+ },
9990
+ getReadyNodes: () => {
9991
+ const ready = [];
9992
+ dag.nodes.forEach((node) => {
9993
+ if (node.status === "pending") {
9994
+ const depsComplete = node.dependencies.every((dep) => {
9995
+ const depNode = dag.nodes.get(dep);
9996
+ return depNode?.status === "completed";
9997
+ });
9998
+ if (depsComplete) {
9999
+ ready.push(node);
10000
+ }
10001
+ }
10002
+ });
10003
+ const priorityOrder = { critical: 0, high: 1, normal: 2, low: 3 };
10004
+ return ready.sort((a, b) => {
10005
+ const pa = priorityOrder[a.task.priority] ?? 2;
10006
+ const pb = priorityOrder[b.task.priority] ?? 2;
10007
+ return pa - pb;
10008
+ });
10009
+ },
10010
+ markComplete: (nodeId, result) => {
10011
+ const node = dag.nodes.get(nodeId);
10012
+ if (node) {
8703
10013
  node.status = "completed";
8704
10014
  node.result = result;
8705
10015
  }
@@ -8752,7 +10062,7 @@ class NexusOrchestrator {
8752
10062
  await this.sleep(this.config.schedulerInterval);
8753
10063
  }
8754
10064
  }
8755
- async spawnAndExecute(node) {
10065
+ async spawnAndExecute(node, transferContext) {
8756
10066
  const complexity = this.analyzeComplexity(node.task);
8757
10067
  const model = this.selectModel(node.task.requiredRole, complexity);
8758
10068
  const agent = await this.spawnAgent({
@@ -8764,9 +10074,9 @@ class NexusOrchestrator {
8764
10074
  node.status = "running";
8765
10075
  node.task.assignedAgent = agent.id;
8766
10076
  this.notifyStateChange();
8767
- await this.executeTask(agent, node);
10077
+ await this.executeTask(agent, node, transferContext);
8768
10078
  }
8769
- async executeTask(agent, node) {
10079
+ async executeTask(agent, node, transferContext) {
8770
10080
  if (!this.ctx || !agent.sessionID) {
8771
10081
  node.status = "failed";
8772
10082
  node.result = {
@@ -8783,7 +10093,7 @@ class NexusOrchestrator {
8783
10093
  const timeout = node.task.timeout || this.config.defaultTimeout;
8784
10094
  try {
8785
10095
  const rolePrompt = this.buildRolePrompt(node.task.requiredRole);
8786
- const taskPrompt = `${rolePrompt}
10096
+ let taskPrompt = `${rolePrompt}
8787
10097
 
8788
10098
  ## Task
8789
10099
  ${node.task.name}
@@ -8792,6 +10102,22 @@ ${node.task.description}
8792
10102
 
8793
10103
  ## Scope
8794
10104
  Files: ${node.task.files.include.join(", ")}`;
10105
+ if (transferContext) {
10106
+ taskPrompt += `
10107
+
10108
+ ## Previous Agent Context (from failed agent ${transferContext.previousAgentId})`;
10109
+ taskPrompt += `
10110
+ Partial results: ${transferContext.partialResults.join(", ") || "None"}`;
10111
+ taskPrompt += `
10112
+ Decisions made: ${transferContext.decisions.join(", ") || "None"}`;
10113
+ taskPrompt += `
10114
+ Progress: ${transferContext.taskProgress}%`;
10115
+ taskPrompt += `
10116
+ Errors encountered: ${transferContext.errorLog.join(", ") || "None"}`;
10117
+ taskPrompt += `
10118
+
10119
+ Please continue from where the previous agent left off.`;
10120
+ }
8795
10121
  await this.ctx.session.prompt({
8796
10122
  sessionID: agent.sessionID,
8797
10123
  text: taskPrompt
@@ -8816,6 +10142,24 @@ Files: ${node.task.files.include.join(", ")}`;
8816
10142
  this.totalSpent += result.cost;
8817
10143
  this.costByAgent.set(agent.id, (this.costByAgent.get(agent.id) || 0) + result.cost);
8818
10144
  this.checkBudget();
10145
+ if (this.notifications?.isEnabled()) {
10146
+ this.notifications.notify({ title: "Nexus: Task Complete", body: `${node.task.name} completed successfully` });
10147
+ }
10148
+ const priorPattern = this.learning.findSolutions(`task ${node.id} failed`);
10149
+ if (priorPattern.length > 0) {
10150
+ this.learning.recordSuccess(priorPattern[0].entry.id);
10151
+ }
10152
+ if (typeof output === "string" && output.length > 0) {
10153
+ const securityIssues = this.securityScanner.scanContent(output, node.task.name);
10154
+ if (securityIssues.length > 0) {
10155
+ this.emit("security:issues-found", {
10156
+ taskId: node.id,
10157
+ taskName: node.task.name,
10158
+ issues: securityIssues,
10159
+ totalIssues: securityIssues.length
10160
+ });
10161
+ }
10162
+ }
8819
10163
  } catch (error) {
8820
10164
  const duration = Date.now() - startTime;
8821
10165
  const result = {
@@ -8847,57 +10191,105 @@ Files: ${node.task.files.include.join(", ")}`;
8847
10191
  };
8848
10192
  return rolePrompts[role] || `You are a ${role}. Complete the assigned task professionally.`;
8849
10193
  }
10194
+ collectContext(agent) {
10195
+ const partialResults = [];
10196
+ const decisions = [];
10197
+ const errorLog = [];
10198
+ if (agent.sessionID && this.ctx) {
10199
+ if (agent.metrics.tasksCompleted > 0) {
10200
+ partialResults.push(`${agent.metrics.tasksCompleted} task(s) completed before failure`);
10201
+ }
10202
+ if (agent.metrics.tasksFailed > 0) {
10203
+ errorLog.push(`${agent.metrics.tasksFailed} task(s) failed`);
10204
+ }
10205
+ }
10206
+ const memoryEntries = [];
10207
+ const byAgentId = this.memoryStore.getByAuthor(agent.id);
10208
+ const byRole = this.memoryStore.getByAuthor(agent.role);
10209
+ memoryEntries.push(...byAgentId, ...byRole);
10210
+ const taskProgress = agent.metrics.tasksCompleted > 0 ? Math.min(50, agent.metrics.tasksCompleted * 25) : 0;
10211
+ return {
10212
+ previousAgentId: agent.id,
10213
+ partialResults,
10214
+ decisions,
10215
+ memoryEntries,
10216
+ taskProgress,
10217
+ errorLog
10218
+ };
10219
+ }
8850
10220
  async handleFailure(agent, node, error) {
8851
- const failureCount = agent.metrics.tasksFailed + 1;
8852
- if (failureCount < this.config.selfHealing.maxRetries) {
8853
- const delay = this.config.selfHealing.retryDelay * Math.pow(this.config.selfHealing.backoffMultiplier, failureCount - 1);
10221
+ const policy = this.escalationPolicy;
10222
+ const retryCount = this.nodeRetryCounts.get(node.id) || 0;
10223
+ const pattern = error.message || "Unknown error";
10224
+ const solution = `Retry (attempt ${retryCount + 1}/${policy.maxRetries})`;
10225
+ const context = `during ${node.task.requiredRole} task "${node.task.name}"`;
10226
+ this.learning.recordFailure(pattern, solution, context, [node.task.requiredRole]);
10227
+ if (retryCount < policy.maxRetries) {
10228
+ this.nodeRetryCounts.set(node.id, retryCount + 1);
10229
+ const delay = policy.retryDelay * Math.pow(2, retryCount);
8854
10230
  await this.sleep(delay);
8855
- if (agent.sessionID) {
8856
- node.status = "running";
10231
+ node.status = "pending";
10232
+ this.notifyStateChange();
10233
+ await this.spawnAndExecute(node);
10234
+ return;
10235
+ }
10236
+ if (policy.enableRespawn && this.config.selfHealing.contextTransfer) {
10237
+ const context = this.collectContext(agent);
10238
+ this.setMemory("session", `context:${agent.id}`, context, agent.id);
10239
+ await this.terminateAgent(agent.id);
10240
+ node.status = "pending";
10241
+ this.notifyStateChange();
10242
+ await this.spawnAndExecute(node, context);
10243
+ return;
10244
+ }
10245
+ if (policy.fallbackModels.length > 0) {
10246
+ const fallbackModel = policy.fallbackModels.shift();
10247
+ if (fallbackModel) {
10248
+ await this.terminateAgent(agent.id);
10249
+ node.status = "pending";
8857
10250
  this.notifyStateChange();
8858
- await this.executeTask(agent, node);
10251
+ await this.spawnAgent({ role: node.task.requiredRole, model: fallbackModel });
10252
+ return;
10253
+ }
10254
+ }
10255
+ if (policy.alertOnFailure) {
10256
+ this.emit("agent:escalation", { agentId: agent.id, taskId: node.id, error: error.message });
10257
+ if (this.notifications?.isEnabled()) {
10258
+ this.notifications.notify({ title: "Nexus: Task Failed", body: `${node.task.name} failed: ${error.message}`, sound: true });
8859
10259
  }
8860
- } else {
8861
- node.status = "failed";
8862
- node.result = {
8863
- success: false,
8864
- error: `Max retries (${this.config.selfHealing.maxRetries}) exceeded: ${error.message}`,
8865
- duration: 0,
8866
- tokensUsed: 0,
8867
- cost: 0
8868
- };
8869
- this.notifyStateChange();
8870
10260
  }
10261
+ node.status = "failed";
10262
+ node.result = {
10263
+ success: false,
10264
+ error: error.message,
10265
+ duration: 0,
10266
+ tokensUsed: 0,
10267
+ cost: 0
10268
+ };
10269
+ this.notifyStateChange();
8871
10270
  }
8872
10271
  async spawnAgent(config) {
8873
10272
  if (!this.ctx) {
8874
10273
  throw new Error("Orchestrator not initialized");
8875
10274
  }
10275
+ if (this.budgetExceeded) {
10276
+ throw new Error("Budget exceeded — cannot spawn new agents");
10277
+ }
8876
10278
  const agentId = `agent-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
8877
- const displayName = `${config.role}-${agentId.slice(0, 8)}`;
10279
+ const modelConfig = config.model || this.configManager.getModelForRole(config.role);
10280
+ const slashIndex = modelConfig.indexOf("/");
10281
+ const provider = slashIndex > -1 ? modelConfig.slice(0, slashIndex) : modelConfig;
10282
+ const modelName = slashIndex > -1 ? modelConfig.slice(slashIndex + 1) : modelConfig;
10283
+ const roleEmoji = this.configManager.getRoleEmoji(config.role);
10284
+ const title = `${roleEmoji} ${this.configManager.getRoleDisplayName(config.role)} — ${modelConfig}`;
8878
10285
  const session = await this.ctx.session.create({
8879
- title: `[Nexus] ${displayName}`
10286
+ title,
10287
+ agent: "build",
10288
+ model: modelName ? { providerID: provider, id: modelName } : undefined
8880
10289
  });
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
- }
8898
10290
  const agent = {
8899
10291
  id: agentId,
8900
- name: displayName,
10292
+ name: title,
8901
10293
  role: config.role,
8902
10294
  status: "idle",
8903
10295
  model: {
@@ -8922,6 +10314,11 @@ Files: ${node.task.files.include.join(", ")}`;
8922
10314
  this.agents.set(agentId, agent);
8923
10315
  this.emit("agent:spawned", agent);
8924
10316
  this.notifyStateChange();
10317
+ this.enforceAgentLimit();
10318
+ const monitor = this.getOrCreateHealthMonitor();
10319
+ if (!monitor.isActive()) {
10320
+ monitor.start(() => Array.from(this.agents.values()));
10321
+ }
8925
10322
  return agent;
8926
10323
  }
8927
10324
  async terminateAgent(agentId) {
@@ -8931,6 +10328,9 @@ Files: ${node.task.files.include.join(", ")}`;
8931
10328
  this.agents.delete(agentId);
8932
10329
  this.emit("agent:terminated", agent);
8933
10330
  this.notifyStateChange();
10331
+ if (this._healthMonitor && this.agents.size === 0) {
10332
+ this._healthMonitor.stop();
10333
+ }
8934
10334
  }
8935
10335
  }
8936
10336
  analyzeComplexity(task) {
@@ -8960,26 +10360,7 @@ Files: ${node.task.files.include.join(", ")}`;
8960
10360
  };
8961
10361
  }
8962
10362
  selectModel(role, complexity) {
8963
- const configModel = this.configManager.getModelForRole(role);
8964
- const [provider, ...modelParts] = configModel.split("/");
8965
- const model = modelParts.join("/");
8966
- const budgetRemaining = this.budget.maxTotalCost - this.totalSpent;
8967
- if (budgetRemaining < 1 && configModel !== "opencode/minimax-m2.5-free") {
8968
- return {
8969
- provider: "opencode",
8970
- model: "minimax-m2.5-free",
8971
- estimatedCost: 0,
8972
- estimatedQuality: 0.5,
8973
- reasoning: "Budget constrained, using free tier"
8974
- };
8975
- }
8976
- return {
8977
- provider,
8978
- model,
8979
- estimatedCost: this.estimateModelCost(model),
8980
- estimatedQuality: this.estimateModelQuality(model),
8981
- reasoning: `Configured model for ${role}`
8982
- };
10363
+ return this.selectBestModel(role, complexity);
8983
10364
  }
8984
10365
  estimateModelCost(model) {
8985
10366
  const costs = {
@@ -9005,12 +10386,72 @@ Files: ${node.task.files.include.join(", ")}`;
9005
10386
  };
9006
10387
  return quality[model] || 0.6;
9007
10388
  }
10389
+ estimateModelSpeed(model) {
10390
+ const speeds = {
10391
+ "claude-opus-4-7": 0.4,
10392
+ "claude-sonnet-4-6": 0.7,
10393
+ "gpt-5": 0.6,
10394
+ "claude-haiku-4-5": 0.9,
10395
+ "gemini-2.5-flash": 0.95,
10396
+ "gpt-5-mini": 0.85,
10397
+ "minimax-m2.5-free": 0.8
10398
+ };
10399
+ return speeds[model] || 0.5;
10400
+ }
10401
+ scoreModel(modelId, role, complexity) {
10402
+ const [provider, ...parts] = modelId.split("/");
10403
+ const model = parts.join("/");
10404
+ const cost = this.estimateModelCost(model);
10405
+ const quality = this.estimateModelQuality(model);
10406
+ const maxCost = 15;
10407
+ const costScore = 1 - cost / maxCost;
10408
+ const speedScore = this.estimateModelSpeed(model);
10409
+ const qualityWeight = complexity.overall > 70 ? 0.6 : complexity.overall > 40 ? 0.4 : 0.2;
10410
+ const costWeight = 1 - qualityWeight;
10411
+ const overallScore = quality * qualityWeight + costScore * costWeight;
10412
+ return {
10413
+ model,
10414
+ provider,
10415
+ costScore,
10416
+ qualityScore: quality,
10417
+ speedScore,
10418
+ overallScore,
10419
+ reasoning: `Score: ${overallScore.toFixed(2)} (quality: ${quality.toFixed(2)}, cost: ${costScore.toFixed(2)}, speed: ${speedScore.toFixed(2)})`
10420
+ };
10421
+ }
10422
+ selectBestModel(role, complexity) {
10423
+ const configModel = this.configManager.getModelForRole(role);
10424
+ const candidates = [
10425
+ configModel,
10426
+ "anthropic/claude-sonnet-4-6",
10427
+ "anthropic/claude-haiku-4-5",
10428
+ "openai/gpt-5-mini",
10429
+ "google/gemini-2.5-flash",
10430
+ "opencode/minimax-m2.5-free"
10431
+ ];
10432
+ const unique = [...new Set(candidates)];
10433
+ const scored = unique.map((m) => this.scoreModel(m, role, complexity));
10434
+ const budgetRemaining = this.budget.maxTotalCost - this.totalSpent;
10435
+ const affordable = scored.filter((s) => {
10436
+ const cost = this.estimateModelCost(s.model);
10437
+ return cost <= budgetRemaining || cost === 0;
10438
+ });
10439
+ const best = (affordable.length > 0 ? affordable : scored).sort((a, b) => b.overallScore - a.overallScore)[0];
10440
+ return {
10441
+ provider: best.provider,
10442
+ model: best.model,
10443
+ estimatedCost: this.estimateModelCost(best.model),
10444
+ estimatedQuality: best.qualityScore,
10445
+ reasoning: best.reasoning
10446
+ };
10447
+ }
9008
10448
  trackCost(agentId, model, cost, tokens) {
9009
10449
  this.totalSpent += cost;
9010
10450
  const agentCost = this.costByAgent.get(agentId) || 0;
9011
10451
  this.costByAgent.set(agentId, agentCost + cost);
9012
10452
  const modelCost = this.costByModel.get(model) || 0;
9013
10453
  this.costByModel.set(model, modelCost + cost);
10454
+ this.costHistory.push({ timestamp: Date.now(), cost, agentId });
9014
10455
  this.checkBudget();
9015
10456
  this.notifyStateChange();
9016
10457
  }
@@ -9019,8 +10460,12 @@ Files: ${node.task.files.include.join(", ")}`;
9019
10460
  const remainingPercent = remaining / this.budget.maxTotalCost;
9020
10461
  if (remainingPercent <= this.config.budget.alertThreshold) {
9021
10462
  this.emit("budget:alert", { remaining, remainingPercent });
10463
+ if (this.notifications?.isEnabled()) {
10464
+ this.notifications.notify({ title: "Nexus: Budget Alert", body: `Budget low: $${remaining.toFixed(2)} remaining (${(remainingPercent * 100).toFixed(1)}%)`, sound: true });
10465
+ }
9022
10466
  }
9023
- if (this.budget.hardLimit && remaining <= 0) {
10467
+ if (this.budget.hardLimit && remaining <= 0 && !this.budgetExceeded) {
10468
+ this.budgetExceeded = true;
9024
10469
  this.emit("budget:exceeded", { totalSpent: this.totalSpent });
9025
10470
  this.pause();
9026
10471
  }
@@ -9028,12 +10473,15 @@ Files: ${node.task.files.include.join(", ")}`;
9028
10473
  publish(topic, message) {
9029
10474
  const fullMessage = {
9030
10475
  ...message,
10476
+ topic: message.topic ?? topic,
9031
10477
  id: `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
9032
10478
  timestamp: new Date
9033
10479
  };
9034
10480
  this.messageQueue.push(fullMessage);
10481
+ this.messageStore.add(fullMessage);
9035
10482
  const handlers = this.subscribers.get(topic) || [];
9036
10483
  handlers.forEach((handler) => handler(fullMessage));
10484
+ this.messageRouter.route(fullMessage);
9037
10485
  }
9038
10486
  subscribe(topic, handler) {
9039
10487
  const handlers = this.subscribers.get(topic) || [];
@@ -9046,29 +10494,27 @@ Files: ${node.task.files.include.join(", ")}`;
9046
10494
  };
9047
10495
  }
9048
10496
  setMemory(scope, key, value, author) {
9049
- const entry = {
9050
- id: `mem-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
10497
+ const entry = this.memoryStore.set({
9051
10498
  key,
9052
10499
  value,
9053
10500
  scope,
9054
10501
  author,
9055
- timestamp: new Date,
9056
10502
  confidence: 1,
9057
10503
  tags: []
9058
- };
9059
- this.memory.set(`${scope}:${key}`, entry);
10504
+ });
9060
10505
  this.emit("memory:set", entry);
9061
10506
  }
9062
10507
  getMemory(scope, key) {
9063
- return this.memory.get(`${scope}:${key}`);
10508
+ return this.memoryStore.get(key, scope) ?? undefined;
9064
10509
  }
9065
10510
  getStatus(detailed) {
9066
10511
  const state = this.getState();
9067
10512
  if (detailed)
9068
- return JSON.stringify(state, null, 2);
10513
+ return JSON.stringify({ ...state, budgetExceeded: this.budgetExceeded }, null, 2);
9069
10514
  return JSON.stringify({
9070
10515
  running: state.running,
9071
10516
  paused: state.paused,
10517
+ budgetExceeded: this.budgetExceeded,
9072
10518
  agents: state.agents.length,
9073
10519
  tasks: state.tasks.length,
9074
10520
  totalCost: state.totalSpent,
@@ -9099,14 +10545,61 @@ Files: ${node.task.files.include.join(", ")}`;
9099
10545
  this.emit("orchestrator:resumed", {});
9100
10546
  this.notifyStateChange();
9101
10547
  }
9102
- shutdown() {
10548
+ isBudgetExceeded() {
10549
+ return this.budgetExceeded;
10550
+ }
10551
+ resetBudgetExceeded() {
10552
+ this.budgetExceeded = false;
10553
+ }
10554
+ async shutdown() {
10555
+ await this.moduleRegistry.teardownAll();
10556
+ this._healthMonitor?.stop();
10557
+ this.stopDashboard();
10558
+ this.memoryStore.close();
10559
+ if (this.cleanupInterval) {
10560
+ clearInterval(this.cleanupInterval);
10561
+ this.cleanupInterval = null;
10562
+ }
10563
+ if (this.stateChangeTimer) {
10564
+ clearTimeout(this.stateChangeTimer);
10565
+ this.stateChangeTimer = null;
10566
+ }
9103
10567
  this.agents.forEach((agent) => {
9104
10568
  agent.status = "terminated";
9105
10569
  });
9106
10570
  this.agents.clear();
9107
10571
  this.running = false;
9108
10572
  this.emit("orchestrator:shutdown", {});
9109
- this.notifyStateChange();
10573
+ this.onStateChange?.();
10574
+ }
10575
+ cleanupStaleData() {
10576
+ const now = Date.now();
10577
+ for (const [id, agent] of this.agents) {
10578
+ if (agent.status === "terminated" && now - agent.lastActivity.getTime() > 3600000) {
10579
+ this.agents.delete(id);
10580
+ }
10581
+ }
10582
+ if (this.costHistory.length > 1000) {
10583
+ this.costHistory = this.costHistory.slice(-500);
10584
+ }
10585
+ }
10586
+ enforceAgentLimit() {
10587
+ const MAX_AGENTS = this.config.maxConcurrency || 10;
10588
+ if (this.agents.size > MAX_AGENTS) {
10589
+ const terminated = [...this.agents.entries()].filter(([_, a]) => a.status === "terminated").sort((a, b) => a[1].lastActivity.getTime() - b[1].lastActivity.getTime());
10590
+ const toRemove = terminated.slice(0, this.agents.size - MAX_AGENTS);
10591
+ for (const [id] of toRemove) {
10592
+ this.agents.delete(id);
10593
+ }
10594
+ }
10595
+ }
10596
+ getOrCreateHealthMonitor() {
10597
+ if (!this._healthMonitor) {
10598
+ this._healthMonitor = new HealthMonitor({
10599
+ checkInterval: this.config.agents.healthCheckInterval
10600
+ });
10601
+ }
10602
+ return this._healthMonitor;
9110
10603
  }
9111
10604
  collectResults() {
9112
10605
  const results = [];
@@ -9159,12 +10652,90 @@ Files: ${node.task.files.include.join(", ")}`;
9159
10652
  }
9160
10653
  }
9161
10654
 
10655
+ // src/templates.ts
10656
+ var TEMPLATES = {
10657
+ feature: {
10658
+ name: "Feature Development",
10659
+ description: "Full feature pipeline: architect → coder → reviewer → tester",
10660
+ steps: [
10661
+ { role: "architect", name: "Design", description: "Design the feature architecture and API", files: { include: ["**/*.ts"], exclude: ["node_modules"] } },
10662
+ { role: "coder", name: "Implement", description: "Implement the feature according to design", files: { include: ["src/**/*.ts"], exclude: ["node_modules"] } },
10663
+ { role: "reviewer", name: "Review", description: "Code review for quality, security, and correctness", files: { include: ["src/**/*.ts"], exclude: ["node_modules"] } },
10664
+ { role: "tester", name: "Test", description: "Write and run tests for the feature", files: { include: ["test/**/*.ts"], exclude: ["node_modules"] } }
10665
+ ]
10666
+ },
10667
+ bugfix: {
10668
+ name: "Bug Fix",
10669
+ description: "Bug investigation and fix: explorer → coder → reviewer",
10670
+ steps: [
10671
+ { role: "explorer", name: "Investigate", description: "Investigate the bug root cause", files: { include: ["src/**/*.ts"], exclude: ["node_modules"] } },
10672
+ { role: "coder", name: "Fix", description: "Implement the bug fix", files: { include: ["src/**/*.ts"], exclude: ["node_modules"] } },
10673
+ { role: "reviewer", name: "Review", description: "Review the fix for correctness", files: { include: ["src/**/*.ts"], exclude: ["node_modules"] } }
10674
+ ]
10675
+ },
10676
+ refactor: {
10677
+ name: "Refactoring",
10678
+ description: "Code refactoring: architect → coder → reviewer → tester",
10679
+ steps: [
10680
+ { role: "architect", name: "Plan", description: "Plan the refactoring approach", files: { include: ["src/**/*.ts"], exclude: ["node_modules"] } },
10681
+ { role: "coder", name: "Refactor", description: "Execute the refactoring", files: { include: ["src/**/*.ts"], exclude: ["node_modules"] } },
10682
+ { role: "reviewer", name: "Review", description: "Review refactored code", files: { include: ["src/**/*.ts"], exclude: ["node_modules"] } },
10683
+ { role: "tester", name: "Verify", description: "Run existing tests to ensure no regressions", files: { include: ["test/**/*.ts"], exclude: ["node_modules"] } }
10684
+ ]
10685
+ },
10686
+ documentation: {
10687
+ name: "Documentation",
10688
+ description: "Documentation update: explorer → documenter → reviewer",
10689
+ steps: [
10690
+ { role: "explorer", name: "Analyze", description: "Analyze codebase to understand what needs documentation", files: { include: ["src/**/*.ts"], exclude: ["node_modules"] } },
10691
+ { role: "documenter", name: "Write", description: "Write documentation", files: { include: ["**/*.md", "src/**/*.ts"], exclude: ["node_modules"] } },
10692
+ { role: "reviewer", name: "Review", description: "Review documentation for accuracy", files: { include: ["**/*.md"], exclude: ["node_modules"] } }
10693
+ ]
10694
+ }
10695
+ };
10696
+ var DEFAULT_COMPLEXITY = {
10697
+ overall: 50,
10698
+ factors: {
10699
+ fileCount: 1,
10700
+ codeLines: 100,
10701
+ dependencyDepth: 1,
10702
+ domainKnowledge: 1,
10703
+ riskLevel: "medium"
10704
+ }
10705
+ };
10706
+ function instantiateTemplate(templateName, baseDir) {
10707
+ const template = TEMPLATES[templateName];
10708
+ if (!template)
10709
+ throw new Error(`Unknown template: ${templateName}. Available: ${Object.keys(TEMPLATES).join(", ")}`);
10710
+ return template.steps.map((step, index) => ({
10711
+ id: `${templateName}-${index + 1}-${step.role}`,
10712
+ name: `${template.name}: ${step.name}`,
10713
+ description: step.description,
10714
+ requiredRole: step.role,
10715
+ complexity: { ...DEFAULT_COMPLEXITY },
10716
+ files: {
10717
+ include: step.files.include.map((f) => f.startsWith("/") ? f : `${baseDir}/${f}`),
10718
+ exclude: step.files.exclude
10719
+ },
10720
+ dependencies: index > 0 ? [`${templateName}-${index}-${template.steps[index - 1].role}`] : [],
10721
+ timeout: step.timeout,
10722
+ status: "pending",
10723
+ priority: "normal"
10724
+ }));
10725
+ }
10726
+ function listTemplates() {
10727
+ return Object.keys(TEMPLATES);
10728
+ }
10729
+ function getTemplate(name) {
10730
+ return TEMPLATES[name];
10731
+ }
10732
+
9162
10733
  // src/index.ts
9163
10734
  var src_default = define({
9164
10735
  id: "nexus",
9165
10736
  async setup(ctx) {
9166
10737
  const orchestrator = new NexusOrchestrator;
9167
- orchestrator.initialize(ctx, () => {
10738
+ await orchestrator.initialize(ctx, () => {
9168
10739
  const state = orchestrator.getState();
9169
10740
  ctx.storage.set("orchestrator-state", JSON.parse(JSON.stringify(state))).catch(() => {});
9170
10741
  });
@@ -9232,6 +10803,117 @@ var src_default = define({
9232
10803
  return { content: JSON.stringify(state, null, 2) };
9233
10804
  }
9234
10805
  });
10806
+ editor.add({
10807
+ name: "queue",
10808
+ description: "Show current task queue with priorities",
10809
+ input: {
10810
+ type: "object",
10811
+ properties: {},
10812
+ additionalProperties: false
10813
+ },
10814
+ execute: async () => {
10815
+ const state = orchestrator.getState();
10816
+ const tasks = state.tasks || [];
10817
+ return { content: JSON.stringify(tasks, null, 2) };
10818
+ }
10819
+ });
10820
+ editor.add({
10821
+ name: "config.save",
10822
+ description: "Save Nexus config to disk (project or global)",
10823
+ input: {
10824
+ type: "object",
10825
+ properties: {
10826
+ level: { type: "string", enum: ["project", "global"], description: "Config level to save" },
10827
+ basePath: { type: "string", description: "Project root (for project-level, defaults to cwd)" }
10828
+ },
10829
+ required: ["level"],
10830
+ additionalProperties: false
10831
+ },
10832
+ execute: async (input) => {
10833
+ const { level, basePath } = input;
10834
+ orchestrator.configManager.saveConfig(level, basePath || process.cwd());
10835
+ const location = level === "project" ? `${basePath || process.cwd()}/.opencode/nexus.jsonc` : "~/.config/opencode/nexus.jsonc";
10836
+ return { content: `Config saved to ${level} level at ${location}` };
10837
+ }
10838
+ });
10839
+ editor.add({
10840
+ name: "config.init",
10841
+ description: "Initialize default config files for project and/or global",
10842
+ input: {
10843
+ type: "object",
10844
+ properties: {
10845
+ level: { type: "string", enum: ["project", "global", "both"], description: "Which config to initialize" },
10846
+ basePath: { type: "string", description: "Project root" }
10847
+ },
10848
+ required: ["level"],
10849
+ additionalProperties: false
10850
+ },
10851
+ execute: async (input) => {
10852
+ const { level, basePath } = input;
10853
+ const path = basePath || process.cwd();
10854
+ const locations = [];
10855
+ if (level === "project" || level === "both") {
10856
+ orchestrator.configManager.saveProjectConfig(path);
10857
+ locations.push(`${path}/.opencode/nexus.jsonc`);
10858
+ }
10859
+ if (level === "global" || level === "both") {
10860
+ orchestrator.configManager.saveGlobalConfig();
10861
+ locations.push("~/.config/opencode/nexus.jsonc");
10862
+ }
10863
+ return { content: `Config initialized at ${level} level(s): ${locations.join(", ")}` };
10864
+ }
10865
+ });
10866
+ editor.add({
10867
+ name: "preset",
10868
+ description: "Apply a preset configuration",
10869
+ input: {
10870
+ type: "object",
10871
+ properties: {
10872
+ name: { type: "string", description: "Preset name (minimal, balanced, enterprise, cost-optimized)" }
10873
+ },
10874
+ required: ["name"],
10875
+ additionalProperties: false
10876
+ },
10877
+ execute: async (input) => {
10878
+ const { name } = input;
10879
+ try {
10880
+ orchestrator.configManager.applyPreset(name);
10881
+ return { content: `Applied preset: ${PRESETS[name]?.name || name}` };
10882
+ } catch (error) {
10883
+ return { content: `Error: ${error.message}` };
10884
+ }
10885
+ }
10886
+ });
10887
+ editor.add({
10888
+ name: "dashboard.start",
10889
+ description: "Start the web dashboard server",
10890
+ input: {
10891
+ type: "object",
10892
+ properties: {
10893
+ port: { type: "number", description: "Port (default: 4747)" },
10894
+ host: { type: "string", description: "Host (default: 127.0.0.1)" }
10895
+ },
10896
+ additionalProperties: false
10897
+ },
10898
+ execute: async (input) => {
10899
+ const { port, host } = input;
10900
+ orchestrator.startDashboard(port, host);
10901
+ return { content: `Dashboard started at http://${host || "127.0.0.1"}:${port || 4747}` };
10902
+ }
10903
+ });
10904
+ editor.add({
10905
+ name: "dashboard.stop",
10906
+ description: "Stop the web dashboard server",
10907
+ input: {
10908
+ type: "object",
10909
+ properties: {},
10910
+ additionalProperties: false
10911
+ },
10912
+ execute: async () => {
10913
+ orchestrator.stopDashboard();
10914
+ return { content: "Dashboard stopped" };
10915
+ }
10916
+ });
9235
10917
  editor.add({
9236
10918
  name: "spawn",
9237
10919
  description: "Spawn a sub-agent for a task",
@@ -9253,13 +10935,80 @@ var src_default = define({
9253
10935
  sessionID: agent.sessionID,
9254
10936
  text: task
9255
10937
  });
10938
+ const complexity = orchestrator.analyzeComplexity({
10939
+ id: `spawn-${Date.now()}`,
10940
+ name: task,
10941
+ description: task,
10942
+ files: { include: [] },
10943
+ dependencies: [],
10944
+ requiredRole: role,
10945
+ complexity: { overall: 0, factors: { fileCount: 0, codeLines: 0, dependencyDepth: 0, domainKnowledge: 0, riskLevel: "low" } },
10946
+ priority: "normal",
10947
+ status: "running"
10948
+ });
10949
+ agent.complexity = complexity;
10950
+ const modelSelection = orchestrator.selectModel(role, complexity);
9256
10951
  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.` };
10952
+ const taskPreview = task.length > 80 ? task.substring(0, 77) + "..." : task;
10953
+ const output = [
10954
+ `${agent.name}`,
10955
+ `\uD83D\uDCCB Task: ${taskPreview}`,
10956
+ `\uD83D\uDCCA Complexity: ${complexity.overall}/100 (${complexity.factors.riskLevel} risk)`,
10957
+ `\uD83E\uDD16 Model reasoning: ${modelSelection.reasoning}`,
10958
+ `\uD83D\uDCCE Session: ${agent.sessionID}`
10959
+ ].join(`
10960
+ `);
10961
+ return { content: output };
9258
10962
  } catch (error) {
9259
10963
  return { content: `Failed to spawn agent: ${error.message}` };
9260
10964
  }
9261
10965
  }
9262
10966
  });
10967
+ editor.add({
10968
+ name: "template",
10969
+ description: "List or instantiate task templates",
10970
+ input: {
10971
+ type: "object",
10972
+ properties: {
10973
+ name: { type: "string", description: "Template name to instantiate (or 'list' to show all)" },
10974
+ baseDir: { type: "string", description: "Base directory for file paths" }
10975
+ },
10976
+ additionalProperties: false
10977
+ },
10978
+ execute: async (input) => {
10979
+ const { name, baseDir } = input;
10980
+ if (!name || name === "list") {
10981
+ const templates = listTemplates();
10982
+ return { content: templates.map((t) => `${t}: ${TEMPLATES[t].description}`).join(`
10983
+ `) };
10984
+ }
10985
+ try {
10986
+ const tasks = instantiateTemplate(name, baseDir || process.cwd());
10987
+ return { content: JSON.stringify(tasks, null, 2) };
10988
+ } catch (error) {
10989
+ return { content: `Error: ${error.message}` };
10990
+ }
10991
+ }
10992
+ });
10993
+ editor.add({
10994
+ name: "security.scan",
10995
+ description: "Scan content for security issues",
10996
+ input: {
10997
+ type: "object",
10998
+ properties: {
10999
+ content: { type: "string", description: "Code content to scan" },
11000
+ filename: { type: "string", description: "Filename for context" }
11001
+ },
11002
+ required: ["content"],
11003
+ additionalProperties: false
11004
+ },
11005
+ execute: async (input) => {
11006
+ const { content, filename } = input;
11007
+ const issues = orchestrator.securityScanner.scanContent(content, filename || "unknown");
11008
+ const result = orchestrator.securityScanner.getResult();
11009
+ return { content: JSON.stringify({ issues: issues.length, score: result.score, details: issues }, null, 2) };
11010
+ }
11011
+ });
9263
11012
  });
9264
11013
  await ctx.session.hook("prompt", (event) => {
9265
11014
  if (event.prompt.text.startsWith("/nexus")) {
@@ -9274,7 +11023,21 @@ var src_default = define({
9274
11023
  });
9275
11024
  export {
9276
11025
  DEFAULT_CONFIG,
11026
+ HealthMonitor,
11027
+ LearningModule,
11028
+ MessageRouter,
11029
+ MessageStore,
11030
+ ModuleRegistry,
9277
11031
  NexusConfigManager,
9278
11032
  NexusOrchestrator,
9279
- src_default as default
11033
+ PRESETS,
11034
+ PersistentMemoryStore,
11035
+ SecurityScanner,
11036
+ StateBroadcaster,
11037
+ TEMPLATES,
11038
+ src_default as default,
11039
+ detectCycles,
11040
+ getTemplate,
11041
+ instantiateTemplate,
11042
+ listTemplates
9280
11043
  };