@merittdev/horus 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +3 -1
  2. package/dist/index.cjs +636 -60
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  **Understand what happened.**
8
8
 
9
- Open-source incident investigation. Horus connects Elasticsearch, Sentry, Grafana, MongoDB, Postgres, Redis (incl. BullMQ queues), Axiom logs, and source intelligence into deterministic reports — installable today.
9
+ Open-source incident investigation. Horus connects Elasticsearch, Sentry, Grafana, MongoDB, Postgres, Redis (incl. BullMQ queues), Axiom logs, Shopify Admin, and source intelligence into deterministic reports — installable today.
10
10
 
11
11
  CLI-only. Read-only against production systems. Horus never writes to your infrastructure.
12
12
 
@@ -129,6 +129,7 @@ Installable today. More connectors and AI providers are in progress.
129
129
  - Grafana metrics
130
130
  - MongoDB / Postgres / Redis state
131
131
  - BullMQ queue evidence
132
+ - Shopify Admin (GraphQL — queries you supply)
132
133
  - Source intelligence (code graph)
133
134
  - Timeline generation
134
135
  - Evidence correlation
@@ -160,6 +161,7 @@ Horus is organized in four layers:
160
161
  - **MongoDB / Postgres** — application/operational state
161
162
  - **Grafana** — metrics via its datasource proxy
162
163
  - **Redis / BullMQ** — cache & queue runtime state
164
+ - **Shopify Admin** — application state via GraphQL queries you supply at investigation time (`--shopify-query`) or declare in config; the connector embeds none. Read-only, Client-Credentials auth.
163
165
  - **Git** — change history, ownership signals
164
166
 
165
167
  **Investigation** (deterministic)
package/dist/index.cjs CHANGED
@@ -3057,7 +3057,7 @@ var init_version = __esm({
3057
3057
  "../../packages/core/src/version.ts"() {
3058
3058
  "use strict";
3059
3059
  init_cjs_shims();
3060
- HORUS_VERSION = true ? "0.18.0" : "dev";
3060
+ HORUS_VERSION = true ? "0.19.0" : "dev";
3061
3061
  PINNED_SOURCE_VERSION = "2.1.0";
3062
3062
  }
3063
3063
  });
@@ -10183,7 +10183,7 @@ var require_jiti = __commonJS({
10183
10183
  }, pathe_M_eThtNZ_dirname = function(e3) {
10184
10184
  const t4 = pathe_M_eThtNZ_normalizeWindowsPath(e3).replace(/\/$/, "").split("/").slice(0, -1);
10185
10185
  return 1 === t4.length && Je3.test(t4[0]) && (t4[0] += "/"), t4.join("/") || (isAbsolute2(e3) ? "/" : ".");
10186
- }, basename5 = function(e3, t4) {
10186
+ }, basename6 = function(e3, t4) {
10187
10187
  const i4 = pathe_M_eThtNZ_normalizeWindowsPath(e3).split("/");
10188
10188
  let n5 = "";
10189
10189
  for (let e4 = i4.length - 1; e4 >= 0; e4--) {
@@ -10904,7 +10904,7 @@ Default "index" lookups for the main are deprecated for ES modules.`, "Deprecati
10904
10904
  if (!e3.opts.fsCache || !t4.filename) return i4();
10905
10905
  const n5 = ` /* v${Ti}-${utils_hash(t4.source, 16)} */
10906
10906
  `;
10907
- let a3 = `${basename5(pathe_M_eThtNZ_dirname(t4.filename))}-${(function(e4) {
10907
+ let a3 = `${basename6(pathe_M_eThtNZ_dirname(t4.filename))}-${(function(e4) {
10908
10908
  const t5 = e4.split(Wt2).pop();
10909
10909
  if (!t5) return;
10910
10910
  const i5 = t5.lastIndexOf(".");
@@ -10950,7 +10950,7 @@ Default "index" lookups for the main are deprecated for ES modules.`, "Deprecati
10950
10950
  return i4.startsWith("#!") && (i4 = "// " + i4), i4;
10951
10951
  }
10952
10952
  function eval_evalModule(t4, i4, n5 = {}) {
10953
- const a3 = n5.id || (n5.filename ? basename5(n5.filename) : `_jitiEval.${n5.ext || (n5.async ? "mjs" : "js")}`), c5 = n5.filename || jitiResolve(t4, a3, { async: n5.async }), l3 = n5.ext || extname(c5), y6 = n5.cache || t4.parentCache || {}, E3 = /\.[cm]?tsx?$/.test(l3), w5 = ".mjs" === l3 || ".js" === l3 && "module" === (function(e3) {
10953
+ const a3 = n5.id || (n5.filename ? basename6(n5.filename) : `_jitiEval.${n5.ext || (n5.async ? "mjs" : "js")}`), c5 = n5.filename || jitiResolve(t4, a3, { async: n5.async }), l3 = n5.ext || extname(c5), y6 = n5.cache || t4.parentCache || {}, E3 = /\.[cm]?tsx?$/.test(l3), w5 = ".mjs" === l3 || ".js" === l3 && "module" === (function(e3) {
10954
10954
  for (; e3 && "." !== e3 && "/" !== e3; ) {
10955
10955
  e3 = pathe_M_eThtNZ_join(e3, "..");
10956
10956
  try {
@@ -10998,7 +10998,7 @@ Default "index" lookups for the main are deprecated for ES modules.`, "Deprecati
10998
10998
  (0, $e3.mkdirSync)(n7, { recursive: true });
10999
10999
  } catch {
11000
11000
  }
11001
- const a5 = pathe_M_eThtNZ_join(n7, `${basename5(i8, extname(i8))}-${Date.now()}-${Math.random().toString(36).slice(2)}.mjs`);
11001
+ const a5 = pathe_M_eThtNZ_join(n7, `${basename6(i8, extname(i8))}-${Date.now()}-${Math.random().toString(36).slice(2)}.mjs`);
11002
11002
  return (0, $e3.writeFileSync)(a5, t6), a5;
11003
11003
  })(l4, n6), debug(t5, "[esm]", "[tempfile]", c7), a4(pathToFileURL2(c7))), E4 = y7 ? a4(y7).catch((e3) => {
11004
11004
  if ("ENAMETOOLONG" !== e3?.code) throw e3;
@@ -11807,6 +11807,18 @@ function resolveEnvironment(config2, opts) {
11807
11807
  ...url !== void 0 ? { url } : {}
11808
11808
  };
11809
11809
  }
11810
+ if (c4.shopify !== void 0) {
11811
+ const sh = c4.shopify;
11812
+ const accessId = sh.accessId ?? process.env[sh.accessIdEnv ?? "SHOPIFY_ACCESS_ID"];
11813
+ const secret = sh.secret ?? process.env[sh.secretEnv ?? "SHOPIFY_SECRET"];
11814
+ resolved.shopify = {
11815
+ store: sh.store,
11816
+ ...sh.apiVersion !== void 0 ? { apiVersion: sh.apiVersion } : {},
11817
+ ...accessId !== void 0 ? { accessId } : {},
11818
+ ...secret !== void 0 ? { secret } : {},
11819
+ ...sh.queries !== void 0 ? { queries: sh.queries } : {}
11820
+ };
11821
+ }
11810
11822
  if (c4.grafana !== void 0) {
11811
11823
  const g4 = c4.grafana;
11812
11824
  resolved.grafana = {
@@ -12126,6 +12138,49 @@ var init_config = __esm({
12126
12138
  /** Env var holding the base URL. Defaults to "AXIOM_URL". */
12127
12139
  urlEnv: external_exports.string().optional()
12128
12140
  }).optional(),
12141
+ /**
12142
+ * Shopify Admin connector — a first-class evidence source for the investigation
12143
+ * engine. The connector embeds NO queries: the GraphQL document is supplied by the
12144
+ * caller at investigation time (`--shopify-query`) and/or declared here as named
12145
+ * default queries that `horus watch` (which has no agent to pass a flag) runs each
12146
+ * cycle. Auth is a read-only Client-Credentials grant: `accessId` is the app
12147
+ * `client_id` (readable), `secret` is the `client_secret` (encrypted at rest).
12148
+ */
12149
+ shopify: external_exports.object({
12150
+ /** Store subdomain, e.g. "my-store" — `.myshopify.com` is added automatically (a full domain is also accepted). */
12151
+ store: external_exports.string(),
12152
+ /** Admin API version segment, e.g. "2025-10". Defaults when omitted. */
12153
+ apiVersion: external_exports.string().optional(),
12154
+ /** App client_id (Client-Credentials grant). Takes priority over accessIdEnv. */
12155
+ accessId: external_exports.string().optional(),
12156
+ /** Env var holding the client_id. Defaults to "SHOPIFY_ACCESS_ID". */
12157
+ accessIdEnv: external_exports.string().optional(),
12158
+ /** App client_secret (encrypted at rest). Takes priority over secretEnv. */
12159
+ secret: external_exports.string().optional(),
12160
+ /** Env var holding the client_secret. Defaults to "SHOPIFY_SECRET". */
12161
+ secretEnv: external_exports.string().optional(),
12162
+ /**
12163
+ * Default named GraphQL queries run when no `--shopify-query` is supplied at
12164
+ * invocation (notably by `horus watch`). Each result set becomes Evidence. The
12165
+ * query text is authored by the operator, executed verbatim — never built in code.
12166
+ */
12167
+ queries: external_exports.array(
12168
+ external_exports.object({
12169
+ /** Label for provenance + the evidence title. */
12170
+ name: external_exports.string(),
12171
+ /** Verbatim Admin GraphQL query document (read-only). */
12172
+ query: external_exports.string(),
12173
+ /** Static variables merged under any engine-bound window variables. */
12174
+ variables: external_exports.record(external_exports.unknown()).optional(),
12175
+ /** EvidenceKind to map results onto (constrained to the existing union). */
12176
+ kind: external_exports.enum(["state", "log", "metric"]).default("state"),
12177
+ /** Base relevance seed (0–1) before hint-term weighting. */
12178
+ relevance: external_exports.number().min(0).max(1).optional(),
12179
+ /** Inject the investigation window as `$from`/`$to` variables when declared. */
12180
+ bindWindow: external_exports.boolean().optional()
12181
+ })
12182
+ ).optional()
12183
+ }).optional(),
12129
12184
  grafana: external_exports.object({
12130
12185
  dashboard: external_exports.string().optional(),
12131
12186
  /** Multiple dashboard UIDs to fetch. Takes precedence over `dashboard` when set. */
@@ -12235,7 +12290,8 @@ var init_config = __esm({
12235
12290
  sentry: ["authToken"],
12236
12291
  axiom: ["token"],
12237
12292
  grafana: ["username", "password"],
12238
- redis: ["url"]
12293
+ redis: ["url"],
12294
+ shopify: ["secret"]
12239
12295
  };
12240
12296
  CONFIG_EXAMPLES = {
12241
12297
  "(root)": 'add `database: { url: "postgresql://..." }` and at least one project',
@@ -21009,7 +21065,7 @@ var require_execute_operation = __commonJS({
21009
21065
  return client.topology;
21010
21066
  }
21011
21067
  var BASE_BACKOFF_MS = 100;
21012
- var MAX_BACKOFF_MS = 1e4;
21068
+ var MAX_BACKOFF_MS2 = 1e4;
21013
21069
  async function executeOperationWithRetries(operation, { topology, timeoutContext, session, readPreference }) {
21014
21070
  let selector;
21015
21071
  if (operation.hasAspect(operation_1.Aspect.MUST_SELECT_SAME_SERVER)) {
@@ -21084,7 +21140,7 @@ var require_execute_operation = __commonJS({
21084
21140
  session.unpin({ force: true });
21085
21141
  }
21086
21142
  if (operationError.hasErrorLabel(error_1.MongoErrorLabel.SystemOverloadedError)) {
21087
- const backoffMS = Math.random() * Math.min(MAX_BACKOFF_MS, BASE_BACKOFF_MS * 2 ** attempt);
21143
+ const backoffMS = Math.random() * Math.min(MAX_BACKOFF_MS2, BASE_BACKOFF_MS * 2 ** attempt);
21088
21144
  if (timeoutContext.csotEnabled() && backoffMS > timeoutContext.remainingTimeMS) {
21089
21145
  throw error3;
21090
21146
  }
@@ -75147,6 +75203,301 @@ function shortTs3(iso) {
75147
75203
  return `${iso.slice(5, 10)} ${iso.slice(11, 16)}`;
75148
75204
  }
75149
75205
 
75206
+ // ../../packages/connectors/src/shopify/client.ts
75207
+ init_cjs_shims();
75208
+ var REQUEST_TIMEOUT_MS3 = 8e3;
75209
+ var DEFAULT_API_VERSION = "2025-10";
75210
+ var TOKEN_REFRESH_MARGIN_S = 120;
75211
+ var MAX_RETRIES = 2;
75212
+ var MAX_BACKOFF_MS = 5e3;
75213
+ function normalizeStore(raw) {
75214
+ const s5 = raw.trim().replace(/^https?:\/\//i, "").replace(/\/+$/, "");
75215
+ return s5.includes(".") ? s5 : `${s5}.myshopify.com`;
75216
+ }
75217
+ function normalizeApiVersion(raw) {
75218
+ const v4 = (raw ?? "").trim();
75219
+ return /^20\d{2}-\d{2}$/.test(v4) ? v4 : DEFAULT_API_VERSION;
75220
+ }
75221
+ function isThrottled(result) {
75222
+ return (result.errors ?? []).some(
75223
+ (e) => e.extensions?.code === "THROTTLED"
75224
+ );
75225
+ }
75226
+ var ShopifyAdminClient = class {
75227
+ store;
75228
+ apiVersion;
75229
+ baseUrl;
75230
+ accessId;
75231
+ secret;
75232
+ token = null;
75233
+ tokenExpiresAtMs = 0;
75234
+ constructor(opts) {
75235
+ this.store = normalizeStore(opts.store);
75236
+ this.apiVersion = normalizeApiVersion(opts.apiVersion);
75237
+ this.baseUrl = (opts.baseUrl ?? `https://${this.store}`).replace(/\/$/, "");
75238
+ this.accessId = opts.accessId;
75239
+ this.secret = opts.secret;
75240
+ }
75241
+ /**
75242
+ * Resolve a valid Admin API token. With an `accessId`, runs the Client-Credentials
75243
+ * grant and caches the result until (expiry - margin); without one, returns `secret`
75244
+ * as a direct token. Throws on a failed grant.
75245
+ */
75246
+ async ensureToken() {
75247
+ if (this.accessId === void 0 || this.accessId === "") return this.secret;
75248
+ if (this.token !== null && Date.now() < this.tokenExpiresAtMs) return this.token;
75249
+ const res = await fetch(`${this.baseUrl}/admin/oauth/access_token`, {
75250
+ method: "POST",
75251
+ headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
75252
+ body: new URLSearchParams({
75253
+ client_id: this.accessId,
75254
+ client_secret: this.secret,
75255
+ grant_type: "client_credentials"
75256
+ }),
75257
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS3)
75258
+ });
75259
+ if (!res.ok) {
75260
+ const text2 = await res.text().catch(() => "");
75261
+ throw new Error(`Shopify token exchange -> ${res.status}: ${text2.slice(0, 200)}`);
75262
+ }
75263
+ const json2 = await res.json();
75264
+ if (!json2.access_token) {
75265
+ throw new Error("Shopify token exchange returned no access_token");
75266
+ }
75267
+ this.token = json2.access_token;
75268
+ const ttl = typeof json2.expires_in === "number" ? json2.expires_in : 86400;
75269
+ this.tokenExpiresAtMs = Date.now() + Math.max(0, ttl - TOKEN_REFRESH_MARGIN_S) * 1e3;
75270
+ return this.token;
75271
+ }
75272
+ /** Drop any cached token so the next request re-runs the grant (used after a 401). */
75273
+ invalidateToken() {
75274
+ this.token = null;
75275
+ this.tokenExpiresAtMs = 0;
75276
+ }
75277
+ /**
75278
+ * Run a GraphQL document verbatim against the Admin API and return the parsed result.
75279
+ * Retries HTTP 429 / GraphQL THROTTLED with bounded backoff; refreshes the token once
75280
+ * on 401. Throws on a non-retryable HTTP error.
75281
+ */
75282
+ async graphql(query, variables) {
75283
+ const url = `${this.baseUrl}/admin/api/${this.apiVersion}/graphql.json`;
75284
+ let httpRetries = 0;
75285
+ let throttleRetries = 0;
75286
+ let refreshedOn401 = false;
75287
+ for (; ; ) {
75288
+ const token = await this.ensureToken();
75289
+ const res = await fetch(url, {
75290
+ method: "POST",
75291
+ headers: {
75292
+ "X-Shopify-Access-Token": token,
75293
+ "Content-Type": "application/json",
75294
+ Accept: "application/json"
75295
+ },
75296
+ body: JSON.stringify({ query, variables: variables ?? {} }),
75297
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS3)
75298
+ });
75299
+ if (res.status === 401 && !refreshedOn401) {
75300
+ refreshedOn401 = true;
75301
+ this.invalidateToken();
75302
+ continue;
75303
+ }
75304
+ if (res.status === 429 && httpRetries < MAX_RETRIES) {
75305
+ httpRetries += 1;
75306
+ await sleep(retryAfterMs(res, httpRetries));
75307
+ continue;
75308
+ }
75309
+ if (!res.ok) {
75310
+ const text2 = await res.text().catch(() => "");
75311
+ throw new Error(`Shopify GraphQL -> ${res.status}: ${text2.slice(0, 200)}`);
75312
+ }
75313
+ const result = await res.json();
75314
+ if (isThrottled(result) && throttleRetries < MAX_RETRIES) {
75315
+ throttleRetries += 1;
75316
+ await sleep(throttleBackoffMs(result, throttleRetries));
75317
+ continue;
75318
+ }
75319
+ return result;
75320
+ }
75321
+ }
75322
+ /**
75323
+ * Liveness probe: run a minimal `{ shop { name } }` query — the connector's ONLY owned
75324
+ * query, used purely for health / connect-time validation, NEVER for evidence. It
75325
+ * validates both the auth (the CC grant or the static token, resolved inside `graphql`)
75326
+ * AND that the token can actually read — so a bad/expired static token is caught here
75327
+ * rather than silently reported as "reachable". Never throws.
75328
+ */
75329
+ async health() {
75330
+ try {
75331
+ const result = await this.graphql("{ shop { name } }");
75332
+ if (result.errors !== void 0 && result.errors.length > 0) {
75333
+ return { ok: false, detail: result.errors[0].message };
75334
+ }
75335
+ return { ok: true, detail: `shopify reachable (${this.store})` };
75336
+ } catch (err2) {
75337
+ return { ok: false, detail: err2.message };
75338
+ }
75339
+ }
75340
+ };
75341
+ function retryAfterMs(res, attempt) {
75342
+ const header = res.headers.get("retry-after");
75343
+ const secs = header !== null ? Number(header) : NaN;
75344
+ const ms = Number.isFinite(secs) ? secs * 1e3 : 500 * 2 ** (attempt - 1);
75345
+ return Math.min(Math.max(0, ms), MAX_BACKOFF_MS);
75346
+ }
75347
+ function throttleBackoffMs(result, attempt) {
75348
+ const cost = result.extensions?.cost;
75349
+ const requested = cost?.requestedQueryCost ?? 0;
75350
+ const rate = cost?.throttleStatus?.restoreRate ?? 0;
75351
+ const ms = requested > 0 && rate > 0 ? requested / rate * 1e3 : 500 * 2 ** (attempt - 1);
75352
+ return Math.min(Math.max(0, ms), MAX_BACKOFF_MS);
75353
+ }
75354
+ function sleep(ms) {
75355
+ return new Promise((resolve13) => setTimeout(resolve13, ms));
75356
+ }
75357
+
75358
+ // ../../packages/connectors/src/shopify/provider.ts
75359
+ init_cjs_shims();
75360
+ init_src();
75361
+ var KIND_TO_SOURCE = {
75362
+ state: "state",
75363
+ log: "logs",
75364
+ metric: "metrics"
75365
+ };
75366
+ var ShopifyProvider = class {
75367
+ constructor(client, opts) {
75368
+ this.client = client;
75369
+ this.opts = opts;
75370
+ }
75371
+ client;
75372
+ opts;
75373
+ id = "shopify";
75374
+ kind = "state";
75375
+ /**
75376
+ * Run each supplied (or, as a fallback, config-declared) query verbatim and return one
75377
+ * record per success. The investigation window is injected as `$from`/`$to` variables for
75378
+ * queries that opt in (`bindWindow`); `hintTerms` are NOT used to rewrite the query — only
75379
+ * to weight relevance later. Best-effort: a failing query is dropped (returns nothing for
75380
+ * that query) so one bad document never aborts the investigation. Never throws.
75381
+ */
75382
+ async collect(opts = {}) {
75383
+ const specs = opts.queries !== void 0 && opts.queries.length > 0 ? opts.queries : this.opts.queries ?? [];
75384
+ if (specs.length === 0) return [];
75385
+ const to = opts.to ?? (/* @__PURE__ */ new Date()).toISOString();
75386
+ const from = opts.from;
75387
+ const records = await Promise.all(
75388
+ specs.map(async (q2) => {
75389
+ try {
75390
+ const result = await this.client.graphql(q2.query, bindWindowVars(q2, from, to));
75391
+ const rec = { name: q2.name, kind: q2.kind, data: result.data, timestamp: to };
75392
+ if (q2.relevance !== void 0) rec.relevance = q2.relevance;
75393
+ if (result.errors !== void 0 && result.errors.length > 0) {
75394
+ rec.errors = result.errors.map((e) => ({ message: e.message }));
75395
+ }
75396
+ if (result.extensions?.cost !== void 0) rec.cost = result.extensions.cost;
75397
+ return rec;
75398
+ } catch {
75399
+ return null;
75400
+ }
75401
+ })
75402
+ );
75403
+ return records.filter((r) => r !== null);
75404
+ }
75405
+ /** Synthesize one Evidence per record. Preserves per-query provenance. */
75406
+ toEvidence(records, hintTerms, collectedAt) {
75407
+ return records.map((rec, i3) => this.recordToEvidence(rec, hintTerms, collectedAt, i3));
75408
+ }
75409
+ /**
75410
+ * One-shot evidence query: collect + convert. Degrades to [] on any failure so a flaky
75411
+ * Shopify never aborts an investigation.
75412
+ */
75413
+ async queryEvidence(opts = {}) {
75414
+ try {
75415
+ const hintTerms = opts.hintTerms ?? [];
75416
+ const records = await this.collect({
75417
+ ...opts.from !== void 0 ? { from: opts.from } : {},
75418
+ ...opts.to !== void 0 ? { to: opts.to } : {},
75419
+ ...opts.queries !== void 0 ? { queries: opts.queries } : {},
75420
+ hintTerms
75421
+ });
75422
+ return this.toEvidence(records, hintTerms, opts.collectedAt ?? (/* @__PURE__ */ new Date()).toISOString());
75423
+ } catch {
75424
+ return [];
75425
+ }
75426
+ }
75427
+ async health() {
75428
+ return this.client.health();
75429
+ }
75430
+ recordToEvidence(rec, hintTerms, collectedAt, index2) {
75431
+ const payload = {
75432
+ source: "shopify",
75433
+ shop: this.opts.store,
75434
+ queryName: rec.name,
75435
+ data: rec.data
75436
+ };
75437
+ if (rec.cost !== void 0) payload["cost"] = rec.cost;
75438
+ if (rec.errors !== void 0) payload["errors"] = rec.errors;
75439
+ const ev = {
75440
+ id: `ev_shopify_${index2}`,
75441
+ source: KIND_TO_SOURCE[rec.kind],
75442
+ kind: rec.kind,
75443
+ title: buildTitle3(rec, this.opts.store),
75444
+ relevance: computeRelevance3(rec, hintTerms),
75445
+ payload,
75446
+ links: {},
75447
+ provenance: { query: `shopify ${this.opts.store} ${rec.name}`, collectedAt }
75448
+ };
75449
+ if (rec.timestamp !== void 0) ev.timestamp = rec.timestamp;
75450
+ return ev;
75451
+ }
75452
+ };
75453
+ function bindWindowVars(q2, from, to) {
75454
+ const base2 = q2.variables ?? {};
75455
+ if (q2.bindWindow !== true) return { ...base2 };
75456
+ const window2 = {};
75457
+ if (from !== void 0) window2["from"] = from;
75458
+ if (to !== void 0) window2["to"] = to;
75459
+ return { ...base2, ...window2 };
75460
+ }
75461
+ function buildTitle3(rec, store) {
75462
+ const ts = rec.timestamp ? ` \xB7 ${shortTs4(rec.timestamp)}` : "";
75463
+ return redactSecrets(`Shopify ${store} ${rec.name}: ${summarize2(rec)}${ts}`).slice(0, 220);
75464
+ }
75465
+ function summarize2(rec) {
75466
+ if (rec.errors !== void 0 && rec.errors.length > 0) return `error: ${rec.errors[0].message}`;
75467
+ const count = countResults(rec.data);
75468
+ if (count !== null) return `${count} result${count === 1 ? "" : "s"}`;
75469
+ return "ok";
75470
+ }
75471
+ function countResults(data) {
75472
+ if (data === null || typeof data !== "object") return null;
75473
+ let total = 0;
75474
+ let found = false;
75475
+ for (const value of Object.values(data)) {
75476
+ if (value !== null && typeof value === "object") {
75477
+ const conn = value;
75478
+ if (Array.isArray(conn["edges"])) {
75479
+ total += conn["edges"].length;
75480
+ found = true;
75481
+ } else if (Array.isArray(conn["nodes"])) {
75482
+ total += conn["nodes"].length;
75483
+ found = true;
75484
+ }
75485
+ }
75486
+ }
75487
+ return found ? total : null;
75488
+ }
75489
+ function computeRelevance3(rec, hintTerms) {
75490
+ let score = rec.relevance ?? 0.6;
75491
+ const hay = JSON.stringify(rec.data ?? {}).toLowerCase();
75492
+ const domainTerms = hintTerms.filter((t2) => t2.length > 2);
75493
+ if (domainTerms.some((t2) => hay.includes(t2.toLowerCase()))) score += 0.2;
75494
+ return Math.min(0.95, Math.max(0.5, score));
75495
+ }
75496
+ function shortTs4(iso) {
75497
+ if (!iso || iso.length < 16) return iso || "\u2014";
75498
+ return `${iso.slice(5, 10)} ${iso.slice(11, 16)}`;
75499
+ }
75500
+
75150
75501
  // ../../packages/connectors/src/bullmq/client.ts
75151
75502
  init_cjs_shims();
75152
75503
  var import_ioredis = __toESM(require_built3(), 1);
@@ -75823,6 +76174,22 @@ function axiomForEnv(renv) {
75823
76174
  { dataset: a2.dataset }
75824
76175
  );
75825
76176
  }
76177
+ function shopifyForEnv(renv) {
76178
+ const s5 = renv.connectors.shopify;
76179
+ if (!s5 || !s5.store || !s5.secret) return null;
76180
+ return new ShopifyProvider(
76181
+ new ShopifyAdminClient({
76182
+ store: s5.store,
76183
+ secret: s5.secret,
76184
+ ...s5.accessId !== void 0 ? { accessId: s5.accessId } : {},
76185
+ ...s5.apiVersion !== void 0 ? { apiVersion: s5.apiVersion } : {}
76186
+ }),
76187
+ {
76188
+ store: s5.store,
76189
+ ...s5.queries !== void 0 ? { queries: s5.queries } : {}
76190
+ }
76191
+ );
76192
+ }
75826
76193
  function codeForRepo(config2, repoName) {
75827
76194
  const renv = resolveEnvironment(config2, { project: repoName });
75828
76195
  const c4 = codeForEnv(renv);
@@ -76130,6 +76497,9 @@ async function redisServerStatus(renv) {
76130
76497
  return { reachable, authFailed, databases };
76131
76498
  }
76132
76499
 
76500
+ // ../../packages/connectors/src/shopify/index.ts
76501
+ init_cjs_shims();
76502
+
76133
76503
  // ../../packages/cli/src/commands/update.ts
76134
76504
  var execFileAsync = (0, import_node_util3.promisify)(import_node_child_process5.execFile);
76135
76505
  async function ensureBackendPinned(write) {
@@ -92491,6 +92861,32 @@ async function checkEnv(renv, deps) {
92491
92861
  ` ${mark("pending")} ${import_picocolors6.default.bold("Axiom")} ${import_picocolors6.default.dim("not configured")}`
92492
92862
  );
92493
92863
  }
92864
+ const shopifyCfg = renv.connectors.shopify;
92865
+ if (shopifyCfg) {
92866
+ const label = shopifyCfg.store ?? "(no store)";
92867
+ const shopify = (deps?.shopifyFactory ?? shopifyForEnv)(renv);
92868
+ if (shopify) {
92869
+ const h3 = await shopify.health();
92870
+ if (!h3.ok) {
92871
+ console.log(
92872
+ ` ${mark(false)} ${import_picocolors6.default.bold("Shopify")} ${import_picocolors6.default.dim(`unreachable \xB7 ${label}`)}`
92873
+ );
92874
+ allOk = false;
92875
+ } else {
92876
+ console.log(
92877
+ ` ${mark(true)} ${import_picocolors6.default.bold("Shopify")} ${import_picocolors6.default.dim(`reachable \xB7 ${label}`)}`
92878
+ );
92879
+ }
92880
+ } else {
92881
+ console.log(
92882
+ ` ${mark(false)} ${import_picocolors6.default.bold("Shopify")} ${import_picocolors6.default.dim(`configured (${label}) but client secret not set`)}`
92883
+ );
92884
+ }
92885
+ } else {
92886
+ console.log(
92887
+ ` ${mark("pending")} ${import_picocolors6.default.bold("Shopify")} ${import_picocolors6.default.dim("not configured")}`
92888
+ );
92889
+ }
92494
92890
  const redisCfg = renv.connectors.redis;
92495
92891
  if (redisCfg?.url) {
92496
92892
  const server = redisServerLabel(redisCfg.url);
@@ -95758,7 +96154,7 @@ function buildRuntimeSourceStatus(evidence2, connectors) {
95758
96154
  const metricsConfigured = !!connectors.grafana;
95759
96155
  const metricsFailed = metricsConfigured && !connectors.metricsCollected;
95760
96156
  const stateCount = evidence2.filter((e) => e.source === "state").length;
95761
- const stateConfigured = !!(connectors.redis || connectors.mongodb || connectors.postgres);
96157
+ const stateConfigured = !!(connectors.redis || connectors.mongodb || connectors.postgres || connectors.shopify);
95762
96158
  const queueConfigured = connectors.queue ?? evidence2.some((e) => e.source === "queue");
95763
96159
  const queueCount = evidence2.filter((e) => e.kind === "queue-state").length;
95764
96160
  return {
@@ -98311,6 +98707,29 @@ async function investigate(input, deps) {
98311
98707
  ownershipEstimate = null;
98312
98708
  }
98313
98709
  }
98710
+ let shopifyCollected = false;
98711
+ if (deps.shopify) {
98712
+ try {
98713
+ const from = logWindowFrom(input.logsSince ?? input.since);
98714
+ const to = (/* @__PURE__ */ new Date()).toISOString();
98715
+ const shopifySeedBase = top ? top.filePath.split("/").pop() ?? "" : "";
98716
+ const shopifyTerms = top ? [.../* @__PURE__ */ new Set([...tokenize(hint), ...tokenize(top.name), ...tokenize(shopifySeedBase)])] : [...new Set(tokenize(hint))];
98717
+ const records = await deps.shopify.collect({
98718
+ from,
98719
+ to,
98720
+ hintTerms: shopifyTerms,
98721
+ ...input.shopifyQueries !== void 0 ? { queries: input.shopifyQueries } : {}
98722
+ });
98723
+ shopifyCollected = true;
98724
+ const collectedAt2 = (/* @__PURE__ */ new Date()).toISOString();
98725
+ for (const ev of deps.shopify.toEvidence(records, shopifyTerms, collectedAt2)) {
98726
+ ev.id = globalThis.crypto.randomUUID();
98727
+ evidence2.push(ev);
98728
+ }
98729
+ } catch {
98730
+ shopifyCollected = false;
98731
+ }
98732
+ }
98314
98733
  normalizeEvidence(evidence2, { service: input.service, environment: input.environment });
98315
98734
  const graph = buildGraph(evidence2);
98316
98735
  const timeline = buildTimeline(evidence2);
@@ -98961,6 +99380,10 @@ async function investigate(input, deps) {
98961
99380
  // collection ran (distinguishes "no matching rows" from "collection failed").
98962
99381
  axiom: deps.axiom != null,
98963
99382
  axiomCollected,
99383
+ // Shopify is configured iff a provider was built (auth present); shopifyCollected
99384
+ // tracks whether its query collection ran (vs "no queries supplied / failed").
99385
+ shopify: deps.shopify != null,
99386
+ shopifyCollected,
98964
99387
  metricsCollected,
98965
99388
  metricsFailureReason,
98966
99389
  logsCollected,
@@ -98974,6 +99397,8 @@ async function investigate(input, deps) {
98974
99397
  sentryCollected,
98975
99398
  axiom: deps.axiom != null,
98976
99399
  axiomCollected,
99400
+ shopify: deps.shopify != null,
99401
+ shopifyCollected,
98977
99402
  grafana: deps.metrics != null,
98978
99403
  queue: deps.queue != null,
98979
99404
  metricsCollected,
@@ -117240,6 +117665,8 @@ function formatAge2(ms) {
117240
117665
  // ../../packages/cli/src/commands/investigate.ts
117241
117666
  init_cjs_shims();
117242
117667
  var import_picocolors17 = __toESM(require_picocolors(), 1);
117668
+ var import_node_fs21 = require("fs");
117669
+ var import_node_path23 = require("path");
117243
117670
  init_src();
117244
117671
 
117245
117672
  // ../../packages/cli/src/lib/cloud/investigation-sync.ts
@@ -118124,6 +118551,7 @@ async function buildInvestigationContext(renv, opts) {
118124
118551
  const postgres2 = postgresForEnv(renv);
118125
118552
  const sentry = sentryForEnv(renv);
118126
118553
  const axiom = axiomForEnv(renv);
118554
+ const shopify = shopifyForEnv(renv);
118127
118555
  const queue = queueForEnv(renv);
118128
118556
  const redisState = redisStateForEnv(renv);
118129
118557
  const metrics = metricsForEnv(renv);
@@ -118141,6 +118569,7 @@ async function buildInvestigationContext(renv, opts) {
118141
118569
  postgres: postgres2,
118142
118570
  sentry,
118143
118571
  axiom,
118572
+ shopify,
118144
118573
  queue,
118145
118574
  redisState,
118146
118575
  metrics,
@@ -118173,7 +118602,10 @@ async function runOneInvestigation(input, ctx, opts = {}) {
118173
118602
  ...input.scope !== void 0 ? { scope: input.scope } : {},
118174
118603
  ...input.since !== void 0 ? { since: input.since } : {},
118175
118604
  ...input.logsSince !== void 0 ? { logsSince: input.logsSince } : {},
118176
- ...(input.service ?? ctx.service) !== void 0 ? { service: input.service ?? ctx.service } : {}
118605
+ ...(input.service ?? ctx.service) !== void 0 ? { service: input.service ?? ctx.service } : {},
118606
+ // Invocation-time Shopify queries override the connector's config defaults; when
118607
+ // omitted the provider falls back to `connectors.shopify.queries` (e.g. under watch).
118608
+ ...input.shopifyQueries !== void 0 ? { shopifyQueries: input.shopifyQueries } : {}
118177
118609
  },
118178
118610
  {
118179
118611
  code: ctx.code,
@@ -118186,6 +118618,7 @@ async function runOneInvestigation(input, ctx, opts = {}) {
118186
118618
  postgres: ctx.postgres,
118187
118619
  sentry: ctx.sentry,
118188
118620
  axiom: ctx.axiom,
118621
+ shopify: ctx.shopify,
118189
118622
  queue: ctx.queue,
118190
118623
  redisState: ctx.redisState,
118191
118624
  metrics: ctx.metrics,
@@ -118197,6 +118630,7 @@ async function runOneInvestigation(input, ctx, opts = {}) {
118197
118630
  postgres: !!renv.connectors.postgres?.url,
118198
118631
  sentry: !!renv.connectors.sentry,
118199
118632
  axiom: !!renv.connectors.axiom,
118633
+ shopify: !!renv.connectors.shopify,
118200
118634
  redis: !!renv.connectors.redis?.url,
118201
118635
  queue: !!ctx.queue
118202
118636
  },
@@ -118236,6 +118670,46 @@ function withDeadline(p5, ms) {
118236
118670
  }
118237
118671
 
118238
118672
  // ../../packages/cli/src/commands/investigate.ts
118673
+ function resolveShopifyQueries(values2, variablesRaw) {
118674
+ if (values2 === void 0 || values2.length === 0) return [];
118675
+ const variables = parseShopifyVariables(variablesRaw);
118676
+ let stdinCache = null;
118677
+ return values2.map((raw, i3) => {
118678
+ const value = raw.trim();
118679
+ let query;
118680
+ let name2 = `q${i3 + 1}`;
118681
+ if (value === "-") {
118682
+ stdinCache ??= (0, import_node_fs21.readFileSync)(0, "utf8");
118683
+ query = stdinCache;
118684
+ name2 = "stdin";
118685
+ } else if (value.startsWith("@")) {
118686
+ const path = value.slice(1);
118687
+ query = (0, import_node_fs21.readFileSync)(path, "utf8");
118688
+ name2 = (0, import_node_path23.basename)(path).replace(/\.[^.]+$/, "") || name2;
118689
+ } else {
118690
+ query = raw;
118691
+ }
118692
+ const spec = { name: name2, query, kind: "state" };
118693
+ if (variables !== void 0) spec.variables = variables;
118694
+ if (/\$(from|to)\b/.test(query)) spec.bindWindow = true;
118695
+ return spec;
118696
+ });
118697
+ }
118698
+ function parseShopifyVariables(raw) {
118699
+ if (raw === void 0 || raw.trim() === "") return void 0;
118700
+ const trimmed = raw.trim();
118701
+ const text2 = trimmed.startsWith("@") ? (0, import_node_fs21.readFileSync)(trimmed.slice(1), "utf8") : raw;
118702
+ let parsed;
118703
+ try {
118704
+ parsed = JSON.parse(text2);
118705
+ } catch {
118706
+ throw new Error("--shopify-variables is not valid JSON");
118707
+ }
118708
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
118709
+ throw new Error("--shopify-variables must be a JSON object");
118710
+ }
118711
+ return parsed;
118712
+ }
118239
118713
  function extractEvidenceExcerpt(e) {
118240
118714
  if (!e.payload || typeof e.payload !== "object") return void 0;
118241
118715
  const p5 = e.payload;
@@ -118343,6 +118817,13 @@ function classifyAIFailure(firstError) {
118343
118817
  async function runInvestigate(hint, opts) {
118344
118818
  try {
118345
118819
  const config2 = await loadConfig(opts.config, { name: opts.name });
118820
+ let shopifyQueries;
118821
+ try {
118822
+ shopifyQueries = resolveShopifyQueries(opts.shopifyQuery, opts.shopifyVariables);
118823
+ } catch (err2) {
118824
+ console.error(import_picocolors17.default.red(`--shopify-query: ${err2.message}`));
118825
+ return 1;
118826
+ }
118346
118827
  const projectName = opts.project ?? opts.repo;
118347
118828
  let renv;
118348
118829
  try {
@@ -118379,7 +118860,8 @@ async function runInvestigate(hint, opts) {
118379
118860
  hint,
118380
118861
  ...opts.scope !== void 0 ? { scope: opts.scope } : {},
118381
118862
  ...opts.since !== void 0 ? { since: opts.since } : {},
118382
- ...opts.logsSince !== void 0 ? { logsSince: opts.logsSince } : {}
118863
+ ...opts.logsSince !== void 0 ? { logsSince: opts.logsSince } : {},
118864
+ ...shopifyQueries.length > 0 ? { shopifyQueries } : {}
118383
118865
  },
118384
118866
  ctx,
118385
118867
  { timeoutMs: timeoutSec * 1e3 }
@@ -119621,8 +120103,8 @@ async function runOwner(query, opts) {
119621
120103
 
119622
120104
  // ../../packages/cli/src/commands/postmortem.ts
119623
120105
  init_cjs_shims();
119624
- var import_node_fs21 = require("fs");
119625
- var import_node_path23 = require("path");
120106
+ var import_node_fs22 = require("fs");
120107
+ var import_node_path24 = require("path");
119626
120108
  var import_picocolors30 = __toESM(require_picocolors(), 1);
119627
120109
  async function runPostmortem(id, opts) {
119628
120110
  const log = opts.write ?? ((line2) => console.log(line2));
@@ -119733,14 +120215,14 @@ _AI summary unavailable: ${validationErrors?.[0] ?? "provider error"}_
119733
120215
  }
119734
120216
  }
119735
120217
  if (opts.output) {
119736
- const outputPath = (0, import_node_path23.resolve)(opts.output);
119737
- if ((0, import_node_fs21.existsSync)(outputPath) && !opts.force) {
120218
+ const outputPath = (0, import_node_path24.resolve)(opts.output);
120219
+ if ((0, import_node_fs22.existsSync)(outputPath) && !opts.force) {
119738
120220
  log(import_picocolors30.default.red("\u2717") + " " + outputPath + " already exists");
119739
120221
  log(import_picocolors30.default.dim(" pass --force to overwrite"));
119740
120222
  return 1;
119741
120223
  }
119742
120224
  try {
119743
- (0, import_node_fs21.writeFileSync)(outputPath, content, "utf8");
120225
+ (0, import_node_fs22.writeFileSync)(outputPath, content, "utf8");
119744
120226
  } catch (err2) {
119745
120227
  log(import_picocolors30.default.red("\u2717 Could not write " + outputPath + ": ") + err2.message);
119746
120228
  return 1;
@@ -122145,13 +122627,13 @@ async function runState(opts) {
122145
122627
  // ../../packages/cli/src/commands/init.ts
122146
122628
  init_cjs_shims();
122147
122629
  var import_picocolors39 = __toESM(require_picocolors(), 1);
122148
- var import_node_path24 = require("path");
122630
+ var import_node_path25 = require("path");
122149
122631
  init_src();
122150
122632
  async function runInit(opts) {
122151
122633
  try {
122152
122634
  const cwd = process.cwd();
122153
- const root = opts.path ? (0, import_node_path24.resolve)(opts.path) : findRepoRoot(cwd) ?? cwd;
122154
- const name2 = opts.name ?? (0, import_node_path24.basename)(root);
122635
+ const root = opts.path ? (0, import_node_path25.resolve)(opts.path) : findRepoRoot(cwd) ?? cwd;
122636
+ const name2 = opts.name ?? (0, import_node_path25.basename)(root);
122155
122637
  const envName = opts.env ?? "production";
122156
122638
  const repo = { name: name2, path: root };
122157
122639
  if (opts.source) repo["source"] = { hostUrl: opts.source };
@@ -122803,7 +123285,7 @@ function parseDbSpec(spec) {
122803
123285
  }
122804
123286
  return { db, roles };
122805
123287
  }
122806
- var SUPPORTED = ["elasticsearch", "mongodb", "postgres", "sentry", "axiom", "grafana", "redis"];
123288
+ var SUPPORTED = ["elasticsearch", "mongodb", "postgres", "sentry", "axiom", "shopify", "grafana", "redis"];
122807
123289
  async function runConnect(type, opts) {
122808
123290
  if (type === "ai") {
122809
123291
  return runConnectAi({
@@ -122843,6 +123325,9 @@ async function runConnect(type, opts) {
122843
123325
  case "axiom":
122844
123326
  patch = buildAxiomPatch(filled);
122845
123327
  break;
123328
+ case "shopify":
123329
+ patch = buildShopifyPatch(filled);
123330
+ break;
122846
123331
  case "grafana":
122847
123332
  patch = buildGrafanaPatch(filled);
122848
123333
  break;
@@ -122993,6 +123478,12 @@ ${import_picocolors44.default.bold(`Connect ${type}`)} ${import_picocolors44.def
122993
123478
  }
122994
123479
  break;
122995
123480
  }
123481
+ case "shopify":
123482
+ filled.store = filled.store ?? await ask("Store name (subdomain only, e.g. 946011-c0)", "my-store");
123483
+ filled.apiVersion = filled.apiVersion ?? await ask("API version", "2025-10");
123484
+ filled.accessId = filled.accessId ?? (await ask("Access id (client_id) \u2014 leave blank for a static Admin API token", "", false) || void 0);
123485
+ filled.secret = filled.secret ?? (await askPassword("Secret \u2014 client_secret, or the static Admin API access token") || void 0);
123486
+ break;
122996
123487
  case "grafana":
122997
123488
  filled.url = filled.url ?? await ask("URL", "https://grafana.example.com");
122998
123489
  filled.username = filled.username ?? (await ask("Username", "", false) || void 0);
@@ -123052,6 +123543,8 @@ function missingRequired(type, opts) {
123052
123543
  return !opts.org || !opts.project || !opts.authToken;
123053
123544
  case "axiom":
123054
123545
  return !opts.dataset || !opts.token;
123546
+ case "shopify":
123547
+ return !opts.store || !opts.secret;
123055
123548
  case "grafana":
123056
123549
  return !opts.url;
123057
123550
  case "redis":
@@ -123157,6 +123650,14 @@ function buildAxiomPatch(opts) {
123157
123650
  if (opts.url && opts.url.replace(/\/$/, "") !== "https://api.axiom.co") patch["url"] = opts.url;
123158
123651
  return patch;
123159
123652
  }
123653
+ function buildShopifyPatch(opts) {
123654
+ if (!opts.store) throw new Error("Store is required for shopify");
123655
+ const patch = { store: opts.store };
123656
+ if (opts.apiVersion) patch["apiVersion"] = opts.apiVersion;
123657
+ if (opts.accessId) patch["accessId"] = opts.accessId;
123658
+ if (opts.secret) patch["secret"] = opts.secret;
123659
+ return patch;
123660
+ }
123160
123661
  function buildGrafanaPatch(opts) {
123161
123662
  const patch = {};
123162
123663
  if (opts.url) patch["url"] = opts.url;
@@ -123303,6 +123804,18 @@ async function probe(type, opts) {
123303
123804
  });
123304
123805
  return client.health();
123305
123806
  }
123807
+ case "shopify": {
123808
+ if (!opts.store || !opts.secret) {
123809
+ return { ok: true, detail: "skipped (missing store/secret)" };
123810
+ }
123811
+ const client = new ShopifyAdminClient({
123812
+ store: opts.store,
123813
+ secret: opts.secret,
123814
+ ...opts.accessId ? { accessId: opts.accessId } : {},
123815
+ ...opts.apiVersion ? { apiVersion: opts.apiVersion } : {}
123816
+ });
123817
+ return client.health();
123818
+ }
123306
123819
  case "grafana": {
123307
123820
  if (!opts.url) return { ok: true, detail: "skipped (no URL)" };
123308
123821
  const headers = { "Content-Type": "application/json" };
@@ -123355,6 +123868,11 @@ function printSummary(type, opts) {
123355
123868
  if (opts.org) lines.push(` org: ${opts.org}`);
123356
123869
  if (opts.project) lines.push(` project: ${opts.project}`);
123357
123870
  if (opts.dataset) lines.push(` dataset: ${opts.dataset}`);
123871
+ if (opts.store) lines.push(` store: ${opts.store}`);
123872
+ if (opts.apiVersion) lines.push(` api-version: ${opts.apiVersion}`);
123873
+ if (opts.accessId) lines.push(` access-id: ${opts.accessId}`);
123874
+ if (opts.secret)
123875
+ lines.push(` secret: ${"\u2022".repeat(Math.min(opts.secret.length, 8))}`);
123358
123876
  if (opts.authToken)
123359
123877
  lines.push(` auth-token: ${"\u2022".repeat(Math.min(opts.authToken.length, 8))}`);
123360
123878
  if (opts.token)
@@ -123750,13 +124268,13 @@ async function runNotify(sub, opts) {
123750
124268
 
123751
124269
  // ../../packages/cli/src/commands/secrets.ts
123752
124270
  init_cjs_shims();
123753
- var import_node_fs22 = require("fs");
124271
+ var import_node_fs23 = require("fs");
123754
124272
  var import_picocolors46 = __toESM(require_picocolors(), 1);
123755
124273
  init_src();
123756
124274
  function locate2(cwd = process.cwd()) {
123757
124275
  const root = findRepoRoot(cwd) ?? cwd;
123758
124276
  const configPath = discoverLocalConfig(cwd) ?? localConfigPath(root);
123759
- return { root, configPath, exists: (0, import_node_fs22.existsSync)(configPath) };
124277
+ return { root, configPath, exists: (0, import_node_fs23.existsSync)(configPath) };
123760
124278
  }
123761
124279
  function countStoredSecrets(root) {
123762
124280
  const stored = readLocalSecrets(root).connectors;
@@ -123894,19 +124412,19 @@ export HORUS_SECRET_KEY=${key.toString("base64")}
123894
124412
  // ../../packages/cli/src/commands/stop.ts
123895
124413
  init_cjs_shims();
123896
124414
  var import_node_child_process10 = require("child_process");
123897
- var import_node_fs23 = require("fs");
124415
+ var import_node_fs24 = require("fs");
123898
124416
  var import_node_util6 = require("util");
123899
- var import_node_path25 = require("path");
124417
+ var import_node_path26 = require("path");
123900
124418
  var import_picocolors47 = __toESM(require_picocolors(), 1);
123901
124419
  init_src();
123902
124420
  var execFileAsync3 = (0, import_node_util6.promisify)(import_node_child_process10.execFile);
123903
- var unlinkAsync = (0, import_node_util6.promisify)(import_node_fs23.unlink);
124421
+ var unlinkAsync = (0, import_node_util6.promisify)(import_node_fs24.unlink);
123904
124422
  var SPAWNED_HOST_FILE2 = "spawned-host.json";
123905
124423
  var START_TIME_TOLERANCE_S = 60;
123906
124424
  var STOP_WAIT_MS = 5e3;
123907
124425
  var STOP_KILL_WAIT_MS = 3e3;
123908
124426
  var STOP_POLL_MS = 200;
123909
- var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
124427
+ var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
123910
124428
  async function runStop(opts) {
123911
124429
  try {
123912
124430
  if (opts.all) {
@@ -124042,11 +124560,11 @@ function isOwnHostProcess(pid, port, root, args2) {
124042
124560
  if (hostArgvRegex(port).test(args2)) return true;
124043
124561
  if (!hostArgvAnyPortRegex().test(args2)) return false;
124044
124562
  const rec = readSourceHostPid(root);
124045
- return rec !== null && rec.pid === pid && Number.isFinite(rec.port) && rec.port === port && !!rec.repoPath && (0, import_node_path25.resolve)(rec.repoPath) === (0, import_node_path25.resolve)(root);
124563
+ return rec !== null && rec.pid === pid && Number.isFinite(rec.port) && rec.port === port && !!rec.repoPath && (0, import_node_path26.resolve)(rec.repoPath) === (0, import_node_path26.resolve)(root);
124046
124564
  }
124047
124565
  async function cleanupSpawnedRecord(root) {
124048
124566
  try {
124049
- await unlinkAsync((0, import_node_path25.join)(root, HORUS_DIR, SPAWNED_HOST_FILE2));
124567
+ await unlinkAsync((0, import_node_path26.join)(root, HORUS_DIR, SPAWNED_HOST_FILE2));
124050
124568
  } catch {
124051
124569
  }
124052
124570
  }
@@ -124150,7 +124668,7 @@ async function terminatePid(pid) {
124150
124668
  }
124151
124669
  const deadline = Date.now() + waitMs;
124152
124670
  while (Date.now() < deadline) {
124153
- await sleep(STOP_POLL_MS);
124671
+ await sleep2(STOP_POLL_MS);
124154
124672
  if (await getProcessInfo(pid) === null) return true;
124155
124673
  }
124156
124674
  }
@@ -124470,6 +124988,29 @@ var CONNECTOR_CHECKS = {
124470
124988
  };
124471
124989
  }
124472
124990
  },
124991
+ // Shopify (Admin GraphQL — application state evidence)
124992
+ shopify: {
124993
+ label: "Shopify",
124994
+ absentNext: "add connectors.shopify to an environment for Shopify Admin state evidence",
124995
+ check: (s5, ctx) => {
124996
+ const secretSet = s5.secret || process.env[s5.secretEnv ?? "SHOPIFY_SECRET"];
124997
+ if (!secretSet) {
124998
+ return {
124999
+ label: "Shopify",
125000
+ status: "warn",
125001
+ detail: `${ctx} \u2014 client secret not set`,
125002
+ next: "set shopify.secret or shopify.secretEnv (default SHOPIFY_SECRET), or run horus connect shopify"
125003
+ };
125004
+ }
125005
+ const queryCount = s5.queries?.length ?? 0;
125006
+ const queryNote = queryCount > 0 ? ` (${queryCount} default quer${queryCount === 1 ? "y" : "ies"})` : " (no default queries \u2014 supply --shopify-query at investigate time)";
125007
+ return {
125008
+ label: "Shopify",
125009
+ status: "pass",
125010
+ detail: `${ctx} \u2014 ${s5.store}${queryNote} [runtime ingestion pending]`
125011
+ };
125012
+ }
125013
+ },
124473
125014
  // Redis / BullMQ (queue + cache/state)
124474
125015
  redis: {
124475
125016
  label: "Redis",
@@ -124752,8 +125293,8 @@ async function runProvidersDoctorCommand(opts) {
124752
125293
 
124753
125294
  // ../../packages/cli/src/commands/generate-config.ts
124754
125295
  init_cjs_shims();
124755
- var import_node_fs24 = require("fs");
124756
- var import_node_path26 = require("path");
125296
+ var import_node_fs25 = require("fs");
125297
+ var import_node_path27 = require("path");
124757
125298
  var import_picocolors51 = __toESM(require_picocolors(), 1);
124758
125299
  init_src();
124759
125300
  function configTemplate(name2, repoPath) {
@@ -124775,6 +125316,16 @@ function configTemplate(name2, repoPath) {
124775
125316
  // elasticsearch: { indexPattern: '${name2}-prod-*' },
124776
125317
  // mongodb: { database: '${name2}' },
124777
125318
  // grafana: {},
125319
+ // shopify: {
125320
+ // store: '${name2}', // subdomain only \u2014 .myshopify.com is added automatically
125321
+ // apiVersion: '2025-10',
125322
+ // // accessId + secret set by \`horus connect shopify\` (secret encrypted at rest).
125323
+ // // Default queries run by \`horus watch\`; \`horus investigate --shopify-query\` overrides.
125324
+ // queries: [
125325
+ // { name: 'recent-orders', kind: 'state', bindWindow: true,
125326
+ // query: 'query($from:DateTime){ orders(first:20, query:"updated_at:>=$from"){ edges{ node{ id name displayFinancialStatus } } } }' },
125327
+ // ],
125328
+ // },
124778
125329
  },
124779
125330
  }],
124780
125331
  }],
@@ -124803,17 +125354,17 @@ async function runGenerateConfig(opts) {
124803
125354
  const defaults2 = localConfigPath2 != null ? projectDefaults(localConfigPath2) : null;
124804
125355
  const hasLocalConfig = defaults2 != null;
124805
125356
  const defaultOut = hasLocalConfig ? "horus.config.example.js" : "horus.config.js";
124806
- const outPath = (0, import_node_path26.resolve)(cwd, opts.out ?? defaultOut);
125357
+ const outPath = (0, import_node_path27.resolve)(cwd, opts.out ?? defaultOut);
124807
125358
  const name2 = opts.name ?? defaults2?.name ?? "my-project";
124808
125359
  const repoPath = opts.repo ?? defaults2?.repoPath ?? `/path/to/${name2}`;
124809
- if ((0, import_node_fs24.existsSync)(outPath) && !opts.force) {
125360
+ if ((0, import_node_fs25.existsSync)(outPath) && !opts.force) {
124810
125361
  log(`${import_picocolors51.default.red("\u2717")} ${outPath} already exists`);
124811
125362
  log(import_picocolors51.default.dim(" pass --force to overwrite"));
124812
125363
  return 1;
124813
125364
  }
124814
125365
  try {
124815
- (0, import_node_fs24.mkdirSync)((0, import_node_path26.dirname)(outPath), { recursive: true });
124816
- (0, import_node_fs24.writeFileSync)(outPath, configTemplate(name2, repoPath), "utf8");
125366
+ (0, import_node_fs25.mkdirSync)((0, import_node_path27.dirname)(outPath), { recursive: true });
125367
+ (0, import_node_fs25.writeFileSync)(outPath, configTemplate(name2, repoPath), "utf8");
124817
125368
  } catch (err2) {
124818
125369
  log(`${import_picocolors51.default.red("\u2717")} Could not write ${outPath}: ${err2.message}`);
124819
125370
  return 1;
@@ -124975,6 +125526,7 @@ async function runReadiness(opts) {
124975
125526
  let anyPostgres = false;
124976
125527
  let anySentry = false;
124977
125528
  let anyAxiom = false;
125529
+ let anyShopify = false;
124978
125530
  let anyRedis = false;
124979
125531
  for (const project of globalConfig2.projects) {
124980
125532
  for (const repo of project.repositories) {
@@ -124988,6 +125540,7 @@ async function runReadiness(opts) {
124988
125540
  if (c4.postgres) anyPostgres = true;
124989
125541
  if (c4.sentry) anySentry = true;
124990
125542
  if (c4.axiom) anyAxiom = true;
125543
+ if (c4.shopify) anyShopify = true;
124991
125544
  if (c4.redis) anyRedis = true;
124992
125545
  }
124993
125546
  }
@@ -125054,6 +125607,15 @@ async function runReadiness(opts) {
125054
125607
  next: "run `horus connect axiom`"
125055
125608
  }
125056
125609
  );
125610
+ checks.push(
125611
+ anyShopify ? { label: "Shopify", status: "pass", blocking: false, detail: "configured" } : {
125612
+ label: "Shopify",
125613
+ status: "warn",
125614
+ blocking: false,
125615
+ detail: "not configured \u2014 no Shopify Admin state evidence",
125616
+ next: "run `horus connect shopify`"
125617
+ }
125618
+ );
125057
125619
  checks.push(
125058
125620
  anyRedis ? { label: "Redis", status: "pass", blocking: false, detail: "configured" } : {
125059
125621
  label: "Redis",
@@ -125124,8 +125686,8 @@ async function runReadiness(opts) {
125124
125686
 
125125
125687
  // ../../packages/cli/src/commands/skill.ts
125126
125688
  init_cjs_shims();
125127
- var import_node_fs25 = require("fs");
125128
- var import_node_path27 = require("path");
125689
+ var import_node_fs26 = require("fs");
125690
+ var import_node_path28 = require("path");
125129
125691
  var import_node_os10 = require("os");
125130
125692
  var import_promises3 = require("readline/promises");
125131
125693
  var import_picocolors53 = __toESM(require_picocolors(), 1);
@@ -125135,22 +125697,22 @@ function isValidTarget(target) {
125135
125697
  }
125136
125698
  function getLegacyClaudeSkillPath(opts) {
125137
125699
  const base2 = opts.global ? (0, import_node_os10.homedir)() : opts.cwd ?? process.cwd();
125138
- return (0, import_node_path27.join)(base2, ".claude", "skills", "horus.md");
125700
+ return (0, import_node_path28.join)(base2, ".claude", "skills", "horus.md");
125139
125701
  }
125140
125702
  function getSkillInstallPath(target, opts) {
125141
125703
  const base2 = opts.global ? (0, import_node_os10.homedir)() : opts.cwd ?? process.cwd();
125142
125704
  switch (target) {
125143
125705
  case "claude":
125144
- return (0, import_node_path27.join)(base2, ".claude", "skills", "horus", "SKILL.md");
125706
+ return (0, import_node_path28.join)(base2, ".claude", "skills", "horus", "SKILL.md");
125145
125707
  case "gemini":
125146
- return opts.global ? (0, import_node_path27.join)(base2, ".gemini", "GEMINI.md") : (0, import_node_path27.join)(base2, "GEMINI.md");
125708
+ return opts.global ? (0, import_node_path28.join)(base2, ".gemini", "GEMINI.md") : (0, import_node_path28.join)(base2, "GEMINI.md");
125147
125709
  case "cursor":
125148
- return opts.global ? (0, import_node_path27.join)(base2, ".cursorrules") : (0, import_node_path27.join)(base2, ".cursor", "rules", "horus.mdc");
125710
+ return opts.global ? (0, import_node_path28.join)(base2, ".cursorrules") : (0, import_node_path28.join)(base2, ".cursor", "rules", "horus.mdc");
125149
125711
  case "codex":
125150
- return opts.global ? (0, import_node_path27.join)(base2, ".codex", "AGENTS.md") : (0, import_node_path27.join)(base2, "AGENTS.md");
125712
+ return opts.global ? (0, import_node_path28.join)(base2, ".codex", "AGENTS.md") : (0, import_node_path28.join)(base2, "AGENTS.md");
125151
125713
  case "generic":
125152
125714
  default:
125153
- return (0, import_node_path27.join)(base2, ".horus", "skills", `horus-${target}.md`);
125715
+ return (0, import_node_path28.join)(base2, ".horus", "skills", `horus-${target}.md`);
125154
125716
  }
125155
125717
  }
125156
125718
  var BASE_SKILL = `# Horus Grounded Investigation Skill
@@ -125475,7 +126037,7 @@ async function runSkillInstall(target, opts) {
125475
126037
  const content = generateSkillContent(target);
125476
126038
  if (target === "claude") {
125477
126039
  const legacyPath = getLegacyClaudeSkillPath({ global: opts.global, cwd: opts.cwd });
125478
- if ((0, import_node_fs25.existsSync)(legacyPath)) {
126040
+ if ((0, import_node_fs26.existsSync)(legacyPath)) {
125479
126041
  if (!opts.force) {
125480
126042
  const confirmed = await (opts.confirm ?? confirm)(
125481
126043
  `Found old flat skill file at ${legacyPath}. Replace it with the new directory layout?`
@@ -125487,7 +126049,7 @@ async function runSkillInstall(target, opts) {
125487
126049
  }
125488
126050
  }
125489
126051
  try {
125490
- (0, import_node_fs25.rmSync)(legacyPath);
126052
+ (0, import_node_fs26.rmSync)(legacyPath);
125491
126053
  log(`${import_picocolors53.default.yellow("!")} Replaced old flat skill file: ${legacyPath}`);
125492
126054
  } catch (err2) {
125493
126055
  log(`${import_picocolors53.default.red("\u2717")} Could not remove legacy skill file ${legacyPath}: ${err2.message}`);
@@ -125495,14 +126057,14 @@ async function runSkillInstall(target, opts) {
125495
126057
  }
125496
126058
  }
125497
126059
  }
125498
- if ((0, import_node_fs25.existsSync)(outPath) && !opts.force) {
126060
+ if ((0, import_node_fs26.existsSync)(outPath) && !opts.force) {
125499
126061
  log(`${import_picocolors53.default.red("\u2717")} ${outPath} already exists`);
125500
126062
  log(import_picocolors53.default.dim(" pass --force to overwrite"));
125501
126063
  return 1;
125502
126064
  }
125503
126065
  try {
125504
- (0, import_node_fs25.mkdirSync)((0, import_node_path27.dirname)(outPath), { recursive: true });
125505
- (0, import_node_fs25.writeFileSync)(outPath, content, "utf8");
126066
+ (0, import_node_fs26.mkdirSync)((0, import_node_path28.dirname)(outPath), { recursive: true });
126067
+ (0, import_node_fs26.writeFileSync)(outPath, content, "utf8");
125506
126068
  } catch (err2) {
125507
126069
  log(`${import_picocolors53.default.red("\u2717")} Could not write ${outPath}: ${err2.message}`);
125508
126070
  return 1;
@@ -125547,7 +126109,7 @@ async function runSkillPath(target, opts) {
125547
126109
  // ../../packages/cli/src/commands/login.ts
125548
126110
  init_cjs_shims();
125549
126111
  var import_picocolors54 = __toESM(require_picocolors(), 1);
125550
- var sleep2 = (ms) => new Promise((resolve13) => setTimeout(resolve13, ms));
126112
+ var sleep3 = (ms) => new Promise((resolve13) => setTimeout(resolve13, ms));
125551
126113
  async function loginWithToken(apiBaseUrl, token) {
125552
126114
  const client = new CloudClient(apiBaseUrl, token);
125553
126115
  try {
@@ -125602,7 +126164,7 @@ async function loginWithBrowser(apiBaseUrl) {
125602
126164
  const deadline = Date.now() + session.expiresIn * 1e3;
125603
126165
  let intervalMs = Math.max(session.interval, 1) * 1e3;
125604
126166
  while (Date.now() < deadline) {
125605
- await sleep2(intervalMs);
126167
+ await sleep3(intervalMs);
125606
126168
  let poll;
125607
126169
  try {
125608
126170
  poll = await client.pollCliSession(session.deviceCode);
@@ -126243,7 +126805,7 @@ function installCommandTelemetry(program2) {
126243
126805
  // ../../packages/cli/src/commands/eval.ts
126244
126806
  init_cjs_shims();
126245
126807
  var import_promises5 = require("fs/promises");
126246
- var import_node_path28 = require("path");
126808
+ var import_node_path29 = require("path");
126247
126809
  var import_picocolors58 = __toESM(require_picocolors(), 1);
126248
126810
  init_src();
126249
126811
  function resolveProject2(config2, repo) {
@@ -126294,7 +126856,7 @@ async function runEvalBuild(opts) {
126294
126856
  const project = resolveProject2(config2, opts.repo);
126295
126857
  if (!project) return 1;
126296
126858
  const version3 = opts.version ?? CORPUS_VERSION;
126297
- const outDir = opts.out ? (0, import_node_path28.isAbsolute)(opts.out) ? opts.out : (0, import_node_path28.resolve)(process.cwd(), opts.out) : (0, import_node_path28.join)(process.cwd(), ".horus", "eval");
126859
+ const outDir = opts.out ? (0, import_node_path29.isAbsolute)(opts.out) ? opts.out : (0, import_node_path29.resolve)(process.cwd(), opts.out) : (0, import_node_path29.join)(process.cwd(), ".horus", "eval");
126298
126860
  const { db, sql: sql2 } = await openDb(config2.database.url);
126299
126861
  try {
126300
126862
  const labels = await listOutcomeLabels(db, {
@@ -126310,8 +126872,8 @@ async function runEvalBuild(opts) {
126310
126872
  const build = buildCorpus(labels, resolveReport);
126311
126873
  const artifact = serializeCorpus(build, version3);
126312
126874
  await (0, import_promises5.mkdir)(outDir, { recursive: true });
126313
- const corpusPath = (0, import_node_path28.join)(outDir, artifact.filename);
126314
- const manifestPath = (0, import_node_path28.join)(outDir, `corpus-${version3}.manifest.json`);
126875
+ const corpusPath = (0, import_node_path29.join)(outDir, artifact.filename);
126876
+ const manifestPath = (0, import_node_path29.join)(outDir, `corpus-${version3}.manifest.json`);
126315
126877
  const manifest = { ...artifact.manifest, generatedAt: (/* @__PURE__ */ new Date()).toISOString(), project };
126316
126878
  await (0, import_promises5.writeFile)(corpusPath, artifact.jsonl, "utf8");
126317
126879
  await (0, import_promises5.writeFile)(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf8");
@@ -126412,11 +126974,11 @@ async function runEvalBaseline(opts) {
126412
126974
  // ../../packages/cli/src/commands/train.ts
126413
126975
  init_cjs_shims();
126414
126976
  var import_promises6 = require("fs/promises");
126415
- var import_node_path29 = require("path");
126977
+ var import_node_path30 = require("path");
126416
126978
  var import_node_os11 = require("os");
126417
126979
  var import_picocolors59 = __toESM(require_picocolors(), 1);
126418
126980
  init_src();
126419
- var RERANKER_MODEL_PATH = (0, import_node_path29.join)((0, import_node_os11.homedir)(), ".horus", "reranker.json");
126981
+ var RERANKER_MODEL_PATH = (0, import_node_path30.join)((0, import_node_os11.homedir)(), ".horus", "reranker.json");
126420
126982
  function candidatesFromReport(blob) {
126421
126983
  if (blob === null || typeof blob !== "object") return null;
126422
126984
  const causes = blob.suspectedCauses;
@@ -126505,7 +127067,7 @@ async function runTrain(opts) {
126505
127067
  return 0;
126506
127068
  }
126507
127069
  try {
126508
- await (0, import_promises6.mkdir)((0, import_node_path29.join)((0, import_node_os11.homedir)(), ".horus"), { recursive: true });
127070
+ await (0, import_promises6.mkdir)((0, import_node_path30.join)((0, import_node_os11.homedir)(), ".horus"), { recursive: true });
126509
127071
  const { ok: _ok, ...model } = result;
126510
127072
  void _ok;
126511
127073
  await (0, import_promises6.writeFile)(RERANKER_MODEL_PATH, JSON.stringify(model, null, 2) + "\n", "utf8");
@@ -126573,8 +127135,8 @@ Examples:
126573
127135
  process.exitCode = await runReadiness({ config: opts.config, ai: opts.ai, aiModel: opts.aiModel });
126574
127136
  });
126575
127137
  program2.command("connect <type>").description(
126576
- "Add or update a connector (elasticsearch / mongodb / postgres / sentry / axiom / grafana / redis / ai) in .horus/config.json"
126577
- ).option("--env <name>", "target environment (default: first environment in config)").option("--provider <name>", "AI provider for `connect ai` (anthropic / claude / codex / gemini)").option("--api-key <key>", "Anthropic API key for `connect ai`").option("--model <id>", "default model for `connect ai`").option("--url <url>", "connector URL or connection string (Redis with auth: redis://:password@host:6379)").option("--username <user>", "username (elasticsearch / grafana)").option("--password <pass>", "password (elasticsearch / grafana; for Redis embed in --url)").option("--index-pattern <pattern>", "Elasticsearch index pattern (required for elasticsearch)").option("--service <name>", "service name scope for log queries").option("--database <name>", "database name (required for mongodb)").option("--collections <list>", "comma-separated collection allowlist (mongodb)").option("--schema <name>", "schema to introspect (postgres; default public)").option("--tables <list>", "comma-separated table allowlist (postgres)").option("--auth-token <token>", "Sentry API auth token (required for sentry)").option("--org <slug>", "Sentry org slug (required for sentry)").option("--project <slug>", "Sentry project slug (required for sentry)").option("--token <token>", "Axiom API token (required for axiom)").option("--dataset <name>", "Axiom dataset to query (required for axiom)").option("--dashboard <uid>", "default dashboard uid (grafana)").option(
127138
+ "Add or update a connector (elasticsearch / mongodb / postgres / sentry / axiom / shopify / grafana / redis / ai) in .horus/config.json"
127139
+ ).option("--env <name>", "target environment (default: first environment in config)").option("--provider <name>", "AI provider for `connect ai` (anthropic / claude / codex / gemini)").option("--api-key <key>", "Anthropic API key for `connect ai`").option("--model <id>", "default model for `connect ai`").option("--url <url>", "connector URL or connection string (Redis with auth: redis://:password@host:6379)").option("--username <user>", "username (elasticsearch / grafana)").option("--password <pass>", "password (elasticsearch / grafana; for Redis embed in --url)").option("--index-pattern <pattern>", "Elasticsearch index pattern (required for elasticsearch)").option("--service <name>", "service name scope for log queries").option("--database <name>", "database name (required for mongodb)").option("--collections <list>", "comma-separated collection allowlist (mongodb)").option("--schema <name>", "schema to introspect (postgres; default public)").option("--tables <list>", "comma-separated table allowlist (postgres)").option("--auth-token <token>", "Sentry API auth token (required for sentry)").option("--org <slug>", "Sentry org slug (required for sentry)").option("--project <slug>", "Sentry project slug (required for sentry)").option("--token <token>", "Axiom API token (required for axiom)").option("--dataset <name>", "Axiom dataset to query (required for axiom)").option("--store <name>", "Shopify store subdomain, e.g. my-store \u2014 .myshopify.com is added automatically (required for shopify)").option("--api-version <ver>", "Shopify Admin API version, e.g. 2025-10 (shopify; defaults to a recent version)").option("--access-id <id>", "Shopify app client_id \u2014 omit for a static Admin API token (shopify)").option("--secret <secret>", "Shopify client_secret, or a static Admin API access token \u2014 encrypted at rest (required for shopify)").option("--dashboard <uid>", "default dashboard uid (grafana)").option(
126578
127140
  "--db <spec>",
126579
127141
  "redis logical DB as db:role1,role2 (e.g. 0:cache,state or 1:bullmq,queues); repeatable",
126580
127142
  (val, acc) => {
@@ -126600,6 +127162,10 @@ Examples:
126600
127162
  project: opts.project,
126601
127163
  token: opts.token,
126602
127164
  dataset: opts.dataset,
127165
+ store: opts.store,
127166
+ apiVersion: opts.apiVersion,
127167
+ accessId: opts.accessId,
127168
+ secret: opts.secret,
126603
127169
  dashboard: opts.dashboard,
126604
127170
  db: opts.db,
126605
127171
  bullmqPrefix: opts.bullmqPrefix,
@@ -126779,7 +127345,15 @@ score, a confidence, or a verdict. It ships OFF; once trained, enable with:
126779
127345
  });
126780
127346
  }
126781
127347
  );
126782
- program2.command("investigate <hint>").description("Run a deterministic investigation for an incident hint").option("-c, --config <path>", "path to horus.config.ts").option("--name <name>", "registered project name (resolves via the registry)").option("--project <name>", "project name to scope to").option("--env <name>", "environment name (e.g. production)").option("--repo <name>", "repository/project to scope to (alias for --project)").option("--scope <path>", "resolve the seed only from symbols under this path (e.g. packages/core) \u2014 useful in monorepos").option("--since <ref>", "git ref/range for change-impact (e.g. HEAD~5)").option("--logs-since <dur>", "runtime-log window as a duration (e.g. 30d, 24h); independent of --since").option("--timeout <sec>", "overall investigation deadline in seconds (default 120)").option(
127348
+ program2.command("investigate <hint>").description("Run a deterministic investigation for an incident hint").option("-c, --config <path>", "path to horus.config.ts").option("--name <name>", "registered project name (resolves via the registry)").option("--project <name>", "project name to scope to").option("--env <name>", "environment name (e.g. production)").option("--repo <name>", "repository/project to scope to (alias for --project)").option("--scope <path>", "resolve the seed only from symbols under this path (e.g. packages/core) \u2014 useful in monorepos").option("--since <ref>", "git ref/range for change-impact (e.g. HEAD~5)").option("--logs-since <dur>", "runtime-log window as a duration (e.g. 30d, 24h); independent of --since").option(
127349
+ "--shopify-query <q>",
127350
+ "Shopify Admin GraphQL query to run as evidence: @file, - (stdin), or a raw string; repeatable",
127351
+ (val, acc) => {
127352
+ acc.push(val);
127353
+ return acc;
127354
+ },
127355
+ []
127356
+ ).option("--shopify-variables <json>", "variables (@file or inline JSON object) applied to every --shopify-query").option("--timeout <sec>", "overall investigation deadline in seconds (default 120)").option(
126783
127357
  "--service <name>",
126784
127358
  "service name to scope runtime logs, e.g. leadcall-api-prod"
126785
127359
  ).option("--json", "output JSON (alias for --format json)").option("--format <fmt>", "output format: text | markdown | json", "text").option("--ai", "enrich report with AI narrative (requires ANTHROPIC_API_KEY; falls back to deterministic on failure)").option("--ai-model <model>", "AI model for --ai (default: claude-opus-4-8)").action(
@@ -126793,6 +127367,8 @@ score, a confidence, or a verdict. It ships OFF; once trained, enable with:
126793
127367
  scope: opts.scope,
126794
127368
  since: opts.since,
126795
127369
  logsSince: opts.logsSince,
127370
+ ...opts.shopifyQuery !== void 0 ? { shopifyQuery: opts.shopifyQuery } : {},
127371
+ ...opts.shopifyVariables !== void 0 ? { shopifyVariables: opts.shopifyVariables } : {},
126796
127372
  timeout: opts.timeout,
126797
127373
  service: opts.service,
126798
127374
  json: opts.json,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@merittdev/horus",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "description": "Local-first, source-aware production-incident investigation engine",
5
5
  "type": "module",
6
6
  "bin": {