@hilbras/omninode 0.6.0 → 0.8.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/dist/index.js CHANGED
@@ -160,7 +160,11 @@ var projectConfigSchema = z.object({
160
160
  agents: z.array(agentConfigSchema).default([]),
161
161
  roles: z.array(roleConfigSchema).default([]),
162
162
  pipelines: z.array(pipelineConfigSchema).default([]),
163
- memory: z.object({ provider: z.string().min(1) }).strict().optional()
163
+ memory: z.object({
164
+ provider: z.string().min(1).default("local"),
165
+ base_url: z.string().url().optional(),
166
+ api_key_env_var: z.string().min(1).optional()
167
+ }).strict().optional()
164
168
  }).strict();
165
169
  var appConfigSchema = z.object({
166
170
  project: projectConfigSchema,
@@ -230,7 +234,13 @@ function toAppConfig(yaml) {
230
234
  agents: yaml.project.agents.map(toAgentConfig),
231
235
  roles: yaml.project.roles.map(toRoleDefinition),
232
236
  pipelines: yaml.project.pipelines.map(toPipelineDefinition),
233
- ...yaml.project.memory !== void 0 ? { memory: yaml.project.memory } : {}
237
+ ...yaml.project.memory !== void 0 ? {
238
+ memory: {
239
+ provider: yaml.project.memory.provider,
240
+ baseUrl: yaml.project.memory.base_url,
241
+ apiKeyEnvVar: yaml.project.memory.api_key_env_var
242
+ }
243
+ } : {}
234
244
  },
235
245
  ...yaml.logging !== void 0 ? { logging: yaml.logging } : {}
236
246
  };
@@ -1079,13 +1089,24 @@ var TaskEngine = class {
1079
1089
  const role = task.role ? this.options.roles.get(task.role) : void 0;
1080
1090
  const running = await this.transition(task, "running");
1081
1091
  this.log.info(`Running task ${task.id} on agent "${task.agent}".`);
1092
+ let background = running.context?.background;
1093
+ if (this.options.memory) {
1094
+ const memoryContext = await this.options.memory.gatherContext({
1095
+ objective: running.objective,
1096
+ ...running.role ? { role: running.role } : {},
1097
+ ...running.project ? { project: running.project } : {}
1098
+ });
1099
+ if (memoryContext.length > 0) {
1100
+ background = [memoryContext, background].filter((part) => part && part.length > 0).join("\n\n");
1101
+ }
1102
+ }
1082
1103
  let output;
1083
1104
  try {
1084
1105
  output = await agent.run({
1085
1106
  taskId: running.id,
1086
1107
  objective: running.objective,
1087
1108
  ...role ? { role } : {},
1088
- ...running.context?.background ? { context: running.context.background } : {}
1109
+ ...background !== void 0 ? { context: background } : {}
1089
1110
  });
1090
1111
  } catch (error) {
1091
1112
  const message = error instanceof Error ? error.message : String(error);
@@ -1097,12 +1118,18 @@ var TaskEngine = class {
1097
1118
  const result = {
1098
1119
  ...output.summary !== void 0 ? { summary: output.summary } : {},
1099
1120
  ...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
1121
+ // Keep the verbatim output (bounded) for report extraction and audit.
1122
+ ...output.rawOutput !== void 0 && output.rawOutput.length > 0 ? { rawOutput: output.rawOutput.slice(0, 1e4) } : {},
1100
1123
  ...output.error !== void 0 ? { error: output.error } : {},
1101
1124
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
1102
1125
  };
1103
1126
  const finalStatus = output.status === "completed" ? "completed" : "failed";
1127
+ const finished = await this.transition(running, finalStatus, { result });
1104
1128
  this.log.info(`Task ${task.id} finished: ${finalStatus}.`);
1105
- return this.transition(running, finalStatus, { result });
1129
+ if (this.options.memory) {
1130
+ await this.options.memory.recordTaskOutcome(finished);
1131
+ }
1132
+ return finished;
1106
1133
  }
1107
1134
  async cancel(id) {
1108
1135
  const task = await this.mustGet(id);
@@ -1147,12 +1174,306 @@ function generateTaskId() {
1147
1174
  return `task-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
1148
1175
  }
1149
1176
 
1177
+ // src/memory/local.ts
1178
+ import { mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "fs/promises";
1179
+ import { dirname as dirname2 } from "path";
1180
+ var STOPWORDS = /* @__PURE__ */ new Set([
1181
+ "the",
1182
+ "and",
1183
+ "for",
1184
+ "with",
1185
+ "this",
1186
+ "that",
1187
+ "from",
1188
+ "into",
1189
+ "was",
1190
+ "were",
1191
+ "have",
1192
+ "has",
1193
+ "had",
1194
+ "are",
1195
+ "but",
1196
+ "not",
1197
+ "you",
1198
+ "his",
1199
+ "her",
1200
+ "its",
1201
+ "their",
1202
+ "them",
1203
+ "then",
1204
+ "than",
1205
+ "when",
1206
+ "what",
1207
+ "which",
1208
+ "will",
1209
+ "would",
1210
+ "been",
1211
+ "were",
1212
+ "also",
1213
+ "because",
1214
+ "about",
1215
+ "after",
1216
+ "before"
1217
+ ]);
1218
+ function keywords(text) {
1219
+ return new Set(
1220
+ (text ?? "").toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= 4 && !STOPWORDS.has(t))
1221
+ );
1222
+ }
1223
+ function relevanceScore(queryKeywords, entry) {
1224
+ if (queryKeywords.size === 0) return 0;
1225
+ const entryKeywords = keywords(`${entry.content} ${entry.key}`);
1226
+ let overlap = 0;
1227
+ for (const keyword of queryKeywords) {
1228
+ if (entryKeywords.has(keyword)) overlap += 1;
1229
+ }
1230
+ let tagMatches = 0;
1231
+ for (const tag of entry.tags ?? []) {
1232
+ if (queryKeywords.has(tag.toLowerCase())) tagMatches += 1;
1233
+ }
1234
+ return overlap * 2 + tagMatches * 1.5;
1235
+ }
1236
+ var LocalMemoryProvider = class {
1237
+ name = "local";
1238
+ filePath;
1239
+ queue = Promise.resolve();
1240
+ constructor(directory = `${process.cwd()}/.omninode`) {
1241
+ this.filePath = `${directory}/memory.json`;
1242
+ }
1243
+ async query(query) {
1244
+ return this.synchronized(async () => {
1245
+ const entries = await this.load();
1246
+ const filtered = entries.filter((entry) => query.scope ? entry.scope === query.scope : true).filter(
1247
+ (entry) => query.tags && query.tags.length > 0 ? query.tags.every((tag) => (entry.tags ?? []).includes(tag)) : true
1248
+ );
1249
+ if (!query.text || query.text.trim().length === 0) {
1250
+ return filtered.sort((a, b) => (b.updatedAt ?? b.createdAt ?? "").localeCompare(a.updatedAt ?? a.createdAt ?? "")).slice(0, query.limit ?? 10);
1251
+ }
1252
+ const queryKeywords = keywords(query.text);
1253
+ return filtered.map((entry) => ({ entry, score: relevanceScore(queryKeywords, entry) })).filter(({ score }) => score > 0).sort((a, b) => b.score - a.score).slice(0, query.limit ?? 10).map(({ entry }) => entry);
1254
+ });
1255
+ }
1256
+ async write(entry) {
1257
+ return this.synchronized(async () => {
1258
+ const entries = await this.load();
1259
+ const index = entries.findIndex((e) => e.key === entry.key);
1260
+ const stamped = {
1261
+ ...entry,
1262
+ createdAt: entry.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
1263
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1264
+ };
1265
+ if (index >= 0) entries[index] = stamped;
1266
+ else entries.push(stamped);
1267
+ await this.saveAll(entries);
1268
+ });
1269
+ }
1270
+ async count() {
1271
+ return (await this.load()).length;
1272
+ }
1273
+ synchronized(operation) {
1274
+ const result = this.queue.then(operation, operation);
1275
+ this.queue = result.then(
1276
+ () => void 0,
1277
+ () => void 0
1278
+ );
1279
+ return result;
1280
+ }
1281
+ async load() {
1282
+ try {
1283
+ const raw = await readFile2(this.filePath, "utf8");
1284
+ const parsed = JSON.parse(raw);
1285
+ return Array.isArray(parsed) ? parsed : [];
1286
+ } catch {
1287
+ return [];
1288
+ }
1289
+ }
1290
+ async saveAll(entries) {
1291
+ const tmp = `${this.filePath}.tmp`;
1292
+ await mkdir2(dirname2(this.filePath), { recursive: true });
1293
+ await writeFile2(tmp, `${JSON.stringify(entries, null, 2)}
1294
+ `, "utf8");
1295
+ await rename2(tmp, this.filePath);
1296
+ }
1297
+ };
1298
+
1299
+ // src/memory/remembera.ts
1300
+ var RememberaMemoryProvider = class {
1301
+ name = "remembera";
1302
+ baseUrl;
1303
+ apiKeyEnvVar;
1304
+ env;
1305
+ log;
1306
+ constructor(config, log = logger) {
1307
+ this.baseUrl = config.baseUrl.replace(/\/+$/, "");
1308
+ this.apiKeyEnvVar = config.apiKeyEnvVar;
1309
+ this.env = config.env ?? process.env;
1310
+ this.log = log.child({ component: "remembera" });
1311
+ }
1312
+ headers() {
1313
+ const key = this.apiKeyEnvVar ? resolveApiKey({ name: "remembera", apiKeyEnvVar: this.apiKeyEnvVar }, this.env) : void 0;
1314
+ return key ? { authorization: `Bearer ${key}` } : {};
1315
+ }
1316
+ async query(query) {
1317
+ const { status, body } = await this.request(`${this.baseUrl}/api/memory/query`, query);
1318
+ if (status !== 200) {
1319
+ throw new MemoryError(`Remembera query returned HTTP ${status}.`);
1320
+ }
1321
+ const entries = body.entries;
1322
+ if (!Array.isArray(entries)) {
1323
+ throw new MemoryError("Remembera query response had no entries array.");
1324
+ }
1325
+ return entries.map((entry) => this.toEntry(entry)).filter((entry) => entry !== void 0);
1326
+ }
1327
+ async write(entry) {
1328
+ const { status } = await this.request(`${this.baseUrl}/api/memory`, entry);
1329
+ if (status !== 200 && status !== 201) {
1330
+ throw new MemoryError(`Remembera write returned HTTP ${status}.`);
1331
+ }
1332
+ }
1333
+ async request(url, body) {
1334
+ const headers = this.headers();
1335
+ try {
1336
+ return await requestJson(url, {
1337
+ method: "POST",
1338
+ headers,
1339
+ body
1340
+ });
1341
+ } catch (error) {
1342
+ const message = error instanceof Error ? error.message : String(error);
1343
+ throw new MemoryError(`Remembera request to ${url} failed: ${message}`, { cause: error });
1344
+ }
1345
+ }
1346
+ toEntry(raw) {
1347
+ if (raw === null || typeof raw !== "object") {
1348
+ this.log.warn("Skipping malformed Remembera memory entry.");
1349
+ return void 0;
1350
+ }
1351
+ const entry = raw;
1352
+ if (typeof entry.key !== "string" || typeof entry.content !== "string") {
1353
+ this.log.warn("Skipping Remembera memory entry without key/content.");
1354
+ return void 0;
1355
+ }
1356
+ const scope = entry.scope;
1357
+ return {
1358
+ key: entry.key,
1359
+ content: entry.content,
1360
+ scope: scope === "project" || scope === "role" || scope === "task" || scope === "knowledge" ? scope : "knowledge",
1361
+ ...Array.isArray(entry.tags) ? { tags: entry.tags.filter((t) => typeof t === "string") } : {},
1362
+ ...typeof entry.createdAt === "string" ? { createdAt: entry.createdAt } : {},
1363
+ ...typeof entry.updatedAt === "string" ? { updatedAt: entry.updatedAt } : {}
1364
+ };
1365
+ }
1366
+ };
1367
+
1368
+ // src/memory/factory.ts
1369
+ function createMemoryProvider(config) {
1370
+ if (!config || config.provider === "local") {
1371
+ return new LocalMemoryProvider();
1372
+ }
1373
+ if (config.provider === "remembera") {
1374
+ if (!config.baseUrl) {
1375
+ throw new ConfigError(
1376
+ "CONFIG_INVALID",
1377
+ `Memory provider "remembera" needs a base_url in omninode.yaml.`
1378
+ );
1379
+ }
1380
+ return new RememberaMemoryProvider({
1381
+ baseUrl: config.baseUrl,
1382
+ apiKeyEnvVar: config.apiKeyEnvVar
1383
+ });
1384
+ }
1385
+ throw new ConfigError(
1386
+ "CONFIG_INVALID",
1387
+ `Unknown memory provider "${config.provider}". Available: local, remembera.`
1388
+ );
1389
+ }
1390
+
1391
+ // src/memory/service.ts
1392
+ var MAX_CONTEXT_CHARS = 4e3;
1393
+ var MemoryService = class {
1394
+ constructor(provider, log = logger) {
1395
+ this.provider = provider;
1396
+ this.log = log.child({ component: "memory", provider: provider.name });
1397
+ }
1398
+ provider;
1399
+ log;
1400
+ get providerRef() {
1401
+ return this.provider;
1402
+ }
1403
+ get providerName() {
1404
+ return this.provider.name;
1405
+ }
1406
+ /** Relevant context for a task objective; empty string when nothing matches. */
1407
+ async gatherContext(query) {
1408
+ try {
1409
+ const entries = await this.provider.query({
1410
+ text: query.objective,
1411
+ ...query.role ? { tags: [query.role] } : {},
1412
+ limit: 8
1413
+ });
1414
+ if (entries.length === 0) return "";
1415
+ const byScope = /* @__PURE__ */ new Map();
1416
+ for (const entry of entries) {
1417
+ byScope.set(entry.scope, [...byScope.get(entry.scope) ?? [], entry]);
1418
+ }
1419
+ const parts = [];
1420
+ for (const scope of ["project", "task", "role", "knowledge"]) {
1421
+ const scopeEntries = byScope.get(scope) ?? [];
1422
+ if (scopeEntries.length === 0) continue;
1423
+ const lines = scopeEntries.map((entry) => `- ${entry.content}`).join("\n");
1424
+ parts.push(`### Relevant memory (${scope})
1425
+ ${lines}`);
1426
+ }
1427
+ const context = parts.join("\n\n");
1428
+ return context.length > MAX_CONTEXT_CHARS ? `${context.slice(0, MAX_CONTEXT_CHARS)}\u2026` : context;
1429
+ } catch (error) {
1430
+ this.log.warn(
1431
+ `Memory query failed (continuing without memory): ${error instanceof Error ? error.message : String(error)}`
1432
+ );
1433
+ return "";
1434
+ }
1435
+ }
1436
+ /** Records a finished task's outcome — best-effort. */
1437
+ async recordTaskOutcome(task) {
1438
+ const outcome = task.result?.summary ?? task.result?.error ?? "(no output)";
1439
+ try {
1440
+ await this.provider.write({
1441
+ key: `task:${task.id}`,
1442
+ scope: "task",
1443
+ content: `Objective: ${task.objective}
1444
+ Status: ${task.status}
1445
+ Outcome: ${outcome}`,
1446
+ tags: ["task", ...task.agent ? [task.agent] : [], ...task.role ? [task.role] : []],
1447
+ createdAt: task.createdAt
1448
+ });
1449
+ const significant = (task.result?.reports ?? []).flatMap(
1450
+ (report) => report.findings.filter((finding) => finding.severity === "high" || finding.severity === "critical").map((finding) => ({ report, finding }))
1451
+ );
1452
+ for (const { report, finding } of significant) {
1453
+ await this.provider.write({
1454
+ key: `finding:${report.id}:${finding.id}`,
1455
+ scope: "project",
1456
+ content: `[${finding.severity}] ${finding.title} \u2014 ${finding.detail}`,
1457
+ tags: ["known-problem", report.agent]
1458
+ });
1459
+ }
1460
+ } catch (error) {
1461
+ this.log.warn(
1462
+ `Memory write failed (continuing): ${error instanceof Error ? error.message : String(error)}`
1463
+ );
1464
+ }
1465
+ }
1466
+ };
1467
+
1150
1468
  // src/tasks/engine-factory.ts
1151
1469
  function createTaskEngine(config, log = logger, directory) {
1470
+ const agents = buildAgentRegistry(config, log);
1471
+ const roles = buildRoleRegistry(config);
1152
1472
  return new TaskEngine({
1153
- agents: buildAgentRegistry(config, log),
1154
- roles: buildRoleRegistry(config),
1473
+ agents,
1474
+ roles,
1155
1475
  store: new FileTaskStore(directory),
1476
+ ...config.project.memory !== void 0 ? { memory: new MemoryService(createMemoryProvider(config.project.memory), log) } : {},
1156
1477
  log
1157
1478
  });
1158
1479
  }
@@ -1328,11 +1649,28 @@ ${contribution}`);
1328
1649
  }
1329
1650
  const failed = [...outcomes.values()].some((o) => o.status === "failed");
1330
1651
  const finalStatus = failed ? "failed" : "completed";
1652
+ let combinedReportId;
1653
+ if (this.options.reports) {
1654
+ const taskIds = [...outcomes.values()].flatMap((o) => o.taskIds);
1655
+ const tasks = (await Promise.all(taskIds.map((id) => this.options.tasks.get(id)))).filter((task) => task !== void 0);
1656
+ const collected = await this.options.reports.collectFromTasks(tasks);
1657
+ await this.options.reports.saveReports(collected);
1658
+ const combined = await this.options.reports.generateCombined(collected, {
1659
+ objective,
1660
+ pipelineRunId: run.id,
1661
+ taskIds
1662
+ });
1663
+ combinedReportId = combined?.id;
1664
+ if (collected.length > 0) {
1665
+ run.reports = collected;
1666
+ }
1667
+ }
1331
1668
  const finished = {
1332
1669
  ...run,
1333
1670
  status: finalStatus,
1334
1671
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
1335
- ...allReports.length > 0 ? { reports: allReports } : {}
1672
+ ...run.reports !== void 0 && run.reports.length > 0 ? { reports: run.reports } : {},
1673
+ ...combinedReportId !== void 0 ? { combinedReportId } : {}
1336
1674
  };
1337
1675
  await this.options.store.save(finished);
1338
1676
  this.log.info(`Pipeline ${def.id} finished: ${finalStatus}.`);
@@ -1512,8 +1850,8 @@ function createDefaultChatFn(providers) {
1512
1850
  }
1513
1851
 
1514
1852
  // src/pipelines/store.ts
1515
- import { mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "fs/promises";
1516
- import { dirname as dirname2 } from "path";
1853
+ import { mkdir as mkdir3, readFile as readFile3, rename as rename3, writeFile as writeFile3 } from "fs/promises";
1854
+ import { dirname as dirname3 } from "path";
1517
1855
  var FilePipelineRunStore = class {
1518
1856
  filePath;
1519
1857
  queue = Promise.resolve();
@@ -1551,7 +1889,7 @@ var FilePipelineRunStore = class {
1551
1889
  }
1552
1890
  async load() {
1553
1891
  try {
1554
- const raw = await readFile2(this.filePath, "utf8");
1892
+ const raw = await readFile3(this.filePath, "utf8");
1555
1893
  const parsed = JSON.parse(raw);
1556
1894
  return Array.isArray(parsed) ? parsed : [];
1557
1895
  } catch {
@@ -1560,10 +1898,302 @@ var FilePipelineRunStore = class {
1560
1898
  }
1561
1899
  async write(runs) {
1562
1900
  const tmp = `${this.filePath}.tmp`;
1563
- await mkdir2(dirname2(this.filePath), { recursive: true });
1564
- await writeFile2(tmp, `${JSON.stringify(runs, null, 2)}
1901
+ await mkdir3(dirname3(this.filePath), { recursive: true });
1902
+ await writeFile3(tmp, `${JSON.stringify(runs, null, 2)}
1565
1903
  `, "utf8");
1566
- await rename2(tmp, this.filePath);
1904
+ await rename3(tmp, this.filePath);
1905
+ }
1906
+ };
1907
+
1908
+ // src/reports/aggregate.ts
1909
+ import { randomBytes as randomBytes3 } from "crypto";
1910
+ var SEVERITY_RANK = {
1911
+ info: 0,
1912
+ low: 1,
1913
+ medium: 2,
1914
+ high: 3,
1915
+ critical: 4
1916
+ };
1917
+ function aggregateReports(reports, options = {}) {
1918
+ const threshold = options.similarityThreshold ?? 0.7;
1919
+ const findingGroups = [];
1920
+ const recommendationGroups = [];
1921
+ for (const report of reports) {
1922
+ for (const finding of report.findings) {
1923
+ mergeFinding(findingGroups, finding, report, threshold);
1924
+ }
1925
+ for (const text of report.recommendations) {
1926
+ mergeRecommendation(recommendationGroups, text, report, threshold);
1927
+ }
1928
+ }
1929
+ const sources = [...new Set(reports.map((r) => r.agent))].sort();
1930
+ const totalRawFindings = reports.reduce((sum, r) => sum + r.findings.length, 0);
1931
+ const summary = `${reports.length} report(s) from ${sources.length > 0 ? sources.join(", ") : "no agents"}: ${findingGroups.length} unique finding group(s) (${totalRawFindings} raw), ${recommendationGroups.length} recommendation group(s).`;
1932
+ return {
1933
+ id: `combined-${Date.now().toString(36)}-${randomBytes3(3).toString("hex")}`,
1934
+ title: options.objective !== void 0 ? truncate(options.objective, 120) : "Combined report",
1935
+ ...options.objective !== void 0 ? { objective: options.objective } : {},
1936
+ ...options.pipelineRunId !== void 0 ? { pipelineRunId: options.pipelineRunId } : {},
1937
+ taskIds: options.taskIds ?? [],
1938
+ summary,
1939
+ findings: findingGroups.sort((a, b) => rank(b) - rank(a)),
1940
+ recommendations: recommendationGroups,
1941
+ sources,
1942
+ reportIds: reports.map((r) => r.id),
1943
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1944
+ };
1945
+ }
1946
+ function rank(finding) {
1947
+ return finding.severity ? SEVERITY_RANK[finding.severity] : -1;
1948
+ }
1949
+ function mergeFinding(groups, finding, report, threshold) {
1950
+ const source = { agent: report.agent, reportId: report.id };
1951
+ const group = groups.find((g) => similar(g.title, finding.title, threshold));
1952
+ if (group) {
1953
+ group.occurrences += 1;
1954
+ group.details.push(finding.detail);
1955
+ group.sources.push(source);
1956
+ group.evidence.push(...finding.evidence ?? []);
1957
+ if (finding.severity && (!group.severity || SEVERITY_RANK[finding.severity] > SEVERITY_RANK[group.severity])) {
1958
+ group.severity = finding.severity;
1959
+ }
1960
+ return;
1961
+ }
1962
+ groups.push({
1963
+ title: finding.title,
1964
+ details: [finding.detail],
1965
+ sources: [source],
1966
+ occurrences: 1,
1967
+ ...finding.severity ? { severity: finding.severity } : {},
1968
+ evidence: [...finding.evidence ?? []]
1969
+ });
1970
+ }
1971
+ function mergeRecommendation(groups, text, report, threshold) {
1972
+ const source = { agent: report.agent, reportId: report.id };
1973
+ const group = groups.find((g) => similar(g.text, text, threshold));
1974
+ if (group) {
1975
+ group.occurrences += 1;
1976
+ group.sources.push(source);
1977
+ return;
1978
+ }
1979
+ groups.push({ text, sources: [source], occurrences: 1 });
1980
+ }
1981
+ function similar(a, b, threshold) {
1982
+ const left = tokens(a);
1983
+ const right = tokens(b);
1984
+ if (left.size === 0 || right.size === 0) return false;
1985
+ let intersection = 0;
1986
+ for (const token of left) {
1987
+ if (right.has(token)) intersection += 1;
1988
+ }
1989
+ const union = left.size + right.size - intersection;
1990
+ return intersection / union >= threshold;
1991
+ }
1992
+ function tokens(text) {
1993
+ return new Set(
1994
+ text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= 3)
1995
+ );
1996
+ }
1997
+ function truncate(text, max) {
1998
+ return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
1999
+ }
2000
+
2001
+ // src/reports/extract.ts
2002
+ var BULLET = /^(?:[-*•]|\d+[.)])\s+(.*)$/;
2003
+ function parseSections(raw) {
2004
+ const sections = [];
2005
+ let current = { heading: "", bullets: [] };
2006
+ for (const line of raw.split("\n")) {
2007
+ const heading = /^(#{1,6})\s+(.*)$/.exec(line.trim());
2008
+ if (heading) {
2009
+ if (current.heading.length > 0 || current.bullets.length > 0) {
2010
+ sections.push(current);
2011
+ }
2012
+ current = { heading: heading[2]?.toLowerCase() ?? "", bullets: [] };
2013
+ continue;
2014
+ }
2015
+ const bullet = BULLET.exec(line.trim());
2016
+ if (bullet && current.heading.length > 0) {
2017
+ current.bullets.push(bullet[1]?.trim() ?? "");
2018
+ }
2019
+ }
2020
+ if (current.heading.length > 0 || current.bullets.length > 0) {
2021
+ sections.push(current);
2022
+ }
2023
+ return sections;
2024
+ }
2025
+ function collectBullets(sections, patterns) {
2026
+ const matches = sections.filter((section) => patterns.some((p) => p.test(section.heading)));
2027
+ return matches.flatMap((section) => section.bullets).filter((b) => b.length > 0);
2028
+ }
2029
+ function guessSeverity(text) {
2030
+ if (/critical|severe/i.test(text)) return "critical";
2031
+ if (/\bhigh\b|major/i.test(text)) return "high";
2032
+ if (/medium|moderate/i.test(text)) return "medium";
2033
+ if (/\blow\b|minor/i.test(text)) return "low";
2034
+ return void 0;
2035
+ }
2036
+ function extractReportFromText(raw, meta) {
2037
+ const trimmed = raw.trim();
2038
+ const nonEmpty = trimmed.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
2039
+ const proseLine = nonEmpty.find((line) => !line.startsWith("#") && !BULLET.test(line)) ?? nonEmpty[0] ?? "";
2040
+ const cleaned = proseLine.replace(/^#{1,6}\s+/, "").replace(BULLET, "$1");
2041
+ const summary = cleaned.length > 0 ? truncate2(cleaned, 200) : "(no output)";
2042
+ const sections = parseSections(trimmed);
2043
+ const findingTexts = collectBullets(sections, [/finding/, /issue/, /problem/, /vulnerab/]);
2044
+ const recommendationTexts = collectBullets(sections, [/recommend/, /suggestion/, /next step/, /action/]);
2045
+ const evidenceTexts = collectBullets(sections, [/evidence/, /proof/, /log/]);
2046
+ const findings = findingTexts.map((text, index) => ({
2047
+ id: `${meta.taskId}-f-${index + 1}`,
2048
+ title: truncate2(text, 120),
2049
+ detail: text,
2050
+ severity: guessSeverity(text)
2051
+ }));
2052
+ return {
2053
+ id: `${meta.taskId}-report`,
2054
+ taskId: meta.taskId,
2055
+ agent: meta.agent,
2056
+ ...meta.model !== void 0 ? { model: meta.model } : {},
2057
+ summary,
2058
+ findings,
2059
+ recommendations: recommendationTexts.map((t) => truncate2(t, 200)),
2060
+ metadata: evidenceTexts.length > 0 ? { evidence: evidenceTexts } : void 0,
2061
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2062
+ };
2063
+ }
2064
+ function truncate2(text, max) {
2065
+ return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
2066
+ }
2067
+
2068
+ // src/reports/service.ts
2069
+ var ReportService = class {
2070
+ constructor(store, log = logger) {
2071
+ this.store = store;
2072
+ this.log = log.child({ component: "reports" });
2073
+ }
2074
+ store;
2075
+ log;
2076
+ get storeRef() {
2077
+ return this.store;
2078
+ }
2079
+ /** Normalizes finished tasks into reports (§17 — normalizer). */
2080
+ async collectFromTasks(tasks) {
2081
+ const reports = [];
2082
+ for (const task of tasks) {
2083
+ const structured = task.result?.reports ?? [];
2084
+ if (structured.length > 0) {
2085
+ reports.push(...structured);
2086
+ continue;
2087
+ }
2088
+ const text = task.result?.rawOutput ?? task.result?.summary ?? "";
2089
+ if (text.trim().length === 0) continue;
2090
+ reports.push(
2091
+ extractReportFromText(text, {
2092
+ taskId: task.id,
2093
+ agent: task.agent ?? "unknown"
2094
+ })
2095
+ );
2096
+ }
2097
+ return reports;
2098
+ }
2099
+ async saveReports(reports) {
2100
+ if (reports.length === 0) return;
2101
+ await this.store.saveReports(reports);
2102
+ this.log.info(`Stored ${reports.length} report(s).`);
2103
+ }
2104
+ async generateCombined(reports, meta = {}) {
2105
+ if (reports.length === 0) return void 0;
2106
+ const options = {
2107
+ ...meta.objective !== void 0 ? { objective: meta.objective } : {},
2108
+ ...meta.pipelineRunId !== void 0 ? { pipelineRunId: meta.pipelineRunId } : {},
2109
+ ...meta.taskIds !== void 0 ? { taskIds: meta.taskIds } : {}
2110
+ };
2111
+ const combined = aggregateReports(reports, options);
2112
+ await this.store.saveCombined(combined);
2113
+ this.log.info(`Generated combined report ${combined.id}.`);
2114
+ return combined;
2115
+ }
2116
+ };
2117
+
2118
+ // src/reports/store.ts
2119
+ import { mkdir as mkdir4, readFile as readFile4, rename as rename4, writeFile as writeFile4 } from "fs/promises";
2120
+ import { dirname as dirname4 } from "path";
2121
+ var FileReportStore = class {
2122
+ filePath;
2123
+ queue = Promise.resolve();
2124
+ constructor(directory = `${process.cwd()}/.omninode`) {
2125
+ this.filePath = `${directory}/reports.json`;
2126
+ }
2127
+ async saveReports(reports) {
2128
+ return this.synchronized(async () => {
2129
+ const entries = await this.load();
2130
+ for (const report of reports) {
2131
+ const index = entries.findIndex(
2132
+ (e) => e.kind === "report" && e.report.id === report.id
2133
+ );
2134
+ const entry = { kind: "report", report };
2135
+ if (index >= 0) entries[index] = entry;
2136
+ else entries.push(entry);
2137
+ }
2138
+ await this.write(entries);
2139
+ });
2140
+ }
2141
+ async listReports(filter = {}) {
2142
+ return this.synchronized(async () => {
2143
+ const entries = await this.load();
2144
+ return entries.filter((e) => e.kind === "report").map((e) => e.report).filter((report) => filter.taskId ? report.taskId === filter.taskId : true).filter((report) => filter.agent ? report.agent === filter.agent : true).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
2145
+ });
2146
+ }
2147
+ async saveCombined(combined) {
2148
+ return this.synchronized(async () => {
2149
+ const entries = await this.load();
2150
+ const index = entries.findIndex(
2151
+ (e) => e.kind === "combined" && e.combined.id === combined.id
2152
+ );
2153
+ const entry = { kind: "combined", combined };
2154
+ if (index >= 0) entries[index] = entry;
2155
+ else entries.push(entry);
2156
+ await this.write(entries);
2157
+ });
2158
+ }
2159
+ async getCombined(id) {
2160
+ return this.synchronized(async () => {
2161
+ const entries = await this.load();
2162
+ const found = entries.find((e) => e.kind === "combined" && e.combined.id === id);
2163
+ return found?.kind === "combined" ? found.combined : void 0;
2164
+ });
2165
+ }
2166
+ async listCombined(filter = {}) {
2167
+ return this.synchronized(async () => {
2168
+ const entries = await this.load();
2169
+ return entries.filter((e) => e.kind === "combined").map((e) => e.combined).filter(
2170
+ (combined) => filter.pipelineRunId ? combined.pipelineRunId === filter.pipelineRunId : true
2171
+ ).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
2172
+ });
2173
+ }
2174
+ synchronized(operation) {
2175
+ const result = this.queue.then(operation, operation);
2176
+ this.queue = result.then(
2177
+ () => void 0,
2178
+ () => void 0
2179
+ );
2180
+ return result;
2181
+ }
2182
+ async load() {
2183
+ try {
2184
+ const raw = await readFile4(this.filePath, "utf8");
2185
+ const parsed = JSON.parse(raw);
2186
+ return Array.isArray(parsed) ? parsed : [];
2187
+ } catch {
2188
+ return [];
2189
+ }
2190
+ }
2191
+ async write(entries) {
2192
+ const tmp = `${this.filePath}.tmp`;
2193
+ await mkdir4(dirname4(this.filePath), { recursive: true });
2194
+ await writeFile4(tmp, `${JSON.stringify(entries, null, 2)}
2195
+ `, "utf8");
2196
+ await rename4(tmp, this.filePath);
1567
2197
  }
1568
2198
  };
1569
2199
 
@@ -1575,6 +2205,7 @@ function buildPipelineEngine(config, log = logger) {
1575
2205
  agents,
1576
2206
  roles,
1577
2207
  store: new FileTaskStore(),
2208
+ ...config.project.memory !== void 0 ? { memory: new MemoryService(createMemoryProvider(config.project.memory), log) } : {},
1578
2209
  log
1579
2210
  });
1580
2211
  return new PipelineEngine({
@@ -1584,21 +2215,25 @@ function buildPipelineEngine(config, log = logger) {
1584
2215
  providers: config.project.providers,
1585
2216
  store: new FilePipelineRunStore(),
1586
2217
  chat: createDefaultChatFn(config.project.providers),
2218
+ reports: new ReportService(new FileReportStore(), log),
1587
2219
  log
1588
2220
  });
1589
2221
  }
1590
2222
 
1591
2223
  // src/version.ts
1592
- var OMNINODE_VERSION = "0.6.0";
2224
+ var OMNINODE_VERSION = "0.8.0";
1593
2225
  export {
1594
2226
  AgentError,
1595
2227
  AgentRegistry,
1596
2228
  ConfigError,
1597
2229
  ConsoleLogSink,
1598
2230
  FilePipelineRunStore,
2231
+ FileReportStore,
1599
2232
  FileTaskStore,
2233
+ LocalMemoryProvider,
1600
2234
  Logger,
1601
2235
  MemoryError,
2236
+ MemoryService,
1602
2237
  ModelRegistry,
1603
2238
  OMNINODE_VERSION,
1604
2239
  OPENAI_COMPATIBLE_TYPES,
@@ -1610,10 +2245,13 @@ export {
1610
2245
  ProcessAgent,
1611
2246
  ProtocolError,
1612
2247
  ProviderError,
2248
+ RememberaMemoryProvider,
2249
+ ReportService,
1613
2250
  RoleRegistry,
1614
2251
  TaskEngine,
1615
2252
  TaskError,
1616
2253
  agentConfigSchema,
2254
+ aggregateReports,
1617
2255
  appConfigSchema,
1618
2256
  authHeaders,
1619
2257
  buildAgentRegistry,
@@ -1622,12 +2260,15 @@ export {
1622
2260
  composeTaskText,
1623
2261
  createAgent,
1624
2262
  createDefaultChatFn,
2263
+ createMemoryProvider,
1625
2264
  createProvider,
1626
2265
  createTaskEngine,
1627
2266
  defaultProjectConfigYaml,
1628
2267
  expandEnvRefs,
2268
+ extractReportFromText,
1629
2269
  findConfigFile,
1630
2270
  isOmniNodeError,
2271
+ keywords,
1631
2272
  loadConfig,
1632
2273
  logger,
1633
2274
  pipelineConfigSchema,
@@ -1637,6 +2278,7 @@ export {
1637
2278
  registryKey,
1638
2279
  requestJson,
1639
2280
  resolveApiKey,
1640
- roleConfigSchema
2281
+ roleConfigSchema,
2282
+ similar
1641
2283
  };
1642
2284
  //# sourceMappingURL=index.js.map