@serkanalgur/opencode-nexus 1.0.5 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +296 -248
- package/dist/index.js +2294 -432
- package/dist/tui.js +328 -20
- 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
|
|
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
|
|
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
|
|
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,
|
|
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:
|
|
6416
|
-
hidden:
|
|
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:
|
|
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:
|
|
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:
|
|
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,
|
|
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:
|
|
6726
|
+
requireReasoning: Boolean3.pipe(optional3),
|
|
6724
6727
|
maxTokensField: MaxTokensField.pipe(optional3),
|
|
6725
|
-
requireFinishReason:
|
|
6726
|
-
requireAssistantAfterTool:
|
|
6727
|
-
supportsPromptCacheKey:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
6913
|
-
codemode:
|
|
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:
|
|
6939
|
-
codemode:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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(
|
|
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: "
|
|
8455
|
-
coder: "
|
|
8456
|
-
reviewer: "
|
|
8457
|
-
tester: "
|
|
8458
|
-
explorer: "
|
|
8459
|
-
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,212 +8647,1423 @@ class NexusConfigManager {
|
|
|
8496
8647
|
`);
|
|
8497
8648
|
}
|
|
8498
8649
|
}
|
|
8499
|
-
|
|
8500
|
-
|
|
8501
|
-
|
|
8502
|
-
|
|
8503
|
-
|
|
8504
|
-
|
|
8505
|
-
|
|
8506
|
-
|
|
8507
|
-
|
|
8508
|
-
|
|
8509
|
-
|
|
8510
|
-
|
|
8511
|
-
|
|
8512
|
-
|
|
8513
|
-
|
|
8514
|
-
|
|
8515
|
-
|
|
8516
|
-
|
|
8517
|
-
|
|
8518
|
-
|
|
8519
|
-
|
|
8520
|
-
|
|
8521
|
-
|
|
8522
|
-
|
|
8523
|
-
|
|
8524
|
-
|
|
8525
|
-
|
|
8526
|
-
|
|
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
|
+
},
|
|
8527
8688
|
budget: {
|
|
8528
8689
|
maxTotalCost: 10,
|
|
8529
8690
|
maxCostPerTask: 1,
|
|
8530
8691
|
maxCostPerAgent: 2,
|
|
8531
|
-
alertThreshold: 0.2
|
|
8532
|
-
hardLimit: false
|
|
8533
|
-
},
|
|
8534
|
-
agents: {
|
|
8535
|
-
defaultRole: "coder",
|
|
8536
|
-
spawnDelay: 100,
|
|
8537
|
-
healthCheckInterval: 30000
|
|
8692
|
+
alertThreshold: 0.2
|
|
8538
8693
|
},
|
|
8539
8694
|
selfHealing: {
|
|
8540
8695
|
enabled: true,
|
|
8541
8696
|
maxRetries: 3,
|
|
8542
|
-
retryDelay: 1000,
|
|
8543
|
-
backoffMultiplier: 2,
|
|
8544
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"
|
|
8545
8712
|
},
|
|
8546
|
-
|
|
8547
|
-
|
|
8548
|
-
|
|
8549
|
-
|
|
8550
|
-
|
|
8713
|
+
budget: {
|
|
8714
|
+
maxTotalCost: 50,
|
|
8715
|
+
maxCostPerTask: 5,
|
|
8716
|
+
maxCostPerAgent: 10,
|
|
8717
|
+
alertThreshold: 0.1
|
|
8551
8718
|
},
|
|
8552
|
-
|
|
8719
|
+
selfHealing: {
|
|
8553
8720
|
enabled: true,
|
|
8554
|
-
|
|
8555
|
-
|
|
8556
|
-
|
|
8557
|
-
|
|
8558
|
-
|
|
8559
|
-
|
|
8560
|
-
|
|
8561
|
-
|
|
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"
|
|
8562
8737
|
},
|
|
8563
|
-
|
|
8564
|
-
|
|
8565
|
-
|
|
8566
|
-
|
|
8738
|
+
budget: {
|
|
8739
|
+
maxTotalCost: 3,
|
|
8740
|
+
maxCostPerTask: 0.3,
|
|
8741
|
+
maxCostPerAgent: 1,
|
|
8742
|
+
alertThreshold: 0.3
|
|
8567
8743
|
},
|
|
8568
|
-
|
|
8744
|
+
selfHealing: {
|
|
8569
8745
|
enabled: true,
|
|
8570
|
-
|
|
8571
|
-
|
|
8746
|
+
maxRetries: 2,
|
|
8747
|
+
contextTransfer: false
|
|
8572
8748
|
}
|
|
8573
|
-
};
|
|
8574
|
-
return {
|
|
8575
|
-
...defaults,
|
|
8576
|
-
...partial,
|
|
8577
|
-
budget: { ...defaults.budget, ...partial?.budget },
|
|
8578
|
-
agents: { ...defaults.agents, ...partial?.agents },
|
|
8579
|
-
selfHealing: { ...defaults.selfHealing, ...partial?.selfHealing },
|
|
8580
|
-
communication: { ...defaults.communication, ...partial?.communication },
|
|
8581
|
-
memory: { ...defaults.memory, ...partial?.memory },
|
|
8582
|
-
dashboard: { ...defaults.dashboard, ...partial?.dashboard },
|
|
8583
|
-
security: { ...defaults.security, ...partial?.security },
|
|
8584
|
-
learning: { ...defaults.learning, ...partial?.learning }
|
|
8585
|
-
};
|
|
8586
|
-
}
|
|
8587
|
-
async execute(request) {
|
|
8588
|
-
if (this.running) {
|
|
8589
|
-
throw new Error("Orchestrator is already running");
|
|
8590
8749
|
}
|
|
8591
|
-
|
|
8592
|
-
|
|
8593
|
-
|
|
8594
|
-
|
|
8595
|
-
|
|
8596
|
-
|
|
8597
|
-
|
|
8598
|
-
|
|
8599
|
-
|
|
8600
|
-
|
|
8601
|
-
|
|
8602
|
-
|
|
8603
|
-
|
|
8604
|
-
|
|
8605
|
-
|
|
8606
|
-
|
|
8607
|
-
|
|
8608
|
-
|
|
8609
|
-
|
|
8610
|
-
|
|
8611
|
-
|
|
8612
|
-
|
|
8613
|
-
|
|
8614
|
-
|
|
8615
|
-
|
|
8616
|
-
|
|
8617
|
-
|
|
8750
|
+
}
|
|
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
|
+
});
|
|
8772
|
+
}
|
|
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);
|
|
8618
8790
|
}
|
|
8619
8791
|
}
|
|
8620
|
-
|
|
8621
|
-
|
|
8622
|
-
|
|
8623
|
-
|
|
8624
|
-
|
|
8625
|
-
|
|
8626
|
-
|
|
8627
|
-
|
|
8628
|
-
|
|
8629
|
-
|
|
8630
|
-
|
|
8631
|
-
|
|
8632
|
-
|
|
8633
|
-
|
|
8634
|
-
|
|
8635
|
-
|
|
8636
|
-
|
|
8637
|
-
|
|
8638
|
-
|
|
8639
|
-
const depsComplete = node.dependencies.every((dep) => {
|
|
8640
|
-
const depNode = dag.nodes.get(dep);
|
|
8641
|
-
return depNode?.status === "completed";
|
|
8642
|
-
});
|
|
8643
|
-
if (depsComplete) {
|
|
8644
|
-
ready.push(node);
|
|
8645
|
-
}
|
|
8646
|
-
}
|
|
8647
|
-
});
|
|
8648
|
-
return ready;
|
|
8649
|
-
},
|
|
8650
|
-
markComplete: (nodeId, result) => {
|
|
8651
|
-
const node = dag.nodes.get(nodeId);
|
|
8652
|
-
if (node) {
|
|
8653
|
-
node.status = "completed";
|
|
8654
|
-
node.result = result;
|
|
8655
|
-
}
|
|
8656
|
-
},
|
|
8657
|
-
markFailed: (nodeId, error) => {
|
|
8658
|
-
const node = dag.nodes.get(nodeId);
|
|
8659
|
-
if (node) {
|
|
8660
|
-
node.status = "failed";
|
|
8661
|
-
}
|
|
8662
|
-
},
|
|
8663
|
-
getParallelGroups: () => {
|
|
8664
|
-
const groups = [];
|
|
8665
|
-
const ready = dag.getReadyNodes();
|
|
8666
|
-
if (ready.length > 0) {
|
|
8667
|
-
groups.push(ready);
|
|
8668
|
-
}
|
|
8669
|
-
return groups;
|
|
8670
|
-
},
|
|
8671
|
-
isComplete: () => {
|
|
8672
|
-
let complete = true;
|
|
8673
|
-
dag.nodes.forEach((node) => {
|
|
8674
|
-
if (node.status !== "completed" && node.status !== "failed") {
|
|
8675
|
-
complete = false;
|
|
8676
|
-
}
|
|
8677
|
-
});
|
|
8678
|
-
return complete;
|
|
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);
|
|
8679
8811
|
}
|
|
8680
|
-
};
|
|
8681
|
-
for (const task of tasks) {
|
|
8682
|
-
const node = {
|
|
8683
|
-
id: task.id,
|
|
8684
|
-
task,
|
|
8685
|
-
dependencies: task.dependencies,
|
|
8686
|
-
status: "pending"
|
|
8687
|
-
};
|
|
8688
|
-
dag.addNode(node);
|
|
8689
8812
|
}
|
|
8690
|
-
return dag;
|
|
8691
8813
|
}
|
|
8692
|
-
|
|
8693
|
-
|
|
8694
|
-
|
|
8695
|
-
|
|
8696
|
-
|
|
8697
|
-
await this.spawnAndExecute(node);
|
|
8698
|
-
}
|
|
8699
|
-
}
|
|
8700
|
-
await this.sleep(this.config.schedulerInterval);
|
|
8814
|
+
sendTo(ws, data) {
|
|
8815
|
+
try {
|
|
8816
|
+
ws.send(JSON.stringify(data));
|
|
8817
|
+
} catch {
|
|
8818
|
+
this.clients.delete(ws);
|
|
8701
8819
|
}
|
|
8702
8820
|
}
|
|
8703
|
-
|
|
8704
|
-
|
|
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) {
|
|
10013
|
+
node.status = "completed";
|
|
10014
|
+
node.result = result;
|
|
10015
|
+
}
|
|
10016
|
+
},
|
|
10017
|
+
markFailed: (nodeId, error) => {
|
|
10018
|
+
const node = dag.nodes.get(nodeId);
|
|
10019
|
+
if (node) {
|
|
10020
|
+
node.status = "failed";
|
|
10021
|
+
}
|
|
10022
|
+
},
|
|
10023
|
+
getParallelGroups: () => {
|
|
10024
|
+
const groups = [];
|
|
10025
|
+
const ready = dag.getReadyNodes();
|
|
10026
|
+
if (ready.length > 0) {
|
|
10027
|
+
groups.push(ready);
|
|
10028
|
+
}
|
|
10029
|
+
return groups;
|
|
10030
|
+
},
|
|
10031
|
+
isComplete: () => {
|
|
10032
|
+
let complete = true;
|
|
10033
|
+
dag.nodes.forEach((node) => {
|
|
10034
|
+
if (node.status !== "completed" && node.status !== "failed") {
|
|
10035
|
+
complete = false;
|
|
10036
|
+
}
|
|
10037
|
+
});
|
|
10038
|
+
return complete;
|
|
10039
|
+
}
|
|
10040
|
+
};
|
|
10041
|
+
for (const task of tasks) {
|
|
10042
|
+
const node = {
|
|
10043
|
+
id: task.id,
|
|
10044
|
+
task,
|
|
10045
|
+
dependencies: task.dependencies,
|
|
10046
|
+
status: "pending"
|
|
10047
|
+
};
|
|
10048
|
+
dag.addNode(node);
|
|
10049
|
+
}
|
|
10050
|
+
return dag;
|
|
10051
|
+
}
|
|
10052
|
+
async executeDAG() {
|
|
10053
|
+
while (!this.dag.isComplete() && !this.paused) {
|
|
10054
|
+
const readyNodes = this.dag.getReadyNodes();
|
|
10055
|
+
const spawnPromises = [];
|
|
10056
|
+
for (const node of readyNodes) {
|
|
10057
|
+
if (this.agents.size < this.config.maxConcurrency) {
|
|
10058
|
+
spawnPromises.push(this.spawnAndExecute(node));
|
|
10059
|
+
}
|
|
10060
|
+
}
|
|
10061
|
+
await Promise.all(spawnPromises);
|
|
10062
|
+
await this.sleep(this.config.schedulerInterval);
|
|
10063
|
+
}
|
|
10064
|
+
}
|
|
10065
|
+
async spawnAndExecute(node, transferContext) {
|
|
10066
|
+
const complexity = this.analyzeComplexity(node.task);
|
|
8705
10067
|
const model = this.selectModel(node.task.requiredRole, complexity);
|
|
8706
10068
|
const agent = await this.spawnAgent({
|
|
8707
10069
|
role: node.task.requiredRole,
|
|
@@ -8710,86 +10072,232 @@ class NexusOrchestrator {
|
|
|
8710
10072
|
});
|
|
8711
10073
|
node.spawnedAgent = agent;
|
|
8712
10074
|
node.status = "running";
|
|
8713
|
-
|
|
10075
|
+
node.task.assignedAgent = agent.id;
|
|
10076
|
+
this.notifyStateChange();
|
|
10077
|
+
await this.executeTask(agent, node, transferContext);
|
|
8714
10078
|
}
|
|
8715
|
-
async
|
|
8716
|
-
|
|
8717
|
-
setTimeout(() => {
|
|
8718
|
-
if (node.status === "running") {
|
|
8719
|
-
this.handleAgentTimeout(agent, node);
|
|
8720
|
-
}
|
|
8721
|
-
}, timeout);
|
|
8722
|
-
}
|
|
8723
|
-
handleAgentTimeout(agent, node) {
|
|
8724
|
-
if (this.config.selfHealing.enabled) {
|
|
8725
|
-
this.handleFailure(agent, node, new Error("Agent timed out"));
|
|
8726
|
-
} else {
|
|
10079
|
+
async executeTask(agent, node, transferContext) {
|
|
10080
|
+
if (!this.ctx || !agent.sessionID) {
|
|
8727
10081
|
node.status = "failed";
|
|
8728
10082
|
node.result = {
|
|
8729
10083
|
success: false,
|
|
8730
|
-
error: "
|
|
8731
|
-
duration:
|
|
10084
|
+
error: "No session available for agent",
|
|
10085
|
+
duration: 0,
|
|
10086
|
+
tokensUsed: 0,
|
|
10087
|
+
cost: 0
|
|
10088
|
+
};
|
|
10089
|
+
this.notifyStateChange();
|
|
10090
|
+
return;
|
|
10091
|
+
}
|
|
10092
|
+
const startTime = Date.now();
|
|
10093
|
+
const timeout = node.task.timeout || this.config.defaultTimeout;
|
|
10094
|
+
try {
|
|
10095
|
+
const rolePrompt = this.buildRolePrompt(node.task.requiredRole);
|
|
10096
|
+
let taskPrompt = `${rolePrompt}
|
|
10097
|
+
|
|
10098
|
+
## Task
|
|
10099
|
+
${node.task.name}
|
|
10100
|
+
|
|
10101
|
+
${node.task.description}
|
|
10102
|
+
|
|
10103
|
+
## Scope
|
|
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
|
+
}
|
|
10121
|
+
await this.ctx.session.prompt({
|
|
10122
|
+
sessionID: agent.sessionID,
|
|
10123
|
+
text: taskPrompt
|
|
10124
|
+
});
|
|
10125
|
+
const waitPromise = this.ctx.session.wait({ sessionID: agent.sessionID });
|
|
10126
|
+
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Task timed out")), timeout));
|
|
10127
|
+
await Promise.race([waitPromise, timeoutPromise]);
|
|
10128
|
+
const messages = await this.ctx.session.context({ sessionID: agent.sessionID });
|
|
10129
|
+
const lastAssistantMsg = messages.filter((m) => m.role === "assistant").pop();
|
|
10130
|
+
const output = lastAssistantMsg?.content || "Task completed";
|
|
10131
|
+
const duration = Date.now() - startTime;
|
|
10132
|
+
const result = {
|
|
10133
|
+
success: true,
|
|
10134
|
+
output: typeof output === "string" ? output : JSON.stringify(output),
|
|
10135
|
+
duration,
|
|
10136
|
+
tokensUsed: 0,
|
|
10137
|
+
cost: this.estimateModelCost(agent.model.model)
|
|
10138
|
+
};
|
|
10139
|
+
this.dag.markComplete(node.id, result);
|
|
10140
|
+
agent.metrics.tasksCompleted++;
|
|
10141
|
+
agent.metrics.totalCost += result.cost;
|
|
10142
|
+
this.totalSpent += result.cost;
|
|
10143
|
+
this.costByAgent.set(agent.id, (this.costByAgent.get(agent.id) || 0) + result.cost);
|
|
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
|
+
}
|
|
10163
|
+
} catch (error) {
|
|
10164
|
+
const duration = Date.now() - startTime;
|
|
10165
|
+
const result = {
|
|
10166
|
+
success: false,
|
|
10167
|
+
error: error.message || "Task failed",
|
|
10168
|
+
duration,
|
|
8732
10169
|
tokensUsed: 0,
|
|
8733
10170
|
cost: 0
|
|
8734
10171
|
};
|
|
10172
|
+
this.dag.markFailed(node.id, new Error(result.error));
|
|
10173
|
+
agent.metrics.tasksFailed++;
|
|
10174
|
+
if (this.config.selfHealing.enabled) {
|
|
10175
|
+
await this.handleFailure(agent, node, new Error(result.error));
|
|
10176
|
+
}
|
|
10177
|
+
} finally {
|
|
10178
|
+
agent.status = "idle";
|
|
10179
|
+
node.task.status = node.status === "completed" ? "completed" : "failed";
|
|
10180
|
+
this.notifyStateChange();
|
|
10181
|
+
}
|
|
10182
|
+
}
|
|
10183
|
+
buildRolePrompt(role) {
|
|
10184
|
+
const rolePrompts = {
|
|
10185
|
+
architect: "You are a software architect. Focus on system design, architecture patterns, and high-level technical decisions. Analyze requirements and propose structured solutions.",
|
|
10186
|
+
coder: "You are a senior software engineer. Write clean, efficient, well-documented code. Follow best practices and coding standards.",
|
|
10187
|
+
reviewer: "You are a code reviewer. Review code for correctness, security, performance, and maintainability. Provide constructive feedback.",
|
|
10188
|
+
tester: "You are a QA engineer. Write comprehensive tests, identify edge cases, and ensure code quality.",
|
|
10189
|
+
explorer: "You are a code explorer. Navigate and analyze codebases, understand architecture, and provide detailed reports.",
|
|
10190
|
+
documenter: "You are a technical writer. Create clear, comprehensive documentation for code and APIs."
|
|
10191
|
+
};
|
|
10192
|
+
return rolePrompts[role] || `You are a ${role}. Complete the assigned task professionally.`;
|
|
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
|
+
}
|
|
8735
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
|
+
};
|
|
8736
10219
|
}
|
|
8737
10220
|
async handleFailure(agent, node, error) {
|
|
8738
|
-
const
|
|
8739
|
-
|
|
8740
|
-
|
|
8741
|
-
|
|
8742
|
-
|
|
8743
|
-
|
|
8744
|
-
|
|
8745
|
-
|
|
8746
|
-
|
|
8747
|
-
|
|
8748
|
-
node.status = "
|
|
8749
|
-
|
|
8750
|
-
|
|
8751
|
-
|
|
8752
|
-
duration: 0,
|
|
8753
|
-
tokensUsed: 0,
|
|
8754
|
-
cost: 0
|
|
8755
|
-
};
|
|
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);
|
|
10230
|
+
await this.sleep(delay);
|
|
10231
|
+
node.status = "pending";
|
|
10232
|
+
this.notifyStateChange();
|
|
10233
|
+
await this.spawnAndExecute(node);
|
|
10234
|
+
return;
|
|
8756
10235
|
}
|
|
8757
|
-
|
|
8758
|
-
|
|
8759
|
-
|
|
8760
|
-
|
|
8761
|
-
|
|
8762
|
-
|
|
8763
|
-
|
|
8764
|
-
|
|
8765
|
-
role: oldAgent.role,
|
|
8766
|
-
task: node.task,
|
|
8767
|
-
model: oldAgent.model.model
|
|
8768
|
-
});
|
|
8769
|
-
if (this.config.selfHealing.contextTransfer) {
|
|
8770
|
-
await this.transferContext(oldAgent, newAgent);
|
|
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;
|
|
8771
10244
|
}
|
|
8772
|
-
|
|
8773
|
-
|
|
8774
|
-
|
|
8775
|
-
|
|
8776
|
-
|
|
8777
|
-
|
|
8778
|
-
|
|
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";
|
|
10250
|
+
this.notifyStateChange();
|
|
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 });
|
|
10259
|
+
}
|
|
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();
|
|
8779
10270
|
}
|
|
8780
10271
|
async spawnAgent(config) {
|
|
10272
|
+
if (!this.ctx) {
|
|
10273
|
+
throw new Error("Orchestrator not initialized");
|
|
10274
|
+
}
|
|
10275
|
+
if (this.budgetExceeded) {
|
|
10276
|
+
throw new Error("Budget exceeded — cannot spawn new agents");
|
|
10277
|
+
}
|
|
8781
10278
|
const agentId = `agent-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
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}`;
|
|
10285
|
+
const session = await this.ctx.session.create({
|
|
10286
|
+
title,
|
|
10287
|
+
agent: "build",
|
|
10288
|
+
model: modelName ? { providerID: provider, id: modelName } : undefined
|
|
10289
|
+
});
|
|
8782
10290
|
const agent = {
|
|
8783
10291
|
id: agentId,
|
|
8784
|
-
name:
|
|
10292
|
+
name: title,
|
|
8785
10293
|
role: config.role,
|
|
8786
|
-
status: "
|
|
10294
|
+
status: "idle",
|
|
8787
10295
|
model: {
|
|
8788
|
-
provider
|
|
8789
|
-
model:
|
|
10296
|
+
provider,
|
|
10297
|
+
model: modelName || "default",
|
|
8790
10298
|
estimatedCost: 0,
|
|
8791
10299
|
estimatedQuality: 0.5,
|
|
8792
|
-
reasoning:
|
|
10300
|
+
reasoning: `Configured for ${config.role}`
|
|
8793
10301
|
},
|
|
8794
10302
|
spawnedAt: new Date,
|
|
8795
10303
|
lastActivity: new Date,
|
|
@@ -8800,12 +10308,17 @@ class NexusOrchestrator {
|
|
|
8800
10308
|
totalCost: 0,
|
|
8801
10309
|
averageResponseTime: 0,
|
|
8802
10310
|
errorRate: 0
|
|
8803
|
-
}
|
|
10311
|
+
},
|
|
10312
|
+
sessionID: session.id
|
|
8804
10313
|
};
|
|
8805
10314
|
this.agents.set(agentId, agent);
|
|
8806
10315
|
this.emit("agent:spawned", agent);
|
|
8807
|
-
|
|
8808
|
-
|
|
10316
|
+
this.notifyStateChange();
|
|
10317
|
+
this.enforceAgentLimit();
|
|
10318
|
+
const monitor = this.getOrCreateHealthMonitor();
|
|
10319
|
+
if (!monitor.isActive()) {
|
|
10320
|
+
monitor.start(() => Array.from(this.agents.values()));
|
|
10321
|
+
}
|
|
8809
10322
|
return agent;
|
|
8810
10323
|
}
|
|
8811
10324
|
async terminateAgent(agentId) {
|
|
@@ -8814,76 +10327,41 @@ class NexusOrchestrator {
|
|
|
8814
10327
|
agent.status = "terminated";
|
|
8815
10328
|
this.agents.delete(agentId);
|
|
8816
10329
|
this.emit("agent:terminated", agent);
|
|
10330
|
+
this.notifyStateChange();
|
|
10331
|
+
if (this._healthMonitor && this.agents.size === 0) {
|
|
10332
|
+
this._healthMonitor.stop();
|
|
10333
|
+
}
|
|
8817
10334
|
}
|
|
8818
10335
|
}
|
|
8819
10336
|
analyzeComplexity(task) {
|
|
8820
10337
|
const fileCount = task.files.include.length;
|
|
8821
|
-
const codeLines =
|
|
10338
|
+
const codeLines = task.files.include.length * 50;
|
|
8822
10339
|
const dependencyDepth = task.dependencies.length;
|
|
8823
|
-
|
|
8824
|
-
const riskLevel = this.estimateRiskLevel(task);
|
|
8825
|
-
const overall = Math.min(100, fileCount * 10 + codeLines / 10 + dependencyDepth * 15 + domainKnowledge * 20 + (riskLevel === "high" ? 30 : riskLevel === "medium" ? 15 : 0));
|
|
8826
|
-
return {
|
|
8827
|
-
overall,
|
|
8828
|
-
factors: {
|
|
8829
|
-
fileCount,
|
|
8830
|
-
codeLines,
|
|
8831
|
-
dependencyDepth,
|
|
8832
|
-
domainKnowledge,
|
|
8833
|
-
riskLevel
|
|
8834
|
-
}
|
|
8835
|
-
};
|
|
8836
|
-
}
|
|
8837
|
-
estimateCodeLines(task) {
|
|
8838
|
-
return task.files.include.length * 50;
|
|
8839
|
-
}
|
|
8840
|
-
estimateDomainKnowledge(task) {
|
|
10340
|
+
let domainKnowledge = 0;
|
|
8841
10341
|
const keywords = ["security", "auth", "payment", "crypto", "database"];
|
|
8842
|
-
let score = 0;
|
|
8843
10342
|
for (const keyword of keywords) {
|
|
8844
10343
|
if (task.description.toLowerCase().includes(keyword)) {
|
|
8845
|
-
|
|
10344
|
+
domainKnowledge += 20;
|
|
8846
10345
|
}
|
|
8847
10346
|
}
|
|
8848
|
-
|
|
8849
|
-
|
|
8850
|
-
estimateRiskLevel(task) {
|
|
10347
|
+
domainKnowledge = Math.min(100, domainKnowledge);
|
|
10348
|
+
let riskLevel = "low";
|
|
8851
10349
|
const highRiskKeywords = ["migration", "production", "security", "payment"];
|
|
8852
|
-
const mediumRiskKeywords = ["refactor", "update", "modify"];
|
|
8853
10350
|
for (const keyword of highRiskKeywords) {
|
|
8854
10351
|
if (task.description.toLowerCase().includes(keyword)) {
|
|
8855
|
-
|
|
8856
|
-
|
|
8857
|
-
}
|
|
8858
|
-
for (const keyword of mediumRiskKeywords) {
|
|
8859
|
-
if (task.description.toLowerCase().includes(keyword)) {
|
|
8860
|
-
return "medium";
|
|
10352
|
+
riskLevel = "high";
|
|
10353
|
+
break;
|
|
8861
10354
|
}
|
|
8862
10355
|
}
|
|
8863
|
-
|
|
8864
|
-
}
|
|
8865
|
-
selectModel(role, complexity) {
|
|
8866
|
-
const configModel = this.configManager.getModelForRole(role);
|
|
8867
|
-
const [provider, ...modelParts] = configModel.split("/");
|
|
8868
|
-
const model = modelParts.join("/");
|
|
8869
|
-
const budgetRemaining = this.budget.maxTotalCost - this.totalSpent;
|
|
8870
|
-
if (budgetRemaining < 1 && configModel !== "opencode/minimax-m2.5-free") {
|
|
8871
|
-
return {
|
|
8872
|
-
provider: "opencode",
|
|
8873
|
-
model: "minimax-m2.5-free",
|
|
8874
|
-
estimatedCost: 0,
|
|
8875
|
-
estimatedQuality: 0.5,
|
|
8876
|
-
reasoning: "Budget constrained, using free tier"
|
|
8877
|
-
};
|
|
8878
|
-
}
|
|
10356
|
+
const overall = Math.min(100, fileCount * 10 + codeLines / 10 + dependencyDepth * 15 + domainKnowledge * 20 + (riskLevel === "high" ? 30 : riskLevel === "medium" ? 15 : 0));
|
|
8879
10357
|
return {
|
|
8880
|
-
|
|
8881
|
-
|
|
8882
|
-
estimatedCost: this.estimateModelCost(model),
|
|
8883
|
-
estimatedQuality: this.estimateModelQuality(model),
|
|
8884
|
-
reasoning: `Configured model for ${role}`
|
|
10358
|
+
overall,
|
|
10359
|
+
factors: { fileCount, codeLines, dependencyDepth, domainKnowledge, riskLevel }
|
|
8885
10360
|
};
|
|
8886
10361
|
}
|
|
10362
|
+
selectModel(role, complexity) {
|
|
10363
|
+
return this.selectBestModel(role, complexity);
|
|
10364
|
+
}
|
|
8887
10365
|
estimateModelCost(model) {
|
|
8888
10366
|
const costs = {
|
|
8889
10367
|
"claude-sonnet-4-6": 0.15,
|
|
@@ -8908,21 +10386,86 @@ class NexusOrchestrator {
|
|
|
8908
10386
|
};
|
|
8909
10387
|
return quality[model] || 0.6;
|
|
8910
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
|
+
}
|
|
8911
10448
|
trackCost(agentId, model, cost, tokens) {
|
|
8912
10449
|
this.totalSpent += cost;
|
|
8913
10450
|
const agentCost = this.costByAgent.get(agentId) || 0;
|
|
8914
10451
|
this.costByAgent.set(agentId, agentCost + cost);
|
|
8915
10452
|
const modelCost = this.costByModel.get(model) || 0;
|
|
8916
10453
|
this.costByModel.set(model, modelCost + cost);
|
|
10454
|
+
this.costHistory.push({ timestamp: Date.now(), cost, agentId });
|
|
8917
10455
|
this.checkBudget();
|
|
10456
|
+
this.notifyStateChange();
|
|
8918
10457
|
}
|
|
8919
10458
|
checkBudget() {
|
|
8920
10459
|
const remaining = this.budget.maxTotalCost - this.totalSpent;
|
|
8921
10460
|
const remainingPercent = remaining / this.budget.maxTotalCost;
|
|
8922
10461
|
if (remainingPercent <= this.config.budget.alertThreshold) {
|
|
8923
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
|
+
}
|
|
8924
10466
|
}
|
|
8925
|
-
if (this.budget.hardLimit && remaining <= 0) {
|
|
10467
|
+
if (this.budget.hardLimit && remaining <= 0 && !this.budgetExceeded) {
|
|
10468
|
+
this.budgetExceeded = true;
|
|
8926
10469
|
this.emit("budget:exceeded", { totalSpent: this.totalSpent });
|
|
8927
10470
|
this.pause();
|
|
8928
10471
|
}
|
|
@@ -8930,12 +10473,15 @@ class NexusOrchestrator {
|
|
|
8930
10473
|
publish(topic, message) {
|
|
8931
10474
|
const fullMessage = {
|
|
8932
10475
|
...message,
|
|
10476
|
+
topic: message.topic ?? topic,
|
|
8933
10477
|
id: `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
8934
10478
|
timestamp: new Date
|
|
8935
10479
|
};
|
|
8936
10480
|
this.messageQueue.push(fullMessage);
|
|
10481
|
+
this.messageStore.add(fullMessage);
|
|
8937
10482
|
const handlers = this.subscribers.get(topic) || [];
|
|
8938
10483
|
handlers.forEach((handler) => handler(fullMessage));
|
|
10484
|
+
this.messageRouter.route(fullMessage);
|
|
8939
10485
|
}
|
|
8940
10486
|
subscribe(topic, handler) {
|
|
8941
10487
|
const handlers = this.subscribers.get(topic) || [];
|
|
@@ -8943,130 +10489,128 @@ class NexusOrchestrator {
|
|
|
8943
10489
|
this.subscribers.set(topic, handlers);
|
|
8944
10490
|
return () => {
|
|
8945
10491
|
const idx = handlers.indexOf(handler);
|
|
8946
|
-
if (idx > -1)
|
|
10492
|
+
if (idx > -1)
|
|
8947
10493
|
handlers.splice(idx, 1);
|
|
8948
|
-
}
|
|
8949
|
-
};
|
|
8950
|
-
}
|
|
8951
|
-
send(agentId, message) {
|
|
8952
|
-
const fullMessage = {
|
|
8953
|
-
...message,
|
|
8954
|
-
id: `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
8955
|
-
to: agentId,
|
|
8956
|
-
timestamp: new Date
|
|
8957
10494
|
};
|
|
8958
|
-
this.messageQueue.push(fullMessage);
|
|
8959
|
-
this.emit("message:sent", fullMessage);
|
|
8960
10495
|
}
|
|
8961
10496
|
setMemory(scope, key, value, author) {
|
|
8962
|
-
const entry = {
|
|
8963
|
-
id: `mem-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
10497
|
+
const entry = this.memoryStore.set({
|
|
8964
10498
|
key,
|
|
8965
10499
|
value,
|
|
8966
10500
|
scope,
|
|
8967
10501
|
author,
|
|
8968
|
-
timestamp: new Date,
|
|
8969
10502
|
confidence: 1,
|
|
8970
10503
|
tags: []
|
|
8971
|
-
};
|
|
8972
|
-
this.memory.set(`${scope}:${key}`, entry);
|
|
10504
|
+
});
|
|
8973
10505
|
this.emit("memory:set", entry);
|
|
8974
10506
|
}
|
|
8975
10507
|
getMemory(scope, key) {
|
|
8976
|
-
return this.
|
|
8977
|
-
}
|
|
8978
|
-
searchMemory(query, scope) {
|
|
8979
|
-
const results = [];
|
|
8980
|
-
this.memory.forEach((entry) => {
|
|
8981
|
-
if (scope && entry.scope !== scope)
|
|
8982
|
-
return;
|
|
8983
|
-
if (entry.key.includes(query) || JSON.stringify(entry.value).includes(query)) {
|
|
8984
|
-
results.push(entry);
|
|
8985
|
-
}
|
|
8986
|
-
});
|
|
8987
|
-
return results;
|
|
10508
|
+
return this.memoryStore.get(key, scope) ?? undefined;
|
|
8988
10509
|
}
|
|
8989
10510
|
getStatus(detailed) {
|
|
8990
|
-
const
|
|
8991
|
-
|
|
8992
|
-
|
|
8993
|
-
|
|
8994
|
-
|
|
8995
|
-
|
|
8996
|
-
|
|
8997
|
-
|
|
8998
|
-
|
|
8999
|
-
|
|
9000
|
-
|
|
9001
|
-
|
|
9002
|
-
costByAgent: Object.fromEntries(this.costByAgent),
|
|
9003
|
-
costByModel: Object.fromEntries(this.costByModel)
|
|
9004
|
-
}, null, 2);
|
|
9005
|
-
}
|
|
9006
|
-
return JSON.stringify(status, null, 2);
|
|
10511
|
+
const state = this.getState();
|
|
10512
|
+
if (detailed)
|
|
10513
|
+
return JSON.stringify({ ...state, budgetExceeded: this.budgetExceeded }, null, 2);
|
|
10514
|
+
return JSON.stringify({
|
|
10515
|
+
running: state.running,
|
|
10516
|
+
paused: state.paused,
|
|
10517
|
+
budgetExceeded: this.budgetExceeded,
|
|
10518
|
+
agents: state.agents.length,
|
|
10519
|
+
tasks: state.tasks.length,
|
|
10520
|
+
totalCost: state.totalSpent,
|
|
10521
|
+
budgetRemaining: state.budgetRemaining
|
|
10522
|
+
}, null, 2);
|
|
9007
10523
|
}
|
|
9008
10524
|
listAgents(filter) {
|
|
9009
|
-
const
|
|
9010
|
-
const filtered = filter ? agents.filter((a) => a.status === filter) : agents;
|
|
10525
|
+
const state = this.getState();
|
|
10526
|
+
const filtered = filter ? state.agents.filter((a) => a.status === filter) : state.agents;
|
|
9011
10527
|
return JSON.stringify(filtered, null, 2);
|
|
9012
10528
|
}
|
|
9013
10529
|
getCostReport() {
|
|
9014
|
-
const
|
|
9015
|
-
totalSpent: this.totalSpent,
|
|
9016
|
-
budgetRemaining: this.budget.maxTotalCost - this.totalSpent,
|
|
9017
|
-
byAgent: new Map,
|
|
9018
|
-
byModel: new Map,
|
|
9019
|
-
timeline: []
|
|
9020
|
-
};
|
|
9021
|
-
this.agents.forEach((agent) => {
|
|
9022
|
-
report.byAgent.set(agent.id, {
|
|
9023
|
-
agentId: agent.id,
|
|
9024
|
-
agentName: agent.name,
|
|
9025
|
-
totalCost: agent.metrics.totalCost,
|
|
9026
|
-
tokenCount: agent.metrics.totalTokens,
|
|
9027
|
-
taskCount: agent.metrics.tasksCompleted
|
|
9028
|
-
});
|
|
9029
|
-
});
|
|
10530
|
+
const state = this.getState();
|
|
9030
10531
|
return JSON.stringify({
|
|
9031
|
-
|
|
9032
|
-
|
|
9033
|
-
|
|
10532
|
+
totalSpent: state.totalSpent,
|
|
10533
|
+
budgetRemaining: state.budgetRemaining,
|
|
10534
|
+
byAgent: Object.fromEntries(this.costByAgent),
|
|
10535
|
+
byModel: Object.fromEntries(this.costByModel)
|
|
9034
10536
|
}, null, 2);
|
|
9035
10537
|
}
|
|
9036
10538
|
pause() {
|
|
9037
10539
|
this.paused = true;
|
|
9038
10540
|
this.emit("orchestrator:paused", {});
|
|
10541
|
+
this.notifyStateChange();
|
|
9039
10542
|
}
|
|
9040
10543
|
resume() {
|
|
9041
10544
|
this.paused = false;
|
|
9042
10545
|
this.emit("orchestrator:resumed", {});
|
|
10546
|
+
this.notifyStateChange();
|
|
9043
10547
|
}
|
|
9044
|
-
|
|
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
|
+
}
|
|
9045
10567
|
this.agents.forEach((agent) => {
|
|
9046
10568
|
agent.status = "terminated";
|
|
9047
10569
|
});
|
|
9048
10570
|
this.agents.clear();
|
|
9049
10571
|
this.running = false;
|
|
9050
10572
|
this.emit("orchestrator:shutdown", {});
|
|
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;
|
|
9051
10603
|
}
|
|
9052
10604
|
collectResults() {
|
|
9053
10605
|
const results = [];
|
|
9054
10606
|
if (this.dag) {
|
|
9055
10607
|
this.dag.nodes.forEach((node) => {
|
|
9056
|
-
if (node.result)
|
|
10608
|
+
if (node.result)
|
|
9057
10609
|
results.push(node.result);
|
|
9058
|
-
}
|
|
9059
10610
|
});
|
|
9060
10611
|
}
|
|
9061
10612
|
return results;
|
|
9062
10613
|
}
|
|
9063
|
-
getAgentsByStatus() {
|
|
9064
|
-
const counts = {};
|
|
9065
|
-
this.agents.forEach((agent) => {
|
|
9066
|
-
counts[agent.status] = (counts[agent.status] || 0) + 1;
|
|
9067
|
-
});
|
|
9068
|
-
return counts;
|
|
9069
|
-
}
|
|
9070
10614
|
sleep(ms) {
|
|
9071
10615
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
9072
10616
|
}
|
|
@@ -9076,9 +10620,8 @@ class NexusOrchestrator {
|
|
|
9076
10620
|
this.eventHandlers.set(event, handlers);
|
|
9077
10621
|
return () => {
|
|
9078
10622
|
const idx = handlers.indexOf(handler);
|
|
9079
|
-
if (idx > -1)
|
|
10623
|
+
if (idx > -1)
|
|
9080
10624
|
handlers.splice(idx, 1);
|
|
9081
|
-
}
|
|
9082
10625
|
};
|
|
9083
10626
|
}
|
|
9084
10627
|
emit(event, data) {
|
|
@@ -9090,31 +10633,113 @@ class NexusOrchestrator {
|
|
|
9090
10633
|
const command = parts[1];
|
|
9091
10634
|
switch (command) {
|
|
9092
10635
|
case "status":
|
|
9093
|
-
|
|
9094
|
-
break;
|
|
10636
|
+
return this.getStatus(true);
|
|
9095
10637
|
case "agents":
|
|
9096
|
-
|
|
9097
|
-
break;
|
|
10638
|
+
return this.listAgents(parts[2]);
|
|
9098
10639
|
case "costs":
|
|
9099
|
-
|
|
9100
|
-
break;
|
|
10640
|
+
return this.getCostReport();
|
|
9101
10641
|
case "pause":
|
|
9102
10642
|
this.pause();
|
|
9103
|
-
|
|
10643
|
+
return "Orchestrator paused";
|
|
9104
10644
|
case "resume":
|
|
9105
10645
|
this.resume();
|
|
9106
|
-
|
|
10646
|
+
return "Orchestrator resumed";
|
|
10647
|
+
case "dashboard":
|
|
10648
|
+
return JSON.stringify(this.getState(), null, 2);
|
|
9107
10649
|
default:
|
|
9108
|
-
|
|
10650
|
+
return "Unknown command. Available: status, agents, costs, pause, resume, dashboard";
|
|
9109
10651
|
}
|
|
9110
10652
|
}
|
|
9111
10653
|
}
|
|
9112
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
|
+
|
|
9113
10733
|
// src/index.ts
|
|
9114
10734
|
var src_default = define({
|
|
9115
10735
|
id: "nexus",
|
|
9116
10736
|
async setup(ctx) {
|
|
9117
10737
|
const orchestrator = new NexusOrchestrator;
|
|
10738
|
+
await orchestrator.initialize(ctx, () => {
|
|
10739
|
+
const state = orchestrator.getState();
|
|
10740
|
+
ctx.storage.set("orchestrator-state", JSON.parse(JSON.stringify(state))).catch(() => {});
|
|
10741
|
+
});
|
|
10742
|
+
await ctx.storage.set("orchestrator-state", JSON.parse(JSON.stringify(orchestrator.getState())));
|
|
9118
10743
|
await ctx.tool.transform((editor) => {
|
|
9119
10744
|
editor.namespace({
|
|
9120
10745
|
name: "nexus",
|
|
@@ -9132,7 +10757,9 @@ var src_default = define({
|
|
|
9132
10757
|
},
|
|
9133
10758
|
execute: async (input) => {
|
|
9134
10759
|
const { detailed } = input;
|
|
9135
|
-
|
|
10760
|
+
const status = orchestrator.getStatus(detailed);
|
|
10761
|
+
await ctx.storage.set("orchestrator-state", JSON.parse(JSON.stringify(orchestrator.getState())));
|
|
10762
|
+
return { content: status };
|
|
9136
10763
|
}
|
|
9137
10764
|
});
|
|
9138
10765
|
editor.add({
|
|
@@ -9162,10 +10789,231 @@ var src_default = define({
|
|
|
9162
10789
|
return { content: orchestrator.getCostReport() };
|
|
9163
10790
|
}
|
|
9164
10791
|
});
|
|
10792
|
+
editor.add({
|
|
10793
|
+
name: "dashboard",
|
|
10794
|
+
description: "Get full orchestrator state for dashboard display",
|
|
10795
|
+
input: {
|
|
10796
|
+
type: "object",
|
|
10797
|
+
properties: {},
|
|
10798
|
+
additionalProperties: false
|
|
10799
|
+
},
|
|
10800
|
+
execute: async () => {
|
|
10801
|
+
const state = orchestrator.getState();
|
|
10802
|
+
await ctx.storage.set("orchestrator-state", JSON.parse(JSON.stringify(state)));
|
|
10803
|
+
return { content: JSON.stringify(state, null, 2) };
|
|
10804
|
+
}
|
|
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
|
+
});
|
|
10917
|
+
editor.add({
|
|
10918
|
+
name: "spawn",
|
|
10919
|
+
description: "Spawn a sub-agent for a task",
|
|
10920
|
+
input: {
|
|
10921
|
+
type: "object",
|
|
10922
|
+
properties: {
|
|
10923
|
+
role: { type: "string", description: "Agent role (architect, coder, reviewer, tester, explorer, documenter)" },
|
|
10924
|
+
task: { type: "string", description: "Task description" },
|
|
10925
|
+
model: { type: "string", description: "Model override (optional)" }
|
|
10926
|
+
},
|
|
10927
|
+
required: ["role", "task"],
|
|
10928
|
+
additionalProperties: false
|
|
10929
|
+
},
|
|
10930
|
+
execute: async (input) => {
|
|
10931
|
+
const { role, task, model } = input;
|
|
10932
|
+
try {
|
|
10933
|
+
const agent = await orchestrator.spawnAgent({ role, model });
|
|
10934
|
+
await orchestrator.ctx.session.prompt({
|
|
10935
|
+
sessionID: agent.sessionID,
|
|
10936
|
+
text: task
|
|
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);
|
|
10951
|
+
await ctx.storage.set("orchestrator-state", JSON.parse(JSON.stringify(orchestrator.getState())));
|
|
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 };
|
|
10962
|
+
} catch (error) {
|
|
10963
|
+
return { content: `Failed to spawn agent: ${error.message}` };
|
|
10964
|
+
}
|
|
10965
|
+
}
|
|
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
|
+
});
|
|
9165
11012
|
});
|
|
9166
11013
|
await ctx.session.hook("prompt", (event) => {
|
|
9167
|
-
if (event.prompt.text.
|
|
9168
|
-
orchestrator.handleCommand(event.prompt.text);
|
|
11014
|
+
if (event.prompt.text.startsWith("/nexus")) {
|
|
11015
|
+
const result = orchestrator.handleCommand(event.prompt.text);
|
|
11016
|
+
event.metadata = { ...event.metadata, nexusResult: result };
|
|
9169
11017
|
}
|
|
9170
11018
|
});
|
|
9171
11019
|
return () => {
|
|
@@ -9175,7 +11023,21 @@ var src_default = define({
|
|
|
9175
11023
|
});
|
|
9176
11024
|
export {
|
|
9177
11025
|
DEFAULT_CONFIG,
|
|
11026
|
+
HealthMonitor,
|
|
11027
|
+
LearningModule,
|
|
11028
|
+
MessageRouter,
|
|
11029
|
+
MessageStore,
|
|
11030
|
+
ModuleRegistry,
|
|
9178
11031
|
NexusConfigManager,
|
|
9179
11032
|
NexusOrchestrator,
|
|
9180
|
-
|
|
11033
|
+
PRESETS,
|
|
11034
|
+
PersistentMemoryStore,
|
|
11035
|
+
SecurityScanner,
|
|
11036
|
+
StateBroadcaster,
|
|
11037
|
+
TEMPLATES,
|
|
11038
|
+
src_default as default,
|
|
11039
|
+
detectCycles,
|
|
11040
|
+
getTemplate,
|
|
11041
|
+
instantiateTemplate,
|
|
11042
|
+
listTemplates
|
|
9181
11043
|
};
|