@merittdev/horus 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.cjs +768 -149
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -50314,7 +50314,7 @@ init_cjs_shims();
50314
50314
 
50315
50315
  // ../../packages/core/src/version.ts
50316
50316
  init_cjs_shims();
50317
- var HORUS_VERSION = true ? "0.1.11" : "dev";
50317
+ var HORUS_VERSION = true ? "0.1.13" : "dev";
50318
50318
  var PINNED_AXON_VERSION = "1.0.7";
50319
50319
  var PINNED_SOURCE_VERSION = PINNED_AXON_VERSION;
50320
50320
 
@@ -54699,6 +54699,19 @@ var modelsSchema = external_exports.object({
54699
54699
  reasoning: external_exports.string().default("claude-opus-4-8"),
54700
54700
  extraction: external_exports.string().default("claude-haiku-4-5")
54701
54701
  });
54702
+ var AI_PROVIDERS = ["anthropic", "claude", "codex", "gemini", "kimi", "cursor"];
54703
+ var aiSchema = external_exports.object({
54704
+ /** Preferred provider for AI narrative. */
54705
+ provider: external_exports.enum(AI_PROVIDERS).optional(),
54706
+ anthropic: external_exports.object({
54707
+ /** Direct API key (takes priority over apiKeyEnv). Stored redacted in display. */
54708
+ apiKey: external_exports.string().optional(),
54709
+ /** Env var holding the key. Defaults to ANTHROPIC_API_KEY. */
54710
+ apiKeyEnv: external_exports.string().optional(),
54711
+ /** Default model for narrative generation. */
54712
+ model: external_exports.string().optional()
54713
+ }).optional()
54714
+ }).optional();
54702
54715
  var horusConfigSchema = external_exports.object({
54703
54716
  projects: external_exports.array(projectSchema).default([]),
54704
54717
  axon: external_exports.object({
@@ -54706,8 +54719,19 @@ var horusConfigSchema = external_exports.object({
54706
54719
  pinnedVersion: external_exports.string().default("1.0.7")
54707
54720
  }).default({}),
54708
54721
  database: databaseSchema,
54709
- models: modelsSchema.default({})
54722
+ models: modelsSchema.default({}),
54723
+ ai: aiSchema
54710
54724
  });
54725
+ function resolveAiSettings(config) {
54726
+ const ai = config.ai;
54727
+ const fromConfig = ai?.anthropic?.apiKey;
54728
+ const key = fromConfig ?? process.env[ai?.anthropic?.apiKeyEnv ?? "ANTHROPIC_API_KEY"];
54729
+ const out = { anthropicKeyFromConfig: fromConfig !== void 0 };
54730
+ if (ai?.provider !== void 0) out.provider = ai.provider;
54731
+ if (key !== void 0) out.anthropicApiKey = key;
54732
+ if (ai?.anthropic?.model !== void 0) out.model = ai.anthropic.model;
54733
+ return out;
54734
+ }
54711
54735
  function listEnvironments(config) {
54712
54736
  const out = [];
54713
54737
  for (const p of config.projects) {
@@ -54892,7 +54916,8 @@ async function loadConfigFile(target) {
54892
54916
  projects: file.project ? [file.project] : [],
54893
54917
  database: file.database ?? {
54894
54918
  url: process.env["DATABASE_URL"] ?? DEFAULT_DB_URL
54895
- }
54919
+ },
54920
+ ...file.ai !== void 0 ? { ai: file.ai } : {}
54896
54921
  };
54897
54922
  return parseConfig(raw, target);
54898
54923
  }
@@ -55307,7 +55332,7 @@ var AxonCodeProvider = class {
55307
55332
  }
55308
55333
  async searchSymbols(query, limit = 10) {
55309
55334
  const E = this.escapeId(query);
55310
- const exactQuery = `MATCH (n) WHERE toLower(n.name) = toLower("${E}") AND NOT n:File RETURN n.id, n.name, n.file_path LIMIT ${limit}`;
55335
+ const exactQuery = `MATCH (n) WHERE toLower(n.name) = toLower("${E}") AND label(n) <> "File" RETURN n.id, n.name, n.file_path LIMIT ${limit}`;
55311
55336
  const [exactRows, semanticRes] = await Promise.all([
55312
55337
  this.rows(exactQuery).catch(() => []),
55313
55338
  this.client.search(query, limit)
@@ -67396,9 +67421,14 @@ async function runExplain(query, opts) {
67396
67421
  }
67397
67422
  async function isQueueBoundary(config, query) {
67398
67423
  try {
67424
+ let project;
67425
+ try {
67426
+ project = resolveEnvironment(config).project;
67427
+ } catch {
67428
+ }
67399
67429
  const { db, sql: sql2 } = createDb(config.database.url);
67400
67430
  try {
67401
- const edges = await listQueueEdges(db, { queueName: query });
67431
+ const edges = await listQueueEdges(db, { queueName: query, project });
67402
67432
  return edges.length > 0;
67403
67433
  } finally {
67404
67434
  await sql2.end().catch(() => {
@@ -67508,22 +67538,22 @@ function extractQueueGraph(input) {
67508
67538
  }
67509
67539
  }
67510
67540
  const producers = [];
67511
- for (const pc34 of input.producerClasses) {
67512
- for (const m of pc34.content.matchAll(INJECT_QUEUE_RE)) {
67541
+ for (const pc35 of input.producerClasses) {
67542
+ for (const m of pc35.content.matchAll(INJECT_QUEUE_RE)) {
67513
67543
  const queue = m[1] ?? "";
67514
- if (queue) producers.push({ queue, symbol: pc34.name, file: pc34.filePath });
67544
+ if (queue) producers.push({ queue, symbol: pc35.name, file: pc35.filePath });
67515
67545
  }
67516
- for (const m of pc34.content.matchAll(NEW_BULL_RE)) {
67546
+ for (const m of pc35.content.matchAll(NEW_BULL_RE)) {
67517
67547
  if (m[2] !== "Queue") continue;
67518
67548
  const queue = resolveArg(m[3] ?? "", constMap);
67519
67549
  if (queue) {
67520
- producers.push({ queue, symbol: (m[1] ?? pc34.name) || baseName(pc34.filePath), file: pc34.filePath });
67550
+ producers.push({ queue, symbol: (m[1] ?? pc35.name) || baseName(pc35.filePath), file: pc35.filePath });
67521
67551
  }
67522
67552
  }
67523
- for (const m of pc34.content.matchAll(QUEUE_NAME_CONST_RE)) {
67553
+ for (const m of pc35.content.matchAll(QUEUE_NAME_CONST_RE)) {
67524
67554
  const ident = m[1] ?? "";
67525
67555
  const queue = m[2] ?? "";
67526
- if (queue) producers.push({ queue, symbol: ident || baseName(pc34.filePath), file: pc34.filePath });
67556
+ if (queue) producers.push({ queue, symbol: ident || baseName(pc35.filePath), file: pc35.filePath });
67527
67557
  }
67528
67558
  }
67529
67559
  const workers = [];
@@ -67796,6 +67826,13 @@ async function runQueues(name, opts) {
67796
67826
  }
67797
67827
  }
67798
67828
  const rows = await listQueueEdges(db, { project, queueName: name });
67829
+ if (opts.json) {
67830
+ const topology = topologyToJson(buildQueueMap(rows));
67831
+ const out = { project: project ?? null, topology };
67832
+ if (opts.live) out["live"] = await gatherLiveState(config, rows, name);
67833
+ console.log(JSON.stringify(out, null, 2));
67834
+ return 0;
67835
+ }
67799
67836
  console.log(
67800
67837
  import_picocolors4.default.bold("Queue topology") + import_picocolors4.default.dim(" \xB7 source: code / source intelligence \xB7 static (run horus index to refresh)")
67801
67838
  );
@@ -67837,6 +67874,55 @@ function buildQueueMap(rows) {
67837
67874
  }
67838
67875
  return byQueue;
67839
67876
  }
67877
+ function endpoints(edges, symKey, fileKey) {
67878
+ const seen = /* @__PURE__ */ new Map();
67879
+ for (const e of edges) {
67880
+ const symbol = e[symKey];
67881
+ if (!symbol) continue;
67882
+ if (!seen.has(symbol)) seen.set(symbol, { symbol, file: e[fileKey] ?? null });
67883
+ }
67884
+ return [...seen.values()];
67885
+ }
67886
+ function topologyToJson(byQueue) {
67887
+ return [...byQueue.entries()].map(([queueName, edges]) => ({
67888
+ queueName,
67889
+ producers: endpoints(edges, "producerSymbol", "producerFile"),
67890
+ workers: endpoints(edges, "workerSymbol", "workerFile")
67891
+ }));
67892
+ }
67893
+ async function gatherLiveState(config, rows, nameFilter) {
67894
+ let renv;
67895
+ try {
67896
+ renv = resolveEnvironment(config);
67897
+ } catch (err) {
67898
+ return { ok: false, error: err.message };
67899
+ }
67900
+ const queueProvider = queueForEnv(renv);
67901
+ if (!queueProvider) return { ok: false, error: "Redis not configured" };
67902
+ try {
67903
+ const health = await queueProvider.health();
67904
+ if (!health.ok) return { ok: false, error: health.detail };
67905
+ const staticNames = new Set(buildQueueMap(rows).keys());
67906
+ let queueNames;
67907
+ if (nameFilter !== void 0) {
67908
+ queueNames = [nameFilter];
67909
+ } else {
67910
+ const discovered = await queueProvider.discoverQueues().catch(() => []);
67911
+ const union2 = /* @__PURE__ */ new Set([...staticNames, ...discovered]);
67912
+ queueNames = union2.size > 0 ? [...union2] : void 0;
67913
+ }
67914
+ const state = await queueProvider.analyzeQueues({ queueNames });
67915
+ return {
67916
+ ok: true,
67917
+ prefix: state.prefix,
67918
+ collectedAt: state.collectedAt,
67919
+ queues: state.queues.map((q) => ({ ...q, runtimeOnly: !staticNames.has(q.queueName) }))
67920
+ };
67921
+ } finally {
67922
+ await queueProvider.close().catch(() => {
67923
+ });
67924
+ }
67925
+ }
67840
67926
  function printTopology(byQueue) {
67841
67927
  for (const [queueName, edges] of byQueue) {
67842
67928
  console.log(import_picocolors4.default.bold(queueName));
@@ -70237,8 +70323,10 @@ async function investigate(input, deps) {
70237
70323
  const latencyMetricEvIds = [];
70238
70324
  const queueMetricEvIds = [];
70239
70325
  const queueMetricEvIdsByQueue = /* @__PURE__ */ new Map();
70326
+ const nominalMetricEvIds = [];
70240
70327
  let metricsCollected = false;
70241
70328
  let metricsFailureReason;
70329
+ let metricSeriesChecked = 0;
70242
70330
  if (deps.metrics) {
70243
70331
  const ac = new AbortController();
70244
70332
  let metricsTimerId;
@@ -70298,6 +70386,19 @@ async function investigate(input, deps) {
70298
70386
  }
70299
70387
  }
70300
70388
  }
70389
+ metricSeriesChecked = mFindings.length;
70390
+ if (metricEvIds.length === 0 && mFindings.length > 0) {
70391
+ const panels = [...new Set(mFindings.map((f) => f.panelTitle))];
70392
+ const ev = mkEv(
70393
+ "metric",
70394
+ `Metrics checked \u2014 ${mFindings.length} series across ${panels.length} panel(s), no anomalies in window`,
70395
+ { seriesChecked: mFindings.length, panelCount: panels.length, panels: panels.slice(0, 10), anomalies: 0, stance: "neutral" },
70396
+ {},
70397
+ collectedAt2,
70398
+ 0.2
70399
+ );
70400
+ nominalMetricEvIds.push(ev.id);
70401
+ }
70301
70402
  metricsCollected = true;
70302
70403
  } catch (metricsErr) {
70303
70404
  metricsFailureReason = metricsErr?.message?.slice(0, 120) ?? "unknown error";
@@ -70495,6 +70596,13 @@ async function investigate(input, deps) {
70495
70596
  confidence: 0.7,
70496
70597
  evidenceIds: metricEvIds
70497
70598
  });
70599
+ } else if (nominalMetricEvIds.length > 0) {
70600
+ findings2.push({
70601
+ kind: "observation",
70602
+ title: `Metrics nominal \u2014 ${metricSeriesChecked} series checked, no anomalies in window`,
70603
+ confidence: 0.5,
70604
+ evidenceIds: nominalMetricEvIds
70605
+ });
70498
70606
  }
70499
70607
  const causeInputs = [];
70500
70608
  const blastRadius = impact.affected;
@@ -71546,7 +71654,7 @@ async function discoverArchitecture(deps) {
71546
71654
  })();
71547
71655
  const asyncBoundaries = await (async () => {
71548
71656
  try {
71549
- const edges = await listQueueEdges(deps.db);
71657
+ const edges = await listQueueEdges(deps.db, { project: deps.project });
71550
71658
  const byQueue = /* @__PURE__ */ new Map();
71551
71659
  for (const edge of edges) {
71552
71660
  const key = edge.queueName;
@@ -71710,7 +71818,7 @@ async function analyzeBlastRadius(query, deps, depth = 3) {
71710
71818
  ]);
71711
71819
  const upstream = ctx.callees;
71712
71820
  const downstream = impact.byDepth;
71713
- const edges = await listQueueEdges(deps.db);
71821
+ const edges = await listQueueEdges(deps.db, { project: deps.project });
71714
71822
  const asyncDownstreamMap = /* @__PURE__ */ new Map();
71715
71823
  for (const edge of edges) {
71716
71824
  if (edge.producerFile === top.filePath || edge.producerSymbol === top.name) {
@@ -72551,6 +72659,167 @@ function refinedToJSON(r, v) {
72551
72659
  );
72552
72660
  }
72553
72661
 
72662
+ // ../../packages/engine/src/qa.ts
72663
+ init_cjs_shims();
72664
+ var CONTRADICTS_RE = /\b(contradict|contradicts|argues?\s+against|evidence\s+against|rule\s+out|disprove|refute|weaken)\b/i;
72665
+ var MISSING_RE = /\b(missing|absent|gaps?|don'?t\s+have|do\s+not\s+have|lack(?:ing)?|what\s+else)\b/i;
72666
+ var CONFIDENCE_RE = /\b(confidence|confident|certainty|sure|why\s+(?:is\s+)?(?:it\s+)?not\s+higher)\b/i;
72667
+ function detectQuestion(text2) {
72668
+ const t = text2.toLowerCase();
72669
+ if (CONFIDENCE_RE.test(t)) return "confidence";
72670
+ if (CONTRADICTS_RE.test(t)) return "contradicts";
72671
+ if (MISSING_RE.test(t)) return "missing-evidence";
72672
+ return null;
72673
+ }
72674
+ function categoriesForTopic(text2) {
72675
+ const t = text2.toLowerCase();
72676
+ for (const [topic, entry2] of Object.entries(TOPIC_MAP)) {
72677
+ if (topic === t.trim()) return { topic, categories: entry2.categories };
72678
+ if (entry2.keywords.some((kw) => new RegExp(`\\b${kw}\\b`).test(t))) {
72679
+ return { topic, categories: entry2.categories };
72680
+ }
72681
+ }
72682
+ return null;
72683
+ }
72684
+ function evidenceById(report) {
72685
+ return new Map(report.evidence.map((e) => [e.id, e]));
72686
+ }
72687
+ function answerContradicts(report, question) {
72688
+ const matched = categoriesForTopic(question);
72689
+ const byId = evidenceById(report);
72690
+ const hyps = matched ? report.hypotheses.filter((h) => matched.categories.includes(h.category)) : [];
72691
+ if (matched && hyps.length === 0) {
72692
+ const evaluated = [...new Set(report.hypotheses.map((h) => h.category))];
72693
+ return {
72694
+ question,
72695
+ kind: "contradicts",
72696
+ headline: `"${matched.topic}" was not among the evaluated hypotheses \u2014 no evidence supports it.`,
72697
+ details: evaluated.length > 0 ? [`Hypotheses evaluated: ${evaluated.join(", ")}.`] : ["No hypotheses were formed for this investigation."],
72698
+ evidence: []
72699
+ };
72700
+ }
72701
+ if (hyps.length === 0) {
72702
+ const ids3 = [...new Set(report.hypotheses.flatMap((h) => h.contradictingEvidenceIds))];
72703
+ const ev2 = ids3.map((id) => byId.get(id)).filter((e) => e !== void 0);
72704
+ return {
72705
+ question,
72706
+ kind: "contradicts",
72707
+ headline: ev2.length > 0 ? `${ev2.length} item(s) contradict the leading hypotheses.` : "No contradicting evidence was recorded for any hypothesis.",
72708
+ details: ev2.length > 0 ? [] : ["Evidence either supports or is neutral to the hypotheses."],
72709
+ evidence: ev2
72710
+ };
72711
+ }
72712
+ const ids2 = [...new Set(hyps.flatMap((h) => h.contradictingEvidenceIds))];
72713
+ const ev = ids2.map((id) => byId.get(id)).filter((e) => e !== void 0);
72714
+ const verdicts = [...new Set(hyps.map((h) => h.verdict))].join(", ");
72715
+ if (ev.length === 0) {
72716
+ return {
72717
+ question,
72718
+ kind: "contradicts",
72719
+ headline: `No evidence contradicts "${matched?.topic ?? "this"}" (verdict: ${verdicts}).`,
72720
+ details: hyps.map((h) => `${h.category}: ${h.rationale ?? h.statement}`),
72721
+ evidence: []
72722
+ };
72723
+ }
72724
+ return {
72725
+ question,
72726
+ kind: "contradicts",
72727
+ headline: `${ev.length} item(s) contradict "${matched?.topic ?? "this"}" (verdict: ${verdicts}).`,
72728
+ details: hyps.map((h) => `${h.category}: ${h.rationale ?? h.statement}`),
72729
+ evidence: ev
72730
+ };
72731
+ }
72732
+ function answerMissing(report, question) {
72733
+ const { gaps, blindSpots } = report.gapAnalysis;
72734
+ const hypMissing = [...new Set(report.hypotheses.flatMap((h) => h.missingEvidence))];
72735
+ if (gaps.length === 0 && hypMissing.length === 0) {
72736
+ return {
72737
+ question,
72738
+ kind: "missing-evidence",
72739
+ headline: "No evidence gaps \u2014 all expected dimensions were collected.",
72740
+ details: [],
72741
+ evidence: []
72742
+ };
72743
+ }
72744
+ const details = gaps.map(
72745
+ (g) => `${g.dimension}: ${g.why} \u2192 ${g.nextSource} (\u2212${(g.confidenceImpact * 100).toFixed(0)}% ceiling)`
72746
+ );
72747
+ if (hypMissing.length > 0) {
72748
+ details.push(`To confirm hypotheses: ${hypMissing.join("; ")}.`);
72749
+ }
72750
+ for (const bs of blindSpots) details.push(`Blind spot: ${bs}`);
72751
+ return {
72752
+ question,
72753
+ kind: "missing-evidence",
72754
+ headline: `${gaps.length} evidence gap(s) limit this investigation.`,
72755
+ details,
72756
+ evidence: []
72757
+ };
72758
+ }
72759
+ function answerConfidence(report, question) {
72760
+ const { gaps, confidenceCeiling } = report.gapAnalysis;
72761
+ const ceilingPct = Math.round(confidenceCeiling * 100);
72762
+ const actualPct = Math.round(report.confidence * 100);
72763
+ const limiting = [...gaps].sort((a, b2) => b2.confidenceImpact - a.confidenceImpact);
72764
+ const details = [];
72765
+ if (limiting.length > 0) {
72766
+ details.push(`Confidence is capped at ${ceilingPct}% by missing evidence:`);
72767
+ for (const g of limiting) {
72768
+ details.push(` \u2022 ${g.dimension} (\u2212${(g.confidenceImpact * 100).toFixed(0)}%): ${g.why}`);
72769
+ }
72770
+ } else {
72771
+ details.push("No evidence gaps cap the ceiling.");
72772
+ }
72773
+ const weak = report.hypotheses.filter((h) => h.verdict === "weakened" || h.verdict === "unconfirmed");
72774
+ if (weak.length > 0) {
72775
+ details.push(
72776
+ `Unconfirmed/weakened hypotheses: ${weak.map((h) => `${h.category} (${h.verdict})`).join(", ")}.`
72777
+ );
72778
+ }
72779
+ return {
72780
+ question,
72781
+ kind: "confidence",
72782
+ headline: `Confidence is ${actualPct}% (ceiling ${ceilingPct}%). ${limiting.length > 0 ? "Missing evidence is the main limiter." : "Limited by hypothesis support, not gaps."}`,
72783
+ details,
72784
+ evidence: []
72785
+ };
72786
+ }
72787
+ function answerQuestion(report, question) {
72788
+ const kind = detectQuestion(question);
72789
+ if (kind === null) return null;
72790
+ if (kind === "contradicts") return answerContradicts(report, question);
72791
+ if (kind === "missing-evidence") return answerMissing(report, question);
72792
+ return answerConfidence(report, question);
72793
+ }
72794
+ function renderQAAnswer(a) {
72795
+ const lines = [];
72796
+ lines.push(`Q: ${a.question}`);
72797
+ lines.push("");
72798
+ lines.push(a.headline);
72799
+ for (const d of a.details) lines.push(d.startsWith(" ") ? d : ` ${d}`);
72800
+ if (a.evidence.length > 0) {
72801
+ lines.push("");
72802
+ lines.push("Evidence:");
72803
+ for (const e of a.evidence) {
72804
+ lines.push(` [${e.id.slice(0, 8)}] (${e.kind}) ${e.title}`);
72805
+ }
72806
+ }
72807
+ return lines.join("\n");
72808
+ }
72809
+ function qaToJSON(a) {
72810
+ return JSON.stringify(
72811
+ {
72812
+ question: a.question,
72813
+ kind: a.kind,
72814
+ headline: a.headline,
72815
+ details: a.details,
72816
+ evidence: a.evidence.map((e) => ({ id: e.id, kind: e.kind, title: e.title }))
72817
+ },
72818
+ null,
72819
+ 2
72820
+ );
72821
+ }
72822
+
72554
72823
  // ../../packages/engine/src/onboard.ts
72555
72824
  init_cjs_shims();
72556
72825
  function tokenize2(text2) {
@@ -72667,7 +72936,11 @@ function filterArchitecture(architecture, tokens) {
72667
72936
  };
72668
72937
  }
72669
72938
  async function buildOnboarding(input, deps) {
72670
- const architecture = await discoverArchitecture({ code: deps.code, db: deps.db });
72939
+ const architecture = await discoverArchitecture({
72940
+ code: deps.code,
72941
+ db: deps.db,
72942
+ project: deps.project
72943
+ });
72671
72944
  const area = input.area?.trim();
72672
72945
  let filteredArchitecture = architecture;
72673
72946
  let pastIncidents = [];
@@ -73394,18 +73667,76 @@ Hard rules:
73394
73667
  - only include services from: ${input.knownServices.join(", ")}
73395
73668
  - hypothesisJudgments must only reference hypothesis IDs from the hypotheses list above
73396
73669
  - verdict must be exactly one of: supported, weakened, eliminated, unconfirmed
73397
- - uncertainty must be exactly one of: low, medium, high`;
73670
+ - uncertainty must be exactly one of: low, medium, high
73671
+ - output raw JSON only: no markdown code fences, no text before or after the JSON`;
73398
73672
  }
73399
- function parseOutput(raw, input, ceiling) {
73400
- let parsed;
73401
- try {
73402
- parsed = JSON.parse(raw);
73403
- } catch {
73404
- const jsonMatch = raw.match(/\{[\s\S]*\}/);
73405
- if (!jsonMatch) {
73406
- throw new Error("Anthropic response did not contain a JSON object");
73673
+ function sliceBalancedObjects(s) {
73674
+ const out = [];
73675
+ let depth = 0;
73676
+ let start = -1;
73677
+ let inStr = false;
73678
+ let escaped = false;
73679
+ for (let i = 0; i < s.length; i++) {
73680
+ const ch = s[i];
73681
+ if (inStr) {
73682
+ if (escaped) escaped = false;
73683
+ else if (ch === "\\") escaped = true;
73684
+ else if (ch === '"') inStr = false;
73685
+ continue;
73686
+ }
73687
+ if (ch === '"') inStr = true;
73688
+ else if (ch === "{") {
73689
+ if (depth === 0) start = i;
73690
+ depth++;
73691
+ } else if (ch === "}") {
73692
+ if (depth > 0) {
73693
+ depth--;
73694
+ if (depth === 0 && start !== -1) out.push(s.slice(start, i + 1));
73695
+ }
73407
73696
  }
73408
- parsed = JSON.parse(jsonMatch[0]);
73697
+ }
73698
+ return out.sort((a, b2) => b2.length - a.length);
73699
+ }
73700
+ function stripTrailingCommas(s) {
73701
+ return s.replace(/,(\s*[}\]])/g, "$1");
73702
+ }
73703
+ function extractJson(raw) {
73704
+ const candidates = [];
73705
+ const trimmed = raw.trim();
73706
+ candidates.push(trimmed);
73707
+ const fence = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
73708
+ if (fence?.[1]) candidates.push(fence[1].trim());
73709
+ candidates.push(...sliceBalancedObjects(trimmed));
73710
+ for (const c of candidates) {
73711
+ for (const variant of [c, stripTrailingCommas(c)]) {
73712
+ try {
73713
+ const parsed = JSON.parse(variant);
73714
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
73715
+ return parsed;
73716
+ }
73717
+ } catch {
73718
+ }
73719
+ }
73720
+ }
73721
+ return null;
73722
+ }
73723
+ function rawNarrativeFallback(raw, input, ceiling) {
73724
+ const cleaned = raw.replace(/```(?:json)?/gi, "").trim();
73725
+ return {
73726
+ what: input.deterministicSummary || "AI returned an unstructured response.",
73727
+ why: cleaned.slice(0, 4e3),
73728
+ whereNext: [],
73729
+ citations: [],
73730
+ confidence: Math.min(input.reportConfidence, ceiling)
73731
+ };
73732
+ }
73733
+ function parseOutput(raw, input, ceiling) {
73734
+ const parsed = extractJson(raw);
73735
+ if (parsed === null) {
73736
+ console.error(
73737
+ `[ai] structured JSON parse failed (response length ${raw.length}); preserving raw narrative fallback`
73738
+ );
73739
+ return rawNarrativeFallback(raw, input, ceiling);
73409
73740
  }
73410
73741
  const confidence = Math.min(
73411
73742
  typeof parsed["confidence"] === "number" ? parsed["confidence"] : input.reportConfidence,
@@ -73460,6 +73791,13 @@ init_cjs_shims();
73460
73791
 
73461
73792
  // ../../packages/ai/src/local-providers.ts
73462
73793
  init_cjs_shims();
73794
+ var LOCAL_PROVIDER_IDS = [
73795
+ "codex",
73796
+ "claude",
73797
+ "kimi",
73798
+ "gemini",
73799
+ "cursor"
73800
+ ];
73463
73801
  var DEFAULT_DESCRIPTORS = [
73464
73802
  { id: "codex", displayName: "OpenAI Codex CLI" },
73465
73803
  { id: "claude", displayName: "Anthropic Claude" },
@@ -73658,8 +73996,12 @@ async function runInvestigate(hint, opts) {
73658
73996
  const rendered = format === "json" ? reportToJSON(report) : format === "markdown" || format === "md" ? reportToMarkdown(report) : renderReport2(report);
73659
73997
  console.log(rendered);
73660
73998
  if (opts.ai && format !== "json") {
73661
- const model = opts.aiModel ?? "claude-opus-4-8";
73662
- const provider = opts._aiProvider ?? new AnthropicNarrativeProvider({ model });
73999
+ const ai = resolveAiSettings(config);
74000
+ const model = opts.aiModel ?? ai.model ?? "claude-opus-4-8";
74001
+ const provider = opts._aiProvider ?? new AnthropicNarrativeProvider({
74002
+ model,
74003
+ ...ai.anthropicApiKey !== void 0 ? { apiKey: ai.anthropicApiKey } : {}
74004
+ });
73663
74005
  console.log(import_picocolors5.default.dim(`[ai] model: ${model}`));
73664
74006
  const narrativeInput = buildNarrativeInput(report);
73665
74007
  const { output, fromProvider, validationErrors } = await renderNarrative(narrativeInput, { provider });
@@ -73714,6 +74056,12 @@ async function runChanges(base2, compare, opts) {
73714
74056
  // ../../packages/cli/src/commands/timeline.ts
73715
74057
  init_cjs_shims();
73716
74058
  var import_picocolors7 = __toESM(require_picocolors(), 1);
74059
+ var DEFAULT_SINCE = "7 days ago";
74060
+ function resolveTimelineWindow(opts) {
74061
+ const usingDefault = !opts.all && opts.since === void 0;
74062
+ const since = opts.all ? void 0 : opts.since ?? DEFAULT_SINCE;
74063
+ return { since, usingDefault };
74064
+ }
73717
74065
  async function runTimeline(service, opts) {
73718
74066
  try {
73719
74067
  const config = await loadConfig(opts.config);
@@ -73725,11 +74073,24 @@ async function runTimeline(service, opts) {
73725
74073
  return 1;
73726
74074
  }
73727
74075
  const { code } = createConnectors(config);
74076
+ const { since, usingDefault: usingDefaultWindow } = resolveTimelineWindow(opts);
73728
74077
  const t = await reconstructChangeTimeline(
73729
- { repoPath: renv.path, since: opts.since, until: opts.until, service },
74078
+ { repoPath: renv.path, since, until: opts.until, service },
73730
74079
  { code }
73731
74080
  );
73732
- console.log(opts.json ? changeTimelineToJSON(t) : renderChangeTimeline(t));
74081
+ if (opts.json) {
74082
+ console.log(changeTimelineToJSON(t));
74083
+ } else {
74084
+ console.log(renderChangeTimeline(t));
74085
+ if (usingDefaultWindow) {
74086
+ console.log(
74087
+ import_picocolors7.default.dim(
74088
+ `
74089
+ Showing the last 7 days (default). Widen with ${import_picocolors7.default.bold('--since "30 days ago"')}, pin a range with ${import_picocolors7.default.bold("--since <when> --until <when>")}, or see everything with ${import_picocolors7.default.bold("--all")}.`
74090
+ )
74091
+ );
74092
+ }
74093
+ }
73733
74094
  return 0;
73734
74095
  } catch (err) {
73735
74096
  console.error(import_picocolors7.default.red(err.message));
@@ -73740,7 +74101,7 @@ async function runTimeline(service, opts) {
73740
74101
  // ../../packages/cli/src/commands/what-changed.ts
73741
74102
  init_cjs_shims();
73742
74103
  var import_picocolors8 = __toESM(require_picocolors(), 1);
73743
- var DEFAULT_SINCE = "7 days ago";
74104
+ var DEFAULT_SINCE2 = "7 days ago";
73744
74105
  async function runWhatChanged(service, opts) {
73745
74106
  try {
73746
74107
  const config = await loadConfig(opts.config);
@@ -73752,7 +74113,7 @@ async function runWhatChanged(service, opts) {
73752
74113
  return 1;
73753
74114
  }
73754
74115
  const { code } = createConnectors(config);
73755
- const since = opts.since ?? DEFAULT_SINCE;
74116
+ const since = opts.since ?? DEFAULT_SINCE2;
73756
74117
  const r = await whatChanged(
73757
74118
  { repoPath: renv.path, since, until: opts.until, service },
73758
74119
  { code }
@@ -73779,9 +74140,14 @@ async function runArchitecture(opts) {
73779
74140
  );
73780
74141
  return 1;
73781
74142
  }
74143
+ let project;
74144
+ try {
74145
+ project = resolveEnvironment(config, { project: opts.repo }).project;
74146
+ } catch {
74147
+ }
73782
74148
  const { db, sql: sql2 } = createDb(config.database.url);
73783
74149
  try {
73784
- const m = await discoverArchitecture({ code, db });
74150
+ const m = await discoverArchitecture({ code, db, project });
73785
74151
  console.log(opts.json ? architectureToJSON(m) : renderArchitecture(m));
73786
74152
  } finally {
73787
74153
  await sql2.end();
@@ -73805,9 +74171,14 @@ async function runBlastRadius(query, opts) {
73805
74171
  console.error(import_picocolors10.default.red("Source-intelligence host unreachable \u2014 run: horus index"));
73806
74172
  return 1;
73807
74173
  }
74174
+ let project;
74175
+ try {
74176
+ project = resolveEnvironment(config, { project: opts.repo }).project;
74177
+ } catch {
74178
+ }
73808
74179
  const { db, sql: sql2 } = createDb(config.database.url);
73809
74180
  try {
73810
- const r = await analyzeBlastRadius(query, { code, db }, opts.depth ?? 3);
74181
+ const r = await analyzeBlastRadius(query, { code, db, project }, opts.depth ?? 3);
73811
74182
  if (!r) {
73812
74183
  console.log(`No symbol found for: ${query}`);
73813
74184
  console.log(import_picocolors10.default.dim(` Tip: use an exact class or function name, e.g. "MyService"`));
@@ -74213,6 +74584,11 @@ async function runAsk(id, directive, opts) {
74213
74584
  return 1;
74214
74585
  }
74215
74586
  const report = migrateReport(row.report);
74587
+ const answer = answerQuestion(report, directive);
74588
+ if (answer) {
74589
+ console.log(opts.json ? qaToJSON(answer) : renderQAAnswer(answer));
74590
+ return 0;
74591
+ }
74216
74592
  const v = refineInvestigation(report, directive);
74217
74593
  console.log(opts.json ? refinedToJSON(report, v) : renderRefined(report, v));
74218
74594
  if (!opts.json && report.aiJudgment) {
@@ -74265,7 +74641,10 @@ async function runOnboard(area, opts) {
74265
74641
  }
74266
74642
  const { db, sql: sql2 } = createDb(config.database.url);
74267
74643
  try {
74268
- const g = await buildOnboarding({ area }, { code, db, repoPath: repo.path });
74644
+ const g = await buildOnboarding(
74645
+ { area },
74646
+ { code, db, repoPath: repo.path, project: renv.project }
74647
+ );
74269
74648
  console.log(opts.json ? onboardingToJSON(g) : renderOnboarding(g));
74270
74649
  } finally {
74271
74650
  await sql2.end();
@@ -74341,6 +74720,10 @@ function sinceToIso(since) {
74341
74720
  const msMap = { m: 6e4, h: 36e5, d: 864e5 };
74342
74721
  return new Date(Date.now() - (msMap[unit] ?? 6e4) * amount).toISOString();
74343
74722
  }
74723
+ function resolveRawLevel(opts) {
74724
+ if (opts.allLevels) return void 0;
74725
+ return opts.level ?? "error";
74726
+ }
74344
74727
  function levelColor(level, text2) {
74345
74728
  if (level === "error" || level === "fatal") return import_picocolors20.default.red(text2);
74346
74729
  if (level === "warn") return import_picocolors20.default.yellow(text2);
@@ -74400,13 +74783,45 @@ async function runLogs(service, opts) {
74400
74783
  const from = sinceToIso(opts.since) ?? new Date(Date.now() - 7 * 864e5).toISOString();
74401
74784
  const fromDisplay = from.slice(0, 16).replace("T", " ");
74402
74785
  if (opts.raw === true) {
74786
+ const rawLevel = resolveRawLevel(opts);
74787
+ const limit = opts.limit !== void 0 ? Math.min(Number(opts.limit), 1e3) : 20;
74403
74788
  const records = await logs.searchLogs({
74404
74789
  service: resolvedService,
74405
74790
  from,
74406
- level: opts.level,
74791
+ level: rawLevel,
74407
74792
  text: opts.grep,
74408
- limit: opts.limit !== void 0 ? Number(opts.limit) : 20
74793
+ limit
74409
74794
  });
74795
+ if (opts.json) {
74796
+ console.log(
74797
+ JSON.stringify(
74798
+ {
74799
+ scope: {
74800
+ project: renv.project,
74801
+ env: renv.env,
74802
+ service: resolvedService ?? null,
74803
+ index: indexPattern,
74804
+ from,
74805
+ level: rawLevel ?? "all",
74806
+ grep: opts.grep ?? null
74807
+ },
74808
+ records: records.map((r) => ({
74809
+ timestamp: r.timestamp,
74810
+ level: r.level,
74811
+ service: r.service ?? null,
74812
+ component: r.component ?? null,
74813
+ eventCode: r.eventCode ?? null,
74814
+ message: r.message,
74815
+ traceId: r.traceId ?? null,
74816
+ requestId: r.requestId ?? null
74817
+ }))
74818
+ },
74819
+ null,
74820
+ 2
74821
+ )
74822
+ );
74823
+ return 0;
74824
+ }
74410
74825
  if (records.length === 0) {
74411
74826
  console.log(import_picocolors20.default.dim("No logs matched."));
74412
74827
  return 0;
@@ -74435,6 +74850,30 @@ async function runLogs(service, opts) {
74435
74850
  scopeService = void 0;
74436
74851
  }
74437
74852
  }
74853
+ if (opts.json) {
74854
+ console.log(
74855
+ JSON.stringify(
74856
+ {
74857
+ scope: {
74858
+ project: renv.project,
74859
+ env: renv.env,
74860
+ service: scopeService ?? null,
74861
+ index: indexPattern,
74862
+ from,
74863
+ severity: "error+",
74864
+ grep: opts.grep ?? null
74865
+ },
74866
+ totalErrors: analysis.totalErrors,
74867
+ signatures: analysis.signatures,
74868
+ newSignatures: analysis.newSignatures,
74869
+ affectedServices: analysis.affectedServices
74870
+ },
74871
+ null,
74872
+ 2
74873
+ )
74874
+ );
74875
+ return 0;
74876
+ }
74438
74877
  console.log(
74439
74878
  import_picocolors20.default.bold(`Error analysis`) + import_picocolors20.default.dim(
74440
74879
  ` \u2014 ${renv.project}/${renv.env}` + (scopeService ? ` \xB7 service ${scopeService}` : "") + (opts.grep ? ` \xB7 grep "${opts.grep}"` : "")
@@ -74559,6 +74998,25 @@ async function runMetrics(hint, opts) {
74559
74998
  const from = to - dur;
74560
74999
  if (opts.query !== void 0 && opts.query !== "") {
74561
75000
  const series = await metrics.rawRange(opts.query, from, to, stepNum);
75001
+ if (opts.json === true) {
75002
+ console.log(
75003
+ JSON.stringify(
75004
+ {
75005
+ query: opts.query,
75006
+ from,
75007
+ to,
75008
+ step: stepNum,
75009
+ series: series.map((s) => {
75010
+ const summary = summarize(s);
75011
+ return { labels: s.labels, last: summary.last, avg: summary.avg };
75012
+ })
75013
+ },
75014
+ null,
75015
+ 2
75016
+ )
75017
+ );
75018
+ return 0;
75019
+ }
74562
75020
  if (series.length === 0) {
74563
75021
  console.log(import_picocolors21.default.dim("No series returned."));
74564
75022
  return 0;
@@ -74669,6 +75127,23 @@ async function runState(opts) {
74669
75127
  const analysis = await mongo.analyzeState(
74670
75128
  staleHours !== void 0 ? { staleHours } : {}
74671
75129
  );
75130
+ if (opts.json) {
75131
+ const staleSignals = analysis.collections.filter((c) => c.isStale === true).length;
75132
+ const anomalousSignals = analysis.collections.reduce((n, c) => n + c.anomalies.length, 0);
75133
+ console.log(
75134
+ JSON.stringify(
75135
+ {
75136
+ scope: { project: renv.project, env: renv.env, database: analysis.database },
75137
+ autoDiscovered: analysis.autoDiscovered,
75138
+ signals: { stale: staleSignals, anomalous: anomalousSignals },
75139
+ collections: analysis.collections
75140
+ },
75141
+ null,
75142
+ 2
75143
+ )
75144
+ );
75145
+ return 0;
75146
+ }
74672
75147
  const discoveryNote = analysis.autoDiscovered ? import_picocolors22.default.dim(` (${analysis.collections.length} collections, auto-discovered)`) : "";
74673
75148
  console.log(
74674
75149
  import_picocolors22.default.bold("State analysis") + import_picocolors22.default.dim(` \u2014 ${renv.project}/${renv.env} \xB7 db ${analysis.database}`) + discoveryNote
@@ -74914,8 +75389,8 @@ async function runSetup(opts) {
74914
75389
  // ../../packages/cli/src/commands/connect.ts
74915
75390
  init_cjs_shims();
74916
75391
  var import_node_readline = require("readline");
74917
- var import_node_child_process5 = require("child_process");
74918
- var import_picocolors27 = __toESM(require_picocolors(), 1);
75392
+ var import_node_child_process6 = require("child_process");
75393
+ var import_picocolors28 = __toESM(require_picocolors(), 1);
74919
75394
 
74920
75395
  // ../../packages/cli/src/lib/tty-selector.ts
74921
75396
  init_cjs_shims();
@@ -75095,6 +75570,122 @@ async function checkboxSearch(opts) {
75095
75570
  });
75096
75571
  }
75097
75572
 
75573
+ // ../../packages/cli/src/commands/connect-ai.ts
75574
+ init_cjs_shims();
75575
+ var import_node_child_process5 = require("child_process");
75576
+ var import_picocolors27 = __toESM(require_picocolors(), 1);
75577
+ function cliInstalled(id) {
75578
+ const r = (0, import_node_child_process5.spawnSync)(id, ["--version"], { stdio: "ignore", timeout: 2e3 });
75579
+ return !r.error;
75580
+ }
75581
+ async function probeAnthropic(apiKey) {
75582
+ try {
75583
+ const res = await fetch("https://api.anthropic.com/v1/models?limit=1", {
75584
+ headers: { "x-api-key": apiKey, "anthropic-version": "2023-06-01" },
75585
+ signal: AbortSignal.timeout(8e3)
75586
+ });
75587
+ if (res.ok) return { ok: true, detail: "API key valid" };
75588
+ if (res.status === 401) return { ok: false, detail: "invalid API key (401)" };
75589
+ return { ok: false, detail: `HTTP ${res.status}` };
75590
+ } catch (err) {
75591
+ return { ok: false, detail: err.message };
75592
+ }
75593
+ }
75594
+ function redactKey(key) {
75595
+ if (key.length <= 12) return "***";
75596
+ return `${key.slice(0, 7)}\u2026${key.slice(-4)}`;
75597
+ }
75598
+ async function runConnectAi(opts) {
75599
+ try {
75600
+ const cwd = process.cwd();
75601
+ const root = findRepoRoot(cwd) ?? cwd;
75602
+ const configPath = discoverLocalConfig(cwd) ?? localConfigPath(root);
75603
+ const installed = LOCAL_PROVIDER_IDS.filter((id) => cliInstalled(id));
75604
+ console.log(import_picocolors27.default.bold("\nAI providers"));
75605
+ console.log(
75606
+ import_picocolors27.default.dim(
75607
+ ` Local CLIs on PATH: ${installed.length > 0 ? installed.join(", ") : "none detected"}`
75608
+ )
75609
+ );
75610
+ console.log(import_picocolors27.default.dim(" Cloud: anthropic (Anthropic Claude API \u2014 used by `investigate --ai`)\n"));
75611
+ let provider = opts.provider;
75612
+ if (provider === void 0) {
75613
+ if (isInteractive()) {
75614
+ const answer = (await ask("Provider", "anthropic", false)).trim().toLowerCase();
75615
+ provider = answer || "anthropic";
75616
+ } else {
75617
+ provider = "anthropic";
75618
+ }
75619
+ }
75620
+ if (!AI_PROVIDERS.includes(provider)) {
75621
+ console.error(import_picocolors27.default.red(`Unknown AI provider: ${provider}`) + import_picocolors27.default.dim(`
75622
+ supported: ${AI_PROVIDERS.join(", ")}`));
75623
+ return 1;
75624
+ }
75625
+ const aiBlock = { provider };
75626
+ let storedKey;
75627
+ if (provider === "anthropic") {
75628
+ let apiKey = opts.apiKey;
75629
+ if (!apiKey && isInteractive()) {
75630
+ apiKey = await askPassword("Anthropic API key") || void 0;
75631
+ }
75632
+ if (!apiKey) {
75633
+ console.error(
75634
+ import_picocolors27.default.red("No API key provided.") + import_picocolors27.default.dim("\n Pass --api-key sk-ant-\u2026 or set ANTHROPIC_API_KEY and re-run.")
75635
+ );
75636
+ return 1;
75637
+ }
75638
+ if (!opts.noTest) {
75639
+ const probe2 = await probeAnthropic(apiKey);
75640
+ if (!probe2.ok) {
75641
+ console.error(`
75642
+ ${import_picocolors27.default.red("\u2717 Anthropic key check failed:")} ${probe2.detail}`);
75643
+ console.error(import_picocolors27.default.dim(" Fix the key and retry, or pass --no-test to skip."));
75644
+ return 1;
75645
+ }
75646
+ console.log(`
75647
+ ${import_picocolors27.default.green("\u2713")} anthropic ${import_picocolors27.default.dim(`(${probe2.detail})`)}`);
75648
+ }
75649
+ const anthropic = { apiKey };
75650
+ if (opts.model) anthropic["model"] = opts.model;
75651
+ aiBlock["anthropic"] = anthropic;
75652
+ storedKey = apiKey;
75653
+ } else {
75654
+ if (!installed.includes(provider)) {
75655
+ console.error(
75656
+ import_picocolors27.default.red(`${provider} CLI not found on PATH.`) + import_picocolors27.default.dim(`
75657
+ Install it first, or choose anthropic.`)
75658
+ );
75659
+ return 1;
75660
+ }
75661
+ const desc2 = DEFAULT_LOCAL_PROVIDER_REGISTRY.get(provider);
75662
+ console.log(`
75663
+ ${import_picocolors27.default.green("\u2713")} ${desc2?.displayName ?? provider} ${import_picocolors27.default.dim("detected on PATH")}`);
75664
+ console.log(
75665
+ import_picocolors27.default.dim(
75666
+ " Note: `investigate --ai` currently uses the Anthropic API for narrative.\n Local-CLI narrative generation is recorded as your preference for future use."
75667
+ )
75668
+ );
75669
+ }
75670
+ const file = discoverLocalConfig(cwd) ? readLocalConfig(configPath) : { version: 1, project: { name: root.split("/").pop() ?? "project", repositories: [{ name: root.split("/").pop() ?? "project", path: root }], environments: [{ name: "production", readOnly: true, connectors: {} }] } };
75671
+ file.ai = aiBlock;
75672
+ writeLocalConfig(root, file);
75673
+ if (storedKey !== void 0) {
75674
+ ensureCredentialGitignore(root);
75675
+ ensureProjectGitignore(root);
75676
+ }
75677
+ console.log(`
75678
+ ${import_picocolors27.default.green("\u2713")} ${import_picocolors27.default.bold("ai")} provider saved \u2192 ${import_picocolors27.default.dim(configPath)}`);
75679
+ console.log(import_picocolors27.default.dim(` provider: ${provider}`));
75680
+ if (storedKey) console.log(import_picocolors27.default.dim(` anthropic key: ${redactKey(storedKey)}`));
75681
+ console.log(import_picocolors27.default.dim(' run: horus investigate "<hint>" --ai'));
75682
+ return 0;
75683
+ } catch (err) {
75684
+ console.error(import_picocolors27.default.red(err instanceof Error ? err.message : String(err)));
75685
+ return 1;
75686
+ }
75687
+ }
75688
+
75098
75689
  // ../../packages/cli/src/commands/connect.ts
75099
75690
  function parseDbSpec(spec) {
75100
75691
  const [dbPart, rolesPart] = spec.split(":");
@@ -75112,10 +75703,18 @@ function parseDbSpec(spec) {
75112
75703
  }
75113
75704
  var SUPPORTED = ["elasticsearch", "mongodb", "grafana", "redis"];
75114
75705
  async function runConnect(type, opts) {
75706
+ if (type === "ai") {
75707
+ return runConnectAi({
75708
+ provider: opts.provider,
75709
+ apiKey: opts.apiKey,
75710
+ model: opts.aiModel,
75711
+ noTest: opts.noTest
75712
+ });
75713
+ }
75115
75714
  if (!SUPPORTED.includes(type)) {
75116
75715
  console.error(
75117
- import_picocolors27.default.red(`Unknown connector type: ${type}`) + import_picocolors27.default.dim(`
75118
- supported: ${SUPPORTED.join(", ")}`)
75716
+ import_picocolors28.default.red(`Unknown connector type: ${type}`) + import_picocolors28.default.dim(`
75717
+ supported: ${SUPPORTED.join(", ")}, ai`)
75119
75718
  );
75120
75719
  return 1;
75121
75720
  }
@@ -75145,20 +75744,20 @@ async function runConnect(type, opts) {
75145
75744
  if (!probeResult.ok) {
75146
75745
  console.error(
75147
75746
  `
75148
- ${import_picocolors27.default.red(`\u2717 Could not reach ${connectorType}:`)} ${probeResult.detail}` + import_picocolors27.default.dim("\n Fix the connection and retry, or pass --no-test to skip.")
75747
+ ${import_picocolors28.default.red(`\u2717 Could not reach ${connectorType}:`)} ${probeResult.detail}` + import_picocolors28.default.dim("\n Fix the connection and retry, or pass --no-test to skip.")
75149
75748
  );
75150
75749
  return 1;
75151
75750
  }
75152
75751
  console.log(
75153
75752
  `
75154
- ${import_picocolors27.default.green("\u2713")} ${connectorType} reachable ${import_picocolors27.default.dim(`(${probeResult.detail})`)}`
75753
+ ${import_picocolors28.default.green("\u2713")} ${connectorType} reachable ${import_picocolors28.default.dim(`(${probeResult.detail})`)}`
75155
75754
  );
75156
75755
  }
75157
75756
  const hasLiteralCredentials = filled.url !== void 0 || filled.password !== void 0 || filled.username !== void 0;
75158
75757
  if (hasLiteralCredentials) {
75159
75758
  if (isGitTracked(configPath, root)) {
75160
75759
  console.error(
75161
- import_picocolors27.default.red(".horus/config.json is already tracked by Git.") + "\nStoring credentials here would expose them in the repository.\n" + import_picocolors27.default.dim(
75760
+ import_picocolors28.default.red(".horus/config.json is already tracked by Git.") + "\nStoring credentials here would expose them in the repository.\n" + import_picocolors28.default.dim(
75162
75761
  " Option A \u2014 remove from Git index then re-run:\n git rm --cached .horus/config.json\n horus connect " + type + " ...\n\n Option B \u2014 keep credentials in the environment and reference them:\n export ES_URL=https://...\n export ES_USERNAME=...\n export ES_PASSWORD=...\n Then edit .horus/config.json manually to use urlEnv/usernameEnv/passwordEnv."
75163
75762
  )
75164
75763
  );
@@ -75168,18 +75767,18 @@ ${import_picocolors27.default.green("\u2713")} ${connectorType} reachable ${impo
75168
75767
  }
75169
75768
  patchLocalConnector(configPath, connectorType, patch, filled.env);
75170
75769
  console.log(
75171
- `${import_picocolors27.default.green("\u2713")} ${import_picocolors27.default.bold(connectorType)} connector saved \u2192 ${import_picocolors27.default.dim(configPath)}`
75770
+ `${import_picocolors28.default.green("\u2713")} ${import_picocolors28.default.bold(connectorType)} connector saved \u2192 ${import_picocolors28.default.dim(configPath)}`
75172
75771
  );
75173
75772
  printSummary(connectorType, filled);
75174
- console.log(import_picocolors27.default.dim(`
75773
+ console.log(import_picocolors28.default.dim(`
75175
75774
  run: horus investigate "<hint>"`));
75176
75775
  return 0;
75177
75776
  } catch (err) {
75178
75777
  if (err instanceof ExitPromptError) {
75179
- console.error(import_picocolors27.default.red("Cancelled."));
75778
+ console.error(import_picocolors28.default.red("Cancelled."));
75180
75779
  return 1;
75181
75780
  }
75182
- console.error(import_picocolors27.default.red(err.message));
75781
+ console.error(import_picocolors28.default.red(err.message));
75183
75782
  return 1;
75184
75783
  }
75185
75784
  }
@@ -75188,7 +75787,7 @@ async function fillInteractive(type, opts) {
75188
75787
  if (!needsInteraction) return opts;
75189
75788
  console.log(
75190
75789
  `
75191
- ${import_picocolors27.default.bold(`Connect ${type}`)} ${import_picocolors27.default.dim("(press Enter to skip optional fields)")}
75790
+ ${import_picocolors28.default.bold(`Connect ${type}`)} ${import_picocolors28.default.dim("(press Enter to skip optional fields)")}
75192
75791
  `
75193
75792
  );
75194
75793
  const filled = { ...opts };
@@ -75243,7 +75842,7 @@ ${import_picocolors27.default.bold(`Connect ${type}`)} ${import_picocolors27.def
75243
75842
  break;
75244
75843
  case "redis": {
75245
75844
  console.log(
75246
- import_picocolors27.default.dim(
75845
+ import_picocolors28.default.dim(
75247
75846
  " Tip: embed credentials directly in the URL \u2014 redis://:password@host:6379\n or enter the URL and password separately below."
75248
75847
  )
75249
75848
  );
@@ -75281,8 +75880,8 @@ function missingRequired(type, opts) {
75281
75880
  }
75282
75881
  function ask(label, placeholder = "", required = true) {
75283
75882
  return new Promise((resolve8) => {
75284
- const hint = placeholder ? import_picocolors27.default.dim(` (${placeholder})`) : "";
75285
- const suffix = required ? "" : import_picocolors27.default.dim(" [optional]");
75883
+ const hint = placeholder ? import_picocolors28.default.dim(` (${placeholder})`) : "";
75884
+ const suffix = required ? "" : import_picocolors28.default.dim(" [optional]");
75286
75885
  process.stdout.write(` ${label}${suffix}${hint}: `);
75287
75886
  const rl = (0, import_node_readline.createInterface)({
75288
75887
  input: process.stdin,
@@ -75299,7 +75898,7 @@ function askPassword(label) {
75299
75898
  return new Promise((resolve8) => {
75300
75899
  const stdin = process.stdin;
75301
75900
  if (typeof stdin.setRawMode === "function") {
75302
- process.stdout.write(` ${label}${import_picocolors27.default.dim(" [optional]")}: `);
75901
+ process.stdout.write(` ${label}${import_picocolors28.default.dim(" [optional]")}: `);
75303
75902
  stdin.setRawMode(true);
75304
75903
  stdin.resume();
75305
75904
  let value = "";
@@ -75399,15 +75998,15 @@ function describeProbe(p) {
75399
75998
  return `${p.keyCount} keys \xB7 ${p.suggestedRoles.join("/")}${examples ? ` \xB7 ${examples}` : ""}`;
75400
75999
  }
75401
76000
  async function discoverAndSelectDbs(url, bullmqPrefix) {
75402
- console.log(import_picocolors27.default.dim("\n Scanning DBs 0-15 (read-only, sampled)\u2026"));
76001
+ console.log(import_picocolors28.default.dim("\n Scanning DBs 0-15 (read-only, sampled)\u2026"));
75403
76002
  const probes = await probeRedisDatabases(url, { bullmqPrefix });
75404
76003
  const nonEmpty = probes.filter((p) => p.reachable && p.keyCount > 0);
75405
76004
  if (nonEmpty.length === 0) {
75406
- console.log(import_picocolors27.default.dim(" No populated DBs found."));
76005
+ console.log(import_picocolors28.default.dim(" No populated DBs found."));
75407
76006
  return [];
75408
76007
  }
75409
76008
  for (const p of nonEmpty) {
75410
- console.log(` ${import_picocolors27.default.bold(`DB ${p.db}`)} ${import_picocolors27.default.dim("\xB7 " + describeProbe(p))}`);
76009
+ console.log(` ${import_picocolors28.default.bold(`DB ${p.db}`)} ${import_picocolors28.default.dim("\xB7 " + describeProbe(p))}`);
75411
76010
  }
75412
76011
  const byLabel = /* @__PURE__ */ new Map();
75413
76012
  const choices = nonEmpty.map((p) => {
@@ -75518,11 +76117,11 @@ function printSummary(type, opts) {
75518
76117
  } else if (opts.dashboard) {
75519
76118
  lines.push(` dashboard: ${opts.dashboard}`);
75520
76119
  }
75521
- if (lines.length > 0) console.log(import_picocolors27.default.dim(lines.join("\n")));
76120
+ if (lines.length > 0) console.log(import_picocolors28.default.dim(lines.join("\n")));
75522
76121
  }
75523
76122
  function isGitTracked(filePath, cwd) {
75524
76123
  try {
75525
- (0, import_node_child_process5.execFileSync)("git", ["ls-files", "--error-unmatch", filePath], {
76124
+ (0, import_node_child_process6.execFileSync)("git", ["ls-files", "--error-unmatch", filePath], {
75526
76125
  cwd,
75527
76126
  stdio: "pipe"
75528
76127
  });
@@ -75557,11 +76156,11 @@ async function askIndexSelection(indices) {
75557
76156
  const shown = indices.slice(0, MAX_DISPLAY);
75558
76157
  console.log("\n Available Elasticsearch indexes/data streams:");
75559
76158
  shown.forEach((name, i) => {
75560
- console.log(` ${import_picocolors27.default.dim(`[${i + 1}]`)} ${name}`);
76159
+ console.log(` ${import_picocolors28.default.dim(`[${i + 1}]`)} ${name}`);
75561
76160
  });
75562
76161
  if (indices.length > MAX_DISPLAY) {
75563
76162
  console.log(
75564
- import_picocolors27.default.dim(
76163
+ import_picocolors28.default.dim(
75565
76164
  ` \u2026 and ${indices.length - MAX_DISPLAY} more (type a pattern manually to match all)`
75566
76165
  )
75567
76166
  );
@@ -75605,11 +76204,11 @@ async function askDashboardSelection(dashboards) {
75605
76204
  const shown = dashboards.slice(0, MAX_DISPLAY);
75606
76205
  console.log("\n Available Grafana dashboards:");
75607
76206
  shown.forEach((d, i) => {
75608
- const folder = d.folderTitle ? import_picocolors27.default.dim(` (${d.folderTitle})`) : "";
75609
- console.log(` ${import_picocolors27.default.dim(`[${i + 1}]`)} ${d.title}${folder}`);
76207
+ const folder = d.folderTitle ? import_picocolors28.default.dim(` (${d.folderTitle})`) : "";
76208
+ console.log(` ${import_picocolors28.default.dim(`[${i + 1}]`)} ${d.title}${folder}`);
75610
76209
  });
75611
76210
  if (dashboards.length > MAX_DISPLAY) {
75612
- console.log(import_picocolors27.default.dim(` \u2026 and ${dashboards.length - MAX_DISPLAY} more`));
76211
+ console.log(import_picocolors28.default.dim(` \u2026 and ${dashboards.length - MAX_DISPLAY} more`));
75613
76212
  }
75614
76213
  const input = (await ask(
75615
76214
  ` Select dashboards to use (e.g. 1,2 or Enter to type uid manually)`,
@@ -75659,12 +76258,12 @@ function redactUrl(raw) {
75659
76258
 
75660
76259
  // ../../packages/cli/src/commands/stop.ts
75661
76260
  init_cjs_shims();
75662
- var import_node_child_process6 = require("child_process");
76261
+ var import_node_child_process7 = require("child_process");
75663
76262
  var import_node_fs7 = require("fs");
75664
76263
  var import_node_util4 = require("util");
75665
76264
  var import_node_path9 = require("path");
75666
- var import_picocolors28 = __toESM(require_picocolors(), 1);
75667
- var execFileAsync = (0, import_node_util4.promisify)(import_node_child_process6.execFile);
76265
+ var import_picocolors29 = __toESM(require_picocolors(), 1);
76266
+ var execFileAsync = (0, import_node_util4.promisify)(import_node_child_process7.execFile);
75668
76267
  var unlinkAsync = (0, import_node_util4.promisify)(import_node_fs7.unlink);
75669
76268
  var SPAWNED_HOST_FILE2 = "spawned-host.json";
75670
76269
  var START_TIME_TOLERANCE_S = 60;
@@ -75680,30 +76279,30 @@ async function runStop(opts) {
75680
76279
  const root = findRepoRoot(cwd) ?? cwd;
75681
76280
  const hostUrl = readSourceHostUrl(root);
75682
76281
  if (!hostUrl) {
75683
- console.log(import_picocolors28.default.dim("No source-intelligence host found for this repo (.horus/source/host.json absent)."));
76282
+ console.log(import_picocolors29.default.dim("No source-intelligence host found for this repo (.horus/source/host.json absent)."));
75684
76283
  return 0;
75685
76284
  }
75686
76285
  return await stopHost(root, hostUrl);
75687
76286
  } catch (err) {
75688
- console.error(import_picocolors28.default.red(err.message));
76287
+ console.error(import_picocolors29.default.red(err.message));
75689
76288
  return 1;
75690
76289
  }
75691
76290
  }
75692
76291
  async function stopHost(root, hostUrl) {
75693
76292
  const alive = await isHostHealthy(hostUrl);
75694
76293
  if (!alive) {
75695
- console.log(import_picocolors28.default.dim(`Host ${hostUrl} is already stopped.`));
76294
+ console.log(import_picocolors29.default.dim(`Host ${hostUrl} is already stopped.`));
75696
76295
  return 0;
75697
76296
  }
75698
76297
  const port = extractPort(hostUrl);
75699
76298
  if (port === null) {
75700
- console.error(import_picocolors28.default.red(`Cannot determine port from host URL: ${hostUrl}`));
76299
+ console.error(import_picocolors29.default.red(`Cannot determine port from host URL: ${hostUrl}`));
75701
76300
  return 1;
75702
76301
  }
75703
76302
  const spawned = readSpawnedHost(root);
75704
76303
  if (spawned === null) {
75705
76304
  console.error(
75706
- import_picocolors28.default.red(
76305
+ import_picocolors29.default.red(
75707
76306
  `No ownership record found (.horus/${SPAWNED_HOST_FILE2} absent). Horus will not stop a host it did not spawn.`
75708
76307
  )
75709
76308
  );
@@ -75711,12 +76310,12 @@ async function stopHost(root, hostUrl) {
75711
76310
  }
75712
76311
  const recordError = validateSpawnedRecord(spawned);
75713
76312
  if (recordError !== null) {
75714
- console.error(import_picocolors28.default.red(`Ownership record is malformed: ${recordError}. Aborting for safety.`));
76313
+ console.error(import_picocolors29.default.red(`Ownership record is malformed: ${recordError}. Aborting for safety.`));
75715
76314
  return 1;
75716
76315
  }
75717
76316
  if (spawned.port !== port) {
75718
76317
  console.error(
75719
- import_picocolors28.default.red(
76318
+ import_picocolors29.default.red(
75720
76319
  `Ownership record port (${spawned.port}) does not match host URL port (${port}). Record may be stale.`
75721
76320
  )
75722
76321
  );
@@ -75724,7 +76323,7 @@ async function stopHost(root, hostUrl) {
75724
76323
  }
75725
76324
  if (spawned.root !== root) {
75726
76325
  console.error(
75727
- import_picocolors28.default.red(
76326
+ import_picocolors29.default.red(
75728
76327
  `Ownership record root (${spawned.root}) does not match resolved root (${root}). Record may be stale.`
75729
76328
  )
75730
76329
  );
@@ -75732,7 +76331,7 @@ async function stopHost(root, hostUrl) {
75732
76331
  }
75733
76332
  const info = await getProcessInfo(spawned.pid);
75734
76333
  if (info === null) {
75735
- console.log(import_picocolors28.default.dim(`Process pid ${spawned.pid} is no longer running \u2014 already stopped.`));
76334
+ console.log(import_picocolors29.default.dim(`Process pid ${spawned.pid} is no longer running \u2014 already stopped.`));
75736
76335
  try {
75737
76336
  await unlinkAsync((0, import_node_path9.join)(root, HORUS_DIR, SPAWNED_HOST_FILE2));
75738
76337
  } catch {
@@ -75745,7 +76344,7 @@ async function stopHost(root, hostUrl) {
75745
76344
  );
75746
76345
  if (!hostPortRe.test(info.args)) {
75747
76346
  console.error(
75748
- import_picocolors28.default.red(
76347
+ import_picocolors29.default.red(
75749
76348
  `Pid ${spawned.pid} args do not match "horus-source host --port ${portStr}". Got: "${info.args.slice(0, 120)}". Aborting for safety.`
75750
76349
  )
75751
76350
  );
@@ -75754,12 +76353,12 @@ async function stopHost(root, hostUrl) {
75754
76353
  const startTs = new Date(spawned.startedAt).getTime();
75755
76354
  const recordedAgeS = Math.round((Date.now() - startTs) / 1e3);
75756
76355
  if (!Number.isFinite(info.etimeSeconds)) {
75757
- console.error(import_picocolors28.default.red(`Could not read elapsed time for pid ${spawned.pid}. Aborting for safety.`));
76356
+ console.error(import_picocolors29.default.red(`Could not read elapsed time for pid ${spawned.pid}. Aborting for safety.`));
75758
76357
  return 1;
75759
76358
  }
75760
76359
  if (Math.abs(info.etimeSeconds - recordedAgeS) > START_TIME_TOLERANCE_S) {
75761
76360
  console.error(
75762
- import_picocolors28.default.red(
76361
+ import_picocolors29.default.red(
75763
76362
  `Pid ${spawned.pid} age mismatch: record says ~${recordedAgeS}s, process reports ${info.etimeSeconds}s elapsed. Possible PID reuse \u2014 aborting for safety.`
75764
76363
  )
75765
76364
  );
@@ -75771,9 +76370,9 @@ async function stopHost(root, hostUrl) {
75771
76370
  signaled = true;
75772
76371
  } catch (err) {
75773
76372
  if (err.code === "ESRCH") {
75774
- console.log(import_picocolors28.default.dim(`Process pid ${spawned.pid} exited before signal \u2014 already stopped.`));
76373
+ console.log(import_picocolors29.default.dim(`Process pid ${spawned.pid} exited before signal \u2014 already stopped.`));
75775
76374
  } else {
75776
- console.error(import_picocolors28.default.red(`Failed to signal pid ${spawned.pid}: ${err.message}`));
76375
+ console.error(import_picocolors29.default.red(`Failed to signal pid ${spawned.pid}: ${err.message}`));
75777
76376
  return 1;
75778
76377
  }
75779
76378
  }
@@ -75789,14 +76388,14 @@ async function stopHost(root, hostUrl) {
75789
76388
  }
75790
76389
  if (!exited) {
75791
76390
  console.error(
75792
- import_picocolors28.default.red(
76391
+ import_picocolors29.default.red(
75793
76392
  `Host pid ${spawned.pid} did not exit within ${STOP_WAIT_MS / 1e3}s after SIGTERM.`
75794
76393
  )
75795
76394
  );
75796
76395
  return 1;
75797
76396
  }
75798
76397
  console.log(
75799
- `${import_picocolors28.default.green("\u2713")} Stopped source-intelligence host ` + import_picocolors28.default.dim(`(pid ${spawned.pid}, port ${port})`) + ` for ${root}`
76398
+ `${import_picocolors29.default.green("\u2713")} Stopped source-intelligence host ` + import_picocolors29.default.dim(`(pid ${spawned.pid}, port ${port})`) + ` for ${root}`
75800
76399
  );
75801
76400
  }
75802
76401
  try {
@@ -75809,7 +76408,7 @@ async function stopAll() {
75809
76408
  const registry = readRegistry();
75810
76409
  const projects2 = Object.entries(registry.projects);
75811
76410
  if (projects2.length === 0) {
75812
- console.log(import_picocolors28.default.dim("No registered projects."));
76411
+ console.log(import_picocolors29.default.dim("No registered projects."));
75813
76412
  return 0;
75814
76413
  }
75815
76414
  let stopped = 0;
@@ -75819,17 +76418,17 @@ async function stopAll() {
75819
76418
  if (!hostUrl) continue;
75820
76419
  const alive = await isHostHealthy(hostUrl);
75821
76420
  if (!alive) continue;
75822
- console.log(` Stopping ${import_picocolors28.default.bold(name)} ${import_picocolors28.default.dim(`(${hostUrl})`)}`);
76421
+ console.log(` Stopping ${import_picocolors29.default.bold(name)} ${import_picocolors29.default.dim(`(${hostUrl})`)}`);
75823
76422
  const code = await stopHost(entry2.root, hostUrl);
75824
76423
  if (code === 0) stopped++;
75825
76424
  else failed++;
75826
76425
  }
75827
76426
  if (stopped === 0 && failed === 0) {
75828
- console.log(import_picocolors28.default.dim("No running source-intelligence hosts found."));
76427
+ console.log(import_picocolors29.default.dim("No running source-intelligence hosts found."));
75829
76428
  } else {
75830
76429
  console.log(
75831
76430
  `
75832
- Stopped ${stopped} host(s)${failed > 0 ? import_picocolors28.default.red(`, ${failed} failed`) : ""}.`
76431
+ Stopped ${stopped} host(s)${failed > 0 ? import_picocolors29.default.red(`, ${failed} failed`) : ""}.`
75833
76432
  );
75834
76433
  }
75835
76434
  return failed > 0 ? 1 : 0;
@@ -75881,12 +76480,12 @@ function extractPort(hostUrl) {
75881
76480
 
75882
76481
  // ../../packages/cli/src/commands/hosts.ts
75883
76482
  init_cjs_shims();
75884
- var import_picocolors29 = __toESM(require_picocolors(), 1);
76483
+ var import_picocolors30 = __toESM(require_picocolors(), 1);
75885
76484
  async function runHosts() {
75886
76485
  const registry = readRegistry();
75887
76486
  const projects2 = Object.entries(registry.projects);
75888
76487
  if (projects2.length === 0) {
75889
- console.log(import_picocolors29.default.dim("No registered projects. Run `horus index` in a repo first."));
76488
+ console.log(import_picocolors30.default.dim("No registered projects. Run `horus index` in a repo first."));
75890
76489
  return 0;
75891
76490
  }
75892
76491
  const rows = [];
@@ -75903,29 +76502,29 @@ async function runHosts() {
75903
76502
  });
75904
76503
  const anyHost = rows.some((r) => r.hostUrl !== null);
75905
76504
  if (!anyHost) {
75906
- console.log(import_picocolors29.default.dim("No source-intelligence hosts found. Run `horus index` to start one."));
76505
+ console.log(import_picocolors30.default.dim("No source-intelligence hosts found. Run `horus index` to start one."));
75907
76506
  return 0;
75908
76507
  }
75909
76508
  console.log("");
75910
76509
  for (const row of rows) {
75911
76510
  if (row.hostUrl === null) continue;
75912
- const status = row.healthy ? import_picocolors29.default.green("\u25CF running") : import_picocolors29.default.red("\u25CF stopped");
76511
+ const status = row.healthy ? import_picocolors30.default.green("\u25CF running") : import_picocolors30.default.red("\u25CF stopped");
75913
76512
  const port = extractPort2(row.hostUrl) ?? "?";
75914
76513
  console.log(
75915
- ` ${status} ${import_picocolors29.default.bold(row.name.padEnd(24))} port ${String(port).padEnd(6)} ${import_picocolors29.default.dim(row.root)}`
76514
+ ` ${status} ${import_picocolors30.default.bold(row.name.padEnd(24))} port ${String(port).padEnd(6)} ${import_picocolors30.default.dim(row.root)}`
75916
76515
  );
75917
76516
  }
75918
76517
  console.log("");
75919
76518
  const noHost = rows.filter((r) => r.hostUrl === null);
75920
76519
  if (noHost.length > 0) {
75921
76520
  for (const row of noHost) {
75922
- console.log(` ${import_picocolors29.default.dim("\u25CB no host")} ${import_picocolors29.default.dim(row.name)}`);
76521
+ console.log(` ${import_picocolors30.default.dim("\u25CB no host")} ${import_picocolors30.default.dim(row.name)}`);
75923
76522
  }
75924
76523
  console.log("");
75925
76524
  }
75926
76525
  const running = rows.filter((r) => r.healthy).length;
75927
76526
  console.log(
75928
- import_picocolors29.default.dim(
76527
+ import_picocolors30.default.dim(
75929
76528
  `${running} running \xB7 horus stop to reap \xB7 horus stop --all to stop everything`
75930
76529
  )
75931
76530
  );
@@ -75942,12 +76541,12 @@ function extractPort2(hostUrl) {
75942
76541
 
75943
76542
  // ../../packages/cli/src/commands/doctor.ts
75944
76543
  init_cjs_shims();
75945
- var import_picocolors30 = __toESM(require_picocolors(), 1);
76544
+ var import_picocolors31 = __toESM(require_picocolors(), 1);
75946
76545
  var DEFAULT_DB_URL4 = "postgresql://horus:horus@localhost:5433/horus";
75947
76546
  function mark2(status) {
75948
- if (status === "pass") return import_picocolors30.default.green("\u2713");
75949
- if (status === "warn") return import_picocolors30.default.yellow("~");
75950
- return import_picocolors30.default.red("\u2717");
76547
+ if (status === "pass") return import_picocolors31.default.green("\u2713");
76548
+ if (status === "warn") return import_picocolors31.default.yellow("~");
76549
+ return import_picocolors31.default.red("\u2717");
75951
76550
  }
75952
76551
  async function runDoctor(opts) {
75953
76552
  const cwd = opts?.cwd ?? process.cwd();
@@ -76174,11 +76773,11 @@ async function runDoctor(opts) {
76174
76773
  write(JSON.stringify(output, null, 2));
76175
76774
  return hasFailure ? 1 : 0;
76176
76775
  }
76177
- write(import_picocolors30.default.bold("\nHorus readiness check\n"));
76776
+ write(import_picocolors31.default.bold("\nHorus readiness check\n"));
76178
76777
  for (const check of checks) {
76179
- write(` ${mark2(check.status)} ${import_picocolors30.default.bold(check.label.padEnd(26))} ${import_picocolors30.default.dim(check.detail)}`);
76778
+ write(` ${mark2(check.status)} ${import_picocolors31.default.bold(check.label.padEnd(26))} ${import_picocolors31.default.dim(check.detail)}`);
76180
76779
  if (check.next) {
76181
- write(` ${import_picocolors30.default.dim("\u2192 " + check.next)}`);
76780
+ write(` ${import_picocolors31.default.dim("\u2192 " + check.next)}`);
76182
76781
  }
76183
76782
  }
76184
76783
  write("");
@@ -76187,20 +76786,20 @@ async function runDoctor(opts) {
76187
76786
 
76188
76787
  // ../../packages/cli/src/commands/providers-doctor.ts
76189
76788
  init_cjs_shims();
76190
- var import_node_child_process7 = require("child_process");
76191
- var import_picocolors31 = __toESM(require_picocolors(), 1);
76789
+ var import_node_child_process8 = require("child_process");
76790
+ var import_picocolors32 = __toESM(require_picocolors(), 1);
76192
76791
  function statusMark(status) {
76193
- if (status === "ready") return import_picocolors31.default.green("\u2713");
76194
- if (status === "installed") return import_picocolors31.default.yellow("~");
76195
- return import_picocolors31.default.red("\u2717");
76792
+ if (status === "ready") return import_picocolors32.default.green("\u2713");
76793
+ if (status === "installed") return import_picocolors32.default.yellow("~");
76794
+ return import_picocolors32.default.red("\u2717");
76196
76795
  }
76197
76796
  function statusLabel(status) {
76198
- if (status === "ready") return import_picocolors31.default.green("ready");
76199
- if (status === "installed") return import_picocolors31.default.yellow("installed (not configured)");
76200
- return import_picocolors31.default.dim("not found on PATH");
76797
+ if (status === "ready") return import_picocolors32.default.green("ready");
76798
+ if (status === "installed") return import_picocolors32.default.yellow("installed (not configured)");
76799
+ return import_picocolors32.default.dim("not found on PATH");
76201
76800
  }
76202
76801
  function detectBinary(id) {
76203
- const result = (0, import_node_child_process7.spawnSync)(id, ["--version"], { stdio: "pipe", timeout: 2e3 });
76802
+ const result = (0, import_node_child_process8.spawnSync)(id, ["--version"], { stdio: "pipe", timeout: 2e3 });
76204
76803
  if (result.error) {
76205
76804
  return { id, status: "unavailable", detail: `${id}: command not found` };
76206
76805
  }
@@ -76214,7 +76813,7 @@ async function runProvidersDoctorCommand(opts) {
76214
76813
  const registry = opts?.registry ?? DEFAULT_LOCAL_PROVIDER_REGISTRY;
76215
76814
  const write = opts?.write ?? ((line2) => console.log(line2));
76216
76815
  const results = buildProviderResults(registry, opts?._detect);
76217
- write(import_picocolors31.default.bold("\nLocal AI providers\n"));
76816
+ write(import_picocolors32.default.bold("\nLocal AI providers\n"));
76218
76817
  for (const result of results) {
76219
76818
  const descriptor = registry.get(result.id);
76220
76819
  const name = descriptor?.displayName ?? result.id;
@@ -76222,22 +76821,37 @@ async function runProvidersDoctorCommand(opts) {
76222
76821
  ` ${statusMark(result.status)} ${result.id.padEnd(8)} ${name.padEnd(22)} ${statusLabel(result.status)}`
76223
76822
  );
76224
76823
  if (result.status !== "ready" && result.detail) {
76225
- write(` ${import_picocolors31.default.dim("\u2192 " + result.detail)}`);
76824
+ write(` ${import_picocolors32.default.dim("\u2192 " + result.detail)}`);
76226
76825
  }
76227
76826
  }
76228
76827
  write("");
76229
- write(import_picocolors31.default.bold("Cloud AI providers\n"));
76230
- const anthropicKey = opts?._anthropicKey !== void 0 ? opts._anthropicKey : process.env["ANTHROPIC_API_KEY"] ?? null;
76231
- if (anthropicKey) {
76828
+ write(import_picocolors32.default.bold("Cloud AI providers\n"));
76829
+ let source = null;
76830
+ if (opts?._anthropicKey !== void 0) {
76831
+ source = opts._anthropicKey ? "env" : null;
76832
+ } else {
76833
+ try {
76834
+ const config = await loadConfig(opts?.config);
76835
+ const ai = resolveAiSettings(config);
76836
+ if (ai.anthropicApiKey) source = ai.anthropicKeyFromConfig ? "config" : "env";
76837
+ } catch {
76838
+ if (process.env["ANTHROPIC_API_KEY"]) source = "env";
76839
+ }
76840
+ }
76841
+ if (source === "config") {
76842
+ write(
76843
+ ` ${import_picocolors32.default.green("\u2713")} ${"anthropic".padEnd(8)} ${"Anthropic Claude API".padEnd(22)} ${import_picocolors32.default.green("configured (.horus/config.json)")}`
76844
+ );
76845
+ } else if (source === "env") {
76232
76846
  write(
76233
- ` ${import_picocolors31.default.green("\u2713")} ${"anthropic".padEnd(8)} ${"Anthropic Claude API".padEnd(22)} ${import_picocolors31.default.green("ANTHROPIC_API_KEY configured")}`
76847
+ ` ${import_picocolors32.default.green("\u2713")} ${"anthropic".padEnd(8)} ${"Anthropic Claude API".padEnd(22)} ${import_picocolors32.default.green("configured (ANTHROPIC_API_KEY env)")}`
76234
76848
  );
76235
76849
  } else {
76236
76850
  write(
76237
- ` ${import_picocolors31.default.red("\u2717")} ${"anthropic".padEnd(8)} ${"Anthropic Claude API".padEnd(22)} ${import_picocolors31.default.dim("ANTHROPIC_API_KEY not set")}`
76851
+ ` ${import_picocolors32.default.red("\u2717")} ${"anthropic".padEnd(8)} ${"Anthropic Claude API".padEnd(22)} ${import_picocolors32.default.dim("not configured")}`
76238
76852
  );
76239
76853
  write(
76240
- ` ${import_picocolors31.default.dim('\u2192 set ANTHROPIC_API_KEY=<key> to enable AI-powered investigation (horus investigate "hint" --ai)')}`
76854
+ ` ${import_picocolors32.default.dim("\u2192 run `horus connect ai` (or set ANTHROPIC_API_KEY) to enable `horus investigate --ai`")}`
76241
76855
  );
76242
76856
  }
76243
76857
  write("");
@@ -76248,7 +76862,7 @@ async function runProvidersDoctorCommand(opts) {
76248
76862
  init_cjs_shims();
76249
76863
  var import_node_fs8 = require("fs");
76250
76864
  var import_node_path10 = require("path");
76251
- var import_picocolors32 = __toESM(require_picocolors(), 1);
76865
+ var import_picocolors33 = __toESM(require_picocolors(), 1);
76252
76866
  function configTemplate(name, repoPath) {
76253
76867
  return `export default {
76254
76868
  database: {
@@ -76300,40 +76914,40 @@ async function runGenerateConfig(opts) {
76300
76914
  const name = opts.name ?? defaults?.name ?? "my-project";
76301
76915
  const repoPath = opts.repo ?? defaults?.repoPath ?? `/path/to/${name}`;
76302
76916
  if ((0, import_node_fs8.existsSync)(outPath) && !opts.force) {
76303
- log(`${import_picocolors32.default.red("\u2717")} ${outPath} already exists`);
76304
- log(import_picocolors32.default.dim(" pass --force to overwrite"));
76917
+ log(`${import_picocolors33.default.red("\u2717")} ${outPath} already exists`);
76918
+ log(import_picocolors33.default.dim(" pass --force to overwrite"));
76305
76919
  return 1;
76306
76920
  }
76307
76921
  try {
76308
76922
  (0, import_node_fs8.mkdirSync)((0, import_node_path10.dirname)(outPath), { recursive: true });
76309
76923
  (0, import_node_fs8.writeFileSync)(outPath, configTemplate(name, repoPath), "utf8");
76310
76924
  } catch (err) {
76311
- log(`${import_picocolors32.default.red("\u2717")} Could not write ${outPath}: ${err.message}`);
76925
+ log(`${import_picocolors33.default.red("\u2717")} Could not write ${outPath}: ${err.message}`);
76312
76926
  return 1;
76313
76927
  }
76314
- log(`${import_picocolors32.default.green("\u2713")} Created ${outPath}`);
76315
- log(import_picocolors32.default.dim(` project: ${name}`));
76316
- log(import_picocolors32.default.dim(` repo: ${repoPath}`));
76928
+ log(`${import_picocolors33.default.green("\u2713")} Created ${outPath}`);
76929
+ log(import_picocolors33.default.dim(` project: ${name}`));
76930
+ log(import_picocolors33.default.dim(` repo: ${repoPath}`));
76317
76931
  if (hasLocalConfig && localConfigPath2 != null) {
76318
76932
  log("");
76319
- log(import_picocolors32.default.yellow("Note:") + ` an initialized Horus project config exists at ${localConfigPath2}`);
76933
+ log(import_picocolors33.default.yellow("Note:") + ` an initialized Horus project config exists at ${localConfigPath2}`);
76320
76934
  log(" \u2022 .horus/config.json \u2014 project config used by `horus investigate` from this repo");
76321
76935
  log(" \u2022 horus.config.js \u2014 standalone/global config used with `horus doctor --config <path>`");
76322
- log(import_picocolors32.default.dim(` next: review ${outPath} and copy/adapt it as needed`));
76936
+ log(import_picocolors33.default.dim(` next: review ${outPath} and copy/adapt it as needed`));
76323
76937
  } else {
76324
- log(import_picocolors32.default.dim(` next: horus doctor --config ${outPath}`));
76938
+ log(import_picocolors33.default.dim(` next: horus doctor --config ${outPath}`));
76325
76939
  }
76326
76940
  return 0;
76327
76941
  }
76328
76942
 
76329
76943
  // ../../packages/cli/src/commands/readiness.ts
76330
76944
  init_cjs_shims();
76331
- var import_picocolors33 = __toESM(require_picocolors(), 1);
76945
+ var import_picocolors34 = __toESM(require_picocolors(), 1);
76332
76946
  var DEFAULT_DB_URL5 = "postgresql://horus:horus@localhost:5433/horus";
76333
76947
  function mark3(status) {
76334
- if (status === "pass") return import_picocolors33.default.green("\u2713");
76335
- if (status === "warn") return import_picocolors33.default.yellow("~");
76336
- return import_picocolors33.default.red("\u2717");
76948
+ if (status === "pass") return import_picocolors34.default.green("\u2713");
76949
+ if (status === "warn") return import_picocolors34.default.yellow("~");
76950
+ return import_picocolors34.default.red("\u2717");
76337
76951
  }
76338
76952
  async function runReadiness(opts) {
76339
76953
  const cwd = opts?.cwd ?? process.cwd();
@@ -76488,20 +77102,20 @@ async function runReadiness(opts) {
76488
77102
  }
76489
77103
  const blockingChecks = checks.filter((c) => c.blocking);
76490
77104
  const optionalChecks = checks.filter((c) => !c.blocking);
76491
- write(import_picocolors33.default.bold("\nHorus release readiness\n"));
76492
- write(import_picocolors33.default.bold(" Blocking"));
77105
+ write(import_picocolors34.default.bold("\nHorus release readiness\n"));
77106
+ write(import_picocolors34.default.bold(" Blocking"));
76493
77107
  for (const check of blockingChecks) {
76494
- write(` ${mark3(check.status)} ${import_picocolors33.default.bold(check.label.padEnd(22))} ${import_picocolors33.default.dim(check.detail)}`);
77108
+ write(` ${mark3(check.status)} ${import_picocolors34.default.bold(check.label.padEnd(22))} ${import_picocolors34.default.dim(check.detail)}`);
76495
77109
  if (check.next) {
76496
- write(` ${import_picocolors33.default.dim("\u2192 " + check.next)}`);
77110
+ write(` ${import_picocolors34.default.dim("\u2192 " + check.next)}`);
76497
77111
  }
76498
77112
  }
76499
77113
  write("");
76500
- write(import_picocolors33.default.bold(" Optional"));
77114
+ write(import_picocolors34.default.bold(" Optional"));
76501
77115
  for (const check of optionalChecks) {
76502
- write(` ${mark3(check.status)} ${import_picocolors33.default.bold(check.label.padEnd(22))} ${import_picocolors33.default.dim(check.detail)}`);
77116
+ write(` ${mark3(check.status)} ${import_picocolors34.default.bold(check.label.padEnd(22))} ${import_picocolors34.default.dim(check.detail)}`);
76503
77117
  if (check.next) {
76504
- write(` ${import_picocolors33.default.dim("\u2192 " + check.next)}`);
77118
+ write(` ${import_picocolors34.default.dim("\u2192 " + check.next)}`);
76505
77119
  }
76506
77120
  }
76507
77121
  write("");
@@ -76509,21 +77123,21 @@ async function runReadiness(opts) {
76509
77123
  const optionalWarns = optionalChecks.filter((c) => c.status === "warn").length;
76510
77124
  if (blockingFails.length === 0) {
76511
77125
  if (optionalWarns === 0) {
76512
- write(import_picocolors33.default.green(" Ready for demo/release."));
77126
+ write(import_picocolors34.default.green(" Ready for demo/release."));
76513
77127
  } else {
76514
77128
  write(
76515
- import_picocolors33.default.yellow(
77129
+ import_picocolors34.default.yellow(
76516
77130
  ` Ready for a basic demo. ${optionalWarns} optional item(s) not configured \u2014 investigation evidence will be limited.`
76517
77131
  )
76518
77132
  );
76519
77133
  }
76520
77134
  } else {
76521
77135
  write(
76522
- import_picocolors33.default.red(
77136
+ import_picocolors34.default.red(
76523
77137
  ` Not ready. ${blockingFails.length} blocking item(s) must be resolved before demo/release.`
76524
77138
  )
76525
77139
  );
76526
- write(import_picocolors33.default.dim(" Re-run `horus readiness` after resolving the items above."));
77140
+ write(import_picocolors34.default.dim(" Re-run `horus readiness` after resolving the items above."));
76527
77141
  }
76528
77142
  write("");
76529
77143
  return blockingFails.length > 0 ? 1 : 0;
@@ -76573,8 +77187,8 @@ Examples:
76573
77187
  process.exitCode = await runReadiness({ config: opts.config });
76574
77188
  });
76575
77189
  program2.command("connect <type>").description(
76576
- "Add or update a runtime connector (elasticsearch / mongodb / grafana / redis) in .horus/config.json"
76577
- ).option("--env <name>", "target environment (default: first environment in config)").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("--dashboard <uid>", "default dashboard uid (grafana)").option(
77190
+ "Add or update a connector (elasticsearch / mongodb / grafana / redis / ai) in .horus/config.json"
77191
+ ).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("--dashboard <uid>", "default dashboard uid (grafana)").option(
76578
77192
  "--db <spec>",
76579
77193
  "redis logical DB as db:role1,role2 (e.g. 0:cache,state or 1:bullmq,queues); repeatable",
76580
77194
  (val, acc) => {
@@ -76597,6 +77211,9 @@ Examples:
76597
77211
  db: opts.db,
76598
77212
  bullmqPrefix: opts.bullmqPrefix,
76599
77213
  scanDbs: opts.scanDbs,
77214
+ provider: opts.provider,
77215
+ apiKey: opts.apiKey,
77216
+ aiModel: opts.model,
76600
77217
  noTest: opts.test === false
76601
77218
  });
76602
77219
  }
@@ -76634,13 +77251,14 @@ Examples:
76634
77251
  process.exitCode = await runIndex(opts);
76635
77252
  }
76636
77253
  );
76637
- program2.command("queues [name]").description("Show queue topology from source intelligence; --live adds real-time Redis/BullMQ state").option("-c, --config <path>", "path to horus.config.ts").option("--name <name>", "registered project name (resolves via registry)").option("--project <name>", "filter edges by project").option("--live", "fetch real-time queue depths and failed-job counts from Redis/BullMQ").action(
77254
+ program2.command("queues [name]").description("Show queue topology from source intelligence; --live adds real-time Redis/BullMQ state").option("-c, --config <path>", "path to horus.config.ts").option("--name <name>", "registered project name (resolves via registry)").option("--project <name>", "filter edges by project").option("--live", "fetch real-time queue depths and failed-job counts from Redis/BullMQ").option("--json", "output JSON").action(
76638
77255
  async (name, opts) => {
76639
77256
  process.exitCode = await runQueues(name, {
76640
77257
  config: opts.config,
76641
77258
  name: opts.name,
76642
77259
  project: opts.project,
76643
- live: opts.live
77260
+ live: opts.live,
77261
+ json: opts.json
76644
77262
  });
76645
77263
  }
76646
77264
  );
@@ -76677,13 +77295,14 @@ Examples:
76677
77295
  });
76678
77296
  program2.command("timeline [service]").description(
76679
77297
  "Reconstruct what changed in a time window (git + change-impact) \u2014 evidence, not conclusions"
76680
- ).option("-c, --config <path>", "path to horus.config.ts").option("--repo <name>", "repository name from config").option("--since <when>", 'git --since (e.g. "7 days ago", a date)').option("--until <when>", "git --until").option("--json", "output JSON").action(
77298
+ ).option("-c, --config <path>", "path to horus.config.ts").option("--repo <name>", "repository name from config").option("--since <when>", 'git --since (default "7 days ago"; e.g. "30 days ago", a date)').option("--until <when>", "git --until").option("--all", "include all history instead of the default recent window").option("--json", "output JSON").action(
76681
77299
  async (service, opts) => {
76682
77300
  process.exitCode = await runTimeline(service, {
76683
77301
  config: opts.config,
76684
77302
  repo: opts.repo,
76685
77303
  since: opts.since,
76686
77304
  until: opts.until,
77305
+ all: opts.all,
76687
77306
  json: opts.json
76688
77307
  });
76689
77308
  }
@@ -76781,7 +77400,7 @@ Examples:
76781
77400
  process.exitCode = await runScore(id, { config: opts.config, json: opts.json });
76782
77401
  });
76783
77402
  program2.command("ask <id> <directive>").description(
76784
- 'Refine a saved investigation with a follow-up directive (e.g. "focus on queue behavior", "ignore deployment changes") \u2014 reuses evidence, no re-query'
77403
+ 'Ask about or refine a saved investigation \u2014 reuses evidence, no re-query.\n Questions (direct answers):\n "what evidence contradicts <topic>?" \xB7 "what evidence is missing?"\n "why is confidence not higher?"\n Topic filters (deterministic scoping):\n "focus on queue behavior" \xB7 "ignore deployment changes" \xB7 "retry"'
76785
77404
  ).option("-c, --config <path>", "path to horus.config.ts").option("--json", "output JSON").action(async (id, directive, opts) => {
76786
77405
  process.exitCode = await runAsk(id, directive, { config: opts.config, json: opts.json });
76787
77406
  });
@@ -76807,14 +77426,14 @@ Examples:
76807
77426
  });
76808
77427
  program2.command("logs [service]").description(
76809
77428
  "Synthesize error evidence from logs (signatures, first/last, affected services); --raw for lines"
76810
- ).option("-c, --config <path>", "path to horus.config.ts").option("--name <name>", "registered project name (resolves via the registry)").option("--project <name>", "project name").option("--env <name>", "environment name (e.g. production)").option("--since <when>", "time window, e.g. 24h, 7d, or an ISO date").option("--level <level>", "minimum level (with --raw): trace|debug|info|warn|error|fatal").option("--grep <text>", "match text in the message").option("--raw", "dump individual log lines instead of synthesized evidence").option("--limit <n>", "max records (with --raw)").action(
77429
+ ).option("-c, --config <path>", "path to horus.config.ts").option("--name <name>", "registered project name (resolves via the registry)").option("--project <name>", "project name").option("--env <name>", "environment name (e.g. production)").option("--since <when>", "time window, e.g. 24h, 7d, or an ISO date").option("--level <level>", "minimum level (with --raw): trace|debug|info|warn|error|fatal").option("--grep <text>", "match text in the message").option("--raw", "dump individual log lines instead of synthesized evidence (error+ by default)").option("--all-levels", "with --raw: show all severity levels, not just error+").option("--limit <n>", "max records (with --raw)").option("--json", "output JSON").action(
76811
77430
  async (service, opts) => {
76812
77431
  process.exitCode = await runLogs(service, opts);
76813
77432
  }
76814
77433
  );
76815
77434
  program2.command("state").description(
76816
77435
  "Surface application-state evidence from MongoDB (read-only, allowlisted): counts, staleness, anomalous statuses"
76817
- ).option("-c, --config <path>", "path to horus.config.ts").option("--name <name>", "registered project name (resolves via the registry)").option("--project <name>", "project name").option("--env <name>", "environment name (e.g. production)").option("--stale-hours <n>", "staleness threshold in hours (default 24)").action(
77436
+ ).option("-c, --config <path>", "path to horus.config.ts").option("--name <name>", "registered project name (resolves via the registry)").option("--project <name>", "project name").option("--env <name>", "environment name (e.g. production)").option("--stale-hours <n>", "staleness threshold in hours (default 24)").option("--json", "output JSON").action(
76818
77437
  async (opts) => {
76819
77438
  process.exitCode = await runState(opts);
76820
77439
  }