agent-enderun 1.1.10 ā 1.10.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 +294 -19
- package/bin/cli.js +7 -0
- package/bin/enderun-dashboard.js +16 -857
- package/bin/validate-agent-army.js +12 -12
- package/dist/framework-mcp/src/index.js +14 -2
- package/dist/framework-mcp/src/index.js.map +1 -1
- package/dist/framework-mcp/src/tools/file_system/batch_surgical_edit.js +2 -1
- package/dist/framework-mcp/src/tools/file_system/batch_surgical_edit.js.map +1 -1
- package/dist/framework-mcp/src/tools/file_system/replace_text.js +2 -1
- package/dist/framework-mcp/src/tools/file_system/replace_text.js.map +1 -1
- package/dist/framework-mcp/src/tools/file_system/write_file.js +3 -3
- package/dist/framework-mcp/src/tools/file_system/write_file.js.map +1 -1
- package/dist/framework-mcp/src/utils/fs.d.ts +16 -0
- package/dist/framework-mcp/src/utils/fs.js +45 -0
- package/dist/framework-mcp/src/utils/fs.js.map +1 -0
- package/dist/framework-mcp/src/utils/metrics.js.map +1 -1
- package/dist/src/cli/adapters/core.js.map +1 -1
- package/dist/src/cli/adapters/scaffold.js +4 -3
- package/dist/src/cli/adapters/scaffold.js.map +1 -1
- package/dist/src/cli/commands/app.js +20 -5
- package/dist/src/cli/commands/app.js.map +1 -1
- package/dist/src/cli/commands/approve.d.ts +1 -1
- package/dist/src/cli/commands/approve.js +7 -7
- package/dist/src/cli/commands/approve.js.map +1 -1
- package/dist/src/cli/commands/check.d.ts +1 -1
- package/dist/src/cli/commands/check.js +35 -4
- package/dist/src/cli/commands/check.js.map +1 -1
- package/dist/src/cli/commands/compliance.d.ts +1 -1
- package/dist/src/cli/commands/compliance.js +1 -1
- package/dist/src/cli/commands/compliance.js.map +1 -1
- package/dist/src/cli/commands/contract.d.ts +2 -2
- package/dist/src/cli/commands/contract.js +8 -14
- package/dist/src/cli/commands/contract.js.map +1 -1
- package/dist/src/cli/commands/dashboard/data-service.d.ts +33 -0
- package/dist/src/cli/commands/dashboard/data-service.js +120 -0
- package/dist/src/cli/commands/dashboard/data-service.js.map +1 -0
- package/dist/src/cli/commands/dashboard/html-template.d.ts +4 -0
- package/dist/src/cli/commands/dashboard/html-template.js +697 -0
- package/dist/src/cli/commands/dashboard/html-template.js.map +1 -0
- package/dist/src/cli/commands/dashboard/server.d.ts +2 -0
- package/dist/src/cli/commands/dashboard/server.js +38 -0
- package/dist/src/cli/commands/dashboard/server.js.map +1 -0
- package/dist/src/cli/commands/dashboard.d.ts +4 -0
- package/dist/src/cli/commands/dashboard.js +10 -0
- package/dist/src/cli/commands/dashboard.js.map +1 -0
- package/dist/src/cli/commands/explorer.d.ts +2 -2
- package/dist/src/cli/commands/explorer.js +2 -2
- package/dist/src/cli/commands/explorer.js.map +1 -1
- package/dist/src/cli/commands/git.d.ts +2 -2
- package/dist/src/cli/commands/git.js +2 -2
- package/dist/src/cli/commands/git.js.map +1 -1
- package/dist/src/cli/commands/init/scaffold-core.d.ts +4 -0
- package/dist/src/cli/commands/init/scaffold-core.js +65 -0
- package/dist/src/cli/commands/init/scaffold-core.js.map +1 -0
- package/dist/src/cli/commands/init/scaffold-ops.d.ts +1 -0
- package/dist/src/cli/commands/init/scaffold-ops.js +83 -0
- package/dist/src/cli/commands/init/scaffold-ops.js.map +1 -0
- package/dist/src/cli/commands/init/scaffold-standards.d.ts +2 -0
- package/dist/src/cli/commands/init/scaffold-standards.js +58 -0
- package/dist/src/cli/commands/init/scaffold-standards.js.map +1 -0
- package/dist/src/cli/commands/init.js +14 -275
- package/dist/src/cli/commands/init.js.map +1 -1
- package/dist/src/cli/commands/knowledge.d.ts +2 -2
- package/dist/src/cli/commands/knowledge.js +4 -3
- package/dist/src/cli/commands/knowledge.js.map +1 -1
- package/dist/src/cli/commands/lint.d.ts +1 -1
- package/dist/src/cli/commands/lint.js +2 -3
- package/dist/src/cli/commands/lint.js.map +1 -1
- package/dist/src/cli/commands/log.d.ts +1 -1
- package/dist/src/cli/commands/log.js +3 -2
- package/dist/src/cli/commands/log.js.map +1 -1
- package/dist/src/cli/commands/memory.d.ts +1 -1
- package/dist/src/cli/commands/memory.js +1 -1
- package/dist/src/cli/commands/memory.js.map +1 -1
- package/dist/src/cli/commands/orchestrate.js +169 -140
- package/dist/src/cli/commands/orchestrate.js.map +1 -1
- package/dist/src/cli/commands/plan.d.ts +1 -1
- package/dist/src/cli/commands/plan.js +1 -1
- package/dist/src/cli/commands/plan.js.map +1 -1
- package/dist/src/cli/commands/script.d.ts +1 -1
- package/dist/src/cli/commands/script.js +1 -1
- package/dist/src/cli/commands/script.js.map +1 -1
- package/dist/src/cli/commands/security.d.ts +1 -1
- package/dist/src/cli/commands/security.js +1 -1
- package/dist/src/cli/commands/security.js.map +1 -1
- package/dist/src/cli/commands/status.d.ts +1 -1
- package/dist/src/cli/commands/status.js +1 -1
- package/dist/src/cli/commands/status.js.map +1 -1
- package/dist/src/cli/commands/trace.d.ts +1 -1
- package/dist/src/cli/commands/trace.js +1 -1
- package/dist/src/cli/commands/trace.js.map +1 -1
- package/dist/src/cli/index.js +36 -21
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/cli/shims.js +6 -6
- package/dist/src/cli/utils/app-backend.js +7 -0
- package/dist/src/cli/utils/app-backend.js.map +1 -1
- package/dist/src/cli/utils/app-docs.js +23 -5
- package/dist/src/cli/utils/app-docs.js.map +1 -1
- package/dist/src/cli/utils/app-frontend.js +5 -0
- package/dist/src/cli/utils/app-frontend.js.map +1 -1
- package/dist/src/cli/utils/app-inferrer.d.ts +1 -0
- package/dist/src/cli/utils/app-inferrer.js +12 -1
- package/dist/src/cli/utils/app-inferrer.js.map +1 -1
- package/dist/src/cli/utils/app-mobile.d.ts +2 -0
- package/dist/src/cli/utils/app-mobile.js +106 -0
- package/dist/src/cli/utils/app-mobile.js.map +1 -0
- package/dist/src/cli/utils/app-types.js +8 -4
- package/dist/src/cli/utils/app-types.js.map +1 -1
- package/dist/src/cli/utils/app.d.ts +1 -0
- package/dist/src/cli/utils/app.js +1 -0
- package/dist/src/cli/utils/app.js.map +1 -1
- package/dist/src/cli/utils/claude.js +2 -1
- package/dist/src/cli/utils/claude.js.map +1 -1
- package/dist/src/cli/utils/compliance.js +37 -6
- package/dist/src/cli/utils/compliance.js.map +1 -1
- package/dist/src/cli/utils/config-schema.d.ts +3 -3
- package/dist/src/cli/utils/config-schema.js +3 -3
- package/dist/src/cli/utils/config-schema.js.map +1 -1
- package/dist/src/cli/utils/fs.d.ts +2 -2
- package/dist/src/cli/utils/fs.js +9 -13
- package/dist/src/cli/utils/fs.js.map +1 -1
- package/dist/src/cli/utils/memory.d.ts +9 -2
- package/dist/src/cli/utils/memory.js +87 -16
- package/dist/src/cli/utils/memory.js.map +1 -1
- package/dist/src/cli/utils/pkg.js +14 -13
- package/dist/src/cli/utils/pkg.js.map +1 -1
- package/dist/src/cli/utils/string.js +1 -1
- package/dist/src/cli/utils/string.js.map +1 -1
- package/dist/src/cli/utils/ui.d.ts +1 -1
- package/dist/src/cli/utils/ui.js +1 -1
- package/dist/src/modules/adapters/shared.js +7 -7
- package/dist/src/modules/adapters/shared.js.map +1 -1
- package/dist/src/modules/agents/definitions.d.ts +3 -34
- package/dist/src/modules/agents/definitions.js +35 -511
- package/dist/src/modules/agents/definitions.js.map +1 -1
- package/dist/src/modules/agents/registry/analyst.d.ts +2 -0
- package/dist/src/modules/agents/registry/analyst.js +36 -0
- package/dist/src/modules/agents/registry/analyst.js.map +1 -0
- package/dist/src/modules/agents/registry/architect.d.ts +2 -0
- package/dist/src/modules/agents/registry/architect.js +42 -0
- package/dist/src/modules/agents/registry/architect.js.map +1 -0
- package/dist/src/modules/agents/registry/backend.d.ts +2 -0
- package/dist/src/modules/agents/registry/backend.js +43 -0
- package/dist/src/modules/agents/registry/backend.js.map +1 -0
- package/dist/src/modules/agents/registry/database.d.ts +2 -0
- package/dist/src/modules/agents/registry/database.js +38 -0
- package/dist/src/modules/agents/registry/database.js.map +1 -0
- package/dist/src/modules/agents/registry/devops.d.ts +2 -0
- package/dist/src/modules/agents/registry/devops.js +38 -0
- package/dist/src/modules/agents/registry/devops.js.map +1 -0
- package/dist/src/modules/agents/registry/explorer.d.ts +2 -0
- package/dist/src/modules/agents/registry/explorer.js +36 -0
- package/dist/src/modules/agents/registry/explorer.js.map +1 -0
- package/dist/src/modules/agents/registry/frontend.d.ts +2 -0
- package/dist/src/modules/agents/registry/frontend.js +44 -0
- package/dist/src/modules/agents/registry/frontend.js.map +1 -0
- package/dist/src/modules/agents/registry/git.d.ts +2 -0
- package/dist/src/modules/agents/registry/git.js +35 -0
- package/dist/src/modules/agents/registry/git.js.map +1 -0
- package/dist/src/modules/agents/registry/manager.d.ts +2 -0
- package/dist/src/modules/agents/registry/manager.js +49 -0
- package/dist/src/modules/agents/registry/manager.js.map +1 -0
- package/dist/src/modules/agents/registry/mobile.d.ts +2 -0
- package/dist/src/modules/agents/registry/mobile.js +40 -0
- package/dist/src/modules/agents/registry/mobile.js.map +1 -0
- package/dist/src/modules/agents/registry/native.d.ts +2 -0
- package/dist/src/modules/agents/registry/native.js +36 -0
- package/dist/src/modules/agents/registry/native.js.map +1 -0
- package/dist/src/modules/agents/registry/quality.d.ts +2 -0
- package/dist/src/modules/agents/registry/quality.js +41 -0
- package/dist/src/modules/agents/registry/quality.js.map +1 -0
- package/dist/src/modules/agents/registry/security.d.ts +2 -0
- package/dist/src/modules/agents/registry/security.js +37 -0
- package/dist/src/modules/agents/registry/security.js.map +1 -0
- package/dist/src/modules/agents/types.d.ts +31 -0
- package/dist/src/modules/agents/types.js +2 -0
- package/dist/src/modules/agents/types.js.map +1 -0
- package/dist/src/modules/skills/definitions.d.ts +1 -1
- package/dist/src/modules/skills/definitions.js +1 -1
- package/dist/src/shared/errors.d.ts +8 -6
- package/dist/src/shared/errors.js +13 -10
- package/dist/src/shared/errors.js.map +1 -1
- package/dist/src/shared/fs.d.ts +4 -0
- package/dist/src/shared/fs.js +17 -2
- package/dist/src/shared/fs.js.map +1 -1
- package/dist/src/shared/types.d.ts +19 -0
- package/dist/src/shared/types.js +13 -0
- package/dist/src/shared/types.js.map +1 -0
- package/package.json +3 -5
- package/src/cli/adapters/core.ts +0 -1
- package/src/cli/adapters/scaffold.ts +4 -3
- package/src/cli/commands/app.ts +23 -5
- package/src/cli/commands/approve.ts +15 -7
- package/src/cli/commands/check.ts +38 -5
- package/src/cli/commands/compliance.ts +1 -1
- package/src/cli/commands/contract.ts +20 -14
- package/src/cli/commands/dashboard/data-service.ts +119 -0
- package/src/cli/commands/dashboard/html-template.ts +696 -0
- package/src/cli/commands/dashboard/server.ts +43 -0
- package/src/cli/commands/dashboard.ts +11 -0
- package/src/cli/commands/explorer.ts +2 -2
- package/src/cli/commands/git.ts +2 -2
- package/src/cli/commands/init/scaffold-core.ts +71 -0
- package/src/cli/commands/init/scaffold-ops.ts +86 -0
- package/src/cli/commands/init/scaffold-standards.ts +59 -0
- package/src/cli/commands/init.ts +21 -292
- package/src/cli/commands/knowledge.ts +5 -3
- package/src/cli/commands/lint.ts +2 -4
- package/src/cli/commands/log.ts +4 -2
- package/src/cli/commands/memory.ts +1 -1
- package/src/cli/commands/orchestrate.ts +177 -142
- package/src/cli/commands/plan.ts +1 -1
- package/src/cli/commands/script.ts +1 -1
- package/src/cli/commands/security.ts +1 -1
- package/src/cli/commands/status.ts +1 -1
- package/src/cli/commands/trace.ts +1 -1
- package/src/cli/index.ts +37 -21
- package/src/cli/shims.ts +6 -6
- package/src/cli/utils/app-backend.ts +8 -0
- package/src/cli/utils/app-docs.ts +23 -5
- package/src/cli/utils/app-frontend.ts +6 -0
- package/src/cli/utils/app-inferrer.ts +11 -1
- package/src/cli/utils/app-mobile.ts +113 -0
- package/src/cli/utils/app-types.ts +9 -4
- package/src/cli/utils/app.ts +1 -0
- package/src/cli/utils/claude.ts +2 -1
- package/src/cli/utils/compliance.ts +38 -7
- package/src/cli/utils/config-schema.ts +4 -4
- package/src/cli/utils/fs.ts +9 -14
- package/src/cli/utils/memory.ts +103 -20
- package/src/cli/utils/pkg.ts +14 -13
- package/src/cli/utils/string.ts +1 -1
- package/src/cli/utils/ui.ts +1 -1
- package/src/modules/adapters/shared.ts +7 -7
- package/src/modules/agents/definitions.ts +39 -598
- package/src/modules/agents/registry/analyst.ts +40 -0
- package/src/modules/agents/registry/architect.ts +46 -0
- package/src/modules/agents/registry/backend.ts +47 -0
- package/src/modules/agents/registry/database.ts +42 -0
- package/src/modules/agents/registry/devops.ts +42 -0
- package/src/modules/agents/registry/explorer.ts +40 -0
- package/src/modules/agents/registry/frontend.ts +48 -0
- package/src/modules/agents/registry/git.ts +39 -0
- package/src/modules/agents/registry/manager.ts +53 -0
- package/src/modules/agents/registry/mobile.ts +44 -0
- package/src/modules/agents/registry/native.ts +40 -0
- package/src/modules/agents/registry/quality.ts +45 -0
- package/src/modules/agents/registry/security.ts +41 -0
- package/src/modules/agents/types.ts +37 -0
- package/src/modules/skills/definitions.ts +1 -1
- package/src/shared/errors.ts +13 -10
- package/src/shared/fs.ts +17 -2
- package/src/shared/types.ts +20 -0
- package/bin/utils/docgen.ts +0 -24
|
@@ -3,9 +3,11 @@ import path from "path";
|
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { getFrameworkDir, updateDocumentStore, readStatus } from "../utils/memory.js";
|
|
5
5
|
import { UI } from "../utils/ui.js";
|
|
6
|
+
import { writeTextFile, appendFile } from "../utils/fs.js";
|
|
6
7
|
import { z } from "zod";
|
|
7
8
|
import { logger } from "../../shared/logger.js";
|
|
8
9
|
import { TaskRequestSchema } from "../../contracts/tasks.js";
|
|
10
|
+
import { sleep } from "../utils/time.js";
|
|
9
11
|
|
|
10
12
|
export const HermesMessageSchema = z.object({
|
|
11
13
|
timestamp: z.string(),
|
|
@@ -22,174 +24,191 @@ export const HermesMessageSchema = z.object({
|
|
|
22
24
|
|
|
23
25
|
export type HermesMessage = z.infer<typeof HermesMessageSchema>;
|
|
24
26
|
|
|
27
|
+
// Global status cache for in-memory optimization
|
|
28
|
+
let agentStatusCache: Record<string, { state: string; task: string; lastUpdated?: string }> = {};
|
|
29
|
+
let isLooping = false;
|
|
30
|
+
|
|
25
31
|
export async function orchestrateCommand() {
|
|
32
|
+
UI.intent("Hermes Message Broker", "Starting the live agent orchestration loop...");
|
|
33
|
+
|
|
26
34
|
const frameworkDir = getFrameworkDir();
|
|
27
35
|
const messagesDir = path.join(frameworkDir, "messages");
|
|
28
36
|
|
|
29
|
-
//
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
// Graceful Shutdown Handling
|
|
38
|
+
const shutdown = async () => {
|
|
39
|
+
if (!isLooping) return;
|
|
40
|
+
isLooping = false;
|
|
41
|
+
UI.warning("\nš Shutdown signal received. Cleaning up Hermes loop...");
|
|
42
|
+
// Additional cleanup like releasing global locks could go here
|
|
43
|
+
process.exit(0);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
process.on("SIGINT", shutdown);
|
|
47
|
+
process.on("SIGTERM", shutdown);
|
|
48
|
+
|
|
49
|
+
isLooping = true;
|
|
50
|
+
|
|
51
|
+
while (isLooping) {
|
|
52
|
+
// Initial load from disk to sync the cache
|
|
53
|
+
if (Object.keys(agentStatusCache).length === 0) {
|
|
54
|
+
const diskStatus = readStatus();
|
|
55
|
+
agentStatusCache = diskStatus || {};
|
|
41
56
|
}
|
|
42
|
-
}
|
|
43
|
-
if (updatedStatus) {
|
|
44
|
-
updateDocumentStore("status", agentStatuses);
|
|
45
|
-
}
|
|
46
57
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
58
|
+
// Check for agent timeouts (e.g. 30 minutes in EXECUTING state)
|
|
59
|
+
const agentStatuses = readStatus();
|
|
60
|
+
let updatedStatus = false;
|
|
61
|
+
for (const [agentName, info] of Object.entries(agentStatuses)) {
|
|
62
|
+
const { state, lastUpdated } = info as { state: string; task: string; lastUpdated?: string };
|
|
63
|
+
if (state === "EXECUTING" && lastUpdated) {
|
|
64
|
+
const lastUpdatedTime = Date.parse(lastUpdated);
|
|
65
|
+
if (!isNaN(lastUpdatedTime) && Date.now() - lastUpdatedTime > 30 * 60 * 1000) {
|
|
66
|
+
UI.error(`ā ļø Agent ${agentName} has TIMED OUT (in EXECUTING state for >30m). Transitioning to BLOCKED.`);
|
|
67
|
+
agentStatuses[agentName] = { state: "BLOCKED", task: "Timeout occurred", lastUpdated: new Date().toISOString() };
|
|
68
|
+
updatedStatus = true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (updatedStatus) {
|
|
73
|
+
updateDocumentStore("status", agentStatuses);
|
|
74
|
+
agentStatusCache = { ...agentStatuses }; // Sync cache
|
|
75
|
+
}
|
|
51
76
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
77
|
+
if (fs.existsSync(messagesDir)) {
|
|
78
|
+
const messageFiles = fs.readdirSync(messagesDir).filter((f) => f.endsWith(".json"));
|
|
79
|
+
const pendingMessages: HermesMessage[] = [];
|
|
80
|
+
const fileMessagesMap: Record<string, HermesMessage[]> = {};
|
|
55
81
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
try {
|
|
59
|
-
const content = fs.readFileSync(filePath, "utf8").trim();
|
|
60
|
-
if (!content) continue;
|
|
61
|
-
const lines = content.split("\n");
|
|
62
|
-
const allMsgs: HermesMessage[] = [];
|
|
63
|
-
lines.forEach((line) => {
|
|
64
|
-
if (!line.trim()) return;
|
|
82
|
+
for (const file of messageFiles) {
|
|
83
|
+
const filePath = path.join(messagesDir, file);
|
|
65
84
|
try {
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
85
|
+
const content = fs.readFileSync(filePath, "utf8").trim();
|
|
86
|
+
if (!content) continue;
|
|
87
|
+
const lines = content.split("\n");
|
|
88
|
+
const allMsgs: HermesMessage[] = [];
|
|
89
|
+
lines.forEach((line) => {
|
|
90
|
+
if (!line.trim()) return;
|
|
71
91
|
try {
|
|
72
|
-
const
|
|
73
|
-
|
|
92
|
+
const parsed = JSON.parse(line);
|
|
93
|
+
const msg = HermesMessageSchema.parse(parsed);
|
|
94
|
+
|
|
95
|
+
// Contract Validation
|
|
96
|
+
if (msg.category === "ACTION" || msg.category === "DELEGATION") {
|
|
97
|
+
try {
|
|
98
|
+
const payload = JSON.parse(msg.content);
|
|
99
|
+
TaskRequestSchema.parse(payload);
|
|
100
|
+
} catch (e) {
|
|
101
|
+
UI.error(`Invalid task contract payload: ${(e as Error).message}`);
|
|
102
|
+
return; // Skip invalid message
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (msg.status === "PENDING") {
|
|
107
|
+
pendingMessages.push(msg);
|
|
108
|
+
}
|
|
109
|
+
allMsgs.push(msg);
|
|
74
110
|
} catch (e) {
|
|
75
|
-
UI.error(`
|
|
76
|
-
return; // Skip invalid message
|
|
111
|
+
UI.error(`Skipping invalid Hermes message format: ${(e as Error).message}`);
|
|
77
112
|
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
allMsgs.push(msg);
|
|
84
|
-
} catch (e) {
|
|
85
|
-
UI.error(`Skipping invalid Hermes message format: ${(e as Error).message}`);
|
|
113
|
+
});
|
|
114
|
+
fileMessagesMap[file] = allMsgs;
|
|
115
|
+
} catch (err) {
|
|
116
|
+
UI.error(`Error occurred while reading ${file}.`);
|
|
117
|
+
logger.debug(`Failed to read message file ${file}`, err);
|
|
86
118
|
}
|
|
87
|
-
}
|
|
88
|
-
fileMessagesMap[file] = allMsgs;
|
|
89
|
-
} catch (err) {
|
|
90
|
-
UI.error(`Error occurred while reading ${file}.`);
|
|
91
|
-
logger.debug(`Failed to read message file ${file}`, err);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (pendingMessages.length === 0) {
|
|
96
|
-
UI.success("All messages processed, no pending tasks.");
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
119
|
+
}
|
|
99
120
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
121
|
+
if (pendingMessages.length > 0) {
|
|
122
|
+
// Sort by priority: HIGH -> NORMAL -> LOW
|
|
123
|
+
const priorityOrder = { HIGH: 1, NORMAL: 2, LOW: 3 };
|
|
124
|
+
pendingMessages.sort((a, b) => {
|
|
125
|
+
const pA = priorityOrder[a.priority || "NORMAL"] || 2;
|
|
126
|
+
const pB = priorityOrder[b.priority || "NORMAL"] || 2;
|
|
127
|
+
if (pA !== pB) return pA - pB;
|
|
128
|
+
return Date.parse(a.timestamp) - Date.parse(b.timestamp);
|
|
129
|
+
});
|
|
108
130
|
|
|
109
|
-
|
|
131
|
+
UI.intent("Multi-Agent Orchestration", `${pendingMessages.length} pending tasks and collaboration messages found.`);
|
|
110
132
|
|
|
111
|
-
|
|
133
|
+
const statusChanges: Record<string, { state: string; task: string }> = {};
|
|
112
134
|
|
|
113
|
-
|
|
114
|
-
|
|
135
|
+
for (const msg of pendingMessages) {
|
|
136
|
+
const { from, to, content, traceId, category, parentId } = msg;
|
|
115
137
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const agentLabel = isSubtask ? `${to} (Sub-Agent)` : to;
|
|
138
|
+
const isSubtask = category === "SUBTASK" || !!parentId;
|
|
139
|
+
const agentLabel = isSubtask ? `${to} (Sub-Agent)` : to;
|
|
119
140
|
|
|
120
|
-
|
|
141
|
+
UI.agentBox(agentLabel, content, `Trace: ${traceId} | From: ${from} | Category: ${category}`);
|
|
121
142
|
|
|
122
|
-
|
|
123
|
-
|
|
143
|
+
let targetState = "EXECUTING";
|
|
144
|
+
if (category === "DELEGATION" || category === "SUBTASK") targetState = "BRIEFED";
|
|
124
145
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
statusChanges[to] = { state: targetState, task: taskDescription };
|
|
146
|
+
let taskDescription = content;
|
|
147
|
+
if (category === "DELEGATION" || category === "ACTION") {
|
|
148
|
+
try {
|
|
149
|
+
const payload = JSON.parse(content);
|
|
150
|
+
taskDescription = payload.task || content;
|
|
151
|
+
} catch {
|
|
152
|
+
// Ignore
|
|
153
|
+
}
|
|
154
|
+
}
|
|
136
155
|
|
|
137
|
-
|
|
138
|
-
UI.success(`${to} informed and took over the task.`);
|
|
139
|
-
statusChanges[to] = { state: "EXECUTING", task: taskDescription };
|
|
140
|
-
}
|
|
156
|
+
statusChanges[to] = { state: targetState, task: taskDescription };
|
|
141
157
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
UI.success(`Collaboration initiated: ${from} -> ${to}`);
|
|
147
|
-
console.warn(chalk.gray(` š Instructions: ${frameworkDir}/agents/${to.replace("@", "")}.md`));
|
|
158
|
+
if (targetState === "BRIEFED") {
|
|
159
|
+
UI.success(`${to} informed and took over the task.`);
|
|
160
|
+
statusChanges[to] = { state: "EXECUTING", task: taskDescription };
|
|
161
|
+
}
|
|
148
162
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
163
|
+
if (isSubtask) {
|
|
164
|
+
UI.success(`Sub-task connection established (Parent: ${parentId || "Unknown"}).`);
|
|
165
|
+
}
|
|
152
166
|
|
|
153
|
-
|
|
154
|
-
|
|
167
|
+
UI.success(`Collaboration initiated: ${from} -> ${to}`);
|
|
168
|
+
console.warn(chalk.gray(` š Instructions: ${frameworkDir}/agents/${to.replace("@", "")}.md`));
|
|
155
169
|
|
|
156
|
-
|
|
157
|
-
const archiveDir = path.join(messagesDir, "archive");
|
|
158
|
-
|
|
159
|
-
for (const file of Object.keys(fileMessagesMap)) {
|
|
160
|
-
const filePath = path.join(messagesDir, file);
|
|
161
|
-
const msgs = fileMessagesMap[file];
|
|
162
|
-
|
|
163
|
-
const activeMsgs = msgs.filter(m => m.status !== "PROCESSED");
|
|
164
|
-
const processedMsgs = msgs.filter(m => m.status === "PROCESSED");
|
|
165
|
-
|
|
166
|
-
try {
|
|
167
|
-
// Write remaining active messages
|
|
168
|
-
if (activeMsgs.length > 0) {
|
|
169
|
-
const updatedContent = activeMsgs.map(m => JSON.stringify(m)).join("\n") + "\n";
|
|
170
|
-
fs.writeFileSync(filePath, updatedContent, "utf8");
|
|
171
|
-
} else {
|
|
172
|
-
// Empty the file if no active messages
|
|
173
|
-
if (fs.existsSync(filePath)) {
|
|
174
|
-
fs.writeFileSync(filePath, "", "utf8");
|
|
170
|
+
msg.status = "PROCESSED";
|
|
175
171
|
}
|
|
176
|
-
}
|
|
177
172
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
173
|
+
applyBatchStatusUpdates(statusChanges);
|
|
174
|
+
|
|
175
|
+
const archiveDir = path.join(messagesDir, "archive");
|
|
176
|
+
|
|
177
|
+
for (const file of Object.keys(fileMessagesMap)) {
|
|
178
|
+
const filePath = path.join(messagesDir, file);
|
|
179
|
+
const msgs = fileMessagesMap[file];
|
|
180
|
+
|
|
181
|
+
const activeMsgs = msgs.filter(m => m.status !== "PROCESSED");
|
|
182
|
+
const processedMsgs = msgs.filter(m => m.status === "PROCESSED");
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
if (activeMsgs.length > 0) {
|
|
186
|
+
const updatedContent = activeMsgs.map(m => JSON.stringify(m)).join("\n") + "\n";
|
|
187
|
+
writeTextFile(filePath, updatedContent);
|
|
188
|
+
} else if (fs.existsSync(filePath)) {
|
|
189
|
+
writeTextFile(filePath, "");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (processedMsgs.length > 0) {
|
|
193
|
+
if (!fs.existsSync(archiveDir)) {
|
|
194
|
+
fs.mkdirSync(archiveDir, { recursive: true });
|
|
195
|
+
}
|
|
196
|
+
const agentName = file.replace(".json", "");
|
|
197
|
+
const archivePath = path.join(archiveDir, `${agentName}_archive.json`);
|
|
198
|
+
const archiveContent = processedMsgs.map(m => JSON.stringify(m)).join("\n") + "\n";
|
|
199
|
+
appendFile(archivePath, archiveContent);
|
|
200
|
+
UI.success(`Archived ${processedMsgs.length} messages for @${agentName}`);
|
|
201
|
+
}
|
|
202
|
+
} catch (err) {
|
|
203
|
+
UI.error(`Error occurred while updating and archiving the message file ${file}.`);
|
|
204
|
+
logger.debug(`Failed to update and archive message file ${file}`, err);
|
|
205
|
+
}
|
|
182
206
|
}
|
|
183
|
-
const agentName = file.replace(".json", "");
|
|
184
|
-
const archivePath = path.join(archiveDir, `${agentName}_archive.json`);
|
|
185
|
-
const archiveContent = processedMsgs.map(m => JSON.stringify(m)).join("\n") + "\n";
|
|
186
|
-
fs.appendFileSync(archivePath, archiveContent, "utf8");
|
|
187
|
-
UI.success(`Archived ${processedMsgs.length} messages for @${agentName}`);
|
|
188
207
|
}
|
|
189
|
-
} catch (err) {
|
|
190
|
-
UI.error(`Error occurred while updating and archiving the message file ${file}.`);
|
|
191
|
-
logger.debug(`Failed to update and archive message file ${file}`, err);
|
|
192
208
|
}
|
|
209
|
+
|
|
210
|
+
// Prevent 100% CPU and provide breathing room for I/O
|
|
211
|
+
await sleep(2000);
|
|
193
212
|
}
|
|
194
213
|
}
|
|
195
214
|
|
|
@@ -211,14 +230,15 @@ export async function sendMessage(args: {
|
|
|
211
230
|
const messagePath = path.join(messagesDir, `${agentName}.json`);
|
|
212
231
|
const lockPath = path.join(messagesDir, `${agentName}.lock`);
|
|
213
232
|
|
|
214
|
-
let retries = 3
|
|
233
|
+
let retries = 20; // Increased retries from 3 to 20
|
|
215
234
|
let acquired = false;
|
|
216
235
|
while (retries > 0) {
|
|
217
236
|
try {
|
|
218
237
|
if (fs.existsSync(lockPath)) {
|
|
219
238
|
try {
|
|
220
239
|
const stats = fs.statSync(lockPath);
|
|
221
|
-
|
|
240
|
+
// Increased stale lock timeout from 5s to 10s for heavy ops
|
|
241
|
+
if (Date.now() - stats.mtimeMs > 10000) {
|
|
222
242
|
fs.unlinkSync(lockPath);
|
|
223
243
|
}
|
|
224
244
|
} catch (err) {
|
|
@@ -236,6 +256,8 @@ export async function sendMessage(args: {
|
|
|
236
256
|
if (error.code === "EEXIST") {
|
|
237
257
|
retries--;
|
|
238
258
|
if (retries > 0) {
|
|
259
|
+
// Exponential backoff or jitter could be added here,
|
|
260
|
+
// but fixed 500ms is usually fine for local file locks.
|
|
239
261
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
240
262
|
}
|
|
241
263
|
} else {
|
|
@@ -259,7 +281,7 @@ export async function sendMessage(args: {
|
|
|
259
281
|
status: "PENDING"
|
|
260
282
|
};
|
|
261
283
|
|
|
262
|
-
|
|
284
|
+
appendFile(messagePath, JSON.stringify(message) + "\n");
|
|
263
285
|
|
|
264
286
|
return message;
|
|
265
287
|
} catch (err) {
|
|
@@ -278,6 +300,19 @@ export async function sendMessage(args: {
|
|
|
278
300
|
|
|
279
301
|
|
|
280
302
|
function applyBatchStatusUpdates(changes: Record<string, { state: string; task: string }>) {
|
|
281
|
-
|
|
282
|
-
|
|
303
|
+
let hasChanges = false;
|
|
304
|
+
const now = new Date().toISOString();
|
|
305
|
+
|
|
306
|
+
for (const [agent, data] of Object.entries(changes)) {
|
|
307
|
+
const current = agentStatusCache[agent];
|
|
308
|
+
if (!current || current.state !== data.state || current.task !== data.task) {
|
|
309
|
+
agentStatusCache[agent] = { ...data, lastUpdated: now };
|
|
310
|
+
hasChanges = true;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (hasChanges) {
|
|
315
|
+
updateDocumentStore("status", agentStatusCache);
|
|
316
|
+
logger.debug("In-memory status cache synced to disk.");
|
|
317
|
+
}
|
|
283
318
|
}
|
package/src/cli/commands/plan.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { UI } from "../utils/ui.js";
|
|
|
7
7
|
/**
|
|
8
8
|
* Parses docs/user/roadmap.md and injects tasks into the Document Store.
|
|
9
9
|
*/
|
|
10
|
-
export function planCommand() {
|
|
10
|
+
export async function planCommand() {
|
|
11
11
|
const projectRoot = process.cwd();
|
|
12
12
|
const roadmapPath = path.join(projectRoot, "docs/user/roadmap.md");
|
|
13
13
|
|
|
@@ -4,7 +4,7 @@ import cp from "child_process";
|
|
|
4
4
|
|
|
5
5
|
const targetDir = process.cwd(); // Assuming targetDir is process.cwd() in the CLI context
|
|
6
6
|
|
|
7
|
-
export function runScriptCommand(script: string, projectPath: string) {
|
|
7
|
+
export async function runScriptCommand(script: string, projectPath: string) {
|
|
8
8
|
const fullPath = path.join(targetDir, projectPath);
|
|
9
9
|
if (!fs.existsSync(fullPath)) {
|
|
10
10
|
console.warn(`ā Project path not found: ${projectPath}`);
|
|
@@ -5,7 +5,7 @@ import { collectFiles } from "../utils/fs.js";
|
|
|
5
5
|
|
|
6
6
|
const targetDir = process.cwd();
|
|
7
7
|
|
|
8
|
-
export function securityAuditCommand(targetPath: string) {
|
|
8
|
+
export async function securityAuditCommand(targetPath: string) {
|
|
9
9
|
console.warn(`š Running Advanced Security Audit on: ${targetPath}...`);
|
|
10
10
|
const scanRules = [
|
|
11
11
|
{ pattern: /sql`/, message: "Potential Raw SQL usage detected", severity: "HIGH" },
|
|
@@ -5,7 +5,7 @@ import { listTasks, readState, readStatus } from "../utils/memory.js";
|
|
|
5
5
|
/**
|
|
6
6
|
* Print the current framework status.
|
|
7
7
|
*/
|
|
8
|
-
export function statusCommand() {
|
|
8
|
+
export async function statusCommand() {
|
|
9
9
|
const state = readState();
|
|
10
10
|
const tasks = listTasks();
|
|
11
11
|
const agentStatuses = readStatus();
|
|
@@ -4,7 +4,7 @@ import { sanitizeInput, normalizeAgentName, normalizePriority } from "../utils/s
|
|
|
4
4
|
/**
|
|
5
5
|
* Generate a new Trace ID and add it to project memory.
|
|
6
6
|
*/
|
|
7
|
-
export function traceNewCommand(description: string, agent = "manager", priority = "P2"): string | void {
|
|
7
|
+
export async function traceNewCommand(description: string, agent = "manager", priority = "P2"): Promise<string | void> {
|
|
8
8
|
const traceId = generateULID();
|
|
9
9
|
const safeDescription = sanitizeInput(description);
|
|
10
10
|
const safeAgent = normalizeAgentName(agent);
|
package/src/cli/index.ts
CHANGED
|
@@ -71,21 +71,21 @@ async function main() {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
case "check":
|
|
74
|
-
checkCommand();
|
|
74
|
+
await checkCommand();
|
|
75
75
|
break;
|
|
76
76
|
|
|
77
77
|
case "plan":
|
|
78
|
-
planCommand();
|
|
78
|
+
await planCommand();
|
|
79
79
|
break;
|
|
80
80
|
case "status":
|
|
81
|
-
statusCommand();
|
|
81
|
+
await statusCommand();
|
|
82
82
|
break;
|
|
83
83
|
|
|
84
84
|
case "trace:new": {
|
|
85
85
|
const description = args[1] || "Default task";
|
|
86
86
|
const agent = args[2] || "manager";
|
|
87
87
|
const priority = args[3] || "P1";
|
|
88
|
-
traceNewCommand(description, agent, priority);
|
|
88
|
+
await traceNewCommand(description, agent, priority);
|
|
89
89
|
break;
|
|
90
90
|
}
|
|
91
91
|
|
|
@@ -96,7 +96,7 @@ async function main() {
|
|
|
96
96
|
console.error("ā Error: section and content are required.");
|
|
97
97
|
process.exit(64);
|
|
98
98
|
}
|
|
99
|
-
updateProjectMemoryCommand(section, content);
|
|
99
|
+
await updateProjectMemoryCommand(section, content);
|
|
100
100
|
break;
|
|
101
101
|
}
|
|
102
102
|
|
|
@@ -104,9 +104,15 @@ async function main() {
|
|
|
104
104
|
await createAppCommand(args.slice(1));
|
|
105
105
|
break;
|
|
106
106
|
|
|
107
|
+
case "dashboard": {
|
|
108
|
+
const { dashboardCommand } = await import("./commands/dashboard.js");
|
|
109
|
+
await dashboardCommand(args.slice(1));
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
|
|
107
113
|
case "orchestrate":
|
|
108
114
|
case "loop":
|
|
109
|
-
orchestrateCommand();
|
|
115
|
+
await orchestrateCommand();
|
|
110
116
|
break;
|
|
111
117
|
|
|
112
118
|
case "approve": {
|
|
@@ -115,16 +121,16 @@ async function main() {
|
|
|
115
121
|
console.error("ā Error: traceId is required.");
|
|
116
122
|
process.exit(64);
|
|
117
123
|
}
|
|
118
|
-
approveCommand(traceId);
|
|
124
|
+
await approveCommand(traceId);
|
|
119
125
|
break;
|
|
120
126
|
}
|
|
121
127
|
|
|
122
128
|
case "verify-contract":
|
|
123
|
-
verifyApiContractCommand();
|
|
129
|
+
await verifyApiContractCommand();
|
|
124
130
|
break;
|
|
125
131
|
|
|
126
132
|
case "update-contract":
|
|
127
|
-
updateApiContractCommand();
|
|
133
|
+
await updateApiContractCommand();
|
|
128
134
|
break;
|
|
129
135
|
|
|
130
136
|
case "validate":
|
|
@@ -144,34 +150,34 @@ async function main() {
|
|
|
144
150
|
const memoryContent = fs.existsSync(getMemoryPath()) ? fs.readFileSync(getMemoryPath(), "utf8") : "";
|
|
145
151
|
const traceId = readActiveTraceId(memoryContent) || "T-000";
|
|
146
152
|
const { gitCommitCommand } = await import("./commands/git.js");
|
|
147
|
-
gitCommitCommand(traceId);
|
|
153
|
+
await gitCommitCommand(traceId);
|
|
148
154
|
break;
|
|
149
155
|
}
|
|
150
156
|
|
|
151
157
|
case "git:sync": {
|
|
152
158
|
const { gitSyncCommand } = await import("./commands/git.js");
|
|
153
|
-
gitSyncCommand();
|
|
159
|
+
await gitSyncCommand();
|
|
154
160
|
break;
|
|
155
161
|
}
|
|
156
162
|
|
|
157
163
|
case "check:compliance": {
|
|
158
164
|
const targetPath = args[1] || "src";
|
|
159
165
|
const { complianceCheckCommand } = await import("./commands/compliance.js");
|
|
160
|
-
complianceCheckCommand(targetPath);
|
|
166
|
+
await complianceCheckCommand(targetPath);
|
|
161
167
|
break;
|
|
162
168
|
}
|
|
163
169
|
|
|
164
170
|
case "explorer:graph": {
|
|
165
171
|
const targetPath = args[1] || "src";
|
|
166
172
|
const { explorerGraphCommand } = await import("./commands/explorer.js");
|
|
167
|
-
explorerGraphCommand(targetPath);
|
|
173
|
+
await explorerGraphCommand(targetPath);
|
|
168
174
|
break;
|
|
169
175
|
}
|
|
170
176
|
|
|
171
177
|
case "explorer:audit": {
|
|
172
178
|
const targetPath = args[1] || "src";
|
|
173
179
|
const { explorerAuditCommand } = await import("./commands/explorer.js");
|
|
174
|
-
explorerAuditCommand(targetPath);
|
|
180
|
+
await explorerAuditCommand(targetPath);
|
|
175
181
|
break;
|
|
176
182
|
}
|
|
177
183
|
|
|
@@ -179,14 +185,14 @@ async function main() {
|
|
|
179
185
|
const topic = args[1];
|
|
180
186
|
const content = args[2];
|
|
181
187
|
const { updateKnowledgeBaseCommand } = await import("./commands/knowledge.js");
|
|
182
|
-
updateKnowledgeBaseCommand(topic, content);
|
|
188
|
+
await updateKnowledgeBaseCommand(topic, content);
|
|
183
189
|
break;
|
|
184
190
|
}
|
|
185
191
|
|
|
186
192
|
case "knowledge:search": {
|
|
187
193
|
const query = args[1];
|
|
188
194
|
const { searchKnowledgeBaseCommand } = await import("./commands/knowledge.js");
|
|
189
|
-
searchKnowledgeBaseCommand(query);
|
|
195
|
+
await searchKnowledgeBaseCommand(query);
|
|
190
196
|
break;
|
|
191
197
|
}
|
|
192
198
|
|
|
@@ -198,7 +204,7 @@ async function main() {
|
|
|
198
204
|
const memoryContent = fs.existsSync(getMemoryPath()) ? fs.readFileSync(getMemoryPath(), "utf8") : "";
|
|
199
205
|
const traceId = readActiveTraceId(memoryContent) || "T-000";
|
|
200
206
|
const { logAgentActionCommand } = await import("./commands/log.js");
|
|
201
|
-
logAgentActionCommand({ agent, action, status, summary, traceId });
|
|
207
|
+
await logAgentActionCommand({ agent, action, status, summary, traceId });
|
|
202
208
|
break;
|
|
203
209
|
}
|
|
204
210
|
|
|
@@ -206,20 +212,20 @@ async function main() {
|
|
|
206
212
|
const script = args[1];
|
|
207
213
|
const projectPath = args[2] || ".";
|
|
208
214
|
const { runScriptCommand } = await import("./commands/script.js");
|
|
209
|
-
runScriptCommand(script, projectPath);
|
|
215
|
+
await runScriptCommand(script, projectPath);
|
|
210
216
|
break;
|
|
211
217
|
}
|
|
212
218
|
|
|
213
219
|
case "security:audit": {
|
|
214
220
|
const targetPath = args[1] || "src";
|
|
215
221
|
const { securityAuditCommand } = await import("./commands/security.js");
|
|
216
|
-
securityAuditCommand(targetPath);
|
|
222
|
+
await securityAuditCommand(targetPath);
|
|
217
223
|
break;
|
|
218
224
|
}
|
|
219
225
|
|
|
220
226
|
case "check:lint": {
|
|
221
227
|
const { lintCommand } = await import("./commands/lint.js");
|
|
222
|
-
lintCommand();
|
|
228
|
+
await lintCommand();
|
|
223
229
|
break;
|
|
224
230
|
}
|
|
225
231
|
|
|
@@ -267,7 +273,7 @@ async function main() {
|
|
|
267
273
|
|
|
268
274
|
function showHelp() {
|
|
269
275
|
console.warn(`
|
|
270
|
-
šļø Enderun
|
|
276
|
+
šļø Agent Enderun CLI (v${getPackageVersion()}) ā The Supreme AI Orchestrator
|
|
271
277
|
|
|
272
278
|
Usage:
|
|
273
279
|
agent-enderun <command> [options]
|
|
@@ -309,8 +315,18 @@ Example:
|
|
|
309
315
|
}
|
|
310
316
|
|
|
311
317
|
import { logger } from "../shared/logger.js";
|
|
318
|
+
import { EnderunBaseError } from "../shared/errors.js";
|
|
312
319
|
|
|
313
320
|
main().catch((err) => {
|
|
321
|
+
if (err instanceof EnderunBaseError) {
|
|
322
|
+
console.error(`\nā [${err.code}] Error: ${err.message}`);
|
|
323
|
+
if (err.solution) {
|
|
324
|
+
console.warn(`š” Solution Tip: ${err.solution}\n`);
|
|
325
|
+
}
|
|
326
|
+
} else {
|
|
327
|
+
console.error(`\nā Fatal Error: ${err.message || String(err)}`);
|
|
328
|
+
}
|
|
329
|
+
|
|
314
330
|
logger.fatal("Fatal exception during CLI execution", err);
|
|
315
331
|
process.exit(1);
|
|
316
332
|
});
|