@hasna/instructions 0.4.34 → 0.4.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -47,7 +47,7 @@ var __export = (target, all) => {
47
47
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
48
48
  var __require = import.meta.require;
49
49
 
50
- // node_modules/commander/lib/error.js
50
+ // ../../node_modules/.bun/commander@13.1.0/node_modules/commander/lib/error.js
51
51
  var require_error = __commonJS((exports) => {
52
52
  class CommanderError extends Error {
53
53
  constructor(exitCode, code, message) {
@@ -71,7 +71,7 @@ var require_error = __commonJS((exports) => {
71
71
  exports.InvalidArgumentError = InvalidArgumentError;
72
72
  });
73
73
 
74
- // node_modules/commander/lib/argument.js
74
+ // ../../node_modules/.bun/commander@13.1.0/node_modules/commander/lib/argument.js
75
75
  var require_argument = __commonJS((exports) => {
76
76
  var { InvalidArgumentError } = require_error();
77
77
 
@@ -150,7 +150,7 @@ var require_argument = __commonJS((exports) => {
150
150
  exports.humanReadableArgName = humanReadableArgName;
151
151
  });
152
152
 
153
- // node_modules/commander/lib/help.js
153
+ // ../../node_modules/.bun/commander@13.1.0/node_modules/commander/lib/help.js
154
154
  var require_help = __commonJS((exports) => {
155
155
  var { humanReadableArgName } = require_argument();
156
156
 
@@ -500,7 +500,7 @@ ${itemIndentStr}`);
500
500
  exports.stripColor = stripColor;
501
501
  });
502
502
 
503
- // node_modules/commander/lib/option.js
503
+ // ../../node_modules/.bun/commander@13.1.0/node_modules/commander/lib/option.js
504
504
  var require_option = __commonJS((exports) => {
505
505
  var { InvalidArgumentError } = require_error();
506
506
 
@@ -678,7 +678,7 @@ var require_option = __commonJS((exports) => {
678
678
  exports.DualOptions = DualOptions;
679
679
  });
680
680
 
681
- // node_modules/commander/lib/suggestSimilar.js
681
+ // ../../node_modules/.bun/commander@13.1.0/node_modules/commander/lib/suggestSimilar.js
682
682
  var require_suggestSimilar = __commonJS((exports) => {
683
683
  var maxDistance = 3;
684
684
  function editDistance(a, b) {
@@ -751,7 +751,7 @@ var require_suggestSimilar = __commonJS((exports) => {
751
751
  exports.suggestSimilar = suggestSimilar;
752
752
  });
753
753
 
754
- // node_modules/commander/lib/command.js
754
+ // ../../node_modules/.bun/commander@13.1.0/node_modules/commander/lib/command.js
755
755
  var require_command = __commonJS((exports) => {
756
756
  var EventEmitter = __require("events").EventEmitter;
757
757
  var childProcess = __require("child_process");
@@ -993,7 +993,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
993
993
  this._exitCallback = (err) => {
994
994
  if (err.code !== "commander.executeSubCommandAsync") {
995
995
  throw err;
996
- }
996
+ } else {}
997
997
  };
998
998
  }
999
999
  return this;
@@ -2061,7 +2061,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2061
2061
  exports.useColor = useColor;
2062
2062
  });
2063
2063
 
2064
- // node_modules/commander/index.js
2064
+ // ../../node_modules/.bun/commander@13.1.0/node_modules/commander/index.js
2065
2065
  var require_commander = __commonJS((exports) => {
2066
2066
  var { Argument } = require_argument();
2067
2067
  var { Command } = require_command();
@@ -2136,6 +2136,30 @@ var init_types = __esm(() => {
2136
2136
  };
2137
2137
  });
2138
2138
 
2139
+ // src/lib/retired-storage-mode.ts
2140
+ function firstDefinedEnvKey(env, keys) {
2141
+ for (const key of keys) {
2142
+ if (Object.hasOwn(env, key) && env[key] !== undefined)
2143
+ return key;
2144
+ }
2145
+ return null;
2146
+ }
2147
+ function assertNoLegacyStorageMode(env = process.env) {
2148
+ const legacyKey = firstDefinedEnvKey(env, LEGACY_STORAGE_MODE_KEYS);
2149
+ if (!legacyKey)
2150
+ return;
2151
+ throw new Error(`${legacyKey} was removed. Deployment modes no longer exist: delete the storage-mode variable. ` + `The client uses the local SQLite store, or the HTTP API selected by ` + `HASNA_INSTRUCTIONS_API_URL + HASNA_INSTRUCTIONS_API_KEY. ` + `On the server, set HASNA_INSTRUCTIONS_DATABASE_URL to select the postgresql backend, ` + `or leave it unset for sqlite.`);
2152
+ }
2153
+ var LEGACY_STORAGE_MODE_KEYS;
2154
+ var init_retired_storage_mode = __esm(() => {
2155
+ LEGACY_STORAGE_MODE_KEYS = [
2156
+ "HASNA_INSTRUCTIONS_STORAGE_MODE",
2157
+ "HASNA_INSTRUCTIONS_MODE",
2158
+ "INSTRUCTIONS_STORAGE_MODE",
2159
+ "INSTRUCTIONS_MODE"
2160
+ ];
2161
+ });
2162
+
2139
2163
  // src/db/database.ts
2140
2164
  import { Database } from "bun:sqlite";
2141
2165
  import { existsSync as existsSync2, mkdirSync, rmSync } from "fs";
@@ -2162,8 +2186,9 @@ function slugify(name) {
2162
2186
  function getDatabase(path) {
2163
2187
  if (_db)
2164
2188
  return _db;
2189
+ assertNoLegacyStorageMode();
2165
2190
  if (!path && process.env["HASNA_INSTRUCTIONS_API_URL"] && process.env["HASNA_INSTRUCTIONS_API_KEY"]) {
2166
- throw new Error("instructions is in self_hosted (cloud) mode: this command is not wired to the cloud API yet. " + "Unset HASNA_INSTRUCTIONS_API_URL / HASNA_INSTRUCTIONS_API_KEY to use it against the local store.");
2191
+ throw new Error("instructions is using the HTTP API transport (HASNA_INSTRUCTIONS_API_URL set): this command is not wired to the API yet. " + "Unset HASNA_INSTRUCTIONS_API_URL / HASNA_INSTRUCTIONS_API_KEY to use it against the local store.");
2167
2192
  }
2168
2193
  const dbPath = path || getDbPath();
2169
2194
  const db = new Database(dbPath);
@@ -2244,6 +2269,7 @@ function insertFeedback(input, db) {
2244
2269
  }
2245
2270
  var MIGRATIONS, _db = null;
2246
2271
  var init_database = __esm(() => {
2272
+ init_retired_storage_mode();
2247
2273
  MIGRATIONS = [
2248
2274
  `
2249
2275
  CREATE TABLE IF NOT EXISTS configs (
@@ -3037,7 +3063,7 @@ Policy reference: \`${CODEWITH_SHARED_TODOS_STORAGE_POLICY_REFERENCE}\`
3037
3063
  `;
3038
3064
  });
3039
3065
 
3040
- // node_modules/zod/v3/helpers/util.js
3066
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/util.js
3041
3067
  var util, objectUtil, ZodParsedType, getParsedType = (data) => {
3042
3068
  const t = typeof data;
3043
3069
  switch (t) {
@@ -3168,7 +3194,7 @@ var init_util = __esm(() => {
3168
3194
  ]);
3169
3195
  });
3170
3196
 
3171
- // node_modules/zod/v3/ZodError.js
3197
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/ZodError.js
3172
3198
  var ZodIssueCode, quotelessJson = (obj) => {
3173
3199
  const json = JSON.stringify(obj, null, 2);
3174
3200
  return json.replace(/"([^"]+)":/g, "$1:");
@@ -3289,7 +3315,7 @@ var init_ZodError = __esm(() => {
3289
3315
  };
3290
3316
  });
3291
3317
 
3292
- // node_modules/zod/v3/locales/en.js
3318
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/locales/en.js
3293
3319
  var errorMap = (issue, _ctx) => {
3294
3320
  let message;
3295
3321
  switch (issue.code) {
@@ -3396,7 +3422,7 @@ var init_en = __esm(() => {
3396
3422
  en_default = errorMap;
3397
3423
  });
3398
3424
 
3399
- // node_modules/zod/v3/errors.js
3425
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/errors.js
3400
3426
  function setErrorMap(map) {
3401
3427
  overrideErrorMap = map;
3402
3428
  }
@@ -3409,7 +3435,7 @@ var init_errors = __esm(() => {
3409
3435
  overrideErrorMap = en_default;
3410
3436
  });
3411
3437
 
3412
- // node_modules/zod/v3/helpers/parseUtil.js
3438
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js
3413
3439
  function addIssueToContext(ctx, issueData) {
3414
3440
  const overrideMap = getErrorMap();
3415
3441
  const issue = makeIssue({
@@ -3514,10 +3540,10 @@ var init_parseUtil = __esm(() => {
3514
3540
  });
3515
3541
  });
3516
3542
 
3517
- // node_modules/zod/v3/helpers/typeAliases.js
3543
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/typeAliases.js
3518
3544
  var init_typeAliases = () => {};
3519
3545
 
3520
- // node_modules/zod/v3/helpers/errorUtil.js
3546
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/errorUtil.js
3521
3547
  var errorUtil;
3522
3548
  var init_errorUtil = __esm(() => {
3523
3549
  (function(errorUtil2) {
@@ -3526,7 +3552,7 @@ var init_errorUtil = __esm(() => {
3526
3552
  })(errorUtil || (errorUtil = {}));
3527
3553
  });
3528
3554
 
3529
- // node_modules/zod/v3/types.js
3555
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/types.js
3530
3556
  class ParseInputLazyPath {
3531
3557
  constructor(parent, value, path, key) {
3532
3558
  this._cachedPath = [];
@@ -6877,7 +6903,7 @@ var init_types2 = __esm(() => {
6877
6903
  NEVER = INVALID;
6878
6904
  });
6879
6905
 
6880
- // node_modules/zod/v3/external.js
6906
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/external.js
6881
6907
  var exports_external = {};
6882
6908
  __export(exports_external, {
6883
6909
  void: () => voidType,
@@ -6997,7 +7023,7 @@ var init_external = __esm(() => {
6997
7023
  init_ZodError();
6998
7024
  });
6999
7025
 
7000
- // node_modules/zod/index.js
7026
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/index.js
7001
7027
  var init_zod = __esm(() => {
7002
7028
  init_external();
7003
7029
  init_external();
@@ -7189,7 +7215,7 @@ var init_redact = __esm(() => {
7189
7215
  VALUE_PATTERNS = [
7190
7216
  { re: /npm_[A-Za-z0-9]{36,}/, reason: "npm token" },
7191
7217
  { re: /gh[pousr]_[A-Za-z0-9_]{36,}/, reason: "GitHub token" },
7192
- { re: /sk-ant-[A-Za-z0-9\-_]{40,}/, reason: "Anthropic API key" },
7218
+ { re: /sk[-]ant-[A-Za-z0-9\-_]{40,}/, reason: "Anthropic API key" },
7193
7219
  { re: /sk-[A-Za-z0-9]{48,}/, reason: "OpenAI API key" },
7194
7220
  { re: /xoxb-[0-9]+-[A-Za-z0-9\-]+/, reason: "Slack bot token" },
7195
7221
  { re: /AIza[0-9A-Za-z\-_]{35}/, reason: "Google API key" },
@@ -10718,34 +10744,18 @@ function observeCursorGlobalAuthorityPath(authorityPath, readFile2) {
10718
10744
  return {
10719
10745
  ...base,
10720
10746
  fileType,
10721
- status: "managed",
10747
+ status: "unmanaged",
10722
10748
  sha256: contentSha256,
10723
10749
  markers,
10724
10750
  markerSha256,
10725
10751
  provenance: {
10726
10752
  source: "filesystem",
10727
- authority: "managed",
10753
+ authority: "unmanaged",
10728
10754
  observedPath: authorityPath,
10729
- detection: "managed-marker"
10755
+ detection: "unknown-content"
10730
10756
  }
10731
10757
  };
10732
10758
  }
10733
- function isCursorGlobalAuthorityPath(path) {
10734
- return resolve3(path) === resolve3(join5(homeDir(), CURSOR_GLOBAL_AUTHORITY_RELATIVE_PATH));
10735
- }
10736
- function stampCursorGlobalAuthorityMarker(content) {
10737
- if (CURSOR_GLOBAL_AUTHORITY_MARKER_PATTERN.test(content))
10738
- return content;
10739
- const digest = sha2564(content);
10740
- const markerLine = `<!-- ${CURSOR_GLOBAL_AUTHORITY_MANAGED_MARKER} hash=sha256:${digest} -->`;
10741
- const frontmatter = content.match(CURSOR_GLOBAL_AUTHORITY_FRONTMATTER_PATTERN)?.[0];
10742
- if (frontmatter) {
10743
- return `${frontmatter}${markerLine}
10744
- ${content.slice(frontmatter.length)}`;
10745
- }
10746
- return `${markerLine}
10747
- ${content}`;
10748
- }
10749
10759
  function detectCursorAuthorityConflicts(observation = observeCursorGlobalAuthority()) {
10750
10760
  if (observation.status === "absent" || observation.status === "managed")
10751
10761
  return [];
@@ -10767,11 +10777,10 @@ function detectCursorAuthorityConflicts(observation = observeCursorGlobalAuthori
10767
10777
  reason: invalid ? `Cursor fixed global authority ${CURSOR_GLOBAL_AUTHORITY_RELATIVE_PATH} is not a verifiable regular managed file; refusing to render project rules.` : `Cursor fixed global authority ${CURSOR_GLOBAL_AUTHORITY_RELATIVE_PATH} is unmanaged; refusing to guess whether it conflicts with the managed project render.`
10768
10778
  }];
10769
10779
  }
10770
- var CURSOR_GLOBAL_AUTHORITY_RELATIVE_PATH = ".cursor/rules/hasna-global.mdc", CURSOR_GLOBAL_AUTHORITY_MAX_BYTES, CURSOR_GLOBAL_AUTHORITY_MANAGED_MARKER = "Managed by @hasna/configs cursor global authority", CURSOR_GLOBAL_AUTHORITY_MARKER_PATTERN, CURSOR_GLOBAL_AUTHORITY_FRONTMATTER_PATTERN;
10780
+ var CURSOR_GLOBAL_AUTHORITY_RELATIVE_PATH = ".cursor/rules/hasna-global.mdc", CURSOR_GLOBAL_AUTHORITY_MAX_BYTES, CURSOR_GLOBAL_AUTHORITY_MANAGED_MARKER = "Managed by @hasna/configs cursor global authority", CURSOR_GLOBAL_AUTHORITY_MARKER_PATTERN;
10771
10781
  var init_cursor_authority = __esm(() => {
10772
10782
  CURSOR_GLOBAL_AUTHORITY_MAX_BYTES = 256 * 1024;
10773
10783
  CURSOR_GLOBAL_AUTHORITY_MARKER_PATTERN = /^<!-- Managed by @hasna\/configs cursor global authority hash=(sha256:[a-f0-9]{64}) -->$/m;
10774
- CURSOR_GLOBAL_AUTHORITY_FRONTMATTER_PATTERN = /^---\n[\s\S]*?\n---(?:\n|$)/;
10775
10784
  });
10776
10785
 
10777
10786
  // src/lib/session-render.ts
@@ -13354,16 +13363,17 @@ function formatCliError(err, env = process.env) {
13354
13363
  return err instanceof Error ? err.message : String(err);
13355
13364
  }
13356
13365
  function resolveCloudConfig(env = process.env) {
13366
+ assertNoLegacyStorageMode(env);
13357
13367
  const apiUrl = env[API_URL_ENV]?.trim();
13358
13368
  const apiKey = env[API_KEY_ENV]?.trim();
13359
13369
  if (!apiUrl && !apiKey)
13360
13370
  return null;
13361
13371
  if (!apiUrl || !apiKey) {
13362
- throw new Error(`API mode requires BOTH ${API_URL_ENV} and ${API_KEY_ENV}; only ` + `${apiUrl ? API_URL_ENV : API_KEY_ENV} is set. Set both to use the cloud API, ` + `or unset both to use the local store.`);
13372
+ throw new Error(`API mode requires BOTH ${API_URL_ENV} and ${API_KEY_ENV}; only ` + `${apiUrl ? API_URL_ENV : API_KEY_ENV} is set. Set both to use the HTTP API, ` + `or unset both to use the local store.`);
13363
13373
  }
13364
13374
  return { apiUrl, apiKey };
13365
13375
  }
13366
- function isCloudMode(env = process.env) {
13376
+ function isApiTransport(env = process.env) {
13367
13377
  return resolveCloudConfig(env) !== null;
13368
13378
  }
13369
13379
 
@@ -13849,6 +13859,7 @@ var init_config_store = __esm(() => {
13849
13859
  init_types();
13850
13860
  init_bounded_read();
13851
13861
  init_instruction_graph();
13862
+ init_retired_storage_mode();
13852
13863
  CloudHttpError = class CloudHttpError extends Error {
13853
13864
  status;
13854
13865
  body;
@@ -13996,9 +14007,8 @@ async function writeConfigResult(config, targetPath, content, opts, meta = {}) {
13996
14007
  throw new ConfigApplyError(`Antigravity rule file ${renderedTargetPath} is ${renderedContent.length} characters; split it before applying because Antigravity limits rule files to ${ANTIGRAVITY_RULE_FILE_CHAR_LIMIT} characters.`);
13997
14008
  }
13998
14009
  const path = expandPath(renderedTargetPath);
13999
- const renderedForTarget = isCursorGlobalAuthorityPath(path) ? stampCursorGlobalAuthorityMarker(renderedContent) : renderedContent;
14000
14010
  const previousContent = existsSync7(path) ? readFileSync5(path, "utf-8") : null;
14001
- const changed = previousContent !== renderedForTarget;
14011
+ const changed = previousContent !== renderedContent;
14002
14012
  if (!opts.dryRun) {
14003
14013
  const dir = dirname4(path);
14004
14014
  if (!existsSync7(dir)) {
@@ -14008,13 +14018,13 @@ async function writeConfigResult(config, targetPath, content, opts, meta = {}) {
14008
14018
  const store = opts.store ?? resolveConfigStore();
14009
14019
  await store.createSnapshot(config.id, previousContent, config.version);
14010
14020
  }
14011
- writeFileSync2(path, renderedForTarget, "utf-8");
14021
+ writeFileSync2(path, renderedContent, "utf-8");
14012
14022
  }
14013
14023
  return {
14014
14024
  config_id: config.id,
14015
14025
  path,
14016
14026
  previous_content: previousContent,
14017
- new_content: renderedForTarget,
14027
+ new_content: renderedContent,
14018
14028
  dry_run: opts.dryRun ?? false,
14019
14029
  changed,
14020
14030
  primary_changed: changed,
@@ -14375,7 +14385,6 @@ var init_apply = __esm(() => {
14375
14385
  init_redact();
14376
14386
  init_template();
14377
14387
  init_transforms();
14378
- init_cursor_authority();
14379
14388
  });
14380
14389
 
14381
14390
  // src/lib/sync-dir.ts
@@ -15396,15 +15405,16 @@ var init_package_manager_guard = __esm(() => {
15396
15405
  TOKEN_VALUE_PATTERNS = [
15397
15406
  { re: /npm_[A-Za-z0-9]{36,}/, rule: "literal-npm-token", detail: "literal npm token-like value" },
15398
15407
  { re: /gh[pousr]_[A-Za-z0-9_]{36,}/, rule: "literal-github-token", detail: "literal GitHub token-like value" },
15399
- { re: /sk-ant-[A-Za-z0-9\-_]{40,}/, rule: "literal-anthropic-key", detail: "literal Anthropic key-like value" },
15408
+ { re: /sk[-]ant-[A-Za-z0-9\-_]{40,}/, rule: "literal-anthropic-key", detail: "literal Anthropic key-like value" },
15400
15409
  { re: /sk-[A-Za-z0-9]{48,}/, rule: "literal-openai-key", detail: "literal OpenAI key-like value" },
15401
15410
  { re: /AKIA[0-9A-Z]{16}/, rule: "literal-aws-access-key", detail: "literal AWS access-key-like value" },
15402
15411
  { re: /xoxb-[0-9]+-[A-Za-z0-9-]+/, rule: "literal-slack-token", detail: "literal Slack token-like value" }
15403
15412
  ];
15404
15413
  });
15405
15414
 
15406
- // node_modules/@hasna/events/dist/commander.js
15415
+ // ../../node_modules/.bun/@hasna+events@0.1.15/node_modules/@hasna/events/dist/commander.js
15407
15416
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
15417
+ import { Buffer as Buffer2 } from "buffer";
15408
15418
  import { existsSync } from "fs";
15409
15419
  import { homedir } from "os";
15410
15420
  import { join } from "path";
@@ -15420,29 +15430,83 @@ function getPathValue(input, path) {
15420
15430
  return;
15421
15431
  }, input);
15422
15432
  }
15423
- function wildcardToRegExp(pattern) {
15424
- const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
15425
- return new RegExp(`^${escaped}$`);
15433
+ function getFieldValues(input, path) {
15434
+ const values = [];
15435
+ const push = (value) => {
15436
+ if (!values.some((item) => Object.is(item, value)))
15437
+ values.push(value);
15438
+ };
15439
+ if (path.includes(".") && path in input)
15440
+ push(input[path]);
15441
+ const nestedValue = getPathValue(input, path);
15442
+ if (nestedValue !== undefined || !path.includes("."))
15443
+ push(nestedValue);
15444
+ return values;
15445
+ }
15446
+ function wildcardToRegExp(pattern, options = {}) {
15447
+ let body = "";
15448
+ for (let index = 0;index < pattern.length; index += 1) {
15449
+ const char = pattern[index];
15450
+ if (char === "*") {
15451
+ if (pattern[index + 1] === "*") {
15452
+ body += ".*";
15453
+ index += 1;
15454
+ } else {
15455
+ body += options.segmentSafe ? "[^/]*" : ".*";
15456
+ }
15457
+ } else {
15458
+ body += char.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
15459
+ }
15460
+ }
15461
+ return new RegExp(`^${body}$`);
15426
15462
  }
15427
- function matchString(value, matcher) {
15463
+ function matchString(value, matcher, options = {}) {
15428
15464
  if (matcher === undefined)
15429
15465
  return true;
15430
15466
  if (value === undefined)
15431
15467
  return false;
15432
15468
  const matchers = Array.isArray(matcher) ? matcher : [matcher];
15433
- return matchers.some((item) => wildcardToRegExp(item).test(value));
15469
+ return matchers.some((item) => wildcardToRegExp(item, options).test(value));
15434
15470
  }
15435
15471
  function matchRecord(input, matcher) {
15436
15472
  if (!matcher)
15437
15473
  return true;
15438
15474
  return Object.entries(matcher).every(([path, expected]) => {
15439
- const actual = getPathValue(input, path);
15440
- if (typeof expected === "string" || Array.isArray(expected)) {
15441
- return matchString(actual === undefined ? undefined : String(actual), expected);
15442
- }
15443
- return actual === expected;
15475
+ const actualValues = getFieldValues(input, path);
15476
+ return matchField(actualValues, expected, path);
15444
15477
  });
15445
15478
  }
15479
+ function matchField(actualValues, expected, path) {
15480
+ if (isNegativeMatcher(expected)) {
15481
+ return !actualValues.some((actual) => matchPositiveField(actual, expected.not, path));
15482
+ }
15483
+ return actualValues.some((actual) => matchPositiveField(actual, expected, path));
15484
+ }
15485
+ function matchPositiveField(actual, expected, path) {
15486
+ if (typeof expected === "string" || Array.isArray(expected)) {
15487
+ return stringCandidates(actual).some((candidate) => matchString(candidate, expected, {
15488
+ segmentSafe: path.endsWith("_path") || path.endsWith(".path")
15489
+ }));
15490
+ }
15491
+ if (Array.isArray(actual)) {
15492
+ return actual.some((item) => item === expected);
15493
+ }
15494
+ return actual === expected;
15495
+ }
15496
+ function stringCandidates(actual) {
15497
+ if (actual === undefined)
15498
+ return [];
15499
+ if (Array.isArray(actual)) {
15500
+ return actual.flatMap((item) => isPrimitiveFieldValue(item) ? [String(item)] : []);
15501
+ }
15502
+ return [String(actual)];
15503
+ }
15504
+ function isPrimitiveFieldValue(value) {
15505
+ return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
15506
+ }
15507
+ function isNegativeMatcher(value) {
15508
+ return Boolean(value && typeof value === "object" && !Array.isArray(value) && "not" in value);
15509
+ }
15446
15510
  function eventMatchesFilter(event, filter) {
15447
15511
  return matchString(event.source, filter.source) && matchString(event.type, filter.type) && matchString(event.subject, filter.subject) && matchString(event.severity, filter.severity) && matchRecord(event.data, filter.data) && matchRecord(event.metadata, filter.metadata);
15448
15512
  }
@@ -15455,16 +15519,29 @@ function channelMatchesEvent(channel, event) {
15455
15519
  }
15456
15520
  var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
15457
15521
  var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
15522
+ var LOCAL_JSON_EVENT_CURSOR_PREFIX = "local-json-v1:";
15523
+ var DEFAULT_EVENT_PAGE_LIMIT = 100;
15524
+ var MAX_EVENT_PAGE_LIMIT = 1000;
15458
15525
  function getEventsDataDir(override) {
15459
15526
  return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join(homedir(), ".hasna", "events");
15460
15527
  }
15528
+ function getActiveEventsDirEnv() {
15529
+ if (process.env[HASNA_EVENTS_DIR_ENV])
15530
+ return HASNA_EVENTS_DIR_ENV;
15531
+ if (process.env[HASNA_EVENTS_HOME_ENV])
15532
+ return HASNA_EVENTS_HOME_ENV;
15533
+ return null;
15534
+ }
15535
+
15461
15536
  class JsonEventsStore {
15462
15537
  dataDir;
15538
+ runtime;
15463
15539
  channelsPath;
15464
15540
  eventsPath;
15465
15541
  deliveriesPath;
15466
15542
  constructor(dataDir = getEventsDataDir()) {
15467
15543
  this.dataDir = dataDir;
15544
+ this.runtime = localJsonRuntime(dataDir);
15468
15545
  this.channelsPath = join(dataDir, "channels.json");
15469
15546
  this.eventsPath = join(dataDir, "events.json");
15470
15547
  this.deliveriesPath = join(dataDir, "deliveries.json");
@@ -15512,13 +15589,58 @@ class JsonEventsStore {
15512
15589
  await this.writeJson(this.eventsPath, events);
15513
15590
  return event;
15514
15591
  }
15515
- async listEvents() {
15592
+ async appendEventOnce(event, options = {}) {
15593
+ await this.init();
15594
+ const events = await this.readJson(this.eventsPath, []);
15595
+ const dedupe = options.dedupe !== false;
15596
+ if (dedupe) {
15597
+ const existing = findEventByIdentity(events, { id: event.id, dedupeKey: event.dedupeKey });
15598
+ if (existing) {
15599
+ return {
15600
+ event: existing,
15601
+ stored: false,
15602
+ deduped: true,
15603
+ identity: { id: existing.id, dedupeKey: existing.dedupeKey }
15604
+ };
15605
+ }
15606
+ }
15607
+ events.push(event);
15608
+ await this.writeJson(this.eventsPath, events);
15609
+ return {
15610
+ event,
15611
+ stored: true,
15612
+ deduped: false,
15613
+ identity: { id: event.id, dedupeKey: event.dedupeKey }
15614
+ };
15615
+ }
15616
+ async listEvents(options = {}) {
15516
15617
  await this.init();
15517
- return this.readJson(this.eventsPath, []);
15618
+ const events = await this.readJson(this.eventsPath, []);
15619
+ return queryEvents(events, options);
15620
+ }
15621
+ async listEventsPage(options = {}) {
15622
+ await this.init();
15623
+ const events = await this.readJson(this.eventsPath, []);
15624
+ const queried = queryEvents(events, {
15625
+ eventId: options.eventId,
15626
+ source: options.source,
15627
+ type: options.type
15628
+ });
15629
+ const offset = decodeLocalJsonEventCursor(options.cursor, options);
15630
+ const limit = normalizeEventPageLimit(options.limit);
15631
+ const pageEvents = queried.slice(offset, offset + limit);
15632
+ const nextOffset = offset + pageEvents.length;
15633
+ const hasMore = nextOffset < queried.length;
15634
+ return {
15635
+ events: pageEvents,
15636
+ cursor: options.cursor,
15637
+ nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
15638
+ hasMore
15639
+ };
15518
15640
  }
15519
15641
  async findEventByIdentity(identity) {
15520
15642
  const events = await this.listEvents();
15521
- return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
15643
+ return findEventByIdentity(events, identity);
15522
15644
  }
15523
15645
  async appendDelivery(result) {
15524
15646
  await this.init();
@@ -15569,6 +15691,130 @@ class JsonEventsStore {
15569
15691
  });
15570
15692
  }
15571
15693
  }
15694
+ function localJsonRuntime(dataDir = getEventsDataDir()) {
15695
+ return {
15696
+ mode: "local-files",
15697
+ name: "json-events-store",
15698
+ remote: false,
15699
+ localFiles: true,
15700
+ localSqlite: false,
15701
+ postgres: false,
15702
+ s3: false,
15703
+ aws: false,
15704
+ durable: true,
15705
+ idempotency: "best-effort-local",
15706
+ replayCursors: true,
15707
+ description: `Local JSON files in ${dataDir}; no SQLite, Postgres, S3, or AWS runtime is configured by this store.`
15708
+ };
15709
+ }
15710
+ function encodeLocalJsonEventCursor(offset, options = {}) {
15711
+ if (!Number.isInteger(offset) || offset < 0)
15712
+ throw new Error(`Invalid event cursor offset: ${offset}`);
15713
+ const payload = {
15714
+ offset,
15715
+ eventId: options.eventId,
15716
+ source: options.source,
15717
+ type: options.type
15718
+ };
15719
+ return `${LOCAL_JSON_EVENT_CURSOR_PREFIX}${Buffer2.from(JSON.stringify(payload), "utf-8").toString("base64url")}`;
15720
+ }
15721
+ function decodeLocalJsonEventCursor(cursor, options = {}) {
15722
+ if (!cursor)
15723
+ return 0;
15724
+ if (!cursor.startsWith(LOCAL_JSON_EVENT_CURSOR_PREFIX))
15725
+ throw new Error(`Invalid local JSON event cursor: ${cursor}`);
15726
+ const rawPayload = cursor.slice(LOCAL_JSON_EVENT_CURSOR_PREFIX.length);
15727
+ let payload;
15728
+ try {
15729
+ payload = JSON.parse(Buffer2.from(rawPayload, "base64url").toString("utf-8"));
15730
+ } catch {
15731
+ throw new Error(`Invalid local JSON event cursor: ${cursor}`);
15732
+ }
15733
+ const offset = payload.offset;
15734
+ if (!Number.isInteger(offset) || offset < 0)
15735
+ throw new Error(`Invalid local JSON event cursor: ${cursor}`);
15736
+ assertCursorFilter("eventId", payload.eventId, options.eventId);
15737
+ assertCursorFilter("source", payload.source, options.source);
15738
+ assertCursorFilter("type", payload.type, options.type);
15739
+ return offset;
15740
+ }
15741
+ function normalizeEventPageLimit(limit) {
15742
+ if (limit === undefined)
15743
+ return DEFAULT_EVENT_PAGE_LIMIT;
15744
+ if (!Number.isInteger(limit) || limit < 1)
15745
+ throw new Error(`Event page limit must be a positive integer, got ${limit}`);
15746
+ return Math.min(limit, MAX_EVENT_PAGE_LIMIT);
15747
+ }
15748
+ function queryEvents(events, options) {
15749
+ let rows = events;
15750
+ if (options.eventId)
15751
+ rows = rows.filter((event) => event.id === options.eventId);
15752
+ if (options.source)
15753
+ rows = rows.filter((event) => event.source === options.source);
15754
+ if (options.type)
15755
+ rows = rows.filter((event) => event.type === options.type);
15756
+ if (options.cursor) {
15757
+ const offset = decodeLocalJsonEventCursor(options.cursor, options);
15758
+ rows = rows.slice(offset);
15759
+ }
15760
+ if (options.limit !== undefined)
15761
+ rows = rows.slice(0, normalizeEventPageLimit(options.limit));
15762
+ return rows;
15763
+ }
15764
+ function assertCursorFilter(name, cursorValue, optionValue) {
15765
+ if (cursorValue !== optionValue)
15766
+ throw new Error(`Local JSON event cursor ${name} filter mismatch`);
15767
+ }
15768
+ function findEventByIdentity(events, identity) {
15769
+ return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
15770
+ }
15771
+ async function getEventsStatus(dataDir) {
15772
+ const store = new JsonEventsStore(dataDir);
15773
+ await store.init();
15774
+ const [channels, events, deliveries] = await Promise.all([
15775
+ store.listChannels(),
15776
+ store.listEvents(),
15777
+ store.listDeliveries()
15778
+ ]);
15779
+ const transports = channels.reduce((counts, channel) => {
15780
+ counts[channel.transport] = (counts[channel.transport] ?? 0) + 1;
15781
+ return counts;
15782
+ }, {});
15783
+ return {
15784
+ service: "events",
15785
+ schemaVersion: "1.0",
15786
+ dataDir: store.dataDir,
15787
+ storage: store.runtime,
15788
+ env: {
15789
+ primary: HASNA_EVENTS_DIR_ENV,
15790
+ fallback: HASNA_EVENTS_HOME_ENV,
15791
+ active: getActiveEventsDirEnv()
15792
+ },
15793
+ files: {
15794
+ channels: statusFile(store.dataDir, "channels.json", channels.length),
15795
+ events: statusFile(store.dataDir, "events.json", events.length),
15796
+ deliveries: statusFile(store.dataDir, "deliveries.json", deliveries.length)
15797
+ },
15798
+ counts: {
15799
+ channels: channels.length,
15800
+ enabledChannels: channels.filter((channel) => channel.enabled).length,
15801
+ disabledChannels: channels.filter((channel) => !channel.enabled).length,
15802
+ events: events.length,
15803
+ deliveries: deliveries.length
15804
+ },
15805
+ transports,
15806
+ safety: {
15807
+ includesEventPayloads: false,
15808
+ includesWebhookSecrets: false,
15809
+ listOutputsRedactSecrets: true,
15810
+ statusOutputIsMetadataOnly: true
15811
+ }
15812
+ };
15813
+ }
15814
+ function statusFile(dataDir, fileName, records) {
15815
+ const path = join(dataDir, fileName);
15816
+ return { path, exists: existsSync(path), records };
15817
+ }
15572
15818
  var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
15573
15819
  function buildSignatureBase(timestamp, body) {
15574
15820
  return `${timestamp}.${body}`;
@@ -15583,21 +15829,27 @@ function now() {
15583
15829
  function truncate(value, max = 4096) {
15584
15830
  return value.length > max ? `${value.slice(0, max)}...` : value;
15585
15831
  }
15586
- function buildWebhookRequest(event, channel) {
15832
+ function buildWebhookRequest(event, channel, options = {}) {
15587
15833
  if (!channel.webhook)
15588
15834
  throw new Error(`Channel ${channel.id} has no webhook config`);
15835
+ for (const name of Object.keys(channel.webhook.headers ?? {})) {
15836
+ if (/^x-hasna-/i.test(name)) {
15837
+ throw new Error(`Webhook header ${name} is reserved for signed delivery metadata`);
15838
+ }
15839
+ }
15589
15840
  const body = JSON.stringify(event);
15590
- const timestamp = event.time;
15841
+ const timestamp = options.timestamp ?? new Date().toISOString();
15591
15842
  const headers = {
15592
15843
  "Content-Type": "application/json",
15593
15844
  "User-Agent": "@hasna/events",
15594
15845
  "X-Hasna-Event-Id": event.id,
15595
15846
  "X-Hasna-Event-Type": event.type,
15596
- "X-Hasna-Timestamp": timestamp,
15597
- ...channel.webhook.headers
15847
+ ...channel.webhook.headers,
15848
+ "X-Hasna-Timestamp": timestamp
15598
15849
  };
15599
- if (channel.webhook.secret) {
15600
- headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp, body);
15850
+ const secret = options.secret ?? channel.webhook.secret;
15851
+ if (secret) {
15852
+ headers["X-Hasna-Signature"] = signPayload(secret, timestamp, body);
15601
15853
  }
15602
15854
  return { body, headers };
15603
15855
  }
@@ -15605,7 +15857,21 @@ async function dispatchWebhook(event, channel, options = {}) {
15605
15857
  if (!channel.webhook)
15606
15858
  throw new Error(`Channel ${channel.id} has no webhook config`);
15607
15859
  const startedAt = now();
15608
- const { body, headers } = buildWebhookRequest(event, channel);
15860
+ let secret = channel.webhook.secret;
15861
+ if (channel.webhook.secretRef) {
15862
+ if (!options.secretResolver) {
15863
+ return failedAttempt(startedAt, "Webhook secret reference has no runtime resolver");
15864
+ }
15865
+ try {
15866
+ secret = await options.secretResolver(channel.webhook.secretRef);
15867
+ } catch {
15868
+ return failedAttempt(startedAt, "Webhook secret reference could not be resolved");
15869
+ }
15870
+ if (!secret)
15871
+ return failedAttempt(startedAt, "Webhook secret reference could not be resolved");
15872
+ }
15873
+ const timestamp = (options.now?.() ?? new Date).toISOString();
15874
+ const { body, headers } = buildWebhookRequest(event, channel, { secret, timestamp });
15609
15875
  const controller = new AbortController;
15610
15876
  const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
15611
15877
  try {
@@ -15637,6 +15903,15 @@ async function dispatchWebhook(event, channel, options = {}) {
15637
15903
  clearTimeout(timeout);
15638
15904
  }
15639
15905
  }
15906
+ function failedAttempt(startedAt, error) {
15907
+ return {
15908
+ attempt: 1,
15909
+ status: "failed",
15910
+ startedAt,
15911
+ completedAt: now(),
15912
+ error
15913
+ };
15914
+ }
15640
15915
  async function dispatchCommand(event, channel) {
15641
15916
  if (!channel.command)
15642
15917
  throw new Error(`Channel ${channel.id} has no command config`);
@@ -15725,6 +16000,90 @@ function createDeliveryResult(event, channel, attempts) {
15725
16000
  completedAt: attempts.at(-1)?.completedAt ?? now()
15726
16001
  };
15727
16002
  }
16003
+
16004
+ class EventValidationError extends Error {
16005
+ eventType;
16006
+ issues;
16007
+ constructor(eventType, issues) {
16008
+ const detail = issues.map((issue) => `${issue.path || "<root>"}: ${issue.message}`).join("; ");
16009
+ super(`Event validation failed for type "${eventType}": ${detail}`);
16010
+ this.name = "EventValidationError";
16011
+ this.eventType = eventType;
16012
+ this.issues = issues;
16013
+ }
16014
+ }
16015
+
16016
+ class EventTypeCatalog {
16017
+ definitions = new Map;
16018
+ register(definition) {
16019
+ this.definitions.set(definition.type, definition);
16020
+ return this;
16021
+ }
16022
+ unregister(type) {
16023
+ return this.definitions.delete(type);
16024
+ }
16025
+ has(type) {
16026
+ return this.definitions.has(type);
16027
+ }
16028
+ get(type) {
16029
+ return this.definitions.get(type);
16030
+ }
16031
+ list() {
16032
+ return [...this.definitions.values()];
16033
+ }
16034
+ validateEvent(event) {
16035
+ const definition = this.definitions.get(event.type);
16036
+ if (!definition)
16037
+ return { ok: true };
16038
+ return definition.validate(event.data, event);
16039
+ }
16040
+ assertEventValid(event) {
16041
+ const result = this.validateEvent(event);
16042
+ if (!result.ok) {
16043
+ throw new EventValidationError(event.type, result.issues);
16044
+ }
16045
+ }
16046
+ }
16047
+ var defaultEventTypeCatalog = new EventTypeCatalog;
16048
+ var APP_EVENT_V1_MAX_DATA_BYTES = 32 * 1024;
16049
+ function redactPaths(event, paths, replacement = "[REDACTED]") {
16050
+ if (paths.length === 0)
16051
+ return event;
16052
+ const copy = structuredClone(event);
16053
+ for (const path of paths) {
16054
+ setPath(copy, path, replacement);
16055
+ }
16056
+ return copy;
16057
+ }
16058
+ function redactSensitiveKeys(event, replacement = "[REDACTED]") {
16059
+ return redactValue(event, replacement);
16060
+ }
16061
+ function shouldRedactKey(key) {
16062
+ return /secret|token|password|api[_-]?key|authorization/i.test(key);
16063
+ }
16064
+ function redactValue(value, replacement) {
16065
+ if (Array.isArray(value))
16066
+ return value.map((item) => redactValue(item, replacement));
16067
+ if (!value || typeof value !== "object")
16068
+ return value;
16069
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
16070
+ key,
16071
+ shouldRedactKey(key) ? replacement : redactValue(item, replacement)
16072
+ ]));
16073
+ }
16074
+ function setPath(input, path, replacement) {
16075
+ const parts = path.split(".");
16076
+ let cursor = input;
16077
+ for (const part of parts.slice(0, -1)) {
16078
+ const next = cursor[part];
16079
+ if (!next || typeof next !== "object")
16080
+ return;
16081
+ cursor = next;
16082
+ }
16083
+ const last = parts.at(-1);
16084
+ if (last && last in cursor)
16085
+ cursor[last] = replacement;
16086
+ }
15728
16087
  function createEvent(input) {
15729
16088
  return {
15730
16089
  id: input.id ?? randomUUID2(),
@@ -15745,10 +16104,18 @@ class EventsClient {
15745
16104
  store;
15746
16105
  redactors;
15747
16106
  transportOptions;
16107
+ catalog;
16108
+ validateCatalogTypes;
15748
16109
  constructor(options = {}) {
15749
16110
  this.store = options.store ?? new JsonEventsStore(options.dataDir);
15750
16111
  this.redactors = options.redactors ?? [];
15751
- this.transportOptions = { fetchImpl: options.fetchImpl };
16112
+ this.transportOptions = {
16113
+ fetchImpl: options.fetchImpl,
16114
+ secretResolver: options.secretResolver,
16115
+ now: options.now
16116
+ };
16117
+ this.catalog = options.catalog ?? defaultEventTypeCatalog;
16118
+ this.validateCatalogTypes = options.validateCatalogTypes ?? false;
15752
16119
  }
15753
16120
  async addChannel(input) {
15754
16121
  const timestamp = new Date().toISOString();
@@ -15766,18 +16133,40 @@ class EventsClient {
15766
16133
  }
15767
16134
  async emit(input, options = {}) {
15768
16135
  const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
15769
- if (options.dedupe !== false) {
15770
- const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
15771
- if (existing) {
15772
- return { event: existing, deliveries: [], deduped: true };
15773
- }
15774
- }
15775
- await this.store.appendEvent(event);
15776
- const deliveries = options.deliver === false ? [] : await this.deliver(event);
15777
- return { event, deliveries, deduped: false };
15778
- }
15779
- async listEvents() {
15780
- return this.store.listEvents();
16136
+ if (options.validate ?? this.validateCatalogTypes) {
16137
+ this.catalog.assertEventValid(event);
16138
+ }
16139
+ const append = await this.appendEvent(event, { dedupe: options.dedupe !== false });
16140
+ if (append.deduped) {
16141
+ return { event: append.event, deliveries: [], deduped: true };
16142
+ }
16143
+ const deliveries = options.deliver === false ? [] : await this.deliver(append.event);
16144
+ return { event: append.event, deliveries, deduped: false };
16145
+ }
16146
+ async listEvents(options = {}) {
16147
+ if (Object.keys(options).length === 0)
16148
+ return this.store.listEvents();
16149
+ return queryClientEvents(await this.store.listEvents(), options);
16150
+ }
16151
+ async listEventsPage(options = {}) {
16152
+ if (this.store.listEventsPage)
16153
+ return this.store.listEventsPage(options);
16154
+ const events = queryClientEvents(await this.store.listEvents(), {
16155
+ eventId: options.eventId,
16156
+ source: options.source,
16157
+ type: options.type
16158
+ });
16159
+ const offset = decodeLocalJsonEventCursor(options.cursor, options);
16160
+ const limit = normalizeEventPageLimit(options.limit);
16161
+ const pageEvents = events.slice(offset, offset + limit);
16162
+ const nextOffset = offset + pageEvents.length;
16163
+ const hasMore = nextOffset < events.length;
16164
+ return {
16165
+ events: pageEvents,
16166
+ cursor: options.cursor,
16167
+ nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
16168
+ hasMore
16169
+ };
15781
16170
  }
15782
16171
  async listDeliveries() {
15783
16172
  return this.store.listDeliveries();
@@ -15794,7 +16183,7 @@ class EventsClient {
15794
16183
  }
15795
16184
  return deliveries;
15796
16185
  }
15797
- async testChannel(id, input = {}) {
16186
+ async matchChannel(id, input = {}) {
15798
16187
  const channel = await this.store.getChannel(id);
15799
16188
  if (!channel)
15800
16189
  throw new Error(`Channel not found: ${id}`);
@@ -15811,28 +16200,71 @@ class EventsClient {
15811
16200
  time: input.time,
15812
16201
  id: input.id
15813
16202
  });
16203
+ const matched = channelMatchesEvent(channel, event);
16204
+ return {
16205
+ channelId: channel.id,
16206
+ matched,
16207
+ event,
16208
+ filters: channel.filters,
16209
+ reason: matched ? undefined : channel.enabled ? "event did not match channel filters" : "channel is disabled"
16210
+ };
16211
+ }
16212
+ async testChannel(id, input = {}, options = {}) {
16213
+ const channel = await this.store.getChannel(id);
16214
+ if (!channel)
16215
+ throw new Error(`Channel not found: ${id}`);
16216
+ const match = await this.matchChannel(id, input);
16217
+ const event = match.event;
16218
+ if (options.honorFilters && !match.matched) {
16219
+ const timestamp = new Date().toISOString();
16220
+ const result2 = createDeliveryResult(event, channel, [{
16221
+ attempt: 1,
16222
+ status: "skipped",
16223
+ startedAt: timestamp,
16224
+ completedAt: timestamp,
16225
+ error: match.reason
16226
+ }]);
16227
+ result2.metadata = { reason: "filter_mismatch" };
16228
+ await this.store.appendDelivery(result2);
16229
+ return result2;
16230
+ }
15814
16231
  const eventForChannel = await this.applyRedaction(event, channel);
15815
16232
  const result = await this.deliverWithRetry(eventForChannel, channel);
15816
16233
  await this.store.appendDelivery(result);
15817
16234
  return result;
15818
16235
  }
15819
16236
  async replay(options = {}) {
15820
- const events = (await this.store.listEvents()).filter((event) => {
15821
- if (options.eventId && event.id !== options.eventId)
15822
- return false;
15823
- if (options.source && event.source !== options.source)
15824
- return false;
15825
- if (options.type && event.type !== options.type)
15826
- return false;
15827
- return true;
15828
- });
16237
+ const page = options.cursor || options.limit !== undefined ? await this.listEventsPage(options) : { events: await this.listEvents(options), hasMore: false };
15829
16238
  if (options.dryRun)
15830
- return { events, deliveries: [] };
16239
+ return { events: page.events, deliveries: [], cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
15831
16240
  const deliveries = [];
15832
- for (const event of events) {
16241
+ for (const event of page.events) {
15833
16242
  deliveries.push(...await this.deliver(event));
15834
16243
  }
15835
- return { events, deliveries };
16244
+ return { events: page.events, deliveries, cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
16245
+ }
16246
+ async appendEvent(event, options) {
16247
+ if (this.store.appendEventOnce) {
16248
+ return this.store.appendEventOnce(event, { dedupe: options.dedupe });
16249
+ }
16250
+ if (options.dedupe) {
16251
+ const existing = await this.store.findEventByIdentity({ id: event.id, dedupeKey: event.dedupeKey });
16252
+ if (existing) {
16253
+ return {
16254
+ event: existing,
16255
+ stored: false,
16256
+ deduped: true,
16257
+ identity: { id: existing.id, dedupeKey: existing.dedupeKey }
16258
+ };
16259
+ }
16260
+ }
16261
+ const stored = await this.store.appendEvent(event);
16262
+ return {
16263
+ event: stored,
16264
+ stored: true,
16265
+ deduped: false,
16266
+ identity: { id: stored.id, dedupeKey: stored.dedupeKey }
16267
+ };
15836
16268
  }
15837
16269
  async applyRedaction(event, channel) {
15838
16270
  let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
@@ -15859,15 +16291,6 @@ class EventsClient {
15859
16291
  return createDeliveryResult(event, channel, attempts);
15860
16292
  }
15861
16293
  }
15862
- function redactPaths(event, paths, replacement = "[REDACTED]") {
15863
- if (paths.length === 0)
15864
- return event;
15865
- const copy = structuredClone(event);
15866
- for (const path of paths) {
15867
- setPath(copy, path, replacement);
15868
- }
15869
- return copy;
15870
- }
15871
16294
  function sanitizeChannelForOutput(channel) {
15872
16295
  const copy = structuredClone(channel);
15873
16296
  if (copy.webhook?.secret)
@@ -15880,34 +16303,19 @@ function sanitizeChannelForOutput(channel) {
15880
16303
  function sanitizeChannelsForOutput(channels) {
15881
16304
  return channels.map(sanitizeChannelForOutput);
15882
16305
  }
15883
- function redactSensitiveKeys(event, replacement = "[REDACTED]") {
15884
- return redactValue(event, replacement);
15885
- }
15886
- function shouldRedactKey(key) {
15887
- return /secret|token|password|api[_-]?key|authorization/i.test(key);
15888
- }
15889
- function redactValue(value, replacement) {
15890
- if (Array.isArray(value))
15891
- return value.map((item) => redactValue(item, replacement));
15892
- if (!value || typeof value !== "object")
15893
- return value;
15894
- return Object.fromEntries(Object.entries(value).map(([key, item]) => [
15895
- key,
15896
- shouldRedactKey(key) ? replacement : redactValue(item, replacement)
15897
- ]));
15898
- }
15899
- function setPath(input, path, replacement) {
15900
- const parts = path.split(".");
15901
- let cursor = input;
15902
- for (const part of parts.slice(0, -1)) {
15903
- const next = cursor[part];
15904
- if (!next || typeof next !== "object")
15905
- return;
15906
- cursor = next;
15907
- }
15908
- const last = parts.at(-1);
15909
- if (last && last in cursor)
15910
- cursor[last] = replacement;
16306
+ function queryClientEvents(events, options) {
16307
+ let rows = events;
16308
+ if (options.eventId)
16309
+ rows = rows.filter((event) => event.id === options.eventId);
16310
+ if (options.source)
16311
+ rows = rows.filter((event) => event.source === options.source);
16312
+ if (options.type)
16313
+ rows = rows.filter((event) => event.type === options.type);
16314
+ if (options.cursor)
16315
+ rows = rows.slice(decodeLocalJsonEventCursor(options.cursor, options));
16316
+ if (options.limit !== undefined)
16317
+ rows = rows.slice(0, normalizeEventPageLimit(options.limit));
16318
+ return rows;
15911
16319
  }
15912
16320
  function normalizeTime(value) {
15913
16321
  if (!value)
@@ -15921,6 +16329,77 @@ function normalizeRetryPolicy(policy) {
15921
16329
  multiplier: Math.max(1, policy?.multiplier ?? 2)
15922
16330
  };
15923
16331
  }
16332
+ function parseFieldMatchers(values, label, typed = false) {
16333
+ if (!values?.length)
16334
+ return;
16335
+ const result = {};
16336
+ for (const value of values) {
16337
+ const parsed = parseMatcherExpression(value, label);
16338
+ const path = parsed.path;
16339
+ if (path in result)
16340
+ throw new Error(`Duplicate ${label} filter path: ${path}`);
16341
+ const matcherValue = typed ? parseTypedMatcherValue(parsed.rawValue, label) : parsed.rawValue;
16342
+ result[path] = parsed.negated ? { not: matcherValue } : matcherValue;
16343
+ }
16344
+ return result;
16345
+ }
16346
+ function parseFilterOptions(options) {
16347
+ const filter2 = {};
16348
+ if (options.source)
16349
+ filter2.source = options.source;
16350
+ if (options.type)
16351
+ filter2.type = options.type;
16352
+ if (options.subject)
16353
+ filter2.subject = options.subject;
16354
+ if (options.severity)
16355
+ filter2.severity = options.severity;
16356
+ const data = mergeMatchers(parseFieldMatchers(options.data, "data"), parseFieldMatchers(options.dataJson, "data-json", true));
16357
+ const metadata = mergeMatchers(parseFieldMatchers(options.metadata, "metadata"), parseFieldMatchers(options.metadataJson, "metadata-json", true));
16358
+ if (Object.keys(data).length > 0)
16359
+ filter2.data = data;
16360
+ if (Object.keys(metadata).length > 0)
16361
+ filter2.metadata = metadata;
16362
+ return Object.keys(filter2).length > 0 ? [filter2] : undefined;
16363
+ }
16364
+ function mergeMatchers(...records) {
16365
+ const result = {};
16366
+ for (const record of records) {
16367
+ if (!record)
16368
+ continue;
16369
+ for (const [path, value] of Object.entries(record)) {
16370
+ if (path in result)
16371
+ throw new Error(`Duplicate filter path: ${path}`);
16372
+ result[path] = value;
16373
+ }
16374
+ }
16375
+ return result;
16376
+ }
16377
+ function parseTypedMatcherValue(value, label) {
16378
+ const parsed = JSON.parse(value);
16379
+ if (parsed === null || typeof parsed === "string" || typeof parsed === "number" || typeof parsed === "boolean" || Array.isArray(parsed) && parsed.every((item) => typeof item === "string")) {
16380
+ return parsed;
16381
+ }
16382
+ throw new Error(`${label} filter JSON values must be string, string[], number, boolean, or null`);
16383
+ }
16384
+ function parseMatcherExpression(value, label) {
16385
+ const negativeSeparator = value.indexOf("!=");
16386
+ if (negativeSeparator > 0) {
16387
+ return {
16388
+ path: value.slice(0, negativeSeparator),
16389
+ rawValue: value.slice(negativeSeparator + 2),
16390
+ negated: true
16391
+ };
16392
+ }
16393
+ const separator = value.indexOf("=");
16394
+ if (separator <= 0)
16395
+ throw new Error(`Invalid ${label} filter, expected path=value or path!=value: ${value}`);
16396
+ return {
16397
+ path: value.slice(0, separator),
16398
+ rawValue: value.slice(separator + 1),
16399
+ negated: false
16400
+ };
16401
+ }
16402
+ var DEFAULT_EVENT_LIST_LIMIT = 100;
15924
16403
  function parseJsonObject(value, fallback) {
15925
16404
  if (!value)
15926
16405
  return fallback;
@@ -15942,18 +16421,6 @@ function parseHeaders(values) {
15942
16421
  }
15943
16422
  return headers;
15944
16423
  }
15945
- function parseFilter(options) {
15946
- const filter2 = {};
15947
- if (options.source)
15948
- filter2.source = options.source;
15949
- if (options.type)
15950
- filter2.type = options.type;
15951
- if (options.subject)
15952
- filter2.subject = options.subject;
15953
- if (options.severity)
15954
- filter2.severity = options.severity;
15955
- return Object.keys(filter2).length > 0 ? [filter2] : undefined;
15956
- }
15957
16424
  function createClient(options) {
15958
16425
  if (options.createClient)
15959
16426
  return options.createClient();
@@ -15965,22 +16432,30 @@ function print(value, json, text) {
15965
16432
  else
15966
16433
  console.log(text);
15967
16434
  }
16435
+ function fail(error, json) {
16436
+ const message = error instanceof Error ? error.message : String(error);
16437
+ if (json)
16438
+ console.log(JSON.stringify({ error: message }, null, 2));
16439
+ else
16440
+ console.error(message);
16441
+ process.exitCode = 1;
16442
+ }
15968
16443
  function hasJsonOption(options) {
15969
16444
  return Boolean(options?.json || options?.opts?.().json || options?.optsWithGlobals?.().json || options?.parent?.opts?.().json || options?.parent?.optsWithGlobals?.().json);
15970
16445
  }
15971
16446
  function wantsJson(actionOptions, command) {
15972
16447
  return hasJsonOption(actionOptions) || hasJsonOption(command);
15973
16448
  }
15974
- function registerWebhookCommands(program, options) {
15975
- const webhooks = program.command(options.webhooksCommandName ?? "webhooks").description("Manage Hasna event webhook subscriptions");
15976
- webhooks.command("add").description("Add or replace a webhook or command subscription").argument("<target>", "Webhook URL or command binary").requiredOption("--id <id>", "Subscription/channel identifier").option("--transport <kind>", "Transport kind: webhook or command", "webhook").option("--name <name>", "Display name").option("--type <pattern>", "Event type filter, e.g. todos.task.*").option("--source <pattern>", "Event source filter").option("--subject <pattern>", "Event subject filter").option("--severity <pattern>", "Event severity filter").option("--secret <secret>", "Webhook HMAC secret").option("--header <name=value...>", "Webhook header", collectValues, []).option("--arg <arg...>", "Command argument", collectValues, []).option("--timeout-ms <ms>", "Transport timeout in milliseconds", parseNumber).option("--retry-attempts <n>", "Maximum delivery attempts", parseNumber).option("--retry-backoff-ms <ms>", "Initial retry backoff in milliseconds", parseNumber).option("--redact <path...>", "Event field path to redact before delivery", collectValues, []).option("--disabled", "Create channel disabled", false).option("-j, --json", "Print JSON output", false).action(async (target, actionOptions, command) => {
16449
+ function registerChannelCommands(program, options) {
16450
+ const channels = program.command(options.channelsCommandName ?? "channels").description("Manage Hasna event channels");
16451
+ channels.command("add").description("Add or replace a channel").argument("<target>", "Webhook URL or command binary").requiredOption("--id <id>", "Channel identifier").option("--transport <kind>", "Transport kind: webhook or command", "webhook").option("--name <name>", "Display name").option("--type <pattern>", "Event type filter, e.g. todos.task.*").option("--source <pattern>", "Event source filter").option("--subject <pattern>", "Event subject filter").option("--severity <pattern>", "Event severity filter").option("--data <path=value...>", "Event data field filter; string values, path!=value negatives, array-member matching, dot paths, * segment wildcard, ** recursive wildcard", collectValues, []).option("--metadata <path=value...>", "Event metadata field filter; string values, path!=value negatives, array-member matching, dot paths, * segment wildcard, ** recursive wildcard", collectValues, []).option("--data-json <path=json...>", "Event data field filter with typed JSON value; path!=json negatives supported", collectValues, []).option("--metadata-json <path=json...>", "Event metadata field filter with typed JSON value; path!=json negatives supported", collectValues, []).option("--secret <secret>", "Webhook HMAC secret").option("--header <name=value...>", "Webhook header", collectValues, []).option("--arg <arg...>", "Command argument", collectValues, []).option("--timeout-ms <ms>", "Transport timeout in milliseconds", parseNumber).option("--retry-attempts <n>", "Maximum delivery attempts", parseNumber).option("--retry-backoff-ms <ms>", "Initial retry backoff in milliseconds", parseNumber).option("--redact <path...>", "Event field path to redact before delivery", collectValues, []).option("--disabled", "Create channel disabled", false).option("-j, --json", "Print JSON output", false).action(async (target, actionOptions, command) => {
15977
16452
  const timestamp = new Date().toISOString();
15978
16453
  const channel = {
15979
16454
  id: actionOptions.id,
15980
16455
  name: actionOptions.name,
15981
16456
  enabled: !actionOptions.disabled,
15982
16457
  transport: actionOptions.transport,
15983
- filters: parseFilter(actionOptions),
16458
+ filters: parseFilterOptions(actionOptions),
15984
16459
  retry: actionOptions.retryAttempts || actionOptions.retryBackoffMs ? { maxAttempts: actionOptions.retryAttempts, backoffMs: actionOptions.retryBackoffMs } : undefined,
15985
16460
  redact: actionOptions.redact?.length ? { paths: actionOptions.redact } : undefined,
15986
16461
  createdAt: timestamp,
@@ -15996,35 +16471,61 @@ function registerWebhookCommands(program, options) {
15996
16471
  const saved = await createClient(options).addChannel(channel);
15997
16472
  print(sanitizeChannelForOutput(saved), wantsJson(actionOptions, command), `Added ${saved.transport} channel ${saved.id}`);
15998
16473
  });
15999
- webhooks.command("list").description("List configured subscriptions").option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
16000
- const channels = await createClient(options).listChannels();
16474
+ channels.command("list").description("List configured channels").option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
16475
+ const channels2 = await createClient(options).listChannels();
16001
16476
  if (wantsJson(actionOptions, command)) {
16002
- console.log(JSON.stringify(sanitizeChannelsForOutput(channels), null, 2));
16477
+ console.log(JSON.stringify(sanitizeChannelsForOutput(channels2), null, 2));
16003
16478
  return;
16004
16479
  }
16005
- if (!channels.length) {
16480
+ if (!channels2.length) {
16006
16481
  console.log("No channels configured.");
16007
16482
  return;
16008
16483
  }
16009
- for (const channel of channels) {
16484
+ for (const channel of channels2) {
16010
16485
  console.log(`${channel.id} ${channel.enabled ? "enabled" : "disabled"} ${channel.transport} ${channel.webhook?.url ?? channel.command?.command ?? channel.transport}`);
16011
16486
  }
16012
16487
  });
16013
- webhooks.command("remove").description("Remove a subscription").argument("<id>", "Subscription/channel identifier").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
16488
+ channels.command("status").description("Show events channel storage status").option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
16489
+ const status = await getEventsStatus(options.dataDir);
16490
+ print(status, wantsJson(actionOptions, command), `events dataDir: ${status.dataDir}`);
16491
+ });
16492
+ channels.command("remove").description("Remove a channel").argument("<id>", "Channel identifier").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
16014
16493
  const removed = await createClient(options).removeChannel(id);
16015
16494
  print({ removed }, wantsJson(actionOptions, command), removed ? `Removed ${id}` : `Channel not found: ${id}`);
16016
16495
  });
16017
- webhooks.command("test").description("Send a test event to one subscription").argument("<id>", "Subscription/channel identifier").option("--type <type>", "Event type", "events.test").option("--subject <subject>", "Event subject").option("--message <message>", "Event message", "Hasna events test delivery").option("--data <json>", "Event data JSON object").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
16018
- const result = await createClient(options).testChannel(id, {
16019
- source: options.source,
16020
- type: actionOptions.type,
16021
- subject: actionOptions.subject ?? id,
16022
- message: actionOptions.message,
16023
- data: parseJsonObject(actionOptions.data, { test: true })
16024
- });
16025
- print(result, wantsJson(actionOptions, command), `${result.status}: ${result.channelId}`);
16496
+ channels.command("test").description("Send a test event to one channel").argument("<id>", "Channel identifier").option("--source <source>", "Event source override").option("--type <type>", "Event type", "events.test").option("--subject <subject>", "Event subject").option("--message <message>", "Event message", "Hasna events test delivery").option("--data <json>", "Event data JSON object").option("--metadata <json>", "Event metadata JSON object").option("--honor-filters", "Skip delivery when the sample event does not match channel filters", false).option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
16497
+ const json = wantsJson(actionOptions, command);
16498
+ try {
16499
+ const result = await createClient(options).testChannel(id, {
16500
+ source: actionOptions.source ?? options.source,
16501
+ type: actionOptions.type,
16502
+ subject: actionOptions.subject ?? id,
16503
+ message: actionOptions.message,
16504
+ data: parseJsonObject(actionOptions.data, { test: true }),
16505
+ metadata: parseJsonObject(actionOptions.metadata, {})
16506
+ }, { honorFilters: actionOptions.honorFilters });
16507
+ print(result, json, `${result.status}: ${result.channelId}`);
16508
+ } catch (error) {
16509
+ fail(error, json);
16510
+ }
16026
16511
  });
16027
- return webhooks;
16512
+ channels.command("match").description("Check whether a sample event matches one channel without delivering").argument("<id>", "Channel identifier").option("--source <source>", "Event source override").option("--type <type>", "Event type", "events.test").option("--subject <subject>", "Event subject").option("--message <message>", "Event message", "Hasna events match preview").option("--data <json>", "Event data JSON object").option("--metadata <json>", "Event metadata JSON object").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
16513
+ const json = wantsJson(actionOptions, command);
16514
+ try {
16515
+ const result = await createClient(options).matchChannel(id, {
16516
+ source: actionOptions.source ?? options.source,
16517
+ type: actionOptions.type,
16518
+ subject: actionOptions.subject ?? id,
16519
+ message: actionOptions.message,
16520
+ data: parseJsonObject(actionOptions.data, { test: true }),
16521
+ metadata: parseJsonObject(actionOptions.metadata, {})
16522
+ });
16523
+ print(result, json, `${result.matched ? "matched" : "skipped"}: ${result.channelId}`);
16524
+ } catch (error) {
16525
+ fail(error, json);
16526
+ }
16527
+ });
16528
+ return channels;
16028
16529
  }
16029
16530
  function registerEventCommands(program, options) {
16030
16531
  const events = program.command(options.eventsCommandName ?? "events").description("Emit, list, and replay Hasna events");
@@ -16041,7 +16542,8 @@ function registerEventCommands(program, options) {
16041
16542
  }, { deliver: actionOptions.deliver, dedupe: actionOptions.dedupe });
16042
16543
  print(result, wantsJson(actionOptions, command), `${result.deduped ? "Deduped" : "Emitted"} ${result.event.id} to ${result.deliveries.length} channel(s)`);
16043
16544
  });
16044
- events.command("list").description("List recorded events").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--limit <n>", "Limit results", parseNumber).option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
16545
+ const defaultListLimit = options.defaultEventListLimit ?? DEFAULT_EVENT_LIST_LIMIT;
16546
+ events.command("list").description("List recorded events").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--limit <n>", `Limit to the most recent <n> events (default ${defaultListLimit}; use 0 for all)`, parseNumber, defaultListLimit).option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
16045
16547
  let rows = await createClient(options).listEvents();
16046
16548
  if (actionOptions.source)
16047
16549
  rows = rows.filter((event) => event.source === actionOptions.source);
@@ -16060,19 +16562,21 @@ function registerEventCommands(program, options) {
16060
16562
  for (const event of rows)
16061
16563
  console.log(`${event.time} ${event.id} ${event.source} ${event.type} ${event.severity}`);
16062
16564
  });
16063
- events.command("replay").description("Replay recorded events").option("--id <id>", "Replay one event id").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--dry-run", "Preview without delivery", false).option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
16565
+ events.command("replay").description("Replay recorded events").option("--id <id>", "Replay one event id").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--cursor <cursor>", "Opaque replay cursor from a previous page").option("--limit <n>", "Maximum events to replay", parseNumber).option("--dry-run", "Preview without delivery", false).option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
16064
16566
  const result = await createClient(options).replay({
16065
16567
  eventId: actionOptions.id,
16066
16568
  source: actionOptions.source,
16067
16569
  type: actionOptions.type,
16570
+ cursor: actionOptions.cursor,
16571
+ limit: actionOptions.limit,
16068
16572
  dryRun: actionOptions.dryRun
16069
16573
  });
16070
- print(result, wantsJson(actionOptions, command), `Replayed ${result.events.length} event(s), ${result.deliveries.length} delivery result(s)`);
16574
+ print(result, wantsJson(actionOptions, command), replaySummary(result.events.length, result.deliveries.length, result.nextCursor));
16071
16575
  });
16072
16576
  return events;
16073
16577
  }
16074
16578
  function registerEventsCommands(program, options) {
16075
- registerWebhookCommands(program, options);
16579
+ registerChannelCommands(program, options);
16076
16580
  registerEventCommands(program, options);
16077
16581
  }
16078
16582
  function parseNumber(value) {
@@ -16085,8 +16589,12 @@ function collectValues(value, previous) {
16085
16589
  previous.push(value);
16086
16590
  return previous;
16087
16591
  }
16592
+ function replaySummary(events, deliveries, nextCursor) {
16593
+ const suffix = nextCursor ? `, next cursor: ${nextCursor}` : "";
16594
+ return `Replayed ${events} event(s), ${deliveries} delivery result(s)${suffix}`;
16595
+ }
16088
16596
 
16089
- // node_modules/commander/esm.mjs
16597
+ // ../../node_modules/.bun/commander@13.1.0/node_modules/commander/esm.mjs
16090
16598
  var import__ = __toESM(require_commander(), 1);
16091
16599
  var {
16092
16600
  program,
@@ -18403,10 +18911,10 @@ program.command("import <file>").description("Import configs from a tar.gz bundl
18403
18911
  });
18404
18912
  program.command("whoami").description("Show setup summary").action(async () => {
18405
18913
  const store = resolveConfigStore();
18406
- const dbPath = isCloudMode() ? `${process.env["HASNA_INSTRUCTIONS_API_URL"]}/v1 (self_hosted)` : process.env["HASNA_INSTRUCTIONS_DB_PATH"] || join16(homedir8(), ".hasna", "instructions", "instructions.db");
18914
+ const dbPath = isApiTransport() ? `${process.env["HASNA_INSTRUCTIONS_API_URL"]}/v1` : process.env["HASNA_INSTRUCTIONS_DB_PATH"] || join16(homedir8(), ".hasna", "instructions", "instructions.db");
18407
18915
  const stats = await store.getConfigStats();
18408
18916
  console.log(chalk.bold("@hasna/instructions") + chalk.dim(" v" + pkg.version));
18409
- console.log(chalk.cyan(isCloudMode() ? "API:" : "DB:") + " " + dbPath);
18917
+ console.log(chalk.cyan(isApiTransport() ? "API:" : "DB:") + " " + dbPath);
18410
18918
  console.log(chalk.cyan("Total configs:") + " " + (stats["total"] || 0));
18411
18919
  console.log();
18412
18920
  console.log(chalk.bold("By category:"));
@@ -18794,8 +19302,6 @@ sessionCmd.command("plan").description("Produce a dry-run render plan for profil
18794
19302
  ...planJsonForOutput(plan),
18795
19303
  ...globalCoverage ? { globalSourceCoverage: globalCoverage } : {}
18796
19304
  });
18797
- if (plan.blocked)
18798
- process.exitCode = 1;
18799
19305
  return;
18800
19306
  }
18801
19307
  console.log(chalk.bold(`${plan.tool} session render plan`) + chalk.dim(` (${plan.adapter.mode})`));
@@ -18822,8 +19328,6 @@ sessionCmd.command("plan").description("Produce a dry-run render plan for profil
18822
19328
  if (globalCoverage.complete)
18823
19329
  console.log(chalk.dim(`global source coverage: ${globalCoverage.expectedSlugs.length}/${globalCoverage.expectedSlugs.length} complete`));
18824
19330
  }
18825
- if (plan.blocked)
18826
- process.exitCode = 1;
18827
19331
  console.log(chalk.dim("Dry run only. No files were written."));
18828
19332
  } catch (e) {
18829
19333
  console.error(chalk.red(formatCliError(e)));
@@ -19252,9 +19756,9 @@ DB stats:`));
19252
19756
  if (count > 0)
19253
19757
  console.log(` ${key.padEnd(18)} ${count}`);
19254
19758
  }
19255
- const location = isCloudMode() ? `${process.env["HASNA_INSTRUCTIONS_API_URL"]}/v1 (self_hosted)` : process.env["HASNA_INSTRUCTIONS_DB_PATH"] || join16(homedir8(), ".hasna", "instructions", "instructions.db");
19759
+ const location = isApiTransport() ? `${process.env["HASNA_INSTRUCTIONS_API_URL"]}/v1` : process.env["HASNA_INSTRUCTIONS_DB_PATH"] || join16(homedir8(), ".hasna", "instructions", "instructions.db");
19256
19760
  console.log(chalk.dim(`
19257
- ${isCloudMode() ? "API" : "DB"}: ${location}`));
19761
+ ${isApiTransport() ? "API" : "DB"}: ${location}`));
19258
19762
  });
19259
19763
  program.command("status").description("Health check: total configs, drift from disk, unredacted secrets").option("--json", "output metadata-only JSON").action(async (opts) => {
19260
19764
  const status = await getConfigsStatus(resolveConfigStore());
@@ -19295,7 +19799,7 @@ program.command("doctor").description("Validate configs: syntax, permissions, mi
19295
19799
  const store = resolveConfigStore();
19296
19800
  let issues = 0;
19297
19801
  const pass = (msg) => console.log(chalk.green(" \u2713 ") + msg);
19298
- const fail = (msg) => {
19802
+ const fail2 = (msg) => {
19299
19803
  issues++;
19300
19804
  console.log(chalk.red(" \u2717 ") + msg);
19301
19805
  };
@@ -19305,9 +19809,9 @@ program.command("doctor").description("Validate configs: syntax, permissions, mi
19305
19809
  console.log(chalk.cyan("Known files on disk:"));
19306
19810
  for (const k of KNOWN_CONFIGS) {
19307
19811
  if (k.rulesDir) {
19308
- existsSync16(expandPath(k.rulesDir)) ? pass(`${k.rulesDir}/ exists`) : k.optional ? skip(`${k.rulesDir}/ (optional)`) : fail(`${k.rulesDir}/ not found`);
19812
+ existsSync16(expandPath(k.rulesDir)) ? pass(`${k.rulesDir}/ exists`) : k.optional ? skip(`${k.rulesDir}/ (optional)`) : fail2(`${k.rulesDir}/ not found`);
19309
19813
  } else {
19310
- existsSync16(expandPath(k.path)) ? pass(k.path) : k.optional ? skip(`${k.path} (optional)`) : fail(`${k.path} not found`);
19814
+ existsSync16(expandPath(k.path)) ? pass(k.path) : k.optional ? skip(`${k.path} (optional)`) : fail2(`${k.path} not found`);
19311
19815
  }
19312
19816
  }
19313
19817
  const allConfigs = await store.listConfigs();
@@ -19320,7 +19824,7 @@ Stored configs (${allConfigs.length}):`));
19320
19824
  JSON.parse(c.content);
19321
19825
  validCount++;
19322
19826
  } catch {
19323
- fail(`${c.slug}: invalid JSON`);
19827
+ fail2(`${c.slug}: invalid JSON`);
19324
19828
  }
19325
19829
  } else {
19326
19830
  validCount++;
@@ -19332,13 +19836,13 @@ Stored configs (${allConfigs.length}):`));
19332
19836
  const found = scanSecrets(c.content, c.format);
19333
19837
  secretCount += found.length;
19334
19838
  }
19335
- secretCount === 0 ? pass("No unredacted secrets") : fail(`${secretCount} unredacted secret(s) \u2014 run \`configs scan --fix\``);
19839
+ secretCount === 0 ? pass("No unredacted secrets") : fail2(`${secretCount} unredacted secret(s) \u2014 run \`configs scan --fix\``);
19336
19840
  const duplicateTargets = findDuplicateTargetPathGroups(allConfigs);
19337
19841
  if (duplicateTargets.length === 0) {
19338
19842
  pass("No target path is claimed by more than one config");
19339
19843
  } else {
19340
19844
  const rowCount = duplicateTargets.reduce((total, group) => total + group.configs.length, 0);
19341
- fail(`${duplicateTargets.length} target path(s) claimed by more than one config (${rowCount} rows) \u2014 apply order between them is undefined`);
19845
+ fail2(`${duplicateTargets.length} target path(s) claimed by more than one config (${rowCount} rows) \u2014 apply order between them is undefined`);
19342
19846
  for (const group of duplicateTargets) {
19343
19847
  console.log(chalk.yellow(` ${group.target_path}`));
19344
19848
  for (const c of group.configs) {
@@ -19352,7 +19856,7 @@ Stored configs (${allConfigs.length}):`));
19352
19856
  pass("No reference config name is claimed by more than one row");
19353
19857
  } else {
19354
19858
  const rowCount = duplicateReferenceNames.reduce((total, group) => total + group.configs.length, 0);
19355
- fail(`${duplicateReferenceNames.length} reference name(s) claimed by more than one row (${rowCount} rows) \u2014 only one is live in the next render`);
19859
+ fail2(`${duplicateReferenceNames.length} reference name(s) claimed by more than one row (${rowCount} rows) \u2014 only one is live in the next render`);
19356
19860
  for (const group of duplicateReferenceNames) {
19357
19861
  console.log(chalk.yellow(` ${group.name}`));
19358
19862
  for (const c of group.configs) {