@merittdev/horus 0.1.12 → 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 +554 -143
  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.12" : "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));
@@ -71568,7 +71654,7 @@ async function discoverArchitecture(deps) {
71568
71654
  })();
71569
71655
  const asyncBoundaries = await (async () => {
71570
71656
  try {
71571
- const edges = await listQueueEdges(deps.db);
71657
+ const edges = await listQueueEdges(deps.db, { project: deps.project });
71572
71658
  const byQueue = /* @__PURE__ */ new Map();
71573
71659
  for (const edge of edges) {
71574
71660
  const key = edge.queueName;
@@ -71732,7 +71818,7 @@ async function analyzeBlastRadius(query, deps, depth = 3) {
71732
71818
  ]);
71733
71819
  const upstream = ctx.callees;
71734
71820
  const downstream = impact.byDepth;
71735
- const edges = await listQueueEdges(deps.db);
71821
+ const edges = await listQueueEdges(deps.db, { project: deps.project });
71736
71822
  const asyncDownstreamMap = /* @__PURE__ */ new Map();
71737
71823
  for (const edge of edges) {
71738
71824
  if (edge.producerFile === top.filePath || edge.producerSymbol === top.name) {
@@ -72850,7 +72936,11 @@ function filterArchitecture(architecture, tokens) {
72850
72936
  };
72851
72937
  }
72852
72938
  async function buildOnboarding(input, deps) {
72853
- 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
+ });
72854
72944
  const area = input.area?.trim();
72855
72945
  let filteredArchitecture = architecture;
72856
72946
  let pastIncidents = [];
@@ -73577,18 +73667,76 @@ Hard rules:
73577
73667
  - only include services from: ${input.knownServices.join(", ")}
73578
73668
  - hypothesisJudgments must only reference hypothesis IDs from the hypotheses list above
73579
73669
  - verdict must be exactly one of: supported, weakened, eliminated, unconfirmed
73580
- - 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`;
73581
73672
  }
73582
- function parseOutput(raw, input, ceiling) {
73583
- let parsed;
73584
- try {
73585
- parsed = JSON.parse(raw);
73586
- } catch {
73587
- const jsonMatch = raw.match(/\{[\s\S]*\}/);
73588
- if (!jsonMatch) {
73589
- 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
+ }
73590
73696
  }
73591
- 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);
73592
73740
  }
73593
73741
  const confidence = Math.min(
73594
73742
  typeof parsed["confidence"] === "number" ? parsed["confidence"] : input.reportConfidence,
@@ -73643,6 +73791,13 @@ init_cjs_shims();
73643
73791
 
73644
73792
  // ../../packages/ai/src/local-providers.ts
73645
73793
  init_cjs_shims();
73794
+ var LOCAL_PROVIDER_IDS = [
73795
+ "codex",
73796
+ "claude",
73797
+ "kimi",
73798
+ "gemini",
73799
+ "cursor"
73800
+ ];
73646
73801
  var DEFAULT_DESCRIPTORS = [
73647
73802
  { id: "codex", displayName: "OpenAI Codex CLI" },
73648
73803
  { id: "claude", displayName: "Anthropic Claude" },
@@ -73841,8 +73996,12 @@ async function runInvestigate(hint, opts) {
73841
73996
  const rendered = format === "json" ? reportToJSON(report) : format === "markdown" || format === "md" ? reportToMarkdown(report) : renderReport2(report);
73842
73997
  console.log(rendered);
73843
73998
  if (opts.ai && format !== "json") {
73844
- const model = opts.aiModel ?? "claude-opus-4-8";
73845
- 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
+ });
73846
74005
  console.log(import_picocolors5.default.dim(`[ai] model: ${model}`));
73847
74006
  const narrativeInput = buildNarrativeInput(report);
73848
74007
  const { output, fromProvider, validationErrors } = await renderNarrative(narrativeInput, { provider });
@@ -73981,9 +74140,14 @@ async function runArchitecture(opts) {
73981
74140
  );
73982
74141
  return 1;
73983
74142
  }
74143
+ let project;
74144
+ try {
74145
+ project = resolveEnvironment(config, { project: opts.repo }).project;
74146
+ } catch {
74147
+ }
73984
74148
  const { db, sql: sql2 } = createDb(config.database.url);
73985
74149
  try {
73986
- const m = await discoverArchitecture({ code, db });
74150
+ const m = await discoverArchitecture({ code, db, project });
73987
74151
  console.log(opts.json ? architectureToJSON(m) : renderArchitecture(m));
73988
74152
  } finally {
73989
74153
  await sql2.end();
@@ -74007,9 +74171,14 @@ async function runBlastRadius(query, opts) {
74007
74171
  console.error(import_picocolors10.default.red("Source-intelligence host unreachable \u2014 run: horus index"));
74008
74172
  return 1;
74009
74173
  }
74174
+ let project;
74175
+ try {
74176
+ project = resolveEnvironment(config, { project: opts.repo }).project;
74177
+ } catch {
74178
+ }
74010
74179
  const { db, sql: sql2 } = createDb(config.database.url);
74011
74180
  try {
74012
- const r = await analyzeBlastRadius(query, { code, db }, opts.depth ?? 3);
74181
+ const r = await analyzeBlastRadius(query, { code, db, project }, opts.depth ?? 3);
74013
74182
  if (!r) {
74014
74183
  console.log(`No symbol found for: ${query}`);
74015
74184
  console.log(import_picocolors10.default.dim(` Tip: use an exact class or function name, e.g. "MyService"`));
@@ -74472,7 +74641,10 @@ async function runOnboard(area, opts) {
74472
74641
  }
74473
74642
  const { db, sql: sql2 } = createDb(config.database.url);
74474
74643
  try {
74475
- 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
+ );
74476
74648
  console.log(opts.json ? onboardingToJSON(g) : renderOnboarding(g));
74477
74649
  } finally {
74478
74650
  await sql2.end();
@@ -74548,6 +74720,10 @@ function sinceToIso(since) {
74548
74720
  const msMap = { m: 6e4, h: 36e5, d: 864e5 };
74549
74721
  return new Date(Date.now() - (msMap[unit] ?? 6e4) * amount).toISOString();
74550
74722
  }
74723
+ function resolveRawLevel(opts) {
74724
+ if (opts.allLevels) return void 0;
74725
+ return opts.level ?? "error";
74726
+ }
74551
74727
  function levelColor(level, text2) {
74552
74728
  if (level === "error" || level === "fatal") return import_picocolors20.default.red(text2);
74553
74729
  if (level === "warn") return import_picocolors20.default.yellow(text2);
@@ -74607,13 +74783,45 @@ async function runLogs(service, opts) {
74607
74783
  const from = sinceToIso(opts.since) ?? new Date(Date.now() - 7 * 864e5).toISOString();
74608
74784
  const fromDisplay = from.slice(0, 16).replace("T", " ");
74609
74785
  if (opts.raw === true) {
74786
+ const rawLevel = resolveRawLevel(opts);
74787
+ const limit = opts.limit !== void 0 ? Math.min(Number(opts.limit), 1e3) : 20;
74610
74788
  const records = await logs.searchLogs({
74611
74789
  service: resolvedService,
74612
74790
  from,
74613
- level: opts.level,
74791
+ level: rawLevel,
74614
74792
  text: opts.grep,
74615
- limit: opts.limit !== void 0 ? Number(opts.limit) : 20
74793
+ limit
74616
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
+ }
74617
74825
  if (records.length === 0) {
74618
74826
  console.log(import_picocolors20.default.dim("No logs matched."));
74619
74827
  return 0;
@@ -74642,6 +74850,30 @@ async function runLogs(service, opts) {
74642
74850
  scopeService = void 0;
74643
74851
  }
74644
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
+ }
74645
74877
  console.log(
74646
74878
  import_picocolors20.default.bold(`Error analysis`) + import_picocolors20.default.dim(
74647
74879
  ` \u2014 ${renv.project}/${renv.env}` + (scopeService ? ` \xB7 service ${scopeService}` : "") + (opts.grep ? ` \xB7 grep "${opts.grep}"` : "")
@@ -74766,6 +74998,25 @@ async function runMetrics(hint, opts) {
74766
74998
  const from = to - dur;
74767
74999
  if (opts.query !== void 0 && opts.query !== "") {
74768
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
+ }
74769
75020
  if (series.length === 0) {
74770
75021
  console.log(import_picocolors21.default.dim("No series returned."));
74771
75022
  return 0;
@@ -74876,6 +75127,23 @@ async function runState(opts) {
74876
75127
  const analysis = await mongo.analyzeState(
74877
75128
  staleHours !== void 0 ? { staleHours } : {}
74878
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
+ }
74879
75147
  const discoveryNote = analysis.autoDiscovered ? import_picocolors22.default.dim(` (${analysis.collections.length} collections, auto-discovered)`) : "";
74880
75148
  console.log(
74881
75149
  import_picocolors22.default.bold("State analysis") + import_picocolors22.default.dim(` \u2014 ${renv.project}/${renv.env} \xB7 db ${analysis.database}`) + discoveryNote
@@ -75121,8 +75389,8 @@ async function runSetup(opts) {
75121
75389
  // ../../packages/cli/src/commands/connect.ts
75122
75390
  init_cjs_shims();
75123
75391
  var import_node_readline = require("readline");
75124
- var import_node_child_process5 = require("child_process");
75125
- var import_picocolors27 = __toESM(require_picocolors(), 1);
75392
+ var import_node_child_process6 = require("child_process");
75393
+ var import_picocolors28 = __toESM(require_picocolors(), 1);
75126
75394
 
75127
75395
  // ../../packages/cli/src/lib/tty-selector.ts
75128
75396
  init_cjs_shims();
@@ -75302,6 +75570,122 @@ async function checkboxSearch(opts) {
75302
75570
  });
75303
75571
  }
75304
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
+
75305
75689
  // ../../packages/cli/src/commands/connect.ts
75306
75690
  function parseDbSpec(spec) {
75307
75691
  const [dbPart, rolesPart] = spec.split(":");
@@ -75319,10 +75703,18 @@ function parseDbSpec(spec) {
75319
75703
  }
75320
75704
  var SUPPORTED = ["elasticsearch", "mongodb", "grafana", "redis"];
75321
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
+ }
75322
75714
  if (!SUPPORTED.includes(type)) {
75323
75715
  console.error(
75324
- import_picocolors27.default.red(`Unknown connector type: ${type}`) + import_picocolors27.default.dim(`
75325
- supported: ${SUPPORTED.join(", ")}`)
75716
+ import_picocolors28.default.red(`Unknown connector type: ${type}`) + import_picocolors28.default.dim(`
75717
+ supported: ${SUPPORTED.join(", ")}, ai`)
75326
75718
  );
75327
75719
  return 1;
75328
75720
  }
@@ -75352,20 +75744,20 @@ async function runConnect(type, opts) {
75352
75744
  if (!probeResult.ok) {
75353
75745
  console.error(
75354
75746
  `
75355
- ${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.")
75356
75748
  );
75357
75749
  return 1;
75358
75750
  }
75359
75751
  console.log(
75360
75752
  `
75361
- ${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})`)}`
75362
75754
  );
75363
75755
  }
75364
75756
  const hasLiteralCredentials = filled.url !== void 0 || filled.password !== void 0 || filled.username !== void 0;
75365
75757
  if (hasLiteralCredentials) {
75366
75758
  if (isGitTracked(configPath, root)) {
75367
75759
  console.error(
75368
- 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(
75369
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."
75370
75762
  )
75371
75763
  );
@@ -75375,18 +75767,18 @@ ${import_picocolors27.default.green("\u2713")} ${connectorType} reachable ${impo
75375
75767
  }
75376
75768
  patchLocalConnector(configPath, connectorType, patch, filled.env);
75377
75769
  console.log(
75378
- `${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)}`
75379
75771
  );
75380
75772
  printSummary(connectorType, filled);
75381
- console.log(import_picocolors27.default.dim(`
75773
+ console.log(import_picocolors28.default.dim(`
75382
75774
  run: horus investigate "<hint>"`));
75383
75775
  return 0;
75384
75776
  } catch (err) {
75385
75777
  if (err instanceof ExitPromptError) {
75386
- console.error(import_picocolors27.default.red("Cancelled."));
75778
+ console.error(import_picocolors28.default.red("Cancelled."));
75387
75779
  return 1;
75388
75780
  }
75389
- console.error(import_picocolors27.default.red(err.message));
75781
+ console.error(import_picocolors28.default.red(err.message));
75390
75782
  return 1;
75391
75783
  }
75392
75784
  }
@@ -75395,7 +75787,7 @@ async function fillInteractive(type, opts) {
75395
75787
  if (!needsInteraction) return opts;
75396
75788
  console.log(
75397
75789
  `
75398
- ${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)")}
75399
75791
  `
75400
75792
  );
75401
75793
  const filled = { ...opts };
@@ -75450,7 +75842,7 @@ ${import_picocolors27.default.bold(`Connect ${type}`)} ${import_picocolors27.def
75450
75842
  break;
75451
75843
  case "redis": {
75452
75844
  console.log(
75453
- import_picocolors27.default.dim(
75845
+ import_picocolors28.default.dim(
75454
75846
  " Tip: embed credentials directly in the URL \u2014 redis://:password@host:6379\n or enter the URL and password separately below."
75455
75847
  )
75456
75848
  );
@@ -75488,8 +75880,8 @@ function missingRequired(type, opts) {
75488
75880
  }
75489
75881
  function ask(label, placeholder = "", required = true) {
75490
75882
  return new Promise((resolve8) => {
75491
- const hint = placeholder ? import_picocolors27.default.dim(` (${placeholder})`) : "";
75492
- 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]");
75493
75885
  process.stdout.write(` ${label}${suffix}${hint}: `);
75494
75886
  const rl = (0, import_node_readline.createInterface)({
75495
75887
  input: process.stdin,
@@ -75506,7 +75898,7 @@ function askPassword(label) {
75506
75898
  return new Promise((resolve8) => {
75507
75899
  const stdin = process.stdin;
75508
75900
  if (typeof stdin.setRawMode === "function") {
75509
- process.stdout.write(` ${label}${import_picocolors27.default.dim(" [optional]")}: `);
75901
+ process.stdout.write(` ${label}${import_picocolors28.default.dim(" [optional]")}: `);
75510
75902
  stdin.setRawMode(true);
75511
75903
  stdin.resume();
75512
75904
  let value = "";
@@ -75606,15 +75998,15 @@ function describeProbe(p) {
75606
75998
  return `${p.keyCount} keys \xB7 ${p.suggestedRoles.join("/")}${examples ? ` \xB7 ${examples}` : ""}`;
75607
75999
  }
75608
76000
  async function discoverAndSelectDbs(url, bullmqPrefix) {
75609
- 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"));
75610
76002
  const probes = await probeRedisDatabases(url, { bullmqPrefix });
75611
76003
  const nonEmpty = probes.filter((p) => p.reachable && p.keyCount > 0);
75612
76004
  if (nonEmpty.length === 0) {
75613
- console.log(import_picocolors27.default.dim(" No populated DBs found."));
76005
+ console.log(import_picocolors28.default.dim(" No populated DBs found."));
75614
76006
  return [];
75615
76007
  }
75616
76008
  for (const p of nonEmpty) {
75617
- 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))}`);
75618
76010
  }
75619
76011
  const byLabel = /* @__PURE__ */ new Map();
75620
76012
  const choices = nonEmpty.map((p) => {
@@ -75725,11 +76117,11 @@ function printSummary(type, opts) {
75725
76117
  } else if (opts.dashboard) {
75726
76118
  lines.push(` dashboard: ${opts.dashboard}`);
75727
76119
  }
75728
- 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")));
75729
76121
  }
75730
76122
  function isGitTracked(filePath, cwd) {
75731
76123
  try {
75732
- (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], {
75733
76125
  cwd,
75734
76126
  stdio: "pipe"
75735
76127
  });
@@ -75764,11 +76156,11 @@ async function askIndexSelection(indices) {
75764
76156
  const shown = indices.slice(0, MAX_DISPLAY);
75765
76157
  console.log("\n Available Elasticsearch indexes/data streams:");
75766
76158
  shown.forEach((name, i) => {
75767
- console.log(` ${import_picocolors27.default.dim(`[${i + 1}]`)} ${name}`);
76159
+ console.log(` ${import_picocolors28.default.dim(`[${i + 1}]`)} ${name}`);
75768
76160
  });
75769
76161
  if (indices.length > MAX_DISPLAY) {
75770
76162
  console.log(
75771
- import_picocolors27.default.dim(
76163
+ import_picocolors28.default.dim(
75772
76164
  ` \u2026 and ${indices.length - MAX_DISPLAY} more (type a pattern manually to match all)`
75773
76165
  )
75774
76166
  );
@@ -75812,11 +76204,11 @@ async function askDashboardSelection(dashboards) {
75812
76204
  const shown = dashboards.slice(0, MAX_DISPLAY);
75813
76205
  console.log("\n Available Grafana dashboards:");
75814
76206
  shown.forEach((d, i) => {
75815
- const folder = d.folderTitle ? import_picocolors27.default.dim(` (${d.folderTitle})`) : "";
75816
- 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}`);
75817
76209
  });
75818
76210
  if (dashboards.length > MAX_DISPLAY) {
75819
- 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`));
75820
76212
  }
75821
76213
  const input = (await ask(
75822
76214
  ` Select dashboards to use (e.g. 1,2 or Enter to type uid manually)`,
@@ -75866,12 +76258,12 @@ function redactUrl(raw) {
75866
76258
 
75867
76259
  // ../../packages/cli/src/commands/stop.ts
75868
76260
  init_cjs_shims();
75869
- var import_node_child_process6 = require("child_process");
76261
+ var import_node_child_process7 = require("child_process");
75870
76262
  var import_node_fs7 = require("fs");
75871
76263
  var import_node_util4 = require("util");
75872
76264
  var import_node_path9 = require("path");
75873
- var import_picocolors28 = __toESM(require_picocolors(), 1);
75874
- 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);
75875
76267
  var unlinkAsync = (0, import_node_util4.promisify)(import_node_fs7.unlink);
75876
76268
  var SPAWNED_HOST_FILE2 = "spawned-host.json";
75877
76269
  var START_TIME_TOLERANCE_S = 60;
@@ -75887,30 +76279,30 @@ async function runStop(opts) {
75887
76279
  const root = findRepoRoot(cwd) ?? cwd;
75888
76280
  const hostUrl = readSourceHostUrl(root);
75889
76281
  if (!hostUrl) {
75890
- 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)."));
75891
76283
  return 0;
75892
76284
  }
75893
76285
  return await stopHost(root, hostUrl);
75894
76286
  } catch (err) {
75895
- console.error(import_picocolors28.default.red(err.message));
76287
+ console.error(import_picocolors29.default.red(err.message));
75896
76288
  return 1;
75897
76289
  }
75898
76290
  }
75899
76291
  async function stopHost(root, hostUrl) {
75900
76292
  const alive = await isHostHealthy(hostUrl);
75901
76293
  if (!alive) {
75902
- console.log(import_picocolors28.default.dim(`Host ${hostUrl} is already stopped.`));
76294
+ console.log(import_picocolors29.default.dim(`Host ${hostUrl} is already stopped.`));
75903
76295
  return 0;
75904
76296
  }
75905
76297
  const port = extractPort(hostUrl);
75906
76298
  if (port === null) {
75907
- 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}`));
75908
76300
  return 1;
75909
76301
  }
75910
76302
  const spawned = readSpawnedHost(root);
75911
76303
  if (spawned === null) {
75912
76304
  console.error(
75913
- import_picocolors28.default.red(
76305
+ import_picocolors29.default.red(
75914
76306
  `No ownership record found (.horus/${SPAWNED_HOST_FILE2} absent). Horus will not stop a host it did not spawn.`
75915
76307
  )
75916
76308
  );
@@ -75918,12 +76310,12 @@ async function stopHost(root, hostUrl) {
75918
76310
  }
75919
76311
  const recordError = validateSpawnedRecord(spawned);
75920
76312
  if (recordError !== null) {
75921
- 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.`));
75922
76314
  return 1;
75923
76315
  }
75924
76316
  if (spawned.port !== port) {
75925
76317
  console.error(
75926
- import_picocolors28.default.red(
76318
+ import_picocolors29.default.red(
75927
76319
  `Ownership record port (${spawned.port}) does not match host URL port (${port}). Record may be stale.`
75928
76320
  )
75929
76321
  );
@@ -75931,7 +76323,7 @@ async function stopHost(root, hostUrl) {
75931
76323
  }
75932
76324
  if (spawned.root !== root) {
75933
76325
  console.error(
75934
- import_picocolors28.default.red(
76326
+ import_picocolors29.default.red(
75935
76327
  `Ownership record root (${spawned.root}) does not match resolved root (${root}). Record may be stale.`
75936
76328
  )
75937
76329
  );
@@ -75939,7 +76331,7 @@ async function stopHost(root, hostUrl) {
75939
76331
  }
75940
76332
  const info = await getProcessInfo(spawned.pid);
75941
76333
  if (info === null) {
75942
- 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.`));
75943
76335
  try {
75944
76336
  await unlinkAsync((0, import_node_path9.join)(root, HORUS_DIR, SPAWNED_HOST_FILE2));
75945
76337
  } catch {
@@ -75952,7 +76344,7 @@ async function stopHost(root, hostUrl) {
75952
76344
  );
75953
76345
  if (!hostPortRe.test(info.args)) {
75954
76346
  console.error(
75955
- import_picocolors28.default.red(
76347
+ import_picocolors29.default.red(
75956
76348
  `Pid ${spawned.pid} args do not match "horus-source host --port ${portStr}". Got: "${info.args.slice(0, 120)}". Aborting for safety.`
75957
76349
  )
75958
76350
  );
@@ -75961,12 +76353,12 @@ async function stopHost(root, hostUrl) {
75961
76353
  const startTs = new Date(spawned.startedAt).getTime();
75962
76354
  const recordedAgeS = Math.round((Date.now() - startTs) / 1e3);
75963
76355
  if (!Number.isFinite(info.etimeSeconds)) {
75964
- 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.`));
75965
76357
  return 1;
75966
76358
  }
75967
76359
  if (Math.abs(info.etimeSeconds - recordedAgeS) > START_TIME_TOLERANCE_S) {
75968
76360
  console.error(
75969
- import_picocolors28.default.red(
76361
+ import_picocolors29.default.red(
75970
76362
  `Pid ${spawned.pid} age mismatch: record says ~${recordedAgeS}s, process reports ${info.etimeSeconds}s elapsed. Possible PID reuse \u2014 aborting for safety.`
75971
76363
  )
75972
76364
  );
@@ -75978,9 +76370,9 @@ async function stopHost(root, hostUrl) {
75978
76370
  signaled = true;
75979
76371
  } catch (err) {
75980
76372
  if (err.code === "ESRCH") {
75981
- 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.`));
75982
76374
  } else {
75983
- 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}`));
75984
76376
  return 1;
75985
76377
  }
75986
76378
  }
@@ -75996,14 +76388,14 @@ async function stopHost(root, hostUrl) {
75996
76388
  }
75997
76389
  if (!exited) {
75998
76390
  console.error(
75999
- import_picocolors28.default.red(
76391
+ import_picocolors29.default.red(
76000
76392
  `Host pid ${spawned.pid} did not exit within ${STOP_WAIT_MS / 1e3}s after SIGTERM.`
76001
76393
  )
76002
76394
  );
76003
76395
  return 1;
76004
76396
  }
76005
76397
  console.log(
76006
- `${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}`
76007
76399
  );
76008
76400
  }
76009
76401
  try {
@@ -76016,7 +76408,7 @@ async function stopAll() {
76016
76408
  const registry = readRegistry();
76017
76409
  const projects2 = Object.entries(registry.projects);
76018
76410
  if (projects2.length === 0) {
76019
- console.log(import_picocolors28.default.dim("No registered projects."));
76411
+ console.log(import_picocolors29.default.dim("No registered projects."));
76020
76412
  return 0;
76021
76413
  }
76022
76414
  let stopped = 0;
@@ -76026,17 +76418,17 @@ async function stopAll() {
76026
76418
  if (!hostUrl) continue;
76027
76419
  const alive = await isHostHealthy(hostUrl);
76028
76420
  if (!alive) continue;
76029
- 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})`)}`);
76030
76422
  const code = await stopHost(entry2.root, hostUrl);
76031
76423
  if (code === 0) stopped++;
76032
76424
  else failed++;
76033
76425
  }
76034
76426
  if (stopped === 0 && failed === 0) {
76035
- 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."));
76036
76428
  } else {
76037
76429
  console.log(
76038
76430
  `
76039
- 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`) : ""}.`
76040
76432
  );
76041
76433
  }
76042
76434
  return failed > 0 ? 1 : 0;
@@ -76088,12 +76480,12 @@ function extractPort(hostUrl) {
76088
76480
 
76089
76481
  // ../../packages/cli/src/commands/hosts.ts
76090
76482
  init_cjs_shims();
76091
- var import_picocolors29 = __toESM(require_picocolors(), 1);
76483
+ var import_picocolors30 = __toESM(require_picocolors(), 1);
76092
76484
  async function runHosts() {
76093
76485
  const registry = readRegistry();
76094
76486
  const projects2 = Object.entries(registry.projects);
76095
76487
  if (projects2.length === 0) {
76096
- 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."));
76097
76489
  return 0;
76098
76490
  }
76099
76491
  const rows = [];
@@ -76110,29 +76502,29 @@ async function runHosts() {
76110
76502
  });
76111
76503
  const anyHost = rows.some((r) => r.hostUrl !== null);
76112
76504
  if (!anyHost) {
76113
- 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."));
76114
76506
  return 0;
76115
76507
  }
76116
76508
  console.log("");
76117
76509
  for (const row of rows) {
76118
76510
  if (row.hostUrl === null) continue;
76119
- 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");
76120
76512
  const port = extractPort2(row.hostUrl) ?? "?";
76121
76513
  console.log(
76122
- ` ${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)}`
76123
76515
  );
76124
76516
  }
76125
76517
  console.log("");
76126
76518
  const noHost = rows.filter((r) => r.hostUrl === null);
76127
76519
  if (noHost.length > 0) {
76128
76520
  for (const row of noHost) {
76129
- 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)}`);
76130
76522
  }
76131
76523
  console.log("");
76132
76524
  }
76133
76525
  const running = rows.filter((r) => r.healthy).length;
76134
76526
  console.log(
76135
- import_picocolors29.default.dim(
76527
+ import_picocolors30.default.dim(
76136
76528
  `${running} running \xB7 horus stop to reap \xB7 horus stop --all to stop everything`
76137
76529
  )
76138
76530
  );
@@ -76149,12 +76541,12 @@ function extractPort2(hostUrl) {
76149
76541
 
76150
76542
  // ../../packages/cli/src/commands/doctor.ts
76151
76543
  init_cjs_shims();
76152
- var import_picocolors30 = __toESM(require_picocolors(), 1);
76544
+ var import_picocolors31 = __toESM(require_picocolors(), 1);
76153
76545
  var DEFAULT_DB_URL4 = "postgresql://horus:horus@localhost:5433/horus";
76154
76546
  function mark2(status) {
76155
- if (status === "pass") return import_picocolors30.default.green("\u2713");
76156
- if (status === "warn") return import_picocolors30.default.yellow("~");
76157
- 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");
76158
76550
  }
76159
76551
  async function runDoctor(opts) {
76160
76552
  const cwd = opts?.cwd ?? process.cwd();
@@ -76381,11 +76773,11 @@ async function runDoctor(opts) {
76381
76773
  write(JSON.stringify(output, null, 2));
76382
76774
  return hasFailure ? 1 : 0;
76383
76775
  }
76384
- write(import_picocolors30.default.bold("\nHorus readiness check\n"));
76776
+ write(import_picocolors31.default.bold("\nHorus readiness check\n"));
76385
76777
  for (const check of checks) {
76386
- 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)}`);
76387
76779
  if (check.next) {
76388
- write(` ${import_picocolors30.default.dim("\u2192 " + check.next)}`);
76780
+ write(` ${import_picocolors31.default.dim("\u2192 " + check.next)}`);
76389
76781
  }
76390
76782
  }
76391
76783
  write("");
@@ -76394,20 +76786,20 @@ async function runDoctor(opts) {
76394
76786
 
76395
76787
  // ../../packages/cli/src/commands/providers-doctor.ts
76396
76788
  init_cjs_shims();
76397
- var import_node_child_process7 = require("child_process");
76398
- var import_picocolors31 = __toESM(require_picocolors(), 1);
76789
+ var import_node_child_process8 = require("child_process");
76790
+ var import_picocolors32 = __toESM(require_picocolors(), 1);
76399
76791
  function statusMark(status) {
76400
- if (status === "ready") return import_picocolors31.default.green("\u2713");
76401
- if (status === "installed") return import_picocolors31.default.yellow("~");
76402
- 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");
76403
76795
  }
76404
76796
  function statusLabel(status) {
76405
- if (status === "ready") return import_picocolors31.default.green("ready");
76406
- if (status === "installed") return import_picocolors31.default.yellow("installed (not configured)");
76407
- 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");
76408
76800
  }
76409
76801
  function detectBinary(id) {
76410
- 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 });
76411
76803
  if (result.error) {
76412
76804
  return { id, status: "unavailable", detail: `${id}: command not found` };
76413
76805
  }
@@ -76421,7 +76813,7 @@ async function runProvidersDoctorCommand(opts) {
76421
76813
  const registry = opts?.registry ?? DEFAULT_LOCAL_PROVIDER_REGISTRY;
76422
76814
  const write = opts?.write ?? ((line2) => console.log(line2));
76423
76815
  const results = buildProviderResults(registry, opts?._detect);
76424
- write(import_picocolors31.default.bold("\nLocal AI providers\n"));
76816
+ write(import_picocolors32.default.bold("\nLocal AI providers\n"));
76425
76817
  for (const result of results) {
76426
76818
  const descriptor = registry.get(result.id);
76427
76819
  const name = descriptor?.displayName ?? result.id;
@@ -76429,22 +76821,37 @@ async function runProvidersDoctorCommand(opts) {
76429
76821
  ` ${statusMark(result.status)} ${result.id.padEnd(8)} ${name.padEnd(22)} ${statusLabel(result.status)}`
76430
76822
  );
76431
76823
  if (result.status !== "ready" && result.detail) {
76432
- write(` ${import_picocolors31.default.dim("\u2192 " + result.detail)}`);
76824
+ write(` ${import_picocolors32.default.dim("\u2192 " + result.detail)}`);
76433
76825
  }
76434
76826
  }
76435
76827
  write("");
76436
- write(import_picocolors31.default.bold("Cloud AI providers\n"));
76437
- const anthropicKey = opts?._anthropicKey !== void 0 ? opts._anthropicKey : process.env["ANTHROPIC_API_KEY"] ?? null;
76438
- 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") {
76439
76842
  write(
76440
- ` ${import_picocolors31.default.green("\u2713")} ${"anthropic".padEnd(8)} ${"Anthropic Claude API".padEnd(22)} ${import_picocolors31.default.green("ANTHROPIC_API_KEY configured")}`
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") {
76846
+ write(
76847
+ ` ${import_picocolors32.default.green("\u2713")} ${"anthropic".padEnd(8)} ${"Anthropic Claude API".padEnd(22)} ${import_picocolors32.default.green("configured (ANTHROPIC_API_KEY env)")}`
76441
76848
  );
76442
76849
  } else {
76443
76850
  write(
76444
- ` ${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")}`
76445
76852
  );
76446
76853
  write(
76447
- ` ${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`")}`
76448
76855
  );
76449
76856
  }
76450
76857
  write("");
@@ -76455,7 +76862,7 @@ async function runProvidersDoctorCommand(opts) {
76455
76862
  init_cjs_shims();
76456
76863
  var import_node_fs8 = require("fs");
76457
76864
  var import_node_path10 = require("path");
76458
- var import_picocolors32 = __toESM(require_picocolors(), 1);
76865
+ var import_picocolors33 = __toESM(require_picocolors(), 1);
76459
76866
  function configTemplate(name, repoPath) {
76460
76867
  return `export default {
76461
76868
  database: {
@@ -76507,40 +76914,40 @@ async function runGenerateConfig(opts) {
76507
76914
  const name = opts.name ?? defaults?.name ?? "my-project";
76508
76915
  const repoPath = opts.repo ?? defaults?.repoPath ?? `/path/to/${name}`;
76509
76916
  if ((0, import_node_fs8.existsSync)(outPath) && !opts.force) {
76510
- log(`${import_picocolors32.default.red("\u2717")} ${outPath} already exists`);
76511
- 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"));
76512
76919
  return 1;
76513
76920
  }
76514
76921
  try {
76515
76922
  (0, import_node_fs8.mkdirSync)((0, import_node_path10.dirname)(outPath), { recursive: true });
76516
76923
  (0, import_node_fs8.writeFileSync)(outPath, configTemplate(name, repoPath), "utf8");
76517
76924
  } catch (err) {
76518
- 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}`);
76519
76926
  return 1;
76520
76927
  }
76521
- log(`${import_picocolors32.default.green("\u2713")} Created ${outPath}`);
76522
- log(import_picocolors32.default.dim(` project: ${name}`));
76523
- 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}`));
76524
76931
  if (hasLocalConfig && localConfigPath2 != null) {
76525
76932
  log("");
76526
- 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}`);
76527
76934
  log(" \u2022 .horus/config.json \u2014 project config used by `horus investigate` from this repo");
76528
76935
  log(" \u2022 horus.config.js \u2014 standalone/global config used with `horus doctor --config <path>`");
76529
- 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`));
76530
76937
  } else {
76531
- log(import_picocolors32.default.dim(` next: horus doctor --config ${outPath}`));
76938
+ log(import_picocolors33.default.dim(` next: horus doctor --config ${outPath}`));
76532
76939
  }
76533
76940
  return 0;
76534
76941
  }
76535
76942
 
76536
76943
  // ../../packages/cli/src/commands/readiness.ts
76537
76944
  init_cjs_shims();
76538
- var import_picocolors33 = __toESM(require_picocolors(), 1);
76945
+ var import_picocolors34 = __toESM(require_picocolors(), 1);
76539
76946
  var DEFAULT_DB_URL5 = "postgresql://horus:horus@localhost:5433/horus";
76540
76947
  function mark3(status) {
76541
- if (status === "pass") return import_picocolors33.default.green("\u2713");
76542
- if (status === "warn") return import_picocolors33.default.yellow("~");
76543
- 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");
76544
76951
  }
76545
76952
  async function runReadiness(opts) {
76546
76953
  const cwd = opts?.cwd ?? process.cwd();
@@ -76695,20 +77102,20 @@ async function runReadiness(opts) {
76695
77102
  }
76696
77103
  const blockingChecks = checks.filter((c) => c.blocking);
76697
77104
  const optionalChecks = checks.filter((c) => !c.blocking);
76698
- write(import_picocolors33.default.bold("\nHorus release readiness\n"));
76699
- write(import_picocolors33.default.bold(" Blocking"));
77105
+ write(import_picocolors34.default.bold("\nHorus release readiness\n"));
77106
+ write(import_picocolors34.default.bold(" Blocking"));
76700
77107
  for (const check of blockingChecks) {
76701
- 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)}`);
76702
77109
  if (check.next) {
76703
- write(` ${import_picocolors33.default.dim("\u2192 " + check.next)}`);
77110
+ write(` ${import_picocolors34.default.dim("\u2192 " + check.next)}`);
76704
77111
  }
76705
77112
  }
76706
77113
  write("");
76707
- write(import_picocolors33.default.bold(" Optional"));
77114
+ write(import_picocolors34.default.bold(" Optional"));
76708
77115
  for (const check of optionalChecks) {
76709
- 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)}`);
76710
77117
  if (check.next) {
76711
- write(` ${import_picocolors33.default.dim("\u2192 " + check.next)}`);
77118
+ write(` ${import_picocolors34.default.dim("\u2192 " + check.next)}`);
76712
77119
  }
76713
77120
  }
76714
77121
  write("");
@@ -76716,21 +77123,21 @@ async function runReadiness(opts) {
76716
77123
  const optionalWarns = optionalChecks.filter((c) => c.status === "warn").length;
76717
77124
  if (blockingFails.length === 0) {
76718
77125
  if (optionalWarns === 0) {
76719
- write(import_picocolors33.default.green(" Ready for demo/release."));
77126
+ write(import_picocolors34.default.green(" Ready for demo/release."));
76720
77127
  } else {
76721
77128
  write(
76722
- import_picocolors33.default.yellow(
77129
+ import_picocolors34.default.yellow(
76723
77130
  ` Ready for a basic demo. ${optionalWarns} optional item(s) not configured \u2014 investigation evidence will be limited.`
76724
77131
  )
76725
77132
  );
76726
77133
  }
76727
77134
  } else {
76728
77135
  write(
76729
- import_picocolors33.default.red(
77136
+ import_picocolors34.default.red(
76730
77137
  ` Not ready. ${blockingFails.length} blocking item(s) must be resolved before demo/release.`
76731
77138
  )
76732
77139
  );
76733
- 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."));
76734
77141
  }
76735
77142
  write("");
76736
77143
  return blockingFails.length > 0 ? 1 : 0;
@@ -76780,8 +77187,8 @@ Examples:
76780
77187
  process.exitCode = await runReadiness({ config: opts.config });
76781
77188
  });
76782
77189
  program2.command("connect <type>").description(
76783
- "Add or update a runtime connector (elasticsearch / mongodb / grafana / redis) in .horus/config.json"
76784
- ).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(
76785
77192
  "--db <spec>",
76786
77193
  "redis logical DB as db:role1,role2 (e.g. 0:cache,state or 1:bullmq,queues); repeatable",
76787
77194
  (val, acc) => {
@@ -76804,6 +77211,9 @@ Examples:
76804
77211
  db: opts.db,
76805
77212
  bullmqPrefix: opts.bullmqPrefix,
76806
77213
  scanDbs: opts.scanDbs,
77214
+ provider: opts.provider,
77215
+ apiKey: opts.apiKey,
77216
+ aiModel: opts.model,
76807
77217
  noTest: opts.test === false
76808
77218
  });
76809
77219
  }
@@ -76841,13 +77251,14 @@ Examples:
76841
77251
  process.exitCode = await runIndex(opts);
76842
77252
  }
76843
77253
  );
76844
- 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(
76845
77255
  async (name, opts) => {
76846
77256
  process.exitCode = await runQueues(name, {
76847
77257
  config: opts.config,
76848
77258
  name: opts.name,
76849
77259
  project: opts.project,
76850
- live: opts.live
77260
+ live: opts.live,
77261
+ json: opts.json
76851
77262
  });
76852
77263
  }
76853
77264
  );
@@ -77015,14 +77426,14 @@ Examples:
77015
77426
  });
77016
77427
  program2.command("logs [service]").description(
77017
77428
  "Synthesize error evidence from logs (signatures, first/last, affected services); --raw for lines"
77018
- ).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(
77019
77430
  async (service, opts) => {
77020
77431
  process.exitCode = await runLogs(service, opts);
77021
77432
  }
77022
77433
  );
77023
77434
  program2.command("state").description(
77024
77435
  "Surface application-state evidence from MongoDB (read-only, allowlisted): counts, staleness, anomalous statuses"
77025
- ).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(
77026
77437
  async (opts) => {
77027
77438
  process.exitCode = await runState(opts);
77028
77439
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@merittdev/horus",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "Local-first, source-aware production-incident investigation engine",
5
5
  "type": "module",
6
6
  "bin": {