@hasna/instructions 0.4.35 → 0.4.36

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/.bun/commander@13.1.0/node_modules/commander/lib/error.js
50
+ // 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/.bun/commander@13.1.0/node_modules/commander/lib/argument.js
74
+ // 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/.bun/commander@13.1.0/node_modules/commander/lib/help.js
153
+ // 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/.bun/commander@13.1.0/node_modules/commander/lib/option.js
503
+ // 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/.bun/commander@13.1.0/node_modules/commander/lib/suggestSimilar.js
681
+ // 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/.bun/commander@13.1.0/node_modules/commander/lib/command.js
754
+ // 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
- } else {}
996
+ }
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/.bun/commander@13.1.0/node_modules/commander/index.js
2064
+ // 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,30 +2136,6 @@ 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
-
2163
2139
  // src/db/database.ts
2164
2140
  import { Database } from "bun:sqlite";
2165
2141
  import { existsSync as existsSync2, mkdirSync, rmSync } from "fs";
@@ -2186,9 +2162,8 @@ function slugify(name) {
2186
2162
  function getDatabase(path) {
2187
2163
  if (_db)
2188
2164
  return _db;
2189
- assertNoLegacyStorageMode();
2190
2165
  if (!path && process.env["HASNA_INSTRUCTIONS_API_URL"] && process.env["HASNA_INSTRUCTIONS_API_KEY"]) {
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.");
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.");
2192
2167
  }
2193
2168
  const dbPath = path || getDbPath();
2194
2169
  const db = new Database(dbPath);
@@ -2269,7 +2244,6 @@ function insertFeedback(input, db) {
2269
2244
  }
2270
2245
  var MIGRATIONS, _db = null;
2271
2246
  var init_database = __esm(() => {
2272
- init_retired_storage_mode();
2273
2247
  MIGRATIONS = [
2274
2248
  `
2275
2249
  CREATE TABLE IF NOT EXISTS configs (
@@ -3063,7 +3037,7 @@ Policy reference: \`${CODEWITH_SHARED_TODOS_STORAGE_POLICY_REFERENCE}\`
3063
3037
  `;
3064
3038
  });
3065
3039
 
3066
- // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/util.js
3040
+ // node_modules/zod/v3/helpers/util.js
3067
3041
  var util, objectUtil, ZodParsedType, getParsedType = (data) => {
3068
3042
  const t = typeof data;
3069
3043
  switch (t) {
@@ -3194,7 +3168,7 @@ var init_util = __esm(() => {
3194
3168
  ]);
3195
3169
  });
3196
3170
 
3197
- // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/ZodError.js
3171
+ // node_modules/zod/v3/ZodError.js
3198
3172
  var ZodIssueCode, quotelessJson = (obj) => {
3199
3173
  const json = JSON.stringify(obj, null, 2);
3200
3174
  return json.replace(/"([^"]+)":/g, "$1:");
@@ -3315,7 +3289,7 @@ var init_ZodError = __esm(() => {
3315
3289
  };
3316
3290
  });
3317
3291
 
3318
- // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/locales/en.js
3292
+ // node_modules/zod/v3/locales/en.js
3319
3293
  var errorMap = (issue, _ctx) => {
3320
3294
  let message;
3321
3295
  switch (issue.code) {
@@ -3422,7 +3396,7 @@ var init_en = __esm(() => {
3422
3396
  en_default = errorMap;
3423
3397
  });
3424
3398
 
3425
- // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/errors.js
3399
+ // node_modules/zod/v3/errors.js
3426
3400
  function setErrorMap(map) {
3427
3401
  overrideErrorMap = map;
3428
3402
  }
@@ -3435,7 +3409,7 @@ var init_errors = __esm(() => {
3435
3409
  overrideErrorMap = en_default;
3436
3410
  });
3437
3411
 
3438
- // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js
3412
+ // node_modules/zod/v3/helpers/parseUtil.js
3439
3413
  function addIssueToContext(ctx, issueData) {
3440
3414
  const overrideMap = getErrorMap();
3441
3415
  const issue = makeIssue({
@@ -3540,10 +3514,10 @@ var init_parseUtil = __esm(() => {
3540
3514
  });
3541
3515
  });
3542
3516
 
3543
- // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/typeAliases.js
3517
+ // node_modules/zod/v3/helpers/typeAliases.js
3544
3518
  var init_typeAliases = () => {};
3545
3519
 
3546
- // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/errorUtil.js
3520
+ // node_modules/zod/v3/helpers/errorUtil.js
3547
3521
  var errorUtil;
3548
3522
  var init_errorUtil = __esm(() => {
3549
3523
  (function(errorUtil2) {
@@ -3552,7 +3526,7 @@ var init_errorUtil = __esm(() => {
3552
3526
  })(errorUtil || (errorUtil = {}));
3553
3527
  });
3554
3528
 
3555
- // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/types.js
3529
+ // node_modules/zod/v3/types.js
3556
3530
  class ParseInputLazyPath {
3557
3531
  constructor(parent, value, path, key) {
3558
3532
  this._cachedPath = [];
@@ -6903,7 +6877,7 @@ var init_types2 = __esm(() => {
6903
6877
  NEVER = INVALID;
6904
6878
  });
6905
6879
 
6906
- // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/external.js
6880
+ // node_modules/zod/v3/external.js
6907
6881
  var exports_external = {};
6908
6882
  __export(exports_external, {
6909
6883
  void: () => voidType,
@@ -7023,7 +6997,7 @@ var init_external = __esm(() => {
7023
6997
  init_ZodError();
7024
6998
  });
7025
6999
 
7026
- // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/index.js
7000
+ // node_modules/zod/index.js
7027
7001
  var init_zod = __esm(() => {
7028
7002
  init_external();
7029
7003
  init_external();
@@ -7215,7 +7189,7 @@ var init_redact = __esm(() => {
7215
7189
  VALUE_PATTERNS = [
7216
7190
  { re: /npm_[A-Za-z0-9]{36,}/, reason: "npm token" },
7217
7191
  { re: /gh[pousr]_[A-Za-z0-9_]{36,}/, reason: "GitHub token" },
7218
- { re: /sk[-]ant-[A-Za-z0-9\-_]{40,}/, reason: "Anthropic API key" },
7192
+ { re: /sk-ant-[A-Za-z0-9\-_]{40,}/, reason: "Anthropic API key" },
7219
7193
  { re: /sk-[A-Za-z0-9]{48,}/, reason: "OpenAI API key" },
7220
7194
  { re: /xoxb-[0-9]+-[A-Za-z0-9\-]+/, reason: "Slack bot token" },
7221
7195
  { re: /AIza[0-9A-Za-z\-_]{35}/, reason: "Google API key" },
@@ -10589,15 +10563,11 @@ function sha2564(content) {
10589
10563
  function homeDir() {
10590
10564
  return process.env["HOME"] || homedir3();
10591
10565
  }
10592
- function markerPayload(content, markerLine) {
10593
- const withTrailingNewline = `${markerLine}
10594
- `;
10595
- if (content.startsWith(withTrailingNewline))
10596
- return content.slice(withTrailingNewline.length);
10597
- if (content.startsWith(markerLine))
10598
- return content.slice(markerLine.length).replace(/^\n/, "");
10599
- return content.replace(`${markerLine}
10600
- `, "").replace(markerLine, "");
10566
+ function markerPayload(content, markerLine, markerIndex) {
10567
+ const index = markerIndex ?? content.indexOf(markerLine);
10568
+ if (index < 0)
10569
+ return content;
10570
+ return content.slice(0, index) + content.slice(index + markerLine.length).replace(/^\n/, "");
10601
10571
  }
10602
10572
  function baseObservation(path) {
10603
10573
  return {
@@ -10724,7 +10694,7 @@ function observeCursorGlobalAuthorityPath(authorityPath, readFile2) {
10724
10694
  };
10725
10695
  }
10726
10696
  const markerLine = markerMatch[0];
10727
- const payloadSha256 = sha2564(markerPayload(content, markerLine));
10697
+ const payloadSha256 = sha2564(markerPayload(content, markerLine, markerMatch.index ?? -1));
10728
10698
  if (payloadSha256 !== markerSha256) {
10729
10699
  return {
10730
10700
  ...base,
@@ -10744,18 +10714,34 @@ function observeCursorGlobalAuthorityPath(authorityPath, readFile2) {
10744
10714
  return {
10745
10715
  ...base,
10746
10716
  fileType,
10747
- status: "unmanaged",
10717
+ status: "managed",
10748
10718
  sha256: contentSha256,
10749
10719
  markers,
10750
10720
  markerSha256,
10751
10721
  provenance: {
10752
10722
  source: "filesystem",
10753
- authority: "unmanaged",
10723
+ authority: "managed",
10754
10724
  observedPath: authorityPath,
10755
- detection: "unknown-content"
10725
+ detection: "managed-marker"
10756
10726
  }
10757
10727
  };
10758
10728
  }
10729
+ function isCursorGlobalAuthorityPath(path) {
10730
+ return resolve3(path) === resolve3(join5(homeDir(), CURSOR_GLOBAL_AUTHORITY_RELATIVE_PATH));
10731
+ }
10732
+ function stampCursorGlobalAuthorityMarker(content) {
10733
+ if (CURSOR_GLOBAL_AUTHORITY_MARKER_PATTERN.test(content))
10734
+ return content;
10735
+ const digest = sha2564(content);
10736
+ const markerLine = `<!-- ${CURSOR_GLOBAL_AUTHORITY_MANAGED_MARKER} hash=sha256:${digest} -->`;
10737
+ const frontmatter = content.match(CURSOR_GLOBAL_AUTHORITY_FRONTMATTER_PATTERN)?.[0];
10738
+ if (frontmatter) {
10739
+ return `${frontmatter}${markerLine}
10740
+ ${content.slice(frontmatter.length)}`;
10741
+ }
10742
+ return `${markerLine}
10743
+ ${content}`;
10744
+ }
10759
10745
  function detectCursorAuthorityConflicts(observation = observeCursorGlobalAuthority()) {
10760
10746
  if (observation.status === "absent" || observation.status === "managed")
10761
10747
  return [];
@@ -10777,10 +10763,11 @@ function detectCursorAuthorityConflicts(observation = observeCursorGlobalAuthori
10777
10763
  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.`
10778
10764
  }];
10779
10765
  }
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;
10766
+ 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;
10781
10767
  var init_cursor_authority = __esm(() => {
10782
10768
  CURSOR_GLOBAL_AUTHORITY_MAX_BYTES = 256 * 1024;
10783
10769
  CURSOR_GLOBAL_AUTHORITY_MARKER_PATTERN = /^<!-- Managed by @hasna\/configs cursor global authority hash=(sha256:[a-f0-9]{64}) -->$/m;
10770
+ CURSOR_GLOBAL_AUTHORITY_FRONTMATTER_PATTERN = /^---\n[\s\S]*?\n---(?:\n|$)/;
10784
10771
  });
10785
10772
 
10786
10773
  // src/lib/session-render.ts
@@ -13363,17 +13350,16 @@ function formatCliError(err, env = process.env) {
13363
13350
  return err instanceof Error ? err.message : String(err);
13364
13351
  }
13365
13352
  function resolveCloudConfig(env = process.env) {
13366
- assertNoLegacyStorageMode(env);
13367
13353
  const apiUrl = env[API_URL_ENV]?.trim();
13368
13354
  const apiKey = env[API_KEY_ENV]?.trim();
13369
13355
  if (!apiUrl && !apiKey)
13370
13356
  return null;
13371
13357
  if (!apiUrl || !apiKey) {
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.`);
13358
+ 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.`);
13373
13359
  }
13374
13360
  return { apiUrl, apiKey };
13375
13361
  }
13376
- function isApiTransport(env = process.env) {
13362
+ function isCloudMode(env = process.env) {
13377
13363
  return resolveCloudConfig(env) !== null;
13378
13364
  }
13379
13365
 
@@ -13859,7 +13845,6 @@ var init_config_store = __esm(() => {
13859
13845
  init_types();
13860
13846
  init_bounded_read();
13861
13847
  init_instruction_graph();
13862
- init_retired_storage_mode();
13863
13848
  CloudHttpError = class CloudHttpError extends Error {
13864
13849
  status;
13865
13850
  body;
@@ -14007,8 +13992,9 @@ async function writeConfigResult(config, targetPath, content, opts, meta = {}) {
14007
13992
  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.`);
14008
13993
  }
14009
13994
  const path = expandPath(renderedTargetPath);
13995
+ const renderedForTarget = isCursorGlobalAuthorityPath(path) ? stampCursorGlobalAuthorityMarker(renderedContent) : renderedContent;
14010
13996
  const previousContent = existsSync7(path) ? readFileSync5(path, "utf-8") : null;
14011
- const changed = previousContent !== renderedContent;
13997
+ const changed = previousContent !== renderedForTarget;
14012
13998
  if (!opts.dryRun) {
14013
13999
  const dir = dirname4(path);
14014
14000
  if (!existsSync7(dir)) {
@@ -14018,13 +14004,13 @@ async function writeConfigResult(config, targetPath, content, opts, meta = {}) {
14018
14004
  const store = opts.store ?? resolveConfigStore();
14019
14005
  await store.createSnapshot(config.id, previousContent, config.version);
14020
14006
  }
14021
- writeFileSync2(path, renderedContent, "utf-8");
14007
+ writeFileSync2(path, renderedForTarget, "utf-8");
14022
14008
  }
14023
14009
  return {
14024
14010
  config_id: config.id,
14025
14011
  path,
14026
14012
  previous_content: previousContent,
14027
- new_content: renderedContent,
14013
+ new_content: renderedForTarget,
14028
14014
  dry_run: opts.dryRun ?? false,
14029
14015
  changed,
14030
14016
  primary_changed: changed,
@@ -14385,6 +14371,7 @@ var init_apply = __esm(() => {
14385
14371
  init_redact();
14386
14372
  init_template();
14387
14373
  init_transforms();
14374
+ init_cursor_authority();
14388
14375
  });
14389
14376
 
14390
14377
  // src/lib/sync-dir.ts
@@ -15405,16 +15392,15 @@ var init_package_manager_guard = __esm(() => {
15405
15392
  TOKEN_VALUE_PATTERNS = [
15406
15393
  { re: /npm_[A-Za-z0-9]{36,}/, rule: "literal-npm-token", detail: "literal npm token-like value" },
15407
15394
  { re: /gh[pousr]_[A-Za-z0-9_]{36,}/, rule: "literal-github-token", detail: "literal GitHub token-like value" },
15408
- { re: /sk[-]ant-[A-Za-z0-9\-_]{40,}/, rule: "literal-anthropic-key", detail: "literal Anthropic key-like value" },
15395
+ { re: /sk-ant-[A-Za-z0-9\-_]{40,}/, rule: "literal-anthropic-key", detail: "literal Anthropic key-like value" },
15409
15396
  { re: /sk-[A-Za-z0-9]{48,}/, rule: "literal-openai-key", detail: "literal OpenAI key-like value" },
15410
15397
  { re: /AKIA[0-9A-Z]{16}/, rule: "literal-aws-access-key", detail: "literal AWS access-key-like value" },
15411
15398
  { re: /xoxb-[0-9]+-[A-Za-z0-9-]+/, rule: "literal-slack-token", detail: "literal Slack token-like value" }
15412
15399
  ];
15413
15400
  });
15414
15401
 
15415
- // ../../node_modules/.bun/@hasna+events@0.1.15/node_modules/@hasna/events/dist/commander.js
15402
+ // node_modules/@hasna/events/dist/commander.js
15416
15403
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
15417
- import { Buffer as Buffer2 } from "buffer";
15418
15404
  import { existsSync } from "fs";
15419
15405
  import { homedir } from "os";
15420
15406
  import { join } from "path";
@@ -15430,83 +15416,29 @@ function getPathValue(input, path) {
15430
15416
  return;
15431
15417
  }, input);
15432
15418
  }
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}$`);
15419
+ function wildcardToRegExp(pattern) {
15420
+ const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
15421
+ return new RegExp(`^${escaped}$`);
15462
15422
  }
15463
- function matchString(value, matcher, options = {}) {
15423
+ function matchString(value, matcher) {
15464
15424
  if (matcher === undefined)
15465
15425
  return true;
15466
15426
  if (value === undefined)
15467
15427
  return false;
15468
15428
  const matchers = Array.isArray(matcher) ? matcher : [matcher];
15469
- return matchers.some((item) => wildcardToRegExp(item, options).test(value));
15429
+ return matchers.some((item) => wildcardToRegExp(item).test(value));
15470
15430
  }
15471
15431
  function matchRecord(input, matcher) {
15472
15432
  if (!matcher)
15473
15433
  return true;
15474
15434
  return Object.entries(matcher).every(([path, expected]) => {
15475
- const actualValues = getFieldValues(input, path);
15476
- return matchField(actualValues, expected, path);
15435
+ const actual = getPathValue(input, path);
15436
+ if (typeof expected === "string" || Array.isArray(expected)) {
15437
+ return matchString(actual === undefined ? undefined : String(actual), expected);
15438
+ }
15439
+ return actual === expected;
15477
15440
  });
15478
15441
  }
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
- }
15510
15442
  function eventMatchesFilter(event, filter) {
15511
15443
  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);
15512
15444
  }
@@ -15519,29 +15451,16 @@ function channelMatchesEvent(channel, event) {
15519
15451
  }
15520
15452
  var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
15521
15453
  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;
15525
15454
  function getEventsDataDir(override) {
15526
15455
  return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join(homedir(), ".hasna", "events");
15527
15456
  }
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
-
15536
15457
  class JsonEventsStore {
15537
15458
  dataDir;
15538
- runtime;
15539
15459
  channelsPath;
15540
15460
  eventsPath;
15541
15461
  deliveriesPath;
15542
15462
  constructor(dataDir = getEventsDataDir()) {
15543
15463
  this.dataDir = dataDir;
15544
- this.runtime = localJsonRuntime(dataDir);
15545
15464
  this.channelsPath = join(dataDir, "channels.json");
15546
15465
  this.eventsPath = join(dataDir, "events.json");
15547
15466
  this.deliveriesPath = join(dataDir, "deliveries.json");
@@ -15589,58 +15508,13 @@ class JsonEventsStore {
15589
15508
  await this.writeJson(this.eventsPath, events);
15590
15509
  return event;
15591
15510
  }
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 = {}) {
15511
+ async listEvents() {
15617
15512
  await this.init();
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
- };
15513
+ return this.readJson(this.eventsPath, []);
15640
15514
  }
15641
15515
  async findEventByIdentity(identity) {
15642
15516
  const events = await this.listEvents();
15643
- return findEventByIdentity(events, identity);
15517
+ return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
15644
15518
  }
15645
15519
  async appendDelivery(result) {
15646
15520
  await this.init();
@@ -15691,130 +15565,6 @@ class JsonEventsStore {
15691
15565
  });
15692
15566
  }
15693
15567
  }
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
- }
15818
15568
  var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
15819
15569
  function buildSignatureBase(timestamp, body) {
15820
15570
  return `${timestamp}.${body}`;
@@ -15829,27 +15579,21 @@ function now() {
15829
15579
  function truncate(value, max = 4096) {
15830
15580
  return value.length > max ? `${value.slice(0, max)}...` : value;
15831
15581
  }
15832
- function buildWebhookRequest(event, channel, options = {}) {
15582
+ function buildWebhookRequest(event, channel) {
15833
15583
  if (!channel.webhook)
15834
15584
  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
- }
15840
15585
  const body = JSON.stringify(event);
15841
- const timestamp = options.timestamp ?? new Date().toISOString();
15586
+ const timestamp = event.time;
15842
15587
  const headers = {
15843
15588
  "Content-Type": "application/json",
15844
15589
  "User-Agent": "@hasna/events",
15845
15590
  "X-Hasna-Event-Id": event.id,
15846
15591
  "X-Hasna-Event-Type": event.type,
15847
- ...channel.webhook.headers,
15848
- "X-Hasna-Timestamp": timestamp
15592
+ "X-Hasna-Timestamp": timestamp,
15593
+ ...channel.webhook.headers
15849
15594
  };
15850
- const secret = options.secret ?? channel.webhook.secret;
15851
- if (secret) {
15852
- headers["X-Hasna-Signature"] = signPayload(secret, timestamp, body);
15595
+ if (channel.webhook.secret) {
15596
+ headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp, body);
15853
15597
  }
15854
15598
  return { body, headers };
15855
15599
  }
@@ -15857,21 +15601,7 @@ async function dispatchWebhook(event, channel, options = {}) {
15857
15601
  if (!channel.webhook)
15858
15602
  throw new Error(`Channel ${channel.id} has no webhook config`);
15859
15603
  const startedAt = now();
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 });
15604
+ const { body, headers } = buildWebhookRequest(event, channel);
15875
15605
  const controller = new AbortController;
15876
15606
  const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
15877
15607
  try {
@@ -15903,15 +15633,6 @@ async function dispatchWebhook(event, channel, options = {}) {
15903
15633
  clearTimeout(timeout);
15904
15634
  }
15905
15635
  }
15906
- function failedAttempt(startedAt, error) {
15907
- return {
15908
- attempt: 1,
15909
- status: "failed",
15910
- startedAt,
15911
- completedAt: now(),
15912
- error
15913
- };
15914
- }
15915
15636
  async function dispatchCommand(event, channel) {
15916
15637
  if (!channel.command)
15917
15638
  throw new Error(`Channel ${channel.id} has no command config`);
@@ -16000,90 +15721,6 @@ function createDeliveryResult(event, channel, attempts) {
16000
15721
  completedAt: attempts.at(-1)?.completedAt ?? now()
16001
15722
  };
16002
15723
  }
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
- }
16087
15724
  function createEvent(input) {
16088
15725
  return {
16089
15726
  id: input.id ?? randomUUID2(),
@@ -16104,18 +15741,10 @@ class EventsClient {
16104
15741
  store;
16105
15742
  redactors;
16106
15743
  transportOptions;
16107
- catalog;
16108
- validateCatalogTypes;
16109
15744
  constructor(options = {}) {
16110
15745
  this.store = options.store ?? new JsonEventsStore(options.dataDir);
16111
15746
  this.redactors = options.redactors ?? [];
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;
15747
+ this.transportOptions = { fetchImpl: options.fetchImpl };
16119
15748
  }
16120
15749
  async addChannel(input) {
16121
15750
  const timestamp = new Date().toISOString();
@@ -16133,40 +15762,18 @@ class EventsClient {
16133
15762
  }
16134
15763
  async emit(input, options = {}) {
16135
15764
  const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
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
- };
15765
+ if (options.dedupe !== false) {
15766
+ const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
15767
+ if (existing) {
15768
+ return { event: existing, deliveries: [], deduped: true };
15769
+ }
15770
+ }
15771
+ await this.store.appendEvent(event);
15772
+ const deliveries = options.deliver === false ? [] : await this.deliver(event);
15773
+ return { event, deliveries, deduped: false };
15774
+ }
15775
+ async listEvents() {
15776
+ return this.store.listEvents();
16170
15777
  }
16171
15778
  async listDeliveries() {
16172
15779
  return this.store.listDeliveries();
@@ -16183,7 +15790,7 @@ class EventsClient {
16183
15790
  }
16184
15791
  return deliveries;
16185
15792
  }
16186
- async matchChannel(id, input = {}) {
15793
+ async testChannel(id, input = {}) {
16187
15794
  const channel = await this.store.getChannel(id);
16188
15795
  if (!channel)
16189
15796
  throw new Error(`Channel not found: ${id}`);
@@ -16200,71 +15807,28 @@ class EventsClient {
16200
15807
  time: input.time,
16201
15808
  id: input.id
16202
15809
  });
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
- }
16231
15810
  const eventForChannel = await this.applyRedaction(event, channel);
16232
15811
  const result = await this.deliverWithRetry(eventForChannel, channel);
16233
15812
  await this.store.appendDelivery(result);
16234
15813
  return result;
16235
15814
  }
16236
15815
  async replay(options = {}) {
16237
- const page = options.cursor || options.limit !== undefined ? await this.listEventsPage(options) : { events: await this.listEvents(options), hasMore: false };
15816
+ const events = (await this.store.listEvents()).filter((event) => {
15817
+ if (options.eventId && event.id !== options.eventId)
15818
+ return false;
15819
+ if (options.source && event.source !== options.source)
15820
+ return false;
15821
+ if (options.type && event.type !== options.type)
15822
+ return false;
15823
+ return true;
15824
+ });
16238
15825
  if (options.dryRun)
16239
- return { events: page.events, deliveries: [], cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
15826
+ return { events, deliveries: [] };
16240
15827
  const deliveries = [];
16241
- for (const event of page.events) {
15828
+ for (const event of events) {
16242
15829
  deliveries.push(...await this.deliver(event));
16243
15830
  }
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
- };
15831
+ return { events, deliveries };
16268
15832
  }
16269
15833
  async applyRedaction(event, channel) {
16270
15834
  let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
@@ -16291,6 +15855,15 @@ class EventsClient {
16291
15855
  return createDeliveryResult(event, channel, attempts);
16292
15856
  }
16293
15857
  }
15858
+ function redactPaths(event, paths, replacement = "[REDACTED]") {
15859
+ if (paths.length === 0)
15860
+ return event;
15861
+ const copy = structuredClone(event);
15862
+ for (const path of paths) {
15863
+ setPath(copy, path, replacement);
15864
+ }
15865
+ return copy;
15866
+ }
16294
15867
  function sanitizeChannelForOutput(channel) {
16295
15868
  const copy = structuredClone(channel);
16296
15869
  if (copy.webhook?.secret)
@@ -16303,19 +15876,34 @@ function sanitizeChannelForOutput(channel) {
16303
15876
  function sanitizeChannelsForOutput(channels) {
16304
15877
  return channels.map(sanitizeChannelForOutput);
16305
15878
  }
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;
15879
+ function redactSensitiveKeys(event, replacement = "[REDACTED]") {
15880
+ return redactValue(event, replacement);
15881
+ }
15882
+ function shouldRedactKey(key) {
15883
+ return /secret|token|password|api[_-]?key|authorization/i.test(key);
15884
+ }
15885
+ function redactValue(value, replacement) {
15886
+ if (Array.isArray(value))
15887
+ return value.map((item) => redactValue(item, replacement));
15888
+ if (!value || typeof value !== "object")
15889
+ return value;
15890
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
15891
+ key,
15892
+ shouldRedactKey(key) ? replacement : redactValue(item, replacement)
15893
+ ]));
15894
+ }
15895
+ function setPath(input, path, replacement) {
15896
+ const parts = path.split(".");
15897
+ let cursor = input;
15898
+ for (const part of parts.slice(0, -1)) {
15899
+ const next = cursor[part];
15900
+ if (!next || typeof next !== "object")
15901
+ return;
15902
+ cursor = next;
15903
+ }
15904
+ const last = parts.at(-1);
15905
+ if (last && last in cursor)
15906
+ cursor[last] = replacement;
16319
15907
  }
16320
15908
  function normalizeTime(value) {
16321
15909
  if (!value)
@@ -16329,77 +15917,6 @@ function normalizeRetryPolicy(policy) {
16329
15917
  multiplier: Math.max(1, policy?.multiplier ?? 2)
16330
15918
  };
16331
15919
  }
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;
16403
15920
  function parseJsonObject(value, fallback) {
16404
15921
  if (!value)
16405
15922
  return fallback;
@@ -16421,6 +15938,18 @@ function parseHeaders(values) {
16421
15938
  }
16422
15939
  return headers;
16423
15940
  }
15941
+ function parseFilter(options) {
15942
+ const filter2 = {};
15943
+ if (options.source)
15944
+ filter2.source = options.source;
15945
+ if (options.type)
15946
+ filter2.type = options.type;
15947
+ if (options.subject)
15948
+ filter2.subject = options.subject;
15949
+ if (options.severity)
15950
+ filter2.severity = options.severity;
15951
+ return Object.keys(filter2).length > 0 ? [filter2] : undefined;
15952
+ }
16424
15953
  function createClient(options) {
16425
15954
  if (options.createClient)
16426
15955
  return options.createClient();
@@ -16432,30 +15961,22 @@ function print(value, json, text) {
16432
15961
  else
16433
15962
  console.log(text);
16434
15963
  }
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
- }
16443
15964
  function hasJsonOption(options) {
16444
15965
  return Boolean(options?.json || options?.opts?.().json || options?.optsWithGlobals?.().json || options?.parent?.opts?.().json || options?.parent?.optsWithGlobals?.().json);
16445
15966
  }
16446
15967
  function wantsJson(actionOptions, command) {
16447
15968
  return hasJsonOption(actionOptions) || hasJsonOption(command);
16448
15969
  }
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) => {
15970
+ function registerWebhookCommands(program, options) {
15971
+ const webhooks = program.command(options.webhooksCommandName ?? "webhooks").description("Manage Hasna event webhook subscriptions");
15972
+ 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) => {
16452
15973
  const timestamp = new Date().toISOString();
16453
15974
  const channel = {
16454
15975
  id: actionOptions.id,
16455
15976
  name: actionOptions.name,
16456
15977
  enabled: !actionOptions.disabled,
16457
15978
  transport: actionOptions.transport,
16458
- filters: parseFilterOptions(actionOptions),
15979
+ filters: parseFilter(actionOptions),
16459
15980
  retry: actionOptions.retryAttempts || actionOptions.retryBackoffMs ? { maxAttempts: actionOptions.retryAttempts, backoffMs: actionOptions.retryBackoffMs } : undefined,
16460
15981
  redact: actionOptions.redact?.length ? { paths: actionOptions.redact } : undefined,
16461
15982
  createdAt: timestamp,
@@ -16471,61 +15992,35 @@ function registerChannelCommands(program, options) {
16471
15992
  const saved = await createClient(options).addChannel(channel);
16472
15993
  print(sanitizeChannelForOutput(saved), wantsJson(actionOptions, command), `Added ${saved.transport} channel ${saved.id}`);
16473
15994
  });
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();
15995
+ webhooks.command("list").description("List configured subscriptions").option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
15996
+ const channels = await createClient(options).listChannels();
16476
15997
  if (wantsJson(actionOptions, command)) {
16477
- console.log(JSON.stringify(sanitizeChannelsForOutput(channels2), null, 2));
15998
+ console.log(JSON.stringify(sanitizeChannelsForOutput(channels), null, 2));
16478
15999
  return;
16479
16000
  }
16480
- if (!channels2.length) {
16001
+ if (!channels.length) {
16481
16002
  console.log("No channels configured.");
16482
16003
  return;
16483
16004
  }
16484
- for (const channel of channels2) {
16005
+ for (const channel of channels) {
16485
16006
  console.log(`${channel.id} ${channel.enabled ? "enabled" : "disabled"} ${channel.transport} ${channel.webhook?.url ?? channel.command?.command ?? channel.transport}`);
16486
16007
  }
16487
16008
  });
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) => {
16009
+ webhooks.command("remove").description("Remove a subscription").argument("<id>", "Subscription/channel identifier").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
16493
16010
  const removed = await createClient(options).removeChannel(id);
16494
16011
  print({ removed }, wantsJson(actionOptions, command), removed ? `Removed ${id}` : `Channel not found: ${id}`);
16495
16012
  });
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
- }
16511
- });
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
- }
16013
+ 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) => {
16014
+ const result = await createClient(options).testChannel(id, {
16015
+ source: options.source,
16016
+ type: actionOptions.type,
16017
+ subject: actionOptions.subject ?? id,
16018
+ message: actionOptions.message,
16019
+ data: parseJsonObject(actionOptions.data, { test: true })
16020
+ });
16021
+ print(result, wantsJson(actionOptions, command), `${result.status}: ${result.channelId}`);
16527
16022
  });
16528
- return channels;
16023
+ return webhooks;
16529
16024
  }
16530
16025
  function registerEventCommands(program, options) {
16531
16026
  const events = program.command(options.eventsCommandName ?? "events").description("Emit, list, and replay Hasna events");
@@ -16542,8 +16037,7 @@ function registerEventCommands(program, options) {
16542
16037
  }, { deliver: actionOptions.deliver, dedupe: actionOptions.dedupe });
16543
16038
  print(result, wantsJson(actionOptions, command), `${result.deduped ? "Deduped" : "Emitted"} ${result.event.id} to ${result.deliveries.length} channel(s)`);
16544
16039
  });
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) => {
16040
+ 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) => {
16547
16041
  let rows = await createClient(options).listEvents();
16548
16042
  if (actionOptions.source)
16549
16043
  rows = rows.filter((event) => event.source === actionOptions.source);
@@ -16562,21 +16056,19 @@ function registerEventCommands(program, options) {
16562
16056
  for (const event of rows)
16563
16057
  console.log(`${event.time} ${event.id} ${event.source} ${event.type} ${event.severity}`);
16564
16058
  });
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) => {
16059
+ 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) => {
16566
16060
  const result = await createClient(options).replay({
16567
16061
  eventId: actionOptions.id,
16568
16062
  source: actionOptions.source,
16569
16063
  type: actionOptions.type,
16570
- cursor: actionOptions.cursor,
16571
- limit: actionOptions.limit,
16572
16064
  dryRun: actionOptions.dryRun
16573
16065
  });
16574
- print(result, wantsJson(actionOptions, command), replaySummary(result.events.length, result.deliveries.length, result.nextCursor));
16066
+ print(result, wantsJson(actionOptions, command), `Replayed ${result.events.length} event(s), ${result.deliveries.length} delivery result(s)`);
16575
16067
  });
16576
16068
  return events;
16577
16069
  }
16578
16070
  function registerEventsCommands(program, options) {
16579
- registerChannelCommands(program, options);
16071
+ registerWebhookCommands(program, options);
16580
16072
  registerEventCommands(program, options);
16581
16073
  }
16582
16074
  function parseNumber(value) {
@@ -16589,12 +16081,8 @@ function collectValues(value, previous) {
16589
16081
  previous.push(value);
16590
16082
  return previous;
16591
16083
  }
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
- }
16596
16084
 
16597
- // ../../node_modules/.bun/commander@13.1.0/node_modules/commander/esm.mjs
16085
+ // node_modules/commander/esm.mjs
16598
16086
  var import__ = __toESM(require_commander(), 1);
16599
16087
  var {
16600
16088
  program,
@@ -18911,10 +18399,10 @@ program.command("import <file>").description("Import configs from a tar.gz bundl
18911
18399
  });
18912
18400
  program.command("whoami").description("Show setup summary").action(async () => {
18913
18401
  const store = resolveConfigStore();
18914
- const dbPath = isApiTransport() ? `${process.env["HASNA_INSTRUCTIONS_API_URL"]}/v1` : process.env["HASNA_INSTRUCTIONS_DB_PATH"] || join16(homedir8(), ".hasna", "instructions", "instructions.db");
18402
+ const dbPath = isCloudMode() ? `${process.env["HASNA_INSTRUCTIONS_API_URL"]}/v1 (self_hosted)` : process.env["HASNA_INSTRUCTIONS_DB_PATH"] || join16(homedir8(), ".hasna", "instructions", "instructions.db");
18915
18403
  const stats = await store.getConfigStats();
18916
18404
  console.log(chalk.bold("@hasna/instructions") + chalk.dim(" v" + pkg.version));
18917
- console.log(chalk.cyan(isApiTransport() ? "API:" : "DB:") + " " + dbPath);
18405
+ console.log(chalk.cyan(isCloudMode() ? "API:" : "DB:") + " " + dbPath);
18918
18406
  console.log(chalk.cyan("Total configs:") + " " + (stats["total"] || 0));
18919
18407
  console.log();
18920
18408
  console.log(chalk.bold("By category:"));
@@ -19302,6 +18790,8 @@ sessionCmd.command("plan").description("Produce a dry-run render plan for profil
19302
18790
  ...planJsonForOutput(plan),
19303
18791
  ...globalCoverage ? { globalSourceCoverage: globalCoverage } : {}
19304
18792
  });
18793
+ if (plan.blocked)
18794
+ process.exitCode = 1;
19305
18795
  return;
19306
18796
  }
19307
18797
  console.log(chalk.bold(`${plan.tool} session render plan`) + chalk.dim(` (${plan.adapter.mode})`));
@@ -19328,6 +18818,8 @@ sessionCmd.command("plan").description("Produce a dry-run render plan for profil
19328
18818
  if (globalCoverage.complete)
19329
18819
  console.log(chalk.dim(`global source coverage: ${globalCoverage.expectedSlugs.length}/${globalCoverage.expectedSlugs.length} complete`));
19330
18820
  }
18821
+ if (plan.blocked)
18822
+ process.exitCode = 1;
19331
18823
  console.log(chalk.dim("Dry run only. No files were written."));
19332
18824
  } catch (e) {
19333
18825
  console.error(chalk.red(formatCliError(e)));
@@ -19756,9 +19248,9 @@ DB stats:`));
19756
19248
  if (count > 0)
19757
19249
  console.log(` ${key.padEnd(18)} ${count}`);
19758
19250
  }
19759
- const location = isApiTransport() ? `${process.env["HASNA_INSTRUCTIONS_API_URL"]}/v1` : process.env["HASNA_INSTRUCTIONS_DB_PATH"] || join16(homedir8(), ".hasna", "instructions", "instructions.db");
19251
+ const location = isCloudMode() ? `${process.env["HASNA_INSTRUCTIONS_API_URL"]}/v1 (self_hosted)` : process.env["HASNA_INSTRUCTIONS_DB_PATH"] || join16(homedir8(), ".hasna", "instructions", "instructions.db");
19760
19252
  console.log(chalk.dim(`
19761
- ${isApiTransport() ? "API" : "DB"}: ${location}`));
19253
+ ${isCloudMode() ? "API" : "DB"}: ${location}`));
19762
19254
  });
19763
19255
  program.command("status").description("Health check: total configs, drift from disk, unredacted secrets").option("--json", "output metadata-only JSON").action(async (opts) => {
19764
19256
  const status = await getConfigsStatus(resolveConfigStore());
@@ -19799,7 +19291,7 @@ program.command("doctor").description("Validate configs: syntax, permissions, mi
19799
19291
  const store = resolveConfigStore();
19800
19292
  let issues = 0;
19801
19293
  const pass = (msg) => console.log(chalk.green(" \u2713 ") + msg);
19802
- const fail2 = (msg) => {
19294
+ const fail = (msg) => {
19803
19295
  issues++;
19804
19296
  console.log(chalk.red(" \u2717 ") + msg);
19805
19297
  };
@@ -19809,9 +19301,9 @@ program.command("doctor").description("Validate configs: syntax, permissions, mi
19809
19301
  console.log(chalk.cyan("Known files on disk:"));
19810
19302
  for (const k of KNOWN_CONFIGS) {
19811
19303
  if (k.rulesDir) {
19812
- existsSync16(expandPath(k.rulesDir)) ? pass(`${k.rulesDir}/ exists`) : k.optional ? skip(`${k.rulesDir}/ (optional)`) : fail2(`${k.rulesDir}/ not found`);
19304
+ existsSync16(expandPath(k.rulesDir)) ? pass(`${k.rulesDir}/ exists`) : k.optional ? skip(`${k.rulesDir}/ (optional)`) : fail(`${k.rulesDir}/ not found`);
19813
19305
  } else {
19814
- existsSync16(expandPath(k.path)) ? pass(k.path) : k.optional ? skip(`${k.path} (optional)`) : fail2(`${k.path} not found`);
19306
+ existsSync16(expandPath(k.path)) ? pass(k.path) : k.optional ? skip(`${k.path} (optional)`) : fail(`${k.path} not found`);
19815
19307
  }
19816
19308
  }
19817
19309
  const allConfigs = await store.listConfigs();
@@ -19824,7 +19316,7 @@ Stored configs (${allConfigs.length}):`));
19824
19316
  JSON.parse(c.content);
19825
19317
  validCount++;
19826
19318
  } catch {
19827
- fail2(`${c.slug}: invalid JSON`);
19319
+ fail(`${c.slug}: invalid JSON`);
19828
19320
  }
19829
19321
  } else {
19830
19322
  validCount++;
@@ -19836,13 +19328,13 @@ Stored configs (${allConfigs.length}):`));
19836
19328
  const found = scanSecrets(c.content, c.format);
19837
19329
  secretCount += found.length;
19838
19330
  }
19839
- secretCount === 0 ? pass("No unredacted secrets") : fail2(`${secretCount} unredacted secret(s) \u2014 run \`configs scan --fix\``);
19331
+ secretCount === 0 ? pass("No unredacted secrets") : fail(`${secretCount} unredacted secret(s) \u2014 run \`configs scan --fix\``);
19840
19332
  const duplicateTargets = findDuplicateTargetPathGroups(allConfigs);
19841
19333
  if (duplicateTargets.length === 0) {
19842
19334
  pass("No target path is claimed by more than one config");
19843
19335
  } else {
19844
19336
  const rowCount = duplicateTargets.reduce((total, group) => total + group.configs.length, 0);
19845
- fail2(`${duplicateTargets.length} target path(s) claimed by more than one config (${rowCount} rows) \u2014 apply order between them is undefined`);
19337
+ fail(`${duplicateTargets.length} target path(s) claimed by more than one config (${rowCount} rows) \u2014 apply order between them is undefined`);
19846
19338
  for (const group of duplicateTargets) {
19847
19339
  console.log(chalk.yellow(` ${group.target_path}`));
19848
19340
  for (const c of group.configs) {
@@ -19856,7 +19348,7 @@ Stored configs (${allConfigs.length}):`));
19856
19348
  pass("No reference config name is claimed by more than one row");
19857
19349
  } else {
19858
19350
  const rowCount = duplicateReferenceNames.reduce((total, group) => total + group.configs.length, 0);
19859
- fail2(`${duplicateReferenceNames.length} reference name(s) claimed by more than one row (${rowCount} rows) \u2014 only one is live in the next render`);
19351
+ fail(`${duplicateReferenceNames.length} reference name(s) claimed by more than one row (${rowCount} rows) \u2014 only one is live in the next render`);
19860
19352
  for (const group of duplicateReferenceNames) {
19861
19353
  console.log(chalk.yellow(` ${group.name}`));
19862
19354
  for (const c of group.configs) {