@merittdev/horus 0.10.0 → 0.12.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.cjs +990 -214
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -65880,8 +65880,8 @@ init_cjs_shims();
|
|
|
65880
65880
|
|
|
65881
65881
|
// ../../packages/core/src/version.ts
|
|
65882
65882
|
init_cjs_shims();
|
|
65883
|
-
var HORUS_VERSION = true ? "0.
|
|
65884
|
-
var PINNED_SOURCE_VERSION = "
|
|
65883
|
+
var HORUS_VERSION = true ? "0.12.0" : "dev";
|
|
65884
|
+
var PINNED_SOURCE_VERSION = "2.0.0";
|
|
65885
65885
|
|
|
65886
65886
|
// ../../packages/core/src/evidence.ts
|
|
65887
65887
|
init_cjs_shims();
|
|
@@ -70251,6 +70251,22 @@ var connectorsSchema = external_exports.object({
|
|
|
70251
70251
|
/** Env var holding the base URL. Defaults to "SENTRY_URL". */
|
|
70252
70252
|
urlEnv: external_exports.string().optional()
|
|
70253
70253
|
}).optional(),
|
|
70254
|
+
/**
|
|
70255
|
+
* Axiom logs connector (HOR-429). Structured log rows queried with APL become
|
|
70256
|
+
* `kind: 'log'` evidence (message + level + fields), alongside Elasticsearch.
|
|
70257
|
+
*/
|
|
70258
|
+
axiom: external_exports.object({
|
|
70259
|
+
/** Dataset to query (the Axiom analogue of an ES index). */
|
|
70260
|
+
dataset: external_exports.string(),
|
|
70261
|
+
/** Direct API token (takes priority over tokenEnv). */
|
|
70262
|
+
token: external_exports.string().optional(),
|
|
70263
|
+
/** Env var holding the API token. Defaults to "AXIOM_TOKEN". */
|
|
70264
|
+
tokenEnv: external_exports.string().optional(),
|
|
70265
|
+
/** Direct base URL (US default https://api.axiom.co; EU https://api.eu.axiom.co). Takes priority over urlEnv. */
|
|
70266
|
+
url: external_exports.string().optional(),
|
|
70267
|
+
/** Env var holding the base URL. Defaults to "AXIOM_URL". */
|
|
70268
|
+
urlEnv: external_exports.string().optional()
|
|
70269
|
+
}).optional(),
|
|
70254
70270
|
grafana: external_exports.object({
|
|
70255
70271
|
dashboard: external_exports.string().optional(),
|
|
70256
70272
|
/** Multiple dashboard UIDs to fetch. Takes precedence over `dashboard` when set. */
|
|
@@ -70469,6 +70485,16 @@ function resolveEnvironment(config2, opts) {
|
|
|
70469
70485
|
...url !== void 0 ? { url } : {}
|
|
70470
70486
|
};
|
|
70471
70487
|
}
|
|
70488
|
+
if (c4.axiom !== void 0) {
|
|
70489
|
+
const a2 = c4.axiom;
|
|
70490
|
+
const token = a2.token ?? process.env[a2.tokenEnv ?? "AXIOM_TOKEN"];
|
|
70491
|
+
const url = a2.url ?? process.env[a2.urlEnv ?? "AXIOM_URL"];
|
|
70492
|
+
resolved.axiom = {
|
|
70493
|
+
dataset: a2.dataset,
|
|
70494
|
+
...token !== void 0 ? { token } : {},
|
|
70495
|
+
...url !== void 0 ? { url } : {}
|
|
70496
|
+
};
|
|
70497
|
+
}
|
|
70472
70498
|
if (c4.grafana !== void 0) {
|
|
70473
70499
|
const g4 = c4.grafana;
|
|
70474
70500
|
resolved.grafana = {
|
|
@@ -70963,8 +70989,72 @@ var SourceHttpClient = class {
|
|
|
70963
70989
|
return this.request("POST", "/api/diff", { base: base2, compare });
|
|
70964
70990
|
}
|
|
70965
70991
|
async nodeCount() {
|
|
70966
|
-
const
|
|
70967
|
-
return Number(
|
|
70992
|
+
const o5 = await this.overview();
|
|
70993
|
+
return Number(o5.totalNodes) || 0;
|
|
70994
|
+
}
|
|
70995
|
+
// -------------------------------------------------------------------------
|
|
70996
|
+
// Typed read-path endpoints (HOR-392). These replace the CLI's raw Cypher:
|
|
70997
|
+
// the host owns the queries and returns shaped JSON, so the client never
|
|
70998
|
+
// emits or escapes Cypher on the read path.
|
|
70999
|
+
// -------------------------------------------------------------------------
|
|
71000
|
+
/** Nodes whose full content contains ANY of `tokens` — full (untruncated) content. */
|
|
71001
|
+
async contentSearch(tokens, limit = 500) {
|
|
71002
|
+
const res = await this.request("POST", "/api/content-search", {
|
|
71003
|
+
tokens,
|
|
71004
|
+
limit
|
|
71005
|
+
});
|
|
71006
|
+
return res.results ?? [];
|
|
71007
|
+
}
|
|
71008
|
+
/** Exact-name symbol lookup (file label excluded), with line ranges. */
|
|
71009
|
+
async exactSymbols(name2, limit = 10) {
|
|
71010
|
+
const res = await this.request(
|
|
71011
|
+
"GET",
|
|
71012
|
+
`/api/symbols/exact?name=${encodeURIComponent(name2)}&limit=${limit}`
|
|
71013
|
+
);
|
|
71014
|
+
return res.results ?? [];
|
|
71015
|
+
}
|
|
71016
|
+
/** Symbol nodes for the given comma-joinable lowercase labels (source-graph extraction). */
|
|
71017
|
+
async symbolsByLabel(labels, limit = 1e3) {
|
|
71018
|
+
const res = await this.request(
|
|
71019
|
+
"GET",
|
|
71020
|
+
`/api/symbols?labels=${encodeURIComponent(labels.join(","))}&limit=${limit}`
|
|
71021
|
+
);
|
|
71022
|
+
return res.symbols ?? [];
|
|
71023
|
+
}
|
|
71024
|
+
/** Batch-resolve node ids to their line ranges (CLI line hydration). */
|
|
71025
|
+
async nodesLines(ids2) {
|
|
71026
|
+
const res = await this.request(
|
|
71027
|
+
"POST",
|
|
71028
|
+
"/api/nodes/lines",
|
|
71029
|
+
{ ids: ids2 }
|
|
71030
|
+
);
|
|
71031
|
+
return res.lines ?? {};
|
|
71032
|
+
}
|
|
71033
|
+
/** Process flows a symbol participates in, with each flow's named ordered steps. */
|
|
71034
|
+
flows(nodeId) {
|
|
71035
|
+
return this.request("GET", `/api/flows/${encodeURI(nodeId)}`);
|
|
71036
|
+
}
|
|
71037
|
+
/** Method symbols of `className` defined in `file`, ordered by start line. */
|
|
71038
|
+
async classMethods(file, className) {
|
|
71039
|
+
const res = await this.request(
|
|
71040
|
+
"GET",
|
|
71041
|
+
`/api/class-methods?file=${encodeURIComponent(file)}&class=${encodeURIComponent(className)}`
|
|
71042
|
+
);
|
|
71043
|
+
return res.methods ?? [];
|
|
71044
|
+
}
|
|
71045
|
+
/** Extended node detail — node + content + callers/callees/typeRefs + imports + coupling + communities. */
|
|
71046
|
+
node(nodeId) {
|
|
71047
|
+
return this.request("GET", `/api/node/${encodeURI(nodeId)}`);
|
|
71048
|
+
}
|
|
71049
|
+
/** Community clusters with their member nodes (source-graph extraction). */
|
|
71050
|
+
async communities() {
|
|
71051
|
+
const res = await this.request("GET", "/api/communities");
|
|
71052
|
+
return res.communities ?? [];
|
|
71053
|
+
}
|
|
71054
|
+
/** Discovered execution processes with their ordered steps (source-graph extraction). */
|
|
71055
|
+
async processes() {
|
|
71056
|
+
const res = await this.request("GET", "/api/processes");
|
|
71057
|
+
return res.processes ?? [];
|
|
70968
71058
|
}
|
|
70969
71059
|
// -------------------------------------------------------------------------
|
|
70970
71060
|
// Memory vector bridge (M2). These reuse `request<T>` (retry/timeout/4xx) so
|
|
@@ -71245,13 +71335,6 @@ var SourceCodeProvider = class {
|
|
|
71245
71335
|
id = "source";
|
|
71246
71336
|
kind = "code";
|
|
71247
71337
|
// --- helpers -------------------------------------------------------------
|
|
71248
|
-
/**
|
|
71249
|
-
* Escape a node id for inlining inside a double-quoted Cypher string literal:
|
|
71250
|
-
* first double every backslash, then escape every double-quote.
|
|
71251
|
-
*/
|
|
71252
|
-
escapeId(id) {
|
|
71253
|
-
return id.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
71254
|
-
}
|
|
71255
71338
|
nodeToSymbol(n4) {
|
|
71256
71339
|
return {
|
|
71257
71340
|
id: n4.id,
|
|
@@ -71264,29 +71347,34 @@ var SourceCodeProvider = class {
|
|
|
71264
71347
|
className: n4.className
|
|
71265
71348
|
};
|
|
71266
71349
|
}
|
|
71267
|
-
|
|
71350
|
+
/** Map a serialized graph node to a `Symbol`, coercing the host's `0` line sentinel to undefined. */
|
|
71351
|
+
serializedToSymbol(n4) {
|
|
71352
|
+
const sym = {
|
|
71353
|
+
id: n4.id ?? "",
|
|
71354
|
+
name: n4.name ?? "",
|
|
71355
|
+
filePath: n4.filePath ?? ""
|
|
71356
|
+
};
|
|
71357
|
+
if (typeof n4.startLine === "number" && n4.startLine > 0) sym.startLine = n4.startLine;
|
|
71358
|
+
if (typeof n4.endLine === "number" && n4.endLine > 0) sym.endLine = n4.endLine;
|
|
71359
|
+
if (n4.className) sym.className = n4.className;
|
|
71360
|
+
if (n4.signature) sym.signature = n4.signature;
|
|
71361
|
+
if (n4.language) sym.language = n4.language;
|
|
71362
|
+
return sym;
|
|
71363
|
+
}
|
|
71364
|
+
/**
|
|
71365
|
+
* Graceful degradation (HOR-353): a 4xx for THIS repo's graph shape must not abort the
|
|
71366
|
+
* whole investigation. The caller passes a default the slice degrades to.
|
|
71367
|
+
*/
|
|
71368
|
+
async degrade(p5, fallback) {
|
|
71268
71369
|
try {
|
|
71269
|
-
return
|
|
71370
|
+
return await p5;
|
|
71270
71371
|
} catch (err2) {
|
|
71271
71372
|
if (err2 instanceof SourceHttpError && err2.status >= 400 && err2.status < 500) {
|
|
71272
|
-
return
|
|
71373
|
+
return fallback;
|
|
71273
71374
|
}
|
|
71274
71375
|
throw err2;
|
|
71275
71376
|
}
|
|
71276
71377
|
}
|
|
71277
|
-
cypherRowToSymbol(row, idIdx, nameIdx, fileIdx, lineIdx, classNameIdx) {
|
|
71278
|
-
const startLine = lineIdx != null && row[lineIdx] != null ? Number(row[lineIdx]) : void 0;
|
|
71279
|
-
const sym = {
|
|
71280
|
-
id: String(row[idIdx] ?? ""),
|
|
71281
|
-
name: String(row[nameIdx] ?? ""),
|
|
71282
|
-
filePath: String(row[fileIdx] ?? ""),
|
|
71283
|
-
startLine
|
|
71284
|
-
};
|
|
71285
|
-
if (classNameIdx != null && row[classNameIdx] != null) {
|
|
71286
|
-
sym.className = String(row[classNameIdx]);
|
|
71287
|
-
}
|
|
71288
|
-
return sym;
|
|
71289
|
-
}
|
|
71290
71378
|
// --- contract ------------------------------------------------------------
|
|
71291
71379
|
async health() {
|
|
71292
71380
|
const h3 = await this.client.health();
|
|
@@ -71296,19 +71384,22 @@ var SourceCodeProvider = class {
|
|
|
71296
71384
|
};
|
|
71297
71385
|
}
|
|
71298
71386
|
async searchSymbols(query, limit = 10) {
|
|
71299
|
-
const
|
|
71300
|
-
|
|
71301
|
-
const [exactRows, semanticRes] = await Promise.all([
|
|
71302
|
-
this.rows(exactQuery).catch(() => []),
|
|
71387
|
+
const [exactHits, semanticRes] = await Promise.all([
|
|
71388
|
+
this.degrade(this.client.exactSymbols(query, limit), []),
|
|
71303
71389
|
this.client.search(query, limit)
|
|
71304
71390
|
]);
|
|
71305
|
-
const exactSymbols =
|
|
71306
|
-
|
|
71307
|
-
|
|
71308
|
-
|
|
71309
|
-
|
|
71310
|
-
|
|
71311
|
-
|
|
71391
|
+
const exactSymbols = exactHits.map((h3) => {
|
|
71392
|
+
const sym = {
|
|
71393
|
+
id: h3.nodeId,
|
|
71394
|
+
name: h3.name,
|
|
71395
|
+
filePath: h3.filePath,
|
|
71396
|
+
score: 1
|
|
71397
|
+
// an exact-name hit is authoritative
|
|
71398
|
+
};
|
|
71399
|
+
if (h3.startLine > 0) sym.startLine = h3.startLine;
|
|
71400
|
+
if (h3.endLine > 0) sym.endLine = h3.endLine;
|
|
71401
|
+
return sym;
|
|
71402
|
+
});
|
|
71312
71403
|
const exactIds = new Set(exactSymbols.map((s5) => s5.id));
|
|
71313
71404
|
const ql = query.toLowerCase();
|
|
71314
71405
|
const semantic = semanticRes.filter((r) => !exactIds.has(r.nodeId)).map((r) => {
|
|
@@ -71330,78 +71421,54 @@ var SourceCodeProvider = class {
|
|
|
71330
71421
|
async hydrateLines(symbols) {
|
|
71331
71422
|
const ids2 = symbols.map((s5) => s5.id).filter(Boolean);
|
|
71332
71423
|
if (ids2.length === 0) return symbols;
|
|
71333
|
-
const
|
|
71334
|
-
const rows = await this.rows(`MATCH (n) WHERE n.id IN [${idList}] RETURN n.id, n.start_line, n.end_line`).catch(() => []);
|
|
71335
|
-
const lineById = /* @__PURE__ */ new Map();
|
|
71336
|
-
for (const row of rows) {
|
|
71337
|
-
const id = String(row[0] ?? "");
|
|
71338
|
-
const start2 = row[1] != null ? Number(row[1]) : void 0;
|
|
71339
|
-
const end = row[2] != null ? Number(row[2]) : void 0;
|
|
71340
|
-
lineById.set(id, { start: start2, end });
|
|
71341
|
-
}
|
|
71424
|
+
const lines = await this.degrade(this.client.nodesLines(ids2), {});
|
|
71342
71425
|
return symbols.map((s5) => {
|
|
71343
|
-
const l2 =
|
|
71426
|
+
const l2 = lines[s5.id];
|
|
71344
71427
|
if (l2 === void 0) return s5;
|
|
71345
71428
|
const out2 = { ...s5 };
|
|
71346
|
-
if (l2.
|
|
71347
|
-
if (l2.
|
|
71429
|
+
if (typeof l2.startLine === "number" && l2.startLine > 0) out2.startLine = l2.startLine;
|
|
71430
|
+
if (typeof l2.endLine === "number" && l2.endLine > 0) out2.endLine = l2.endLine;
|
|
71348
71431
|
return out2;
|
|
71349
71432
|
});
|
|
71350
71433
|
}
|
|
71351
71434
|
async context(symbolId) {
|
|
71352
|
-
const
|
|
71353
|
-
|
|
71354
|
-
|
|
71355
|
-
|
|
71356
|
-
|
|
71357
|
-
|
|
71358
|
-
|
|
71359
|
-
|
|
71360
|
-
|
|
71361
|
-
|
|
71362
|
-
|
|
71363
|
-
|
|
71364
|
-
|
|
71365
|
-
|
|
71366
|
-
]);
|
|
71367
|
-
const node = nodeRows[0];
|
|
71435
|
+
const detail = await this.degrade(this.client.node(symbolId), null);
|
|
71436
|
+
if (detail === null) {
|
|
71437
|
+
return {
|
|
71438
|
+
symbol: { id: symbolId, name: "", filePath: "" },
|
|
71439
|
+
callers: [],
|
|
71440
|
+
callees: [],
|
|
71441
|
+
imports: [],
|
|
71442
|
+
usesType: [],
|
|
71443
|
+
community: null,
|
|
71444
|
+
coupledWith: [],
|
|
71445
|
+
isDead: false
|
|
71446
|
+
};
|
|
71447
|
+
}
|
|
71448
|
+
const n4 = detail.node;
|
|
71368
71449
|
const symbol = {
|
|
71369
|
-
id:
|
|
71370
|
-
name:
|
|
71371
|
-
filePath:
|
|
71372
|
-
|
|
71373
|
-
|
|
71374
|
-
|
|
71375
|
-
|
|
71376
|
-
|
|
71377
|
-
|
|
71378
|
-
const
|
|
71379
|
-
const content = node?.[9];
|
|
71450
|
+
id: n4.id ?? symbolId,
|
|
71451
|
+
name: n4.name ?? "",
|
|
71452
|
+
filePath: n4.filePath ?? ""
|
|
71453
|
+
};
|
|
71454
|
+
if (typeof n4.startLine === "number" && n4.startLine > 0) symbol.startLine = n4.startLine;
|
|
71455
|
+
if (typeof n4.endLine === "number" && n4.endLine > 0) symbol.endLine = n4.endLine;
|
|
71456
|
+
if (n4.signature) symbol.signature = n4.signature;
|
|
71457
|
+
if (n4.className) symbol.className = n4.className;
|
|
71458
|
+
if (n4.language) symbol.language = n4.language;
|
|
71459
|
+
const content = n4.content;
|
|
71380
71460
|
const snippet = typeof content === "string" ? content.slice(0, 600) : void 0;
|
|
71381
71461
|
const sourceBody = typeof content === "string" ? content.slice(0, 2e4) : void 0;
|
|
71382
|
-
const callees =
|
|
71383
|
-
const callers =
|
|
71384
|
-
const usesType =
|
|
71385
|
-
const communityRow =
|
|
71386
|
-
const community = communityRow ? { id: String(communityRow
|
|
71387
|
-
|
|
71388
|
-
|
|
71389
|
-
|
|
71390
|
-
|
|
71391
|
-
|
|
71392
|
-
const FE = this.escapeId(String(fileId));
|
|
71393
|
-
const importsQuery = `MATCH (f)-[r:CodeRelation]->(g:File) WHERE f.id = "${FE}" AND r.rel_type = "imports" RETURN g.file_path`;
|
|
71394
|
-
const coupledQuery = `MATCH (f)-[r:CodeRelation]->(g:File) WHERE f.id = "${FE}" AND r.rel_type = "coupled_with" RETURN g.file_path, r.co_changes`;
|
|
71395
|
-
const [importRows, coupledRows] = await Promise.all([
|
|
71396
|
-
this.rows(importsQuery),
|
|
71397
|
-
this.rows(coupledQuery)
|
|
71398
|
-
]);
|
|
71399
|
-
imports = importRows.map((row) => String(row[0] ?? ""));
|
|
71400
|
-
coupledWith = coupledRows.map((row) => ({
|
|
71401
|
-
file: String(row[0] ?? ""),
|
|
71402
|
-
coChanges: Number(row[1] ?? 0)
|
|
71403
|
-
}));
|
|
71404
|
-
}
|
|
71462
|
+
const callees = (detail.callees ?? []).map((c4) => this.serializedToSymbol(c4.node));
|
|
71463
|
+
const callers = (detail.callers ?? []).map((c4) => this.serializedToSymbol(c4.node));
|
|
71464
|
+
const usesType = (detail.typeRefs ?? []).map((t2) => this.serializedToSymbol(t2));
|
|
71465
|
+
const communityRow = (detail.communities ?? [])[0];
|
|
71466
|
+
const community = communityRow ? { id: String(communityRow.id ?? ""), name: String(communityRow.name ?? "") } : null;
|
|
71467
|
+
const imports = (detail.imports ?? []).map((i3) => String(i3));
|
|
71468
|
+
const coupledWith = (detail.coupledWith ?? []).map((c4) => ({
|
|
71469
|
+
file: String(c4.file ?? ""),
|
|
71470
|
+
coChanges: Number(c4.coChanges ?? 0)
|
|
71471
|
+
}));
|
|
71405
71472
|
return {
|
|
71406
71473
|
symbol,
|
|
71407
71474
|
snippet,
|
|
@@ -71412,7 +71479,7 @@ var SourceCodeProvider = class {
|
|
|
71412
71479
|
usesType,
|
|
71413
71480
|
community,
|
|
71414
71481
|
coupledWith,
|
|
71415
|
-
isDead
|
|
71482
|
+
isDead: Boolean(n4.isDead)
|
|
71416
71483
|
};
|
|
71417
71484
|
}
|
|
71418
71485
|
async impact(symbolId, depth = 3) {
|
|
@@ -71427,21 +71494,22 @@ var SourceCodeProvider = class {
|
|
|
71427
71494
|
};
|
|
71428
71495
|
}
|
|
71429
71496
|
async flowsFor(symbolId) {
|
|
71430
|
-
const
|
|
71431
|
-
const
|
|
71432
|
-
|
|
71433
|
-
|
|
71434
|
-
|
|
71435
|
-
|
|
71436
|
-
|
|
71437
|
-
|
|
71438
|
-
|
|
71439
|
-
|
|
71440
|
-
|
|
71441
|
-
|
|
71442
|
-
|
|
71443
|
-
|
|
71444
|
-
|
|
71497
|
+
const res = await this.degrade(this.client.flows(symbolId), { processes: [], steps: [] });
|
|
71498
|
+
const steps = (res.steps ?? []).map((s5) => {
|
|
71499
|
+
const sym = { id: s5.nodeId, name: s5.name, filePath: s5.filePath };
|
|
71500
|
+
if (typeof s5.startLine === "number" && s5.startLine > 0) sym.startLine = s5.startLine;
|
|
71501
|
+
return sym;
|
|
71502
|
+
});
|
|
71503
|
+
return (res.processes ?? []).map((p5) => ({
|
|
71504
|
+
id: String(p5.id ?? ""),
|
|
71505
|
+
name: String(p5.name ?? ""),
|
|
71506
|
+
steps
|
|
71507
|
+
}));
|
|
71508
|
+
}
|
|
71509
|
+
/** Method symbols of a class (typed /api/class-methods). Used for class-seed walkthroughs. */
|
|
71510
|
+
async classMethods(file, className) {
|
|
71511
|
+
const methods = await this.degrade(this.client.classMethods(file, className), []);
|
|
71512
|
+
return methods.map((m5) => this.serializedToSymbol(m5)).filter((s5) => s5.id !== "" && s5.name !== "");
|
|
71445
71513
|
}
|
|
71446
71514
|
async detectChanges(diff) {
|
|
71447
71515
|
const d3 = await this.client.diff(diff.base, diff.compare);
|
|
@@ -73724,6 +73792,296 @@ function shortTs2(iso) {
|
|
|
73724
73792
|
return `${iso.slice(5, 10)} ${iso.slice(11, 16)}`;
|
|
73725
73793
|
}
|
|
73726
73794
|
|
|
73795
|
+
// ../../packages/connectors/src/axiom/client.ts
|
|
73796
|
+
init_cjs_shims();
|
|
73797
|
+
var REQUEST_TIMEOUT_MS2 = 8e3;
|
|
73798
|
+
var APL_PATH = "/v1/datasets/_apl?format=tabular";
|
|
73799
|
+
var AxiomClient = class {
|
|
73800
|
+
baseUrl;
|
|
73801
|
+
token;
|
|
73802
|
+
dataset;
|
|
73803
|
+
constructor(opts) {
|
|
73804
|
+
this.baseUrl = (opts.baseUrl ?? "https://api.axiom.co").replace(/\/$/, "");
|
|
73805
|
+
this.token = opts.token;
|
|
73806
|
+
this.dataset = opts.dataset;
|
|
73807
|
+
}
|
|
73808
|
+
async request(method, path, body2) {
|
|
73809
|
+
const url = `${this.baseUrl}${path}`;
|
|
73810
|
+
const init2 = {
|
|
73811
|
+
method,
|
|
73812
|
+
headers: {
|
|
73813
|
+
Authorization: `Bearer ${this.token}`,
|
|
73814
|
+
"Content-Type": "application/json"
|
|
73815
|
+
},
|
|
73816
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2)
|
|
73817
|
+
};
|
|
73818
|
+
if (body2 !== void 0) init2.body = JSON.stringify(body2);
|
|
73819
|
+
const res = await fetch(url, init2);
|
|
73820
|
+
if (!res.ok) {
|
|
73821
|
+
const text2 = await res.text().catch(() => "");
|
|
73822
|
+
throw new Error(`Axiom ${method} ${path} -> ${res.status}: ${text2.slice(0, 200)}`);
|
|
73823
|
+
}
|
|
73824
|
+
return res.json();
|
|
73825
|
+
}
|
|
73826
|
+
/**
|
|
73827
|
+
* List the datasets reachable with the configured token. Returns [] on any error so
|
|
73828
|
+
* callers degrade gracefully (never throws past here).
|
|
73829
|
+
*/
|
|
73830
|
+
async listDatasets() {
|
|
73831
|
+
try {
|
|
73832
|
+
const raw = await this.request("GET", "/v1/datasets");
|
|
73833
|
+
if (!Array.isArray(raw)) return [];
|
|
73834
|
+
return raw.map(parseDataset);
|
|
73835
|
+
} catch {
|
|
73836
|
+
return [];
|
|
73837
|
+
}
|
|
73838
|
+
}
|
|
73839
|
+
/**
|
|
73840
|
+
* Run an APL query over the configured time window. `startTime`/`endTime` are ISO
|
|
73841
|
+
* strings and bound the query server-side. Returns parsed log records, or [] on any
|
|
73842
|
+
* failure (never throws past here).
|
|
73843
|
+
*/
|
|
73844
|
+
async query(apl, startTime, endTime) {
|
|
73845
|
+
try {
|
|
73846
|
+
const raw = await this.request("POST", APL_PATH, { apl, startTime, endTime });
|
|
73847
|
+
return parseTabular(raw);
|
|
73848
|
+
} catch {
|
|
73849
|
+
return [];
|
|
73850
|
+
}
|
|
73851
|
+
}
|
|
73852
|
+
/**
|
|
73853
|
+
* Cheap reachability probe: list datasets. Returns a structured status; never throws.
|
|
73854
|
+
*/
|
|
73855
|
+
async health() {
|
|
73856
|
+
try {
|
|
73857
|
+
await this.request("GET", "/v1/datasets");
|
|
73858
|
+
return { ok: true, detail: `axiom reachable (${this.dataset})` };
|
|
73859
|
+
} catch (err2) {
|
|
73860
|
+
return { ok: false, detail: err2.message };
|
|
73861
|
+
}
|
|
73862
|
+
}
|
|
73863
|
+
};
|
|
73864
|
+
function parseDataset(raw) {
|
|
73865
|
+
const ds = { name: String(raw["name"] ?? "") };
|
|
73866
|
+
if (typeof raw["description"] === "string" && raw["description"] !== "") {
|
|
73867
|
+
ds.description = raw["description"];
|
|
73868
|
+
}
|
|
73869
|
+
return ds;
|
|
73870
|
+
}
|
|
73871
|
+
var AXIOM_ERROR_LEVELS = ["error", "warn", "fatal"];
|
|
73872
|
+
function datasetRef(dataset) {
|
|
73873
|
+
return `['${dataset.replace(/'/g, "")}']`;
|
|
73874
|
+
}
|
|
73875
|
+
function searchTerms(hintTerms) {
|
|
73876
|
+
return hintTerms.map((t2) => t2.trim()).filter((t2) => t2.length > 2);
|
|
73877
|
+
}
|
|
73878
|
+
function searchClause(hintTerms) {
|
|
73879
|
+
const terms = searchTerms(hintTerms);
|
|
73880
|
+
return terms.length > 0 ? ` | search ${terms.map((t2) => `"${t2.replace(/"/g, "")}"`).join(" or ")}` : "";
|
|
73881
|
+
}
|
|
73882
|
+
function errorLevelClause() {
|
|
73883
|
+
const levels = AXIOM_ERROR_LEVELS.map((l2) => `'${l2}'`).join(", ");
|
|
73884
|
+
return ` | where tolower(tostring(level)) in (${levels})`;
|
|
73885
|
+
}
|
|
73886
|
+
function buildApl(dataset, hintTerms = [], limit = 100) {
|
|
73887
|
+
const lim = Math.max(1, Math.min(limit, 1e3));
|
|
73888
|
+
return `${datasetRef(dataset)}${searchClause(hintTerms)} | sort by _time desc | limit ${lim}`;
|
|
73889
|
+
}
|
|
73890
|
+
function buildErrorSignatureApl(dataset, hintTerms = [], topN = 10) {
|
|
73891
|
+
const lim = Math.max(1, Math.min(topN, 1e3));
|
|
73892
|
+
return `${datasetRef(dataset)}${errorLevelClause()}${searchClause(hintTerms)} | summarize ['count'] = count(), ['_time'] = max(_time) by message, level | sort by ['count'] desc | limit ${lim}`;
|
|
73893
|
+
}
|
|
73894
|
+
function buildRecentErrorsApl(dataset, hintTerms = [], limit = 5) {
|
|
73895
|
+
const lim = Math.max(1, Math.min(limit, 1e3));
|
|
73896
|
+
return `${datasetRef(dataset)}${errorLevelClause()}${searchClause(hintTerms)} | sort by _time desc | limit ${lim}`;
|
|
73897
|
+
}
|
|
73898
|
+
function parseTabular(raw) {
|
|
73899
|
+
if (raw === null || raw === void 0 || typeof raw !== "object") return [];
|
|
73900
|
+
const tables = raw["tables"];
|
|
73901
|
+
if (!Array.isArray(tables)) return [];
|
|
73902
|
+
const records = [];
|
|
73903
|
+
for (const table of tables) {
|
|
73904
|
+
const fields = table["fields"];
|
|
73905
|
+
if (!Array.isArray(fields)) continue;
|
|
73906
|
+
const names = fields.map((f5) => String(f5["name"] ?? ""));
|
|
73907
|
+
const columns = table["columns"];
|
|
73908
|
+
const rows = table["rows"];
|
|
73909
|
+
if (Array.isArray(columns)) {
|
|
73910
|
+
const first = columns[0];
|
|
73911
|
+
const rowCount = Array.isArray(first) ? first.length : 0;
|
|
73912
|
+
for (let r = 0; r < rowCount; r++) {
|
|
73913
|
+
const row = {};
|
|
73914
|
+
for (let c4 = 0; c4 < names.length; c4++) {
|
|
73915
|
+
const col = columns[c4];
|
|
73916
|
+
if (Array.isArray(col)) row[names[c4]] = col[r];
|
|
73917
|
+
}
|
|
73918
|
+
records.push(toRecord(row));
|
|
73919
|
+
}
|
|
73920
|
+
} else if (Array.isArray(rows)) {
|
|
73921
|
+
for (const rowVals of rows) {
|
|
73922
|
+
if (!Array.isArray(rowVals)) continue;
|
|
73923
|
+
const row = {};
|
|
73924
|
+
for (let c4 = 0; c4 < names.length; c4++) {
|
|
73925
|
+
row[names[c4]] = rowVals[c4];
|
|
73926
|
+
}
|
|
73927
|
+
records.push(toRecord(row));
|
|
73928
|
+
}
|
|
73929
|
+
}
|
|
73930
|
+
}
|
|
73931
|
+
return records;
|
|
73932
|
+
}
|
|
73933
|
+
function toRecord(fields) {
|
|
73934
|
+
const rec = { fields };
|
|
73935
|
+
const t2 = fields["_time"];
|
|
73936
|
+
if (typeof t2 === "string") rec.timestamp = t2;
|
|
73937
|
+
return rec;
|
|
73938
|
+
}
|
|
73939
|
+
|
|
73940
|
+
// ../../packages/connectors/src/axiom/provider.ts
|
|
73941
|
+
init_cjs_shims();
|
|
73942
|
+
var DEFAULT_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
73943
|
+
var AxiomProvider = class {
|
|
73944
|
+
constructor(client, opts) {
|
|
73945
|
+
this.client = client;
|
|
73946
|
+
this.opts = opts;
|
|
73947
|
+
}
|
|
73948
|
+
client;
|
|
73949
|
+
opts;
|
|
73950
|
+
id = "axiom";
|
|
73951
|
+
kind = "logs";
|
|
73952
|
+
/**
|
|
73953
|
+
* Collect log rows over the window, INCIDENT-AWARE. Rather than a broad newest-first
|
|
73954
|
+
* scan (which on a noisy dataset returns the 15 freshest `info` rows and misses a
|
|
73955
|
+
* high-volume error storm), this runs two error-biased queries in parallel:
|
|
73956
|
+
*
|
|
73957
|
+
* 1. TOP ERROR SIGNATURES — `summarize count() + max(_time) by message, level` over
|
|
73958
|
+
* error/warn/fatal rows (AND the hint terms), highest-volume first. This is what
|
|
73959
|
+
* surfaces a 3k-row `level=error` signature that a newest-first scan buries.
|
|
73960
|
+
* 2. RECENT RAW ERRORS — a few newest error rows with full fields, for grounding.
|
|
73961
|
+
*
|
|
73962
|
+
* Signatures lead so high-volume errors aren't crowded out. When NO error-level row
|
|
73963
|
+
* matches (neither query returns anything) it broadens to the all-levels fallback so
|
|
73964
|
+
* non-error datasets still produce evidence. Best-effort: a failing query yields [].
|
|
73965
|
+
* Never throws.
|
|
73966
|
+
*/
|
|
73967
|
+
async collect(opts = {}) {
|
|
73968
|
+
const to = opts.to ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
73969
|
+
const from = opts.from ?? new Date(Date.parse(to) - (this.opts.windowMs ?? DEFAULT_WINDOW_MS)).toISOString();
|
|
73970
|
+
const hintTerms = opts.hintTerms ?? [];
|
|
73971
|
+
const limit = this.opts.limit ?? 100;
|
|
73972
|
+
const topN = Math.max(1, Math.min(limit, 10));
|
|
73973
|
+
const recentN = Math.max(1, Math.min(limit, 5));
|
|
73974
|
+
const [signatures, recent] = await Promise.all([
|
|
73975
|
+
this.client.query(buildErrorSignatureApl(this.opts.dataset, hintTerms, topN), from, to),
|
|
73976
|
+
this.client.query(buildRecentErrorsApl(this.opts.dataset, hintTerms, recentN), from, to)
|
|
73977
|
+
]);
|
|
73978
|
+
if (signatures.length === 0 && recent.length === 0) {
|
|
73979
|
+
return this.client.query(buildApl(this.opts.dataset, hintTerms, limit), from, to);
|
|
73980
|
+
}
|
|
73981
|
+
return [...signatures, ...recent];
|
|
73982
|
+
}
|
|
73983
|
+
/**
|
|
73984
|
+
* Synthesize Evidence from Axiom rows. One `kind: 'log'` Evidence per row, with the
|
|
73985
|
+
* row fields on the payload (under `source: 'axiom'`), a redacted message in the
|
|
73986
|
+
* title, and relevance weighted by hint-term match / recency / level.
|
|
73987
|
+
*/
|
|
73988
|
+
toEvidence(records, hintTerms, collectedAt) {
|
|
73989
|
+
const query = `axiom ${this.opts.dataset}`;
|
|
73990
|
+
return records.map(
|
|
73991
|
+
(rec, i3) => this.recordToEvidence(rec, hintTerms, query, collectedAt, i3)
|
|
73992
|
+
);
|
|
73993
|
+
}
|
|
73994
|
+
/**
|
|
73995
|
+
* One-shot evidence query: collect rows + convert to Evidence. Preferred entry point
|
|
73996
|
+
* for the investigation engine. Degrades to [] on any failure so a flaky Axiom never
|
|
73997
|
+
* aborts an investigation.
|
|
73998
|
+
*/
|
|
73999
|
+
async queryEvidence(opts = {}) {
|
|
74000
|
+
try {
|
|
74001
|
+
const hintTerms = opts.hintTerms ?? [];
|
|
74002
|
+
const records = await this.collect({
|
|
74003
|
+
...opts.from !== void 0 ? { from: opts.from } : {},
|
|
74004
|
+
...opts.to !== void 0 ? { to: opts.to } : {},
|
|
74005
|
+
hintTerms
|
|
74006
|
+
});
|
|
74007
|
+
return this.toEvidence(records, hintTerms, opts.collectedAt ?? (/* @__PURE__ */ new Date()).toISOString());
|
|
74008
|
+
} catch {
|
|
74009
|
+
return [];
|
|
74010
|
+
}
|
|
74011
|
+
}
|
|
74012
|
+
async health() {
|
|
74013
|
+
return this.client.health();
|
|
74014
|
+
}
|
|
74015
|
+
recordToEvidence(rec, hintTerms, query, collectedAt, index2) {
|
|
74016
|
+
const title = buildTitle2(rec, this.opts.dataset);
|
|
74017
|
+
const relevance = computeRelevance2(rec, hintTerms);
|
|
74018
|
+
const payload = {
|
|
74019
|
+
source: "axiom",
|
|
74020
|
+
dataset: this.opts.dataset,
|
|
74021
|
+
...rec.fields
|
|
74022
|
+
};
|
|
74023
|
+
const ev = {
|
|
74024
|
+
id: `ev_axiom_${index2}`,
|
|
74025
|
+
source: "logs",
|
|
74026
|
+
kind: "log",
|
|
74027
|
+
title,
|
|
74028
|
+
relevance,
|
|
74029
|
+
payload,
|
|
74030
|
+
links: {},
|
|
74031
|
+
provenance: { query, collectedAt }
|
|
74032
|
+
};
|
|
74033
|
+
if (rec.timestamp !== void 0) ev.timestamp = rec.timestamp;
|
|
74034
|
+
return ev;
|
|
74035
|
+
}
|
|
74036
|
+
};
|
|
74037
|
+
var MESSAGE_FIELDS = ["message", "msg", "body", "event", "log", "_raw"];
|
|
74038
|
+
var LEVEL_FIELDS = ["level", "severity", "lvl", "loglevel", "status"];
|
|
74039
|
+
function pickField2(fields, candidates) {
|
|
74040
|
+
for (const key of candidates) {
|
|
74041
|
+
const v4 = fields[key];
|
|
74042
|
+
if (typeof v4 === "string" && v4 !== "") return v4;
|
|
74043
|
+
if (typeof v4 === "number") return String(v4);
|
|
74044
|
+
}
|
|
74045
|
+
return void 0;
|
|
74046
|
+
}
|
|
74047
|
+
function buildTitle2(rec, dataset) {
|
|
74048
|
+
const msg = pickField2(rec.fields, MESSAGE_FIELDS);
|
|
74049
|
+
const level = pickField2(rec.fields, LEVEL_FIELDS);
|
|
74050
|
+
const lvl = level ? `[${level}] ` : "";
|
|
74051
|
+
const body2 = msg ? redactSecrets(msg) : "(log event)";
|
|
74052
|
+
const rawCount = rec.fields["count"];
|
|
74053
|
+
const count = typeof rawCount === "number" && Number.isFinite(rawCount) && rawCount > 1 ? rawCount : void 0;
|
|
74054
|
+
if (count !== void 0) {
|
|
74055
|
+
const latest = rec.timestamp ? ` (latest ${shortTs3(rec.timestamp)})` : "";
|
|
74056
|
+
return `Axiom ${dataset}: ${lvl}${body2} \xD7${count.toLocaleString("en-US")}${latest}`.slice(0, 220);
|
|
74057
|
+
}
|
|
74058
|
+
const ts = rec.timestamp ? ` \xB7 ${shortTs3(rec.timestamp)}` : "";
|
|
74059
|
+
return `Axiom ${dataset}: ${lvl}${body2}${ts}`.slice(0, 220);
|
|
74060
|
+
}
|
|
74061
|
+
function computeRelevance2(rec, hintTerms, now = Date.now()) {
|
|
74062
|
+
let score = 0.6;
|
|
74063
|
+
const hay = JSON.stringify(rec.fields).toLowerCase();
|
|
74064
|
+
const domainTerms = hintTerms.filter((t2) => t2.length > 2);
|
|
74065
|
+
if (domainTerms.some((t2) => hay.includes(t2.toLowerCase()))) score += 0.2;
|
|
74066
|
+
if (rec.timestamp) {
|
|
74067
|
+
const ageMs = now - Date.parse(rec.timestamp);
|
|
74068
|
+
if (Number.isFinite(ageMs)) {
|
|
74069
|
+
if (ageMs <= 864e5) score += 0.1;
|
|
74070
|
+
else if (ageMs <= 7 * 864e5) score += 0.05;
|
|
74071
|
+
}
|
|
74072
|
+
}
|
|
74073
|
+
const level = (pickField2(rec.fields, LEVEL_FIELDS) ?? "").toLowerCase();
|
|
74074
|
+
if (level === "error" || level === "fatal" || level === "critical") score += 0.1;
|
|
74075
|
+
else if (level === "warn" || level === "warning") score += 0.05;
|
|
74076
|
+
const count = rec.fields["count"];
|
|
74077
|
+
if (typeof count === "number" && count >= 10) score += 0.1;
|
|
74078
|
+
return Math.min(0.95, Math.max(0.5, score));
|
|
74079
|
+
}
|
|
74080
|
+
function shortTs3(iso) {
|
|
74081
|
+
if (!iso || iso.length < 16) return iso || "\u2014";
|
|
74082
|
+
return `${iso.slice(5, 10)} ${iso.slice(11, 16)}`;
|
|
74083
|
+
}
|
|
74084
|
+
|
|
73727
74085
|
// ../../packages/connectors/src/bullmq/client.ts
|
|
73728
74086
|
init_cjs_shims();
|
|
73729
74087
|
var import_ioredis = __toESM(require_built3(), 1);
|
|
@@ -74388,6 +74746,18 @@ function sentryForEnv(renv) {
|
|
|
74388
74746
|
{ org: s5.org, project: s5.project }
|
|
74389
74747
|
);
|
|
74390
74748
|
}
|
|
74749
|
+
function axiomForEnv(renv) {
|
|
74750
|
+
const a2 = renv.connectors.axiom;
|
|
74751
|
+
if (!a2 || !a2.token || !a2.dataset) return null;
|
|
74752
|
+
return new AxiomProvider(
|
|
74753
|
+
new AxiomClient({
|
|
74754
|
+
token: a2.token,
|
|
74755
|
+
dataset: a2.dataset,
|
|
74756
|
+
...a2.url !== void 0 ? { baseUrl: a2.url } : {}
|
|
74757
|
+
}),
|
|
74758
|
+
{ dataset: a2.dataset }
|
|
74759
|
+
);
|
|
74760
|
+
}
|
|
74391
74761
|
function codeForRepo(config2, repoName) {
|
|
74392
74762
|
const renv = resolveEnvironment(config2, { project: repoName });
|
|
74393
74763
|
const c4 = codeForEnv(renv);
|
|
@@ -74563,6 +74933,9 @@ init_cjs_shims();
|
|
|
74563
74933
|
// ../../packages/connectors/src/sentry/index.ts
|
|
74564
74934
|
init_cjs_shims();
|
|
74565
74935
|
|
|
74936
|
+
// ../../packages/connectors/src/axiom/index.ts
|
|
74937
|
+
init_cjs_shims();
|
|
74938
|
+
|
|
74566
74939
|
// ../../packages/connectors/src/bullmq/index.ts
|
|
74567
74940
|
init_cjs_shims();
|
|
74568
74941
|
|
|
@@ -90486,6 +90859,32 @@ async function checkEnv(renv, deps) {
|
|
|
90486
90859
|
` ${mark("pending")} ${import_picocolors3.default.bold("Sentry")} ${import_picocolors3.default.dim("not configured")}`
|
|
90487
90860
|
);
|
|
90488
90861
|
}
|
|
90862
|
+
const axiomCfg = renv.connectors.axiom;
|
|
90863
|
+
if (axiomCfg) {
|
|
90864
|
+
const label = axiomCfg.dataset ?? "(no dataset)";
|
|
90865
|
+
const axiom = (deps?.axiomFactory ?? axiomForEnv)(renv);
|
|
90866
|
+
if (axiom) {
|
|
90867
|
+
const h3 = await axiom.health();
|
|
90868
|
+
if (!h3.ok) {
|
|
90869
|
+
console.log(
|
|
90870
|
+
` ${mark(false)} ${import_picocolors3.default.bold("Axiom")} ${import_picocolors3.default.dim(`unreachable \xB7 ${label}`)}`
|
|
90871
|
+
);
|
|
90872
|
+
allOk = false;
|
|
90873
|
+
} else {
|
|
90874
|
+
console.log(
|
|
90875
|
+
` ${mark(true)} ${import_picocolors3.default.bold("Axiom")} ${import_picocolors3.default.dim(`reachable \xB7 ${label}`)}`
|
|
90876
|
+
);
|
|
90877
|
+
}
|
|
90878
|
+
} else {
|
|
90879
|
+
console.log(
|
|
90880
|
+
` ${mark(false)} ${import_picocolors3.default.bold("Axiom")} ${import_picocolors3.default.dim(`configured (${label}) but API token not set`)}`
|
|
90881
|
+
);
|
|
90882
|
+
}
|
|
90883
|
+
} else {
|
|
90884
|
+
console.log(
|
|
90885
|
+
` ${mark("pending")} ${import_picocolors3.default.bold("Axiom")} ${import_picocolors3.default.dim("not configured")}`
|
|
90886
|
+
);
|
|
90887
|
+
}
|
|
90489
90888
|
const redisCfg = renv.connectors.redis;
|
|
90490
90889
|
if (redisCfg?.url) {
|
|
90491
90890
|
const server = redisServerLabel(redisCfg.url);
|
|
@@ -92915,12 +93314,15 @@ function detectMissingEvidence(r, connectors = {}, mode) {
|
|
|
92915
93314
|
if (!hasLog && !sourceImpact) {
|
|
92916
93315
|
let logWhy;
|
|
92917
93316
|
let logNextSource;
|
|
92918
|
-
if (!connectors.elasticsearch && !connectors.sentry) {
|
|
92919
|
-
logWhy = "No Elasticsearch connector (nor Sentry) configured for this environment \u2014 no runtime error evidence.";
|
|
92920
|
-
logNextSource = "Add an `elasticsearch` and/or `
|
|
93317
|
+
if (!connectors.elasticsearch && !connectors.sentry && !connectors.axiom) {
|
|
93318
|
+
logWhy = "No Elasticsearch connector (nor Sentry / Axiom) configured for this environment \u2014 no runtime error evidence.";
|
|
93319
|
+
logNextSource = "Add an `elasticsearch`, `sentry`, and/or `axiom` connector to the project/environment";
|
|
92921
93320
|
} else if (!connectors.elasticsearch && connectors.sentry) {
|
|
92922
93321
|
logWhy = connectors.sentryCollected ? "No open Sentry issues matched in the window \u2014 cannot confirm the actual error signatures." : "Sentry collection failed or timed out \u2014 cannot confirm the actual error signatures.";
|
|
92923
93322
|
logNextSource = connectors.sentryCollected ? "Widen the window (--since) or check the Sentry project for open issues" : "Check the Sentry auth token / project, then retry";
|
|
93323
|
+
} else if (!connectors.elasticsearch && connectors.axiom) {
|
|
93324
|
+
logWhy = connectors.axiomCollected ? "No Axiom log rows matched in the window \u2014 cannot confirm the actual error signatures." : "Axiom log collection failed or timed out \u2014 cannot confirm the actual error signatures.";
|
|
93325
|
+
logNextSource = connectors.axiomCollected ? "Widen the window (--since) or check the Axiom dataset for matching rows" : "Check the Axiom API token / dataset, then retry";
|
|
92924
93326
|
} else if (connectors.logsCompatibilityError) {
|
|
92925
93327
|
logWhy = `Elasticsearch field mapping is incompatible with the index \u2014 log collection was blocked. ` + connectors.logsCompatibilityError;
|
|
92926
93328
|
logNextSource = "Fix fields.* overrides in your connector config or choose the correct preset (meritt / ecs)";
|
|
@@ -92931,7 +93333,7 @@ function detectMissingEvidence(r, connectors = {}, mode) {
|
|
|
92931
93333
|
logWhy = "No error logs matched in the window \u2014 cannot confirm the actual error signatures.";
|
|
92932
93334
|
logNextSource = "Widen the window (--since) or inspect `horus logs <service>`";
|
|
92933
93335
|
}
|
|
92934
|
-
const logRouteHint = !connectors.elasticsearch && !connectors.sentry ? { nextTool: "connect", args: "elasticsearch", reason: logNextSource } : { nextTool: "logs", args: service, reason: logNextSource };
|
|
93336
|
+
const logRouteHint = !connectors.elasticsearch && !connectors.sentry && !connectors.axiom ? { nextTool: "connect", args: "elasticsearch", reason: logNextSource } : { nextTool: "logs", args: service, reason: logNextSource };
|
|
92935
93337
|
gaps.push({
|
|
92936
93338
|
dimension: "logs",
|
|
92937
93339
|
why: logWhy,
|
|
@@ -92958,8 +93360,10 @@ function detectMissingEvidence(r, connectors = {}, mode) {
|
|
|
92958
93360
|
if (hasQueueTopology && !hasQueueState && !sourceImpact) {
|
|
92959
93361
|
gaps.push({
|
|
92960
93362
|
dimension: "queue runtime state",
|
|
92961
|
-
why: connectors.redis ? "Queue topology is known but live depth + failed/delayed counts were not collected." : "Queue topology is known but there is no Redis
|
|
92962
|
-
|
|
93363
|
+
why: connectors.redis ? "Queue topology is known but live depth + failed/delayed counts were not collected." : "Queue topology is known but there is no Redis connector for live queue depth/failures.",
|
|
93364
|
+
// Stack-agnostic: the tip names Redis (the connector to add) but NOT a specific queue
|
|
93365
|
+
// library — "BullMQ" is Node-only and was leaking onto Python/Redis repos (HOR-428).
|
|
93366
|
+
nextSource: connectors.redis ? "Run `horus queues --live` to see real-time queue depths and failed-job counts" : "Add a `redis` connector to read live queue state",
|
|
92963
93367
|
confidenceImpact: 0.1,
|
|
92964
93368
|
// Redis wired → read live state with `queues --live`; otherwise `connect redis` first.
|
|
92965
93369
|
routeHint: connectors.redis ? {
|
|
@@ -92969,7 +93373,7 @@ function detectMissingEvidence(r, connectors = {}, mode) {
|
|
|
92969
93373
|
} : {
|
|
92970
93374
|
nextTool: "connect",
|
|
92971
93375
|
args: "redis",
|
|
92972
|
-
reason: "Add a `redis` connector to read live
|
|
93376
|
+
reason: "Add a `redis` connector to read live queue state"
|
|
92973
93377
|
}
|
|
92974
93378
|
});
|
|
92975
93379
|
blindSpots.push("Cannot determine if the queue is actually backed up.");
|
|
@@ -93047,10 +93451,11 @@ function buildEntry(source, configured, evidenceCount, failed, detail) {
|
|
|
93047
93451
|
}
|
|
93048
93452
|
function buildRuntimeSourceStatus(evidence2, connectors) {
|
|
93049
93453
|
const logsCount = evidence2.filter((e) => e.source === "logs").length;
|
|
93050
|
-
const logsConfigured = !!(connectors.elasticsearch || connectors.sentry);
|
|
93454
|
+
const logsConfigured = !!(connectors.elasticsearch || connectors.sentry || connectors.axiom);
|
|
93051
93455
|
const esFailed = !!connectors.elasticsearch && !connectors.logsCollected;
|
|
93052
93456
|
const sentryFailed = !!connectors.sentry && !connectors.sentryCollected;
|
|
93053
|
-
const
|
|
93457
|
+
const axiomFailed = !!connectors.axiom && !connectors.axiomCollected;
|
|
93458
|
+
const logsFailed = logsConfigured && logsCount === 0 && (esFailed || sentryFailed || axiomFailed);
|
|
93054
93459
|
const metricsCount = evidence2.filter((e) => e.source === "metrics").length;
|
|
93055
93460
|
const metricsConfigured = !!connectors.grafana;
|
|
93056
93461
|
const metricsFailed = metricsConfigured && !connectors.metricsCollected;
|
|
@@ -94016,6 +94421,17 @@ function classifyLogRelevance(sigKey, sigServices, seedTerms, inputService) {
|
|
|
94016
94421
|
relevanceReason: "no structural link to seed \u2014 ambient runtime noise"
|
|
94017
94422
|
};
|
|
94018
94423
|
}
|
|
94424
|
+
var AXIOM_MESSAGE_FIELDS = ["message", "msg", "body", "event", "log", "_raw"];
|
|
94425
|
+
var AXIOM_LEVEL_FIELDS = ["level", "severity", "lvl", "loglevel", "status"];
|
|
94426
|
+
var AXIOM_SERVICE_FIELDS = ["service", "app", "logger", "source", "namespace"];
|
|
94427
|
+
function pickAxiomField(fields, candidates) {
|
|
94428
|
+
for (const key of candidates) {
|
|
94429
|
+
const v4 = fields[key];
|
|
94430
|
+
if (typeof v4 === "string" && v4 !== "") return v4;
|
|
94431
|
+
if (typeof v4 === "number") return String(v4);
|
|
94432
|
+
}
|
|
94433
|
+
return void 0;
|
|
94434
|
+
}
|
|
94019
94435
|
function extractEventCodes(...texts) {
|
|
94020
94436
|
const RE = /\b(?=[A-Z0-9_]*[0-9_])[A-Z][A-Z0-9_]{3,}\b/g;
|
|
94021
94437
|
const out2 = [];
|
|
@@ -94203,20 +94619,10 @@ function isClassSymbol(sym) {
|
|
|
94203
94619
|
return sym !== void 0 && sym.id.split(":", 1)[0]?.toLowerCase() === "class";
|
|
94204
94620
|
}
|
|
94205
94621
|
async function fetchClassMethods(code, cls) {
|
|
94206
|
-
const
|
|
94207
|
-
|
|
94208
|
-
if (!name2) return [];
|
|
94209
|
-
const file = safe(cls.filePath);
|
|
94622
|
+
const name2 = cls.name?.trim();
|
|
94623
|
+
if (!name2 || typeof code.classMethods !== "function") return [];
|
|
94210
94624
|
try {
|
|
94211
|
-
|
|
94212
|
-
`MATCH (m:Method) WHERE m.class_name = "${name2}"` + (file ? ` AND m.file_path = "${file}"` : "") + " RETURN m.id, m.name, m.file_path, m.start_line ORDER BY m.start_line LIMIT 40"
|
|
94213
|
-
);
|
|
94214
|
-
return res.rows.map((r) => ({
|
|
94215
|
-
id: String(r[0] ?? ""),
|
|
94216
|
-
name: String(r[1] ?? ""),
|
|
94217
|
-
filePath: String(r[2] ?? ""),
|
|
94218
|
-
...typeof r[3] === "number" ? { startLine: r[3] } : {}
|
|
94219
|
-
})).filter((s5) => s5.id !== "" && s5.name !== "");
|
|
94625
|
+
return await code.classMethods(cls.filePath ?? "", name2);
|
|
94220
94626
|
} catch {
|
|
94221
94627
|
return [];
|
|
94222
94628
|
}
|
|
@@ -94965,6 +95371,61 @@ async function investigate(input, deps) {
|
|
|
94965
95371
|
sentryCollected = false;
|
|
94966
95372
|
}
|
|
94967
95373
|
}
|
|
95374
|
+
let axiomCollected = false;
|
|
95375
|
+
if (deps.axiom) {
|
|
95376
|
+
try {
|
|
95377
|
+
const from = logWindowFrom(input.logsSince ?? input.since);
|
|
95378
|
+
const to = (/* @__PURE__ */ new Date()).toISOString();
|
|
95379
|
+
const axiomSeedBase = top ? top.filePath.split("/").pop() ?? "" : "";
|
|
95380
|
+
const axiomTerms = top ? [.../* @__PURE__ */ new Set([...tokenize(hint), ...tokenize(top.name), ...tokenize(axiomSeedBase)])] : [...new Set(tokenize(hint))];
|
|
95381
|
+
const records = await deps.axiom.collect({ from, to, hintTerms: axiomTerms });
|
|
95382
|
+
axiomCollected = true;
|
|
95383
|
+
for (const rec of records.slice(0, 15)) {
|
|
95384
|
+
const msg = pickAxiomField(rec.fields, AXIOM_MESSAGE_FIELDS);
|
|
95385
|
+
const level = pickAxiomField(rec.fields, AXIOM_LEVEL_FIELDS);
|
|
95386
|
+
const svc = pickAxiomField(rec.fields, AXIOM_SERVICE_FIELDS) ?? "";
|
|
95387
|
+
const sigKey = [msg ?? "", svc].filter(Boolean).join(" ");
|
|
95388
|
+
const { relevanceClass, relevanceReason } = classifyLogRelevance(
|
|
95389
|
+
sigKey,
|
|
95390
|
+
svc ? [svc] : [],
|
|
95391
|
+
axiomTerms,
|
|
95392
|
+
input.service
|
|
95393
|
+
);
|
|
95394
|
+
const lvl = level ? `[${level}] ` : "";
|
|
95395
|
+
const body2 = msg ?? "(log event)";
|
|
95396
|
+
const tag = relevanceClass === "ambient" ? " [ambient]" : "";
|
|
95397
|
+
const title = `Axiom log: ${lvl}${body2}${tag}`.slice(0, 220);
|
|
95398
|
+
const isError = ["error", "fatal", "critical"].includes((level ?? "").toLowerCase());
|
|
95399
|
+
const ev = mkEv(
|
|
95400
|
+
"log",
|
|
95401
|
+
title,
|
|
95402
|
+
{
|
|
95403
|
+
source: "axiom",
|
|
95404
|
+
signature: msg ?? null,
|
|
95405
|
+
level: level ?? null,
|
|
95406
|
+
service: svc || null,
|
|
95407
|
+
relevanceClass,
|
|
95408
|
+
relevanceReason,
|
|
95409
|
+
fields: rec.fields
|
|
95410
|
+
},
|
|
95411
|
+
{},
|
|
95412
|
+
rec.timestamp,
|
|
95413
|
+
// Direct: weighted up for error-level rows. Ambient: demoted so unrelated
|
|
95414
|
+
// high-volume log lines don't inflate confidence.
|
|
95415
|
+
relevanceClass === "direct" ? isError ? 0.9 : 0.75 : 0.4
|
|
95416
|
+
);
|
|
95417
|
+
logEvIds.push(ev.id);
|
|
95418
|
+
if (relevanceClass === "direct") {
|
|
95419
|
+
directLogEvIds.push(ev.id);
|
|
95420
|
+
directSignatures.push({ key: msg ?? "(log event)", count: 1, message: msg ?? "" });
|
|
95421
|
+
} else {
|
|
95422
|
+
ambientLogEvIds.push(ev.id);
|
|
95423
|
+
}
|
|
95424
|
+
}
|
|
95425
|
+
} catch {
|
|
95426
|
+
axiomCollected = false;
|
|
95427
|
+
}
|
|
95428
|
+
}
|
|
94968
95429
|
let stateAnalysis = null;
|
|
94969
95430
|
const stateEvIds = [];
|
|
94970
95431
|
const stateCollections = /* @__PURE__ */ new Set();
|
|
@@ -95725,6 +96186,10 @@ async function investigate(input, deps) {
|
|
|
95725
96186
|
// its collection ran (distinguishes "no open issues" from "collection failed").
|
|
95726
96187
|
sentry: deps.sentry != null,
|
|
95727
96188
|
sentryCollected,
|
|
96189
|
+
// Axiom is configured iff a provider was built; axiomCollected tracks whether its
|
|
96190
|
+
// collection ran (distinguishes "no matching rows" from "collection failed").
|
|
96191
|
+
axiom: deps.axiom != null,
|
|
96192
|
+
axiomCollected,
|
|
95728
96193
|
metricsCollected,
|
|
95729
96194
|
metricsFailureReason,
|
|
95730
96195
|
logsCollected,
|
|
@@ -95736,6 +96201,8 @@ async function investigate(input, deps) {
|
|
|
95736
96201
|
postgres: deps.postgres != null,
|
|
95737
96202
|
sentry: deps.sentry != null,
|
|
95738
96203
|
sentryCollected,
|
|
96204
|
+
axiom: deps.axiom != null,
|
|
96205
|
+
axiomCollected,
|
|
95739
96206
|
grafana: deps.metrics != null,
|
|
95740
96207
|
queue: deps.queue != null,
|
|
95741
96208
|
metricsCollected,
|
|
@@ -101056,31 +101523,50 @@ function dedupeBySymbolFile(rows) {
|
|
|
101056
101523
|
// ../../packages/stitcher/src/stitch.ts
|
|
101057
101524
|
init_cjs_shims();
|
|
101058
101525
|
async function stitch(client, db, opts = {}) {
|
|
101059
|
-
const producerRows =
|
|
101526
|
+
const producerRows = await client.contentSearch([
|
|
101060
101527
|
// "new Queue" (no paren) so generic-typed `new Queue<T>(...)` also matches.
|
|
101061
|
-
|
|
101062
|
-
|
|
101528
|
+
"@InjectQueue(",
|
|
101529
|
+
"new Queue",
|
|
101530
|
+
"QUEUE_NAME",
|
|
101531
|
+
"QueueName",
|
|
101532
|
+
"enum ",
|
|
101533
|
+
"Object.values(",
|
|
101534
|
+
// HOR-341: dispatch tables `[Enum.MEMBER]: () => this.ctrl.handler()` map runtime
|
|
101535
|
+
// queues to owning code, and often live in a file with NO new Queue/Worker of their
|
|
101536
|
+
// own (e.g. a scheduler controller) — so pull computed-key arrow entries too.
|
|
101537
|
+
"]: ("
|
|
101538
|
+
]);
|
|
101063
101539
|
const producerClasses = producerRows.map((r) => ({
|
|
101064
|
-
name:
|
|
101065
|
-
filePath:
|
|
101066
|
-
content:
|
|
101540
|
+
name: r.name,
|
|
101541
|
+
filePath: r.filePath,
|
|
101542
|
+
content: r.content
|
|
101067
101543
|
}));
|
|
101068
|
-
const workerRows =
|
|
101069
|
-
// "new Worker" (no paren) so generic-typed `new Worker<T>(...)` also matches.
|
|
101070
|
-
'MATCH (n) WHERE n.content CONTAINS "@Processor(" OR n.content CONTAINS "new Worker" RETURN n.name, n.file_path, n.content'
|
|
101071
|
-
)).rows;
|
|
101544
|
+
const workerRows = await client.contentSearch(["@Processor(", "new Worker"]);
|
|
101072
101545
|
const workerFiles = workerRows.map((r) => ({
|
|
101073
|
-
filePath:
|
|
101074
|
-
content:
|
|
101546
|
+
filePath: r.filePath,
|
|
101547
|
+
content: r.content
|
|
101075
101548
|
}));
|
|
101076
101549
|
const graph = extractQueueGraph({ producerClasses, workerFiles });
|
|
101077
|
-
const celeryRows =
|
|
101078
|
-
|
|
101079
|
-
|
|
101550
|
+
const celeryRows = await client.contentSearch([
|
|
101551
|
+
".delay(",
|
|
101552
|
+
".apply_async(",
|
|
101553
|
+
".schedule(",
|
|
101554
|
+
".defer(",
|
|
101555
|
+
".defer_async(",
|
|
101556
|
+
".send(",
|
|
101557
|
+
".enqueue_job(",
|
|
101558
|
+
".enqueue(",
|
|
101559
|
+
"@shared_task",
|
|
101560
|
+
"@task",
|
|
101561
|
+
".task",
|
|
101562
|
+
"@actor",
|
|
101563
|
+
"periodic_task",
|
|
101564
|
+
"db_task"
|
|
101565
|
+
]);
|
|
101080
101566
|
const celeryNodes = celeryRows.map((r) => ({
|
|
101081
|
-
name:
|
|
101082
|
-
filePath:
|
|
101083
|
-
content:
|
|
101567
|
+
name: r.name,
|
|
101568
|
+
filePath: r.filePath,
|
|
101569
|
+
content: r.content
|
|
101084
101570
|
}));
|
|
101085
101571
|
const celery = extractCeleryQueueGraph(celeryNodes);
|
|
101086
101572
|
const dramatiq = extractDramatiqQueueGraph(celeryNodes);
|
|
@@ -101232,6 +101718,10 @@ async function stitchQueueMap(hostUrl, dbUrl, label) {
|
|
|
101232
101718
|
console.log(
|
|
101233
101719
|
import_picocolors6.default.dim(`[${label}] `) + `Stitched ${summary.edges} queue edge(s) across ${summary.queues} queue(s) \u2014 ${summary.producers} producer(s), ${summary.workers} worker(s).`
|
|
101234
101720
|
);
|
|
101721
|
+
} catch (err2) {
|
|
101722
|
+
console.log(
|
|
101723
|
+
import_picocolors6.default.dim(`[${label}] `) + `Queue map skipped (degraded): ${err2.message}. Stitched 0 queue edge(s).`
|
|
101724
|
+
);
|
|
101235
101725
|
} finally {
|
|
101236
101726
|
await sql2.end();
|
|
101237
101727
|
}
|
|
@@ -101257,60 +101747,49 @@ function findConfiguredRepo(config2, root) {
|
|
|
101257
101747
|
return null;
|
|
101258
101748
|
}
|
|
101259
101749
|
var SOURCE_GRAPH_PULL_LIMIT = 5e3;
|
|
101750
|
+
var SOURCE_SYMBOL_LABELS = [
|
|
101751
|
+
"class",
|
|
101752
|
+
"interface",
|
|
101753
|
+
"type_alias",
|
|
101754
|
+
"function",
|
|
101755
|
+
"method",
|
|
101756
|
+
"enum"
|
|
101757
|
+
];
|
|
101260
101758
|
async function extractSourceGraph(hostUrl, repo) {
|
|
101261
101759
|
const client = new SourceHttpClient({ baseUrl: hostUrl });
|
|
101262
101760
|
const N2 = SOURCE_GRAPH_PULL_LIMIT;
|
|
101263
|
-
const
|
|
101264
|
-
try {
|
|
101265
|
-
return (await client.cypher(query)).rows;
|
|
101266
|
-
} catch {
|
|
101267
|
-
return [];
|
|
101268
|
-
}
|
|
101269
|
-
};
|
|
101270
|
-
const symbolRows = await rows(
|
|
101271
|
-
`MATCH (n) WHERE label(n) IN ["Class","Interface","TypeAlias","Function","Method","Enum"] RETURN label(n), n.name, n.file_path, n.start_line, n.end_line, n.class_name, n.is_entry_point, n.is_exported, n.signature LIMIT ${N2 * 2}`
|
|
101272
|
-
);
|
|
101761
|
+
const symbolRows = await client.symbolsByLabel(SOURCE_SYMBOL_LABELS, N2 * 2).catch(() => []);
|
|
101273
101762
|
const symbols = symbolRows.map((r) => ({
|
|
101274
|
-
label:
|
|
101275
|
-
name:
|
|
101276
|
-
filePath: r
|
|
101277
|
-
startLine: r
|
|
101278
|
-
endLine: r
|
|
101279
|
-
className: r
|
|
101280
|
-
isEntryPoint: Boolean(r
|
|
101281
|
-
isExported: Boolean(r
|
|
101282
|
-
signature: r
|
|
101763
|
+
label: r.label,
|
|
101764
|
+
name: r.name,
|
|
101765
|
+
filePath: r.filePath || void 0,
|
|
101766
|
+
startLine: r.startLine > 0 ? r.startLine : void 0,
|
|
101767
|
+
endLine: r.endLine > 0 ? r.endLine : void 0,
|
|
101768
|
+
className: r.className || void 0,
|
|
101769
|
+
isEntryPoint: Boolean(r.isEntryPoint),
|
|
101770
|
+
isExported: Boolean(r.isExported),
|
|
101771
|
+
signature: r.signature || void 0
|
|
101283
101772
|
}));
|
|
101284
|
-
const
|
|
101285
|
-
|
|
101286
|
-
|
|
101287
|
-
|
|
101288
|
-
|
|
101289
|
-
|
|
101773
|
+
const nameById = /* @__PURE__ */ new Map();
|
|
101774
|
+
for (const r of symbolRows) {
|
|
101775
|
+
if (r.id) nameById.set(r.id, { name: r.name, filePath: r.filePath || void 0 });
|
|
101776
|
+
}
|
|
101777
|
+
const communityRows = await client.communities().catch(() => []);
|
|
101778
|
+
const communities = communityRows.map((c4) => ({
|
|
101779
|
+
id: c4.id ? String(c4.id) : void 0,
|
|
101780
|
+
name: String(c4.name ?? "")
|
|
101781
|
+
}));
|
|
101782
|
+
const processRows = await client.processes().catch(() => []);
|
|
101783
|
+
const processes = processRows.map((p5) => ({
|
|
101784
|
+
name: String(p5.name ?? ""),
|
|
101785
|
+
steps: (p5.steps ?? []).map((s5) => {
|
|
101786
|
+
const resolved = nameById.get(s5.nodeId);
|
|
101787
|
+
const component = resolved?.name ?? s5.nodeId;
|
|
101788
|
+
if (!component) return null;
|
|
101789
|
+
const detail = resolved?.filePath;
|
|
101790
|
+
return { component, ...detail ? { detail } : {} };
|
|
101791
|
+
}).filter((s5) => s5 !== null)
|
|
101290
101792
|
}));
|
|
101291
|
-
const processRows = await rows(`MATCH (n:Process) RETURN n.id, n.name LIMIT ${N2}`);
|
|
101292
|
-
const stepRows = await rows(
|
|
101293
|
-
`MATCH (s)-[r:CodeRelation]->(p:Process) WHERE r.rel_type = "step_in_process" RETURN p.id, s.name, s.file_path, r.step_number ORDER BY p.id, r.step_number LIMIT ${N2 * 4}`
|
|
101294
|
-
);
|
|
101295
|
-
const stepsByProcess = /* @__PURE__ */ new Map();
|
|
101296
|
-
for (const r of stepRows) {
|
|
101297
|
-
const pid = String(r[0] ?? "");
|
|
101298
|
-
if (!pid) continue;
|
|
101299
|
-
const component = String(r[1] ?? "");
|
|
101300
|
-
if (!component) continue;
|
|
101301
|
-
const file = r[2] != null ? String(r[2]) : void 0;
|
|
101302
|
-
const list = stepsByProcess.get(pid) ?? [];
|
|
101303
|
-
list.push({ component, ...file ? { detail: file } : {} });
|
|
101304
|
-
stepsByProcess.set(pid, list);
|
|
101305
|
-
}
|
|
101306
|
-
const processes = processRows.map((r) => {
|
|
101307
|
-
const id = r[0] != null ? String(r[0]) : void 0;
|
|
101308
|
-
return {
|
|
101309
|
-
id,
|
|
101310
|
-
name: String(r[1] ?? ""),
|
|
101311
|
-
steps: id ? stepsByProcess.get(id) ?? [] : []
|
|
101312
|
-
};
|
|
101313
|
-
});
|
|
101314
101793
|
if (symbols.length === 0 && communities.length === 0 && processes.length === 0) {
|
|
101315
101794
|
return null;
|
|
101316
101795
|
}
|
|
@@ -101484,7 +101963,13 @@ async function runIndex(opts) {
|
|
|
101484
101963
|
);
|
|
101485
101964
|
return 1;
|
|
101486
101965
|
}
|
|
101487
|
-
|
|
101966
|
+
try {
|
|
101967
|
+
await stitchQueueMap(hostUrl, dbUrl, label);
|
|
101968
|
+
} catch (err2) {
|
|
101969
|
+
console.log(
|
|
101970
|
+
import_picocolors6.default.dim(`[${label}] `) + `Queue map skipped (degraded): ${err2.message}. Stitched 0 queue edge(s).`
|
|
101971
|
+
);
|
|
101972
|
+
}
|
|
101488
101973
|
await buildKnowledgeIndex(root, config2, label, configuredProject, opts, hostUrl);
|
|
101489
101974
|
const hasLocalConfig = discoverLocalConfig(root) !== null;
|
|
101490
101975
|
if (!isConfigured && !hasLocalConfig) {
|
|
@@ -114838,6 +115323,7 @@ async function buildInvestigationContext(renv, opts) {
|
|
|
114838
115323
|
const mongo = mongoForEnv(renv);
|
|
114839
115324
|
const postgres2 = postgresForEnv(renv);
|
|
114840
115325
|
const sentry = sentryForEnv(renv);
|
|
115326
|
+
const axiom = axiomForEnv(renv);
|
|
114841
115327
|
const queue = queueForEnv(renv);
|
|
114842
115328
|
const redisState = redisStateForEnv(renv);
|
|
114843
115329
|
const metrics = metricsForEnv(renv);
|
|
@@ -114854,6 +115340,7 @@ async function buildInvestigationContext(renv, opts) {
|
|
|
114854
115340
|
mongo,
|
|
114855
115341
|
postgres: postgres2,
|
|
114856
115342
|
sentry,
|
|
115343
|
+
axiom,
|
|
114857
115344
|
queue,
|
|
114858
115345
|
redisState,
|
|
114859
115346
|
metrics,
|
|
@@ -114882,6 +115369,7 @@ async function runOneInvestigation(input, ctx, opts = {}) {
|
|
|
114882
115369
|
mongo: ctx.mongo,
|
|
114883
115370
|
postgres: ctx.postgres,
|
|
114884
115371
|
sentry: ctx.sentry,
|
|
115372
|
+
axiom: ctx.axiom,
|
|
114885
115373
|
queue: ctx.queue,
|
|
114886
115374
|
redisState: ctx.redisState,
|
|
114887
115375
|
metrics: ctx.metrics,
|
|
@@ -114892,6 +115380,7 @@ async function runOneInvestigation(input, ctx, opts = {}) {
|
|
|
114892
115380
|
mongodb: !!renv.connectors.mongodb?.url,
|
|
114893
115381
|
postgres: !!renv.connectors.postgres?.url,
|
|
114894
115382
|
sentry: !!renv.connectors.sentry,
|
|
115383
|
+
axiom: !!renv.connectors.axiom,
|
|
114895
115384
|
redis: !!renv.connectors.redis?.url,
|
|
114896
115385
|
queue: !!ctx.queue
|
|
114897
115386
|
}
|
|
@@ -117828,6 +118317,8 @@ function outcomeLabelToJSON(l2) {
|
|
|
117828
118317
|
at: l2.at instanceof Date ? l2.at.toISOString() : l2.at
|
|
117829
118318
|
};
|
|
117830
118319
|
}
|
|
118320
|
+
var DEFAULT_ACCURACY_WINDOW_DAYS = 30;
|
|
118321
|
+
var MS_PER_DAY2 = 24 * 60 * 60 * 1e3;
|
|
117831
118322
|
async function runMemoryAccuracy(opts) {
|
|
117832
118323
|
try {
|
|
117833
118324
|
let source;
|
|
@@ -117839,12 +118330,29 @@ async function runMemoryAccuracy(opts) {
|
|
|
117839
118330
|
source = opts.source;
|
|
117840
118331
|
}
|
|
117841
118332
|
let since;
|
|
117842
|
-
|
|
118333
|
+
let window2;
|
|
118334
|
+
if (opts.all) {
|
|
118335
|
+
window2 = "all time";
|
|
118336
|
+
} else if (opts.since !== void 0) {
|
|
118337
|
+
const parsed = new Date(opts.since);
|
|
118338
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
118339
|
+
console.error(
|
|
118340
|
+
import_picocolors31.default.red("--since must be a parseable date (e.g. 2026-06-01 or an ISO timestamp).")
|
|
118341
|
+
);
|
|
118342
|
+
return 1;
|
|
118343
|
+
}
|
|
118344
|
+
since = parsed;
|
|
118345
|
+
window2 = `since ${parsed.toISOString().slice(0, 10)}`;
|
|
118346
|
+
} else if (opts.days !== void 0) {
|
|
117843
118347
|
if (!Number.isFinite(opts.days) || opts.days <= 0) {
|
|
117844
118348
|
console.error(import_picocolors31.default.red("--days must be a positive number."));
|
|
117845
118349
|
return 1;
|
|
117846
118350
|
}
|
|
117847
|
-
since = new Date(Date.now() - opts.days *
|
|
118351
|
+
since = new Date(Date.now() - opts.days * MS_PER_DAY2);
|
|
118352
|
+
window2 = `last ${opts.days} day(s)`;
|
|
118353
|
+
} else {
|
|
118354
|
+
since = new Date(Date.now() - DEFAULT_ACCURACY_WINDOW_DAYS * MS_PER_DAY2);
|
|
118355
|
+
window2 = `last ${DEFAULT_ACCURACY_WINDOW_DAYS} day(s) (default \u2014 pass --all for full history)`;
|
|
117848
118356
|
}
|
|
117849
118357
|
const config2 = await loadConfig(opts.config);
|
|
117850
118358
|
const project = resolveProject(config2, opts.repo);
|
|
@@ -117865,6 +118373,7 @@ async function runMemoryAccuracy(opts) {
|
|
|
117865
118373
|
project,
|
|
117866
118374
|
source: source ?? null,
|
|
117867
118375
|
since: since ? since.toISOString() : null,
|
|
118376
|
+
window: window2,
|
|
117868
118377
|
summary,
|
|
117869
118378
|
labels: labels.map(outcomeLabelToJSON)
|
|
117870
118379
|
},
|
|
@@ -117874,11 +118383,11 @@ async function runMemoryAccuracy(opts) {
|
|
|
117874
118383
|
);
|
|
117875
118384
|
} else {
|
|
117876
118385
|
const pct = (n4) => `${(n4 * 100).toFixed(0)}%`;
|
|
117877
|
-
console.log(import_picocolors31.default.bold(`Investigation accuracy \u2014 ${project}`));
|
|
118386
|
+
console.log(import_picocolors31.default.bold(`Investigation accuracy \u2014 ${project}`) + import_picocolors31.default.dim(` \xB7 window: ${window2}`));
|
|
117878
118387
|
if (summary.evaluated === 0) {
|
|
117879
118388
|
console.log(
|
|
117880
118389
|
import_picocolors31.default.dim(
|
|
117881
|
-
|
|
118390
|
+
`No outcome labels in this window (${window2}). Confirm an investigation (\`horus memory confirm <id>\`) or leave feedback (\`horus feedback <id> --resolved yes|partly|no\`) to add to the eval set` + (opts.all ? "." : ", or pass --all to include older labels.")
|
|
117882
118391
|
)
|
|
117883
118392
|
);
|
|
117884
118393
|
} else {
|
|
@@ -119060,6 +119569,148 @@ async function checkboxSearch(opts) {
|
|
|
119060
119569
|
render();
|
|
119061
119570
|
});
|
|
119062
119571
|
}
|
|
119572
|
+
async function selectSearch(opts) {
|
|
119573
|
+
const { message, choices } = opts;
|
|
119574
|
+
const pageSize = opts.pageSize ?? 10;
|
|
119575
|
+
const input = opts.input ?? process.stdin;
|
|
119576
|
+
const output = opts.output ?? process.stdout;
|
|
119577
|
+
if (!isInteractive(input)) {
|
|
119578
|
+
throw new Error("selectSearch requires an interactive TTY");
|
|
119579
|
+
}
|
|
119580
|
+
if (choices.length === 0) {
|
|
119581
|
+
throw new ExitPromptError("No choices to select from");
|
|
119582
|
+
}
|
|
119583
|
+
return new Promise((resolve12, reject) => {
|
|
119584
|
+
let filter = "";
|
|
119585
|
+
let cursor = 0;
|
|
119586
|
+
let filtered = choices;
|
|
119587
|
+
let visibleOffset = 0;
|
|
119588
|
+
let lastRenderLines = 0;
|
|
119589
|
+
const wasRaw = input.isRaw;
|
|
119590
|
+
function updateFiltered() {
|
|
119591
|
+
const f5 = filter.toLowerCase();
|
|
119592
|
+
filtered = f5 ? choices.filter((c4) => c4.toLowerCase().includes(f5)) : choices;
|
|
119593
|
+
cursor = Math.min(cursor, Math.max(0, filtered.length - 1));
|
|
119594
|
+
updateVisibleOffset();
|
|
119595
|
+
}
|
|
119596
|
+
function updateVisibleOffset() {
|
|
119597
|
+
if (cursor < visibleOffset) {
|
|
119598
|
+
visibleOffset = cursor;
|
|
119599
|
+
} else if (cursor >= visibleOffset + pageSize) {
|
|
119600
|
+
visibleOffset = cursor - pageSize + 1;
|
|
119601
|
+
}
|
|
119602
|
+
const maxOffset = Math.max(0, filtered.length - pageSize);
|
|
119603
|
+
visibleOffset = Math.max(0, Math.min(visibleOffset, maxOffset));
|
|
119604
|
+
}
|
|
119605
|
+
function clear() {
|
|
119606
|
+
output.write("\x1B[?25l");
|
|
119607
|
+
if (lastRenderLines > 0) {
|
|
119608
|
+
output.write(`\x1B[${lastRenderLines}A`);
|
|
119609
|
+
output.write("\x1B[0J");
|
|
119610
|
+
}
|
|
119611
|
+
}
|
|
119612
|
+
function render() {
|
|
119613
|
+
clear();
|
|
119614
|
+
const lines = [];
|
|
119615
|
+
lines.push(
|
|
119616
|
+
`? ${import_picocolors39.default.bold(message)} ${import_picocolors39.default.dim("(\u2191\u2193 navigate \u2022 enter confirm \u2022 type filter)")}`
|
|
119617
|
+
);
|
|
119618
|
+
const visible = filtered.slice(visibleOffset, visibleOffset + pageSize);
|
|
119619
|
+
for (let i3 = 0; i3 < visible.length; i3++) {
|
|
119620
|
+
const idx = visibleOffset + i3;
|
|
119621
|
+
const choice = visible[i3];
|
|
119622
|
+
const isCursor = idx === cursor;
|
|
119623
|
+
const pointer = isCursor ? import_picocolors39.default.cyan("\u276F") : " ";
|
|
119624
|
+
const label = isCursor ? import_picocolors39.default.cyan(choice) : choice;
|
|
119625
|
+
lines.push(` ${pointer} ${label}`);
|
|
119626
|
+
}
|
|
119627
|
+
if (filtered.length === 0) {
|
|
119628
|
+
lines.push(import_picocolors39.default.dim(" No matches"));
|
|
119629
|
+
}
|
|
119630
|
+
const statusParts = [`${filtered.length}/${choices.length} matches`];
|
|
119631
|
+
if (filter) statusParts.push(`filter: ${filter}`);
|
|
119632
|
+
lines.push(import_picocolors39.default.dim(` ${statusParts.join(" \xB7 ")}`));
|
|
119633
|
+
output.write(lines.join("\n"));
|
|
119634
|
+
lastRenderLines = lines.length;
|
|
119635
|
+
}
|
|
119636
|
+
function finish(value) {
|
|
119637
|
+
cleanup();
|
|
119638
|
+
output.write(`
|
|
119639
|
+
? ${import_picocolors39.default.bold(message)} ${value}
|
|
119640
|
+
`);
|
|
119641
|
+
resolve12(value);
|
|
119642
|
+
}
|
|
119643
|
+
function cancel() {
|
|
119644
|
+
cleanup();
|
|
119645
|
+
reject(new ExitPromptError());
|
|
119646
|
+
}
|
|
119647
|
+
function cleanup() {
|
|
119648
|
+
input.setRawMode(wasRaw);
|
|
119649
|
+
input.pause();
|
|
119650
|
+
input.removeListener("data", onData);
|
|
119651
|
+
output.write("\x1B[?25h");
|
|
119652
|
+
}
|
|
119653
|
+
function onData(chunk) {
|
|
119654
|
+
const s5 = chunk.toString();
|
|
119655
|
+
if (s5 === "") {
|
|
119656
|
+
cancel();
|
|
119657
|
+
return;
|
|
119658
|
+
}
|
|
119659
|
+
if (s5 === "\x1B") {
|
|
119660
|
+
if (filter) {
|
|
119661
|
+
filter = "";
|
|
119662
|
+
cursor = 0;
|
|
119663
|
+
updateFiltered();
|
|
119664
|
+
} else {
|
|
119665
|
+
cancel();
|
|
119666
|
+
}
|
|
119667
|
+
render();
|
|
119668
|
+
return;
|
|
119669
|
+
}
|
|
119670
|
+
if (s5 === "\r" || s5 === "\n") {
|
|
119671
|
+
const choice = filtered[cursor];
|
|
119672
|
+
if (choice !== void 0) finish(choice);
|
|
119673
|
+
return;
|
|
119674
|
+
}
|
|
119675
|
+
if (s5 === "\x7F" || s5 === "\b") {
|
|
119676
|
+
if (filter.length > 0) {
|
|
119677
|
+
filter = filter.slice(0, -1);
|
|
119678
|
+
cursor = 0;
|
|
119679
|
+
updateFiltered();
|
|
119680
|
+
}
|
|
119681
|
+
render();
|
|
119682
|
+
return;
|
|
119683
|
+
}
|
|
119684
|
+
if (s5 === "\x1B[A") {
|
|
119685
|
+
if (cursor > 0) {
|
|
119686
|
+
cursor -= 1;
|
|
119687
|
+
updateVisibleOffset();
|
|
119688
|
+
}
|
|
119689
|
+
render();
|
|
119690
|
+
return;
|
|
119691
|
+
}
|
|
119692
|
+
if (s5 === "\x1B[B") {
|
|
119693
|
+
if (cursor < filtered.length - 1) {
|
|
119694
|
+
cursor += 1;
|
|
119695
|
+
updateVisibleOffset();
|
|
119696
|
+
}
|
|
119697
|
+
render();
|
|
119698
|
+
return;
|
|
119699
|
+
}
|
|
119700
|
+
if (s5.length === 1 && s5.charCodeAt(0) >= 32 && s5.charCodeAt(0) <= 126) {
|
|
119701
|
+
filter += s5;
|
|
119702
|
+
cursor = 0;
|
|
119703
|
+
updateFiltered();
|
|
119704
|
+
render();
|
|
119705
|
+
}
|
|
119706
|
+
}
|
|
119707
|
+
input.setRawMode(true);
|
|
119708
|
+
input.resume();
|
|
119709
|
+
input.on("data", onData);
|
|
119710
|
+
updateFiltered();
|
|
119711
|
+
render();
|
|
119712
|
+
});
|
|
119713
|
+
}
|
|
119063
119714
|
|
|
119064
119715
|
// ../../packages/cli/src/commands/connect-ai.ts
|
|
119065
119716
|
init_cjs_shims();
|
|
@@ -119197,7 +119848,7 @@ function parseDbSpec(spec) {
|
|
|
119197
119848
|
}
|
|
119198
119849
|
return { db, roles };
|
|
119199
119850
|
}
|
|
119200
|
-
var SUPPORTED = ["elasticsearch", "mongodb", "postgres", "sentry", "grafana", "redis"];
|
|
119851
|
+
var SUPPORTED = ["elasticsearch", "mongodb", "postgres", "sentry", "axiom", "grafana", "redis"];
|
|
119201
119852
|
async function runConnect(type, opts) {
|
|
119202
119853
|
if (type === "ai") {
|
|
119203
119854
|
return runConnectAi({
|
|
@@ -119234,6 +119885,9 @@ async function runConnect(type, opts) {
|
|
|
119234
119885
|
case "sentry":
|
|
119235
119886
|
patch = buildSentryPatch(filled);
|
|
119236
119887
|
break;
|
|
119888
|
+
case "axiom":
|
|
119889
|
+
patch = buildAxiomPatch(filled);
|
|
119890
|
+
break;
|
|
119237
119891
|
case "grafana":
|
|
119238
119892
|
patch = buildGrafanaPatch(filled);
|
|
119239
119893
|
break;
|
|
@@ -119255,7 +119909,7 @@ ${import_picocolors41.default.red(`\u2717 Could not reach ${connectorType}:`)} $
|
|
|
119255
119909
|
${import_picocolors41.default.green("\u2713")} ${connectorType} reachable ${import_picocolors41.default.dim(`(${probeResult.detail})`)}`
|
|
119256
119910
|
);
|
|
119257
119911
|
}
|
|
119258
|
-
const hasLiteralCredentials = filled.url !== void 0 || filled.password !== void 0 || filled.username !== void 0 || filled.authToken !== void 0;
|
|
119912
|
+
const hasLiteralCredentials = filled.url !== void 0 || filled.password !== void 0 || filled.username !== void 0 || filled.authToken !== void 0 || filled.token !== void 0;
|
|
119259
119913
|
if (hasLiteralCredentials) {
|
|
119260
119914
|
if (isGitTracked(configPath, root)) {
|
|
119261
119915
|
console.error(
|
|
@@ -119332,6 +119986,35 @@ ${import_picocolors41.default.bold(`Connect ${type}`)} ${import_picocolors41.def
|
|
|
119332
119986
|
filled.authToken = filled.authToken ?? (await askPassword("Auth token") || void 0);
|
|
119333
119987
|
filled.url = filled.url ?? (await ask("Base URL (self-hosted)", "https://sentry.io", false) || void 0);
|
|
119334
119988
|
break;
|
|
119989
|
+
case "axiom": {
|
|
119990
|
+
filled.token = filled.token ?? (await askPassword("Axiom API token") || void 0);
|
|
119991
|
+
if (filled.url === void 0) {
|
|
119992
|
+
filled.url = await askAxiomRegion();
|
|
119993
|
+
}
|
|
119994
|
+
if (filled.dataset === void 0) {
|
|
119995
|
+
const baseUrl = filled.url ?? "https://api.axiom.co";
|
|
119996
|
+
let names = [];
|
|
119997
|
+
if (filled.token) {
|
|
119998
|
+
const client = new AxiomClient({ token: filled.token, dataset: "", baseUrl });
|
|
119999
|
+
names = (await client.listDatasets()).map((d3) => d3.name).filter(Boolean);
|
|
120000
|
+
}
|
|
120001
|
+
if (names.length > 0) {
|
|
120002
|
+
const picked = await askDatasetSelection(names);
|
|
120003
|
+
if (picked) filled.dataset = picked;
|
|
120004
|
+
}
|
|
120005
|
+
if (filled.dataset === void 0) {
|
|
120006
|
+
if (names.length === 0) {
|
|
120007
|
+
console.log(
|
|
120008
|
+
import_picocolors41.default.yellow(
|
|
120009
|
+
" Couldn't list datasets (check token / network / region) \u2014 enter the name manually."
|
|
120010
|
+
)
|
|
120011
|
+
);
|
|
120012
|
+
}
|
|
120013
|
+
filled.dataset = await ask("Dataset name", "logs");
|
|
120014
|
+
}
|
|
120015
|
+
}
|
|
120016
|
+
break;
|
|
120017
|
+
}
|
|
119335
120018
|
case "grafana":
|
|
119336
120019
|
filled.url = filled.url ?? await ask("URL", "https://grafana.example.com");
|
|
119337
120020
|
filled.username = filled.username ?? (await ask("Username", "", false) || void 0);
|
|
@@ -119389,6 +120072,8 @@ function missingRequired(type, opts) {
|
|
|
119389
120072
|
return !opts.url;
|
|
119390
120073
|
case "sentry":
|
|
119391
120074
|
return !opts.org || !opts.project || !opts.authToken;
|
|
120075
|
+
case "axiom":
|
|
120076
|
+
return !opts.dataset || !opts.token;
|
|
119392
120077
|
case "grafana":
|
|
119393
120078
|
return !opts.url;
|
|
119394
120079
|
case "redis":
|
|
@@ -119487,6 +120172,13 @@ function buildSentryPatch(opts) {
|
|
|
119487
120172
|
if (opts.url && opts.url.replace(/\/$/, "") !== "https://sentry.io") patch["url"] = opts.url;
|
|
119488
120173
|
return patch;
|
|
119489
120174
|
}
|
|
120175
|
+
function buildAxiomPatch(opts) {
|
|
120176
|
+
if (!opts.dataset) throw new Error("Dataset is required for axiom");
|
|
120177
|
+
const patch = { dataset: opts.dataset };
|
|
120178
|
+
if (opts.token) patch["token"] = opts.token;
|
|
120179
|
+
if (opts.url && opts.url.replace(/\/$/, "") !== "https://api.axiom.co") patch["url"] = opts.url;
|
|
120180
|
+
return patch;
|
|
120181
|
+
}
|
|
119490
120182
|
function buildGrafanaPatch(opts) {
|
|
119491
120183
|
const patch = {};
|
|
119492
120184
|
if (opts.url) patch["url"] = opts.url;
|
|
@@ -119622,6 +120314,17 @@ async function probe(type, opts) {
|
|
|
119622
120314
|
});
|
|
119623
120315
|
return client.health();
|
|
119624
120316
|
}
|
|
120317
|
+
case "axiom": {
|
|
120318
|
+
if (!opts.token || !opts.dataset) {
|
|
120319
|
+
return { ok: true, detail: "skipped (missing dataset/token)" };
|
|
120320
|
+
}
|
|
120321
|
+
const client = new AxiomClient({
|
|
120322
|
+
token: opts.token,
|
|
120323
|
+
dataset: opts.dataset,
|
|
120324
|
+
...opts.url ? { baseUrl: opts.url } : {}
|
|
120325
|
+
});
|
|
120326
|
+
return client.health();
|
|
120327
|
+
}
|
|
119625
120328
|
case "grafana": {
|
|
119626
120329
|
if (!opts.url) return { ok: true, detail: "skipped (no URL)" };
|
|
119627
120330
|
const headers = { "Content-Type": "application/json" };
|
|
@@ -119673,8 +120376,11 @@ function printSummary(type, opts) {
|
|
|
119673
120376
|
if (opts.collections) lines.push(` collections: ${opts.collections}`);
|
|
119674
120377
|
if (opts.org) lines.push(` org: ${opts.org}`);
|
|
119675
120378
|
if (opts.project) lines.push(` project: ${opts.project}`);
|
|
120379
|
+
if (opts.dataset) lines.push(` dataset: ${opts.dataset}`);
|
|
119676
120380
|
if (opts.authToken)
|
|
119677
120381
|
lines.push(` auth-token: ${"\u2022".repeat(Math.min(opts.authToken.length, 8))}`);
|
|
120382
|
+
if (opts.token)
|
|
120383
|
+
lines.push(` token: ${"\u2022".repeat(Math.min(opts.token.length, 8))}`);
|
|
119678
120384
|
if (opts.dashboardUids && opts.dashboardUids.length > 0) {
|
|
119679
120385
|
lines.push(` dashboards: ${opts.dashboardUids.join(", ")}`);
|
|
119680
120386
|
} else if (opts.dashboard) {
|
|
@@ -119785,6 +120491,63 @@ async function askDashboardSelection(dashboards) {
|
|
|
119785
120491
|
}
|
|
119786
120492
|
return [];
|
|
119787
120493
|
}
|
|
120494
|
+
async function askAxiomRegion() {
|
|
120495
|
+
const US = "US \u2014 api.axiom.co";
|
|
120496
|
+
const EU = "EU \u2014 api.eu.axiom.co";
|
|
120497
|
+
const CUSTOM = "Custom URL\u2026";
|
|
120498
|
+
const US_URL = "https://api.axiom.co";
|
|
120499
|
+
const EU_URL = "https://api.eu.axiom.co";
|
|
120500
|
+
let choice = US;
|
|
120501
|
+
if (isInteractive()) {
|
|
120502
|
+
try {
|
|
120503
|
+
choice = await selectSearch({
|
|
120504
|
+
message: "Axiom server / region",
|
|
120505
|
+
choices: [US, EU, CUSTOM],
|
|
120506
|
+
pageSize: 3
|
|
120507
|
+
});
|
|
120508
|
+
} catch (err2) {
|
|
120509
|
+
if (err2 instanceof ExitPromptError) throw err2;
|
|
120510
|
+
return US_URL;
|
|
120511
|
+
}
|
|
120512
|
+
} else {
|
|
120513
|
+
return US_URL;
|
|
120514
|
+
}
|
|
120515
|
+
if (choice === EU) return EU_URL;
|
|
120516
|
+
if (choice === CUSTOM) {
|
|
120517
|
+
return await ask("Custom Axiom base URL", US_URL) || US_URL;
|
|
120518
|
+
}
|
|
120519
|
+
return US_URL;
|
|
120520
|
+
}
|
|
120521
|
+
async function askDatasetSelection(names) {
|
|
120522
|
+
if (names.length === 0) return void 0;
|
|
120523
|
+
if (isInteractive()) {
|
|
120524
|
+
try {
|
|
120525
|
+
return await selectSearch({
|
|
120526
|
+
message: "Select an Axiom dataset",
|
|
120527
|
+
choices: names,
|
|
120528
|
+
pageSize: 12
|
|
120529
|
+
});
|
|
120530
|
+
} catch (err2) {
|
|
120531
|
+
if (err2 instanceof ExitPromptError) throw err2;
|
|
120532
|
+
}
|
|
120533
|
+
}
|
|
120534
|
+
const MAX_DISPLAY = 25;
|
|
120535
|
+
const shown = names.slice(0, MAX_DISPLAY);
|
|
120536
|
+
console.log("\n Available Axiom datasets:");
|
|
120537
|
+
shown.forEach((name2, i3) => {
|
|
120538
|
+
console.log(` ${import_picocolors41.default.dim(`[${i3 + 1}]`)} ${name2}`);
|
|
120539
|
+
});
|
|
120540
|
+
if (names.length > MAX_DISPLAY) {
|
|
120541
|
+
console.log(import_picocolors41.default.dim(` \u2026 and ${names.length - MAX_DISPLAY} more`));
|
|
120542
|
+
}
|
|
120543
|
+
const input = (await ask(" Select a dataset (e.g. 1 or Enter to type a name manually)", "", false)).trim();
|
|
120544
|
+
if (!input) return void 0;
|
|
120545
|
+
if (/^\d+$/.test(input)) {
|
|
120546
|
+
const idx = parseInt(input, 10) - 1;
|
|
120547
|
+
if (idx >= 0 && idx < shown.length) return shown[idx];
|
|
120548
|
+
}
|
|
120549
|
+
return input;
|
|
120550
|
+
}
|
|
119788
120551
|
function redisUrlHasPassword(raw) {
|
|
119789
120552
|
if (!raw) return false;
|
|
119790
120553
|
try {
|
|
@@ -120879,6 +121642,7 @@ async function runReadiness(opts) {
|
|
|
120879
121642
|
let anyMongo = false;
|
|
120880
121643
|
let anyPostgres = false;
|
|
120881
121644
|
let anySentry = false;
|
|
121645
|
+
let anyAxiom = false;
|
|
120882
121646
|
let anyRedis = false;
|
|
120883
121647
|
for (const project of globalConfig2.projects) {
|
|
120884
121648
|
for (const repo of project.repositories) {
|
|
@@ -120891,6 +121655,7 @@ async function runReadiness(opts) {
|
|
|
120891
121655
|
if (c4.mongodb) anyMongo = true;
|
|
120892
121656
|
if (c4.postgres) anyPostgres = true;
|
|
120893
121657
|
if (c4.sentry) anySentry = true;
|
|
121658
|
+
if (c4.axiom) anyAxiom = true;
|
|
120894
121659
|
if (c4.redis) anyRedis = true;
|
|
120895
121660
|
}
|
|
120896
121661
|
}
|
|
@@ -120948,6 +121713,15 @@ async function runReadiness(opts) {
|
|
|
120948
121713
|
next: "run `horus connect sentry`"
|
|
120949
121714
|
}
|
|
120950
121715
|
);
|
|
121716
|
+
checks.push(
|
|
121717
|
+
anyAxiom ? { label: "Axiom", status: "pass", blocking: false, detail: "configured" } : {
|
|
121718
|
+
label: "Axiom",
|
|
121719
|
+
status: "warn",
|
|
121720
|
+
blocking: false,
|
|
121721
|
+
detail: "not configured \u2014 no runtime log evidence",
|
|
121722
|
+
next: "run `horus connect axiom`"
|
|
121723
|
+
}
|
|
121724
|
+
);
|
|
120951
121725
|
checks.push(
|
|
120952
121726
|
anyRedis ? { label: "Redis", status: "pass", blocking: false, detail: "configured" } : {
|
|
120953
121727
|
label: "Redis",
|
|
@@ -122675,8 +123449,8 @@ Examples:
|
|
|
122675
123449
|
process.exitCode = await runReadiness({ config: opts.config, ai: opts.ai, aiModel: opts.aiModel });
|
|
122676
123450
|
});
|
|
122677
123451
|
program2.command("connect <type>").description(
|
|
122678
|
-
"Add or update a connector (elasticsearch / mongodb / postgres / sentry / grafana / redis / ai) in .horus/config.json"
|
|
122679
|
-
).option("--env <name>", "target environment (default: first environment in config)").option("--provider <name>", "AI provider for `connect ai` (anthropic / claude / codex / gemini)").option("--api-key <key>", "Anthropic API key for `connect ai`").option("--model <id>", "default model for `connect ai`").option("--url <url>", "connector URL or connection string (Redis with auth: redis://:password@host:6379)").option("--username <user>", "username (elasticsearch / grafana)").option("--password <pass>", "password (elasticsearch / grafana; for Redis embed in --url)").option("--index-pattern <pattern>", "Elasticsearch index pattern (required for elasticsearch)").option("--service <name>", "service name scope for log queries").option("--database <name>", "database name (required for mongodb)").option("--collections <list>", "comma-separated collection allowlist (mongodb)").option("--schema <name>", "schema to introspect (postgres; default public)").option("--tables <list>", "comma-separated table allowlist (postgres)").option("--auth-token <token>", "Sentry API auth token (required for sentry)").option("--org <slug>", "Sentry org slug (required for sentry)").option("--project <slug>", "Sentry project slug (required for sentry)").option("--dashboard <uid>", "default dashboard uid (grafana)").option(
|
|
123452
|
+
"Add or update a connector (elasticsearch / mongodb / postgres / sentry / axiom / grafana / redis / ai) in .horus/config.json"
|
|
123453
|
+
).option("--env <name>", "target environment (default: first environment in config)").option("--provider <name>", "AI provider for `connect ai` (anthropic / claude / codex / gemini)").option("--api-key <key>", "Anthropic API key for `connect ai`").option("--model <id>", "default model for `connect ai`").option("--url <url>", "connector URL or connection string (Redis with auth: redis://:password@host:6379)").option("--username <user>", "username (elasticsearch / grafana)").option("--password <pass>", "password (elasticsearch / grafana; for Redis embed in --url)").option("--index-pattern <pattern>", "Elasticsearch index pattern (required for elasticsearch)").option("--service <name>", "service name scope for log queries").option("--database <name>", "database name (required for mongodb)").option("--collections <list>", "comma-separated collection allowlist (mongodb)").option("--schema <name>", "schema to introspect (postgres; default public)").option("--tables <list>", "comma-separated table allowlist (postgres)").option("--auth-token <token>", "Sentry API auth token (required for sentry)").option("--org <slug>", "Sentry org slug (required for sentry)").option("--project <slug>", "Sentry project slug (required for sentry)").option("--token <token>", "Axiom API token (required for axiom)").option("--dataset <name>", "Axiom dataset to query (required for axiom)").option("--dashboard <uid>", "default dashboard uid (grafana)").option(
|
|
122680
123454
|
"--db <spec>",
|
|
122681
123455
|
"redis logical DB as db:role1,role2 (e.g. 0:cache,state or 1:bullmq,queues); repeatable",
|
|
122682
123456
|
(val, acc) => {
|
|
@@ -122700,6 +123474,8 @@ Examples:
|
|
|
122700
123474
|
authToken: opts.authToken,
|
|
122701
123475
|
org: opts.org,
|
|
122702
123476
|
project: opts.project,
|
|
123477
|
+
token: opts.token,
|
|
123478
|
+
dataset: opts.dataset,
|
|
122703
123479
|
dashboard: opts.dashboard,
|
|
122704
123480
|
db: opts.db,
|
|
122705
123481
|
bullmqPrefix: opts.bullmqPrefix,
|
|
@@ -122814,7 +123590,7 @@ Examples:
|
|
|
122814
123590
|
process.exitCode = await runMemoryDetect(opts);
|
|
122815
123591
|
}
|
|
122816
123592
|
);
|
|
122817
|
-
memory.command("accuracy").description("Report Horus's measured hit-rate from the converged outcome-label eval set").option("-c, --config <path>", "path to horus.config.ts").option("--repo <name>", "project/repository to scope to (default: inferred from cwd)").option("--source <source>", "filter to one signal source: feedback | confirm").option("--days <n>", "only count labels from the last N days", (v4) => Number(v4)).option("--limit <n>", "max labels to scan", (v4) => Number(v4)).option("--json", "output JSON").action(
|
|
123593
|
+
memory.command("accuracy").description("Report Horus's measured hit-rate from the converged outcome-label eval set").option("-c, --config <path>", "path to horus.config.ts").option("--repo <name>", "project/repository to scope to (default: inferred from cwd)").option("--source <source>", "filter to one signal source: feedback | confirm").option("--since <date>", "only count labels on/after this date (e.g. 2026-06-01)").option("--days <n>", "only count labels from the last N days", (v4) => Number(v4)).option("--all", "count the full retained history (disable the default recent window)").option("--limit <n>", "max labels to scan", (v4) => Number(v4)).option("--json", "output JSON").action(
|
|
122818
123594
|
async (opts) => {
|
|
122819
123595
|
process.exitCode = await runMemoryAccuracy(opts);
|
|
122820
123596
|
}
|