@hasna/contacts 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +93 -13
  2. package/dist/browser/cli.d.ts +4 -0
  3. package/dist/browser/cli.d.ts.map +1 -0
  4. package/dist/browser/identity.d.ts +3 -0
  5. package/dist/browser/identity.d.ts.map +1 -0
  6. package/dist/browser/install.d.ts +32 -0
  7. package/dist/browser/install.d.ts.map +1 -0
  8. package/dist/browser/native.d.ts +3 -0
  9. package/dist/browser/native.d.ts.map +1 -0
  10. package/dist/browser/native.js +822 -0
  11. package/dist/browser/protocol.d.ts +26 -0
  12. package/dist/browser/protocol.d.ts.map +1 -0
  13. package/dist/browser/values.d.ts +18 -0
  14. package/dist/browser/values.d.ts.map +1 -0
  15. package/dist/cli/commands/core.d.ts.map +1 -1
  16. package/dist/cli/index.js +479 -149
  17. package/dist/cli/status-domain.preload.d.ts +1 -1
  18. package/dist/cli/status-domain.preload.d.ts.map +1 -1
  19. package/dist/cli/status-fixture.d.ts +17 -0
  20. package/dist/cli/status-fixture.d.ts.map +1 -0
  21. package/dist/cloud/http-storage.d.ts +20 -12
  22. package/dist/cloud/http-storage.d.ts.map +1 -1
  23. package/dist/cloud/resolver-inputs.d.ts +51 -0
  24. package/dist/cloud/resolver-inputs.d.ts.map +1 -0
  25. package/dist/db/paths.d.ts +4 -4
  26. package/dist/db/paths.d.ts.map +1 -1
  27. package/dist/generated/storage-kit/backend.d.ts +19 -0
  28. package/dist/generated/storage-kit/backend.d.ts.map +1 -0
  29. package/dist/generated/storage-kit/index.d.ts +2 -1
  30. package/dist/generated/storage-kit/index.d.ts.map +1 -1
  31. package/dist/generated/storage-kit/migrations.d.ts +21 -0
  32. package/dist/generated/storage-kit/migrations.d.ts.map +1 -1
  33. package/dist/generated/storage-kit/own.d.ts +11 -0
  34. package/dist/generated/storage-kit/own.d.ts.map +1 -0
  35. package/dist/generated/storage-kit/pool.d.ts +5 -17
  36. package/dist/generated/storage-kit/pool.d.ts.map +1 -1
  37. package/dist/generated/storage-kit/query.d.ts +1 -1
  38. package/dist/generated/storage-kit/query.d.ts.map +1 -1
  39. package/dist/generated/storage-kit/tls.d.ts +30 -3
  40. package/dist/generated/storage-kit/tls.d.ts.map +1 -1
  41. package/dist/index.d.ts +2 -2
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +148 -28
  44. package/dist/lib/audience-contract.d.ts +9 -9
  45. package/dist/lib/compact-output.d.ts +67 -0
  46. package/dist/lib/compact-output.d.ts.map +1 -0
  47. package/dist/mcp/handlers/core.d.ts.map +1 -1
  48. package/dist/mcp/index.d.ts +11 -1
  49. package/dist/mcp/index.d.ts.map +1 -1
  50. package/dist/mcp/index.js +490 -212
  51. package/dist/mcp/profile.d.ts +8 -0
  52. package/dist/mcp/profile.d.ts.map +1 -0
  53. package/dist/mcp/register-tools.d.ts +3 -1
  54. package/dist/mcp/register-tools.d.ts.map +1 -1
  55. package/dist/mcp/startup-gate.d.ts +48 -0
  56. package/dist/mcp/startup-gate.d.ts.map +1 -0
  57. package/dist/mcp/storage-tools.d.ts +2 -1
  58. package/dist/mcp/storage-tools.d.ts.map +1 -1
  59. package/dist/mcp/tools.d.ts +7 -45
  60. package/dist/mcp/tools.d.ts.map +1 -1
  61. package/dist/sdk/index.d.ts +70 -3
  62. package/dist/sdk/index.d.ts.map +1 -1
  63. package/dist/sdk/index.js +990 -7
  64. package/dist/server/index.js +231 -59
  65. package/dist/server/pg-store.d.ts +1 -0
  66. package/dist/server/pg-store.d.ts.map +1 -1
  67. package/dist/store/index.d.ts.map +1 -1
  68. package/docs/chrome-autofill.md +45 -0
  69. package/extension/background.js +284 -0
  70. package/extension/content.js +387 -0
  71. package/extension/detect.js +228 -0
  72. package/extension/fill.js +112 -0
  73. package/extension/icons/128.png +0 -0
  74. package/extension/icons/16.png +0 -0
  75. package/extension/icons/32.png +0 -0
  76. package/extension/icons/48.png +0 -0
  77. package/extension/icons/icon.svg +17 -0
  78. package/extension/manifest.json +54 -0
  79. package/extension/popup.css +78 -0
  80. package/extension/popup.html +47 -0
  81. package/extension/popup.js +166 -0
  82. package/hasna.contract.json +20 -6
  83. package/package.json +10 -6
  84. package/dist/lib/config.d.ts +0 -7
  85. package/dist/lib/config.d.ts.map +0 -1
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");
@@ -1290,8 +1290,8 @@ Expecting one of '${allowedValues.join("', '")}'`);
1290
1290
  args = args.slice();
1291
1291
  let launchWithNode = false;
1292
1292
  const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
1293
- function findFile(baseDir, baseName) {
1294
- const localBin = path.resolve(baseDir, baseName);
1293
+ function findFile(baseDir2, baseName) {
1294
+ const localBin = path.resolve(baseDir2, baseName);
1295
1295
  if (fs.existsSync(localBin))
1296
1296
  return localBin;
1297
1297
  if (sourceExt.includes(path.extname(baseName)))
@@ -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();
@@ -2182,7 +2182,7 @@ Only include fields that are actually visible in the document. Return valid JSON
2182
2182
  }
2183
2183
  var init_document_scanner = () => {};
2184
2184
 
2185
- // ../events/dist/commander.js
2185
+ // node_modules/@hasna/events/dist/commander.js
2186
2186
  var exports_commander = {};
2187
2187
  __export(exports_commander, {
2188
2188
  registerEventsCommands: () => registerEventsCommands,
@@ -2193,12 +2193,12 @@ __export(exports_commander, {
2193
2193
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
2194
2194
  import { Buffer as Buffer2 } from "buffer";
2195
2195
  import { existsSync as existsSync2 } from "fs";
2196
- import { join as join2 } from "path";
2196
+ import { join as join3 } from "path";
2197
2197
  import { existsSync } from "fs";
2198
+ import { homedir as homedir2 } from "os";
2199
+ import { join as join2, resolve } from "path";
2198
2200
  import { homedir } from "os";
2199
- import { join, resolve } from "path";
2200
- import { homedir as pathsResolverHomedir } from "os";
2201
- import { join as pathsResolverJoin } from "path";
2201
+ import { join } from "path";
2202
2202
  import { createHmac, timingSafeEqual } from "crypto";
2203
2203
  import { lookup as dnsLookup } from "dns/promises";
2204
2204
  import { isIP } from "net";
@@ -2302,72 +2302,75 @@ function channelMatchesEvent(channel, event) {
2302
2302
  return true;
2303
2303
  return channel.filters.some((filter) => eventMatchesFilter(event, filter));
2304
2304
  }
2305
- var PATHS_RESOLVER_KIND_ENV = {
2305
+ var KIND_ENV = {
2306
2306
  config: "HASNA_CONFIG_HOME",
2307
2307
  data: "HASNA_DATA_HOME",
2308
2308
  state: "HASNA_STATE_HOME",
2309
2309
  cache: "HASNA_CACHE_HOME"
2310
2310
  };
2311
- var PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
2312
- function pathsResolverAssertApp(app) {
2311
+ var APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
2312
+ function assertApp(app) {
2313
2313
  if (typeof app !== "string" || app.length === 0) {
2314
2314
  throw new TypeError("paths: app must be a non-empty string");
2315
2315
  }
2316
- if (!PATHS_RESOLVER_APP_SLUG_RE.test(app)) {
2316
+ if (!APP_SLUG_RE.test(app)) {
2317
2317
  throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
2318
2318
  }
2319
2319
  }
2320
- function pathsResolverAssertKind(kind) {
2321
- if (!Object.keys(PATHS_RESOLVER_KIND_ENV).includes(kind)) {
2322
- throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV).join(", ")}`);
2323
- }
2320
+ function envOf(options) {
2321
+ return options.env ?? process.env;
2324
2322
  }
2325
- function pathsResolverBaseDir(kind, options) {
2326
- pathsResolverAssertKind(kind);
2327
- const env = options.env ?? process.env;
2328
- const override = env[PATHS_RESOLVER_KIND_ENV[kind]];
2329
- if (typeof override === "string" && override.length > 0)
2323
+ function envValue(options, kind) {
2324
+ const value = envOf(options)[KIND_ENV[kind]];
2325
+ return typeof value === "string" && value.length > 0 ? value : undefined;
2326
+ }
2327
+ function isMacOS(platform) {
2328
+ return platform === "darwin";
2329
+ }
2330
+ function baseDir(kind, options) {
2331
+ const override = envValue(options, kind);
2332
+ if (override)
2330
2333
  return override;
2331
- const home = options.home ?? pathsResolverHomedir();
2334
+ const home = options.home ?? homedir();
2332
2335
  const platform = options.platform ?? process.platform;
2333
- if (platform === "darwin") {
2336
+ if (isMacOS(platform)) {
2334
2337
  switch (kind) {
2335
2338
  case "config":
2336
2339
  case "data":
2337
- return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
2340
+ return join(home, "Library", "Application Support", "Hasna");
2338
2341
  case "cache":
2339
- return pathsResolverJoin(home, "Library", "Caches", "Hasna");
2342
+ return join(home, "Library", "Caches", "Hasna");
2340
2343
  case "state":
2341
- return pathsResolverJoin(home, "Library", "Logs", "Hasna");
2344
+ return join(home, "Library", "Logs", "Hasna");
2342
2345
  }
2343
2346
  }
2344
2347
  switch (kind) {
2345
2348
  case "config":
2346
- return pathsResolverJoin(home, ".config", "hasna");
2349
+ return join(home, ".config", "hasna");
2347
2350
  case "data":
2348
- return pathsResolverJoin(home, ".local", "share", "hasna");
2351
+ return join(home, ".local", "share", "hasna");
2349
2352
  case "state":
2350
- return pathsResolverJoin(home, ".local", "state", "hasna");
2353
+ return join(home, ".local", "state", "hasna");
2351
2354
  case "cache":
2352
- return pathsResolverJoin(home, ".cache", "hasna");
2355
+ return join(home, ".cache", "hasna");
2353
2356
  }
2354
2357
  }
2355
- function pathsResolverResolve(kind, options) {
2356
- pathsResolverAssertApp(options.app);
2357
- const appSegment = options.internal === true ? pathsResolverJoin("internal", options.app) : options.app;
2358
- return pathsResolverJoin(pathsResolverBaseDir(kind, options), appSegment);
2358
+ function resolvePath(kind, options) {
2359
+ assertApp(options.app);
2360
+ const appSegment = options.internal === true ? join("internal", options.app) : options.app;
2361
+ return join(baseDir(kind, options), appSegment);
2359
2362
  }
2360
2363
  function dataDir(options) {
2361
- return pathsResolverResolve("data", options);
2364
+ return resolvePath("data", options);
2362
2365
  }
2363
2366
  var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
2364
2367
  var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
2365
2368
  var EVENTS_STORE_SENTINEL_FILE = "events.json";
2366
2369
  function effectiveHome() {
2367
- return process.env["HOME"] || process.env["USERPROFILE"] || homedir();
2370
+ return process.env["HOME"] || process.env["USERPROFILE"] || homedir2();
2368
2371
  }
2369
2372
  function legacyHomeDir() {
2370
- return join(effectiveHome(), ".hasna", "events");
2373
+ return join2(effectiveHome(), ".hasna", "events");
2371
2374
  }
2372
2375
  function resolverHome() {
2373
2376
  return dataDir({ app: "events", home: effectiveHome() || undefined });
@@ -2376,7 +2379,7 @@ function adoptResolverHome(resolved, env = process.env) {
2376
2379
  const dataOverride = env.HASNA_DATA_HOME;
2377
2380
  if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
2378
2381
  return true;
2379
- return existsSync(join(resolved, EVENTS_STORE_SENTINEL_FILE));
2382
+ return existsSync(join2(resolved, EVENTS_STORE_SENTINEL_FILE));
2380
2383
  }
2381
2384
  function exactEventsHome() {
2382
2385
  const dir = process.env[HASNA_EVENTS_DIR_ENV];
@@ -2417,9 +2420,9 @@ class JsonEventsStore {
2417
2420
  constructor(dataDir2 = getEventsDataDir()) {
2418
2421
  this.dataDir = dataDir2;
2419
2422
  this.runtime = localJsonRuntime(dataDir2);
2420
- this.channelsPath = join2(dataDir2, "channels.json");
2421
- this.eventsPath = join2(dataDir2, "events.json");
2422
- this.deliveriesPath = join2(dataDir2, "deliveries.json");
2423
+ this.channelsPath = join3(dataDir2, "channels.json");
2424
+ this.eventsPath = join3(dataDir2, "events.json");
2425
+ this.deliveriesPath = join3(dataDir2, "deliveries.json");
2423
2426
  }
2424
2427
  async init() {
2425
2428
  await mkdir(this.dataDir, { recursive: true, mode: 448 });
@@ -2687,7 +2690,7 @@ async function getEventsStatus(dataDir2) {
2687
2690
  };
2688
2691
  }
2689
2692
  function statusFile(dataDir2, fileName, records) {
2690
- const path = join2(dataDir2, fileName);
2693
+ const path = join3(dataDir2, fileName);
2691
2694
  return { path, exists: existsSync2(path), records };
2692
2695
  }
2693
2696
  var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
@@ -3816,7 +3819,7 @@ function replaySummary(events, deliveries, nextCursor) {
3816
3819
  return `Replayed ${events} event(s), ${deliveries} delivery result(s)${suffix}`;
3817
3820
  }
3818
3821
 
3819
- // ../../node_modules/.bun/commander@13.1.0/node_modules/commander/esm.mjs
3822
+ // node_modules/commander/esm.mjs
3820
3823
  var import__ = __toESM(require_commander(), 1);
3821
3824
  var {
3822
3825
  program,
@@ -3840,6 +3843,7 @@ import chalk2 from "chalk";
3840
3843
 
3841
3844
  // src/cloud/http-storage.ts
3842
3845
  import {
3846
+ ClientTransportConfigurationError,
3843
3847
  createHasnaHttpTransport,
3844
3848
  resolveCredential,
3845
3849
  resolveClientTransport as resolveSharedClientTransport
@@ -3929,6 +3933,27 @@ function assertConfigurationUnchanged(env, expected) {
3929
3933
  invalid();
3930
3934
  }
3931
3935
 
3936
+ // src/cloud/resolver-inputs.ts
3937
+ import {
3938
+ clientTransportEnvKeys as clientTransportEnvKeys2,
3939
+ credentialOverrideEnvKey,
3940
+ credentialPointerEnvKey,
3941
+ CREDENTIAL_PROFILE_ENV_KEY
3942
+ } from "@hasna/contracts/client";
3943
+ var CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
3944
+ function isAmbientContactsEnv(env) {
3945
+ if (typeof process !== "undefined" && env === process.env)
3946
+ return true;
3947
+ return env[CONTRACTS_AMBIENT_ENVIRONMENT] === true;
3948
+ }
3949
+ function contactsResolverCredentials(env, credentials = {}) {
3950
+ const keychain = { ...credentials.keychain };
3951
+ if (keychain.enabled === undefined && keychain.run === undefined) {
3952
+ keychain.enabled = isAmbientContactsEnv(env);
3953
+ }
3954
+ return { ...credentials, keychain };
3955
+ }
3956
+
3932
3957
  // src/cloud/http-storage.ts
3933
3958
  var RETIRED_CLIENT_SELECTOR_KEYS = [
3934
3959
  "HASNA_CONTACTS_STORAGE_MODE",
@@ -3964,37 +3989,44 @@ class ContactsClientConfigurationError extends Error {
3964
3989
  this.name = "ContactsClientConfigurationError";
3965
3990
  }
3966
3991
  }
3967
- function unconfiguredResolution(resolution, issue) {
3992
+ function unconfiguredResolution(issue, resolution, warning = null) {
3968
3993
  return {
3969
3994
  transport: "unconfigured",
3970
3995
  baseUrl: null,
3971
- apiUrlSource: resolution.apiUrlSource,
3972
- apiKeyPresent: resolution.apiKeyPresent,
3973
- apiKeySource: resolution.apiKeySource,
3974
- apiKeyTier: resolution.apiKeyTier,
3996
+ apiUrlSource: resolution?.apiUrlSource ?? null,
3997
+ apiKeyPresent: resolution?.apiKeyPresent ?? false,
3998
+ apiKeySource: resolution?.apiKeySource ?? null,
3999
+ apiKeyTier: resolution?.apiKeyTier ?? null,
3975
4000
  configured: false,
3976
4001
  misconfigured: true,
3977
4002
  issue,
3978
- warning: null
4003
+ warning
3979
4004
  };
3980
4005
  }
3981
- function resolveContactsClientTransport(name, env = process.env) {
4006
+ function resolveContactsClientTransport(name, env = process.env, credentials = {}) {
3982
4007
  if (name !== "contacts") {
3983
4008
  throw new ContactsClientConfigurationError("CONTACTS_CLIENT_NAME_INVALID", "This resolver only accepts the contacts app slug.");
3984
4009
  }
3985
4010
  assertNoRetiredClientSelectors(env);
3986
4011
  const stamp = clientConfigurationStamp(env);
3987
- const resolution = resolveSharedClientTransport(name, env);
3988
- assertConfigurationUnchanged(env, stamp);
3989
- if (resolution.transport !== "http" || !resolution.baseUrl) {
3990
- const issue = resolution.misconfigured ? "The configured contacts API URL or credential is invalid or incomplete." : "HASNA_CONTACTS_API_URL and a resolvable contacts API key are required; no local fallback exists.";
3991
- return unconfiguredResolution(resolution, issue);
4012
+ let resolution;
4013
+ try {
4014
+ resolution = resolveSharedClientTransport(name, env, {
4015
+ credentials: contactsResolverCredentials(env, credentials)
4016
+ });
4017
+ } catch (error) {
4018
+ assertConfigurationUnchanged(env, stamp);
4019
+ if (error instanceof ClientTransportConfigurationError) {
4020
+ return unconfiguredResolution(error.message, null);
4021
+ }
4022
+ throw error;
3992
4023
  }
4024
+ assertConfigurationUnchanged(env, stamp);
3993
4025
  try {
3994
4026
  assertHttpsBaseUrl(resolution.baseUrl);
3995
4027
  } catch (error) {
3996
4028
  return {
3997
- ...unconfiguredResolution(resolution, error instanceof Error ? error.message : String(error)),
4029
+ ...unconfiguredResolution(error instanceof Error ? error.message : String(error), resolution),
3998
4030
  apiKeyPresent: resolution.apiKeyPresent
3999
4031
  };
4000
4032
  }
@@ -4008,7 +4040,7 @@ function resolveContactsClientTransport(name, env = process.env) {
4008
4040
  configured: true,
4009
4041
  misconfigured: false,
4010
4042
  issue: null,
4011
- warning: null
4043
+ warning: resolution.warning
4012
4044
  };
4013
4045
  }
4014
4046
  function resourcePath(resource) {
@@ -4062,20 +4094,21 @@ function createStorageClient(name, transport) {
4062
4094
  }
4063
4095
  };
4064
4096
  }
4065
- function resolveContactsStorageClient(name, env = process.env) {
4066
- const resolution = resolveContactsClientTransport(name, env);
4097
+ function resolveContactsStorageClient(name, env = process.env, credentials = {}) {
4098
+ const resolution = resolveContactsClientTransport(name, env, credentials);
4067
4099
  if (!resolution.configured || !resolution.baseUrl) {
4068
4100
  throw new ContactsClientConfigurationError("CONTACTS_API_NOT_CONFIGURED", `${resolution.issue ?? "The contacts API client is not configured."} ` + "Configure HASNA_CONTACTS_API_URL and a contacts API key; the client will not read or create a local SQLite database.");
4069
4101
  }
4070
4102
  const baseUrl = resolution.baseUrl;
4103
+ const chainOptions = contactsResolverCredentials(env, credentials);
4071
4104
  const request = async (method, path, body, opts) => {
4072
4105
  const stamp = clientConfigurationStamp(env);
4073
4106
  const snapshot = { ...env };
4074
- const current = resolveContactsClientTransport(name, snapshot);
4107
+ const current = resolveContactsClientTransport(name, snapshot, chainOptions);
4075
4108
  if (!current.configured || current.baseUrl !== baseUrl) {
4076
4109
  throw new ContactsClientConfigurationError("CONTACTS_AUTHORITY_CHANGED", "Client authority changed or disappeared; construct a new client before sending data.");
4077
4110
  }
4078
- const credential = resolveCredential(name, snapshot);
4111
+ const credential = resolveCredential(name, snapshot, chainOptions);
4079
4112
  if (!credential)
4080
4113
  throw new ContactsClientConfigurationError("CONTACTS_API_NOT_CONFIGURED", "No credential is available.");
4081
4114
  assertConfigurationUnchanged(env, stamp);
@@ -4125,6 +4158,39 @@ function stripUndefined(input) {
4125
4158
  return out;
4126
4159
  }
4127
4160
 
4161
+ class ContactsApiResponseError extends Error {
4162
+ constructor(operation, detail) {
4163
+ super(`contacts: malformed /v1 response for ${operation}: ${detail}`);
4164
+ this.name = "ContactsApiResponseError";
4165
+ }
4166
+ }
4167
+ function requireAppendedContact(response, contactId, collection, matchKey, value) {
4168
+ const operation = collection === "emails" ? "addEmailToContact" : "addPhoneToContact";
4169
+ const contact = pick(response, "contact");
4170
+ if (!contact || typeof contact !== "object" || Array.isArray(contact)) {
4171
+ throw new ContactsApiResponseError(operation, "expected an object at response.contact");
4172
+ }
4173
+ if (contact.id !== contactId) {
4174
+ throw new ContactsApiResponseError(operation, "response.contact.id did not match the requested contact");
4175
+ }
4176
+ const items = contact[collection];
4177
+ if (!Array.isArray(items)) {
4178
+ throw new ContactsApiResponseError(operation, `response.contact.${collection} was not an array`);
4179
+ }
4180
+ const normalize = collection === "emails" ? (input) => input.toLowerCase() : (input) => input;
4181
+ const wanted = normalize(value);
4182
+ const appended = items.find((item) => {
4183
+ if (!item || typeof item !== "object" || Array.isArray(item))
4184
+ return false;
4185
+ const record = item;
4186
+ return typeof record.id === "string" && record.contact_id === contactId && typeof record[matchKey] === "string" && normalize(record[matchKey]) === wanted;
4187
+ });
4188
+ if (!appended) {
4189
+ throw new ContactsApiResponseError(operation, `response.contact.${collection} did not contain the requested value`);
4190
+ }
4191
+ return contact;
4192
+ }
4193
+
4128
4194
  class ApiStore {
4129
4195
  client;
4130
4196
  mode = "api";
@@ -4194,11 +4260,17 @@ class ApiStore {
4194
4260
  async mergeContacts() {
4195
4261
  return unavailable("mergeContacts");
4196
4262
  }
4197
- async addEmailToContact() {
4198
- return unavailable("addEmailToContact");
4263
+ async addEmailToContact(contactId, email) {
4264
+ const res = await this.patch(`/contacts/${this.enc(contactId)}`, {
4265
+ emails_add: [stripUndefined(email)]
4266
+ });
4267
+ return requireAppendedContact(res, contactId, "emails", "address", email.address);
4199
4268
  }
4200
- async addPhoneToContact() {
4201
- return unavailable("addPhoneToContact");
4269
+ async addPhoneToContact(contactId, phone) {
4270
+ const res = await this.patch(`/contacts/${this.enc(contactId)}`, {
4271
+ phones_add: [stripUndefined(phone)]
4272
+ });
4273
+ return requireAppendedContact(res, contactId, "phones", "number", phone.number);
4202
4274
  }
4203
4275
  async archiveContact() {
4204
4276
  return unavailable("archiveContact");
@@ -5369,18 +5441,117 @@ async function confirmUser(question) {
5369
5441
  return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
5370
5442
  }
5371
5443
 
5444
+ // src/lib/compact-output.ts
5445
+ var CONTACTS_COMPACT_LIMIT = 20;
5446
+ var CONTACTS_MAX_LIMIT = 500;
5447
+ function normalizeContactsLimit(value, fallback = CONTACTS_COMPACT_LIMIT) {
5448
+ const parsed = typeof value === "number" ? value : Number(value ?? fallback);
5449
+ if (!Number.isFinite(parsed))
5450
+ return fallback;
5451
+ return Math.max(1, Math.min(CONTACTS_MAX_LIMIT, Math.trunc(parsed)));
5452
+ }
5453
+ function normalizeContactsOffset(value) {
5454
+ const parsed = typeof value === "number" ? value : Number(value ?? 0);
5455
+ if (!Number.isFinite(parsed))
5456
+ return 0;
5457
+ return Math.max(0, Math.trunc(parsed));
5458
+ }
5459
+ function summarizeContact(contact) {
5460
+ return {
5461
+ id: contact.id,
5462
+ display_name: contact.display_name,
5463
+ job_title: contact.job_title,
5464
+ company: contact.company ? { id: contact.company.id, name: contact.company.name } : null,
5465
+ primary_email: contact.emails?.find((email) => email.is_primary)?.address ?? contact.emails?.[0]?.address ?? null,
5466
+ primary_phone: contact.phones?.find((phone) => phone.is_primary)?.number ?? contact.phones?.[0]?.number ?? null,
5467
+ status: contact.status,
5468
+ follow_up_at: contact.follow_up_at,
5469
+ last_contacted_at: contact.last_contacted_at,
5470
+ archived: contact.archived,
5471
+ sensitivity: contact.sensitivity,
5472
+ tags: (contact.tags ?? []).map((tag) => ({ id: tag.id, name: tag.name })),
5473
+ updated_at: contact.updated_at
5474
+ };
5475
+ }
5476
+ function compareContactsByDisplayNameAndId(left, right) {
5477
+ return left.display_name.localeCompare(right.display_name) || left.id.localeCompare(right.id);
5478
+ }
5479
+ function compactContactsPage(contacts, options) {
5480
+ const ordered = [...contacts].sort(compareContactsByDisplayNameAndId);
5481
+ const nextCursor = options.offset + ordered.length < options.total ? options.offset + contacts.length : null;
5482
+ return {
5483
+ contacts: ordered.map(summarizeContact),
5484
+ count: ordered.length,
5485
+ total: options.total,
5486
+ limit: options.limit,
5487
+ cursor: options.offset,
5488
+ next_cursor: nextCursor,
5489
+ has_more: nextCursor !== null,
5490
+ compact: true,
5491
+ sort: { fields: ["display_name", "id"], direction: "asc" },
5492
+ hint: "Use contacts show <id> for one full record; pass --full for the legacy list payload."
5493
+ };
5494
+ }
5495
+
5496
+ // src/mcp/profile.ts
5497
+ var CONTACTS_FULL_TOOL_COUNT = 190;
5498
+ var CONTACTS_CORE_TOOL_NAMES = [
5499
+ "create_contact",
5500
+ "get_contact",
5501
+ "update_contact",
5502
+ "delete_contact",
5503
+ "list_contacts",
5504
+ "search_contacts",
5505
+ "find_or_create_contact",
5506
+ "upsert_contact",
5507
+ "get_contact_by_email",
5508
+ "add_email_to_contact",
5509
+ "add_phone_to_contact",
5510
+ "create_company",
5511
+ "get_company",
5512
+ "update_company",
5513
+ "delete_company",
5514
+ "list_companies",
5515
+ "search_companies",
5516
+ "create_tag",
5517
+ "list_tags",
5518
+ "add_tag_to_contact",
5519
+ "remove_tag_from_contact",
5520
+ "add_relationship",
5521
+ "list_relationships",
5522
+ "delete_relationship",
5523
+ "get_contact_brief",
5524
+ "get_contact_timeline",
5525
+ "get_recent_contact_events",
5526
+ "send_feedback",
5527
+ "contacts_connection_status",
5528
+ "search_tools",
5529
+ "describe_tools"
5530
+ ];
5531
+ var CORE_TOOLS = new Set(CONTACTS_CORE_TOOL_NAMES);
5532
+
5372
5533
  // src/cli/commands/core.tsx
5373
5534
  function collect(val, prev) {
5374
5535
  return [...prev, val];
5375
5536
  }
5376
5537
  function registerCoreCommands(program2, version) {
5377
5538
  program2.command("status").description("Show CLI version, API endpoint, storage mode, and record counts").option("--json", "Output as JSON").action(async (opts) => {
5378
- const baseUrl = process.env["HASNA_CONTACTS_API_URL"]?.trim();
5379
- const apiLine = baseUrl ? baseUrl : "(not configured \u2014 set HASNA_CONTACTS_API_URL plus a contacts API key)";
5539
+ let apiLine = "(not configured \u2014 set HASNA_CONTACTS_API_URL plus a contacts API key)";
5540
+ let apiUrlSource = null;
5541
+ let apiKeySource = null;
5542
+ let apiKeyTier = null;
5543
+ let issue = null;
5380
5544
  let counts;
5381
5545
  let storage;
5382
5546
  let failure;
5383
5547
  try {
5548
+ const resolution = resolveContactsClientTransport("contacts");
5549
+ apiUrlSource = resolution.apiUrlSource;
5550
+ apiKeySource = resolution.apiKeySource;
5551
+ apiKeyTier = resolution.apiKeyTier;
5552
+ issue = resolution.issue;
5553
+ if (resolution.configured && resolution.baseUrl)
5554
+ apiLine = resolution.baseUrl;
5384
5555
  const store = getStore();
5385
5556
  const [contacts, companies] = await Promise.all([
5386
5557
  store.listContacts({ limit: 1 }),
@@ -5400,7 +5571,11 @@ function registerCoreCommands(program2, version) {
5400
5571
  service: "contacts",
5401
5572
  version,
5402
5573
  api: apiLine,
5574
+ api_url_source: apiUrlSource,
5575
+ api_key_source: apiKeySource,
5576
+ api_key_tier: apiKeyTier,
5403
5577
  storage,
5578
+ ...issue !== null ? { issue } : {},
5404
5579
  ...failure !== undefined ? { error: failure } : {},
5405
5580
  ...counts ? { counts } : {}
5406
5581
  };
@@ -5410,7 +5585,13 @@ function registerCoreCommands(program2, version) {
5410
5585
  }
5411
5586
  console.log(chalk2.bold(`contacts v${version}`));
5412
5587
  console.log(`API: ${apiLine}`);
5588
+ if (apiUrlSource)
5589
+ console.log(`API URL source: ${apiUrlSource}`);
5590
+ if (apiKeySource)
5591
+ console.log(`API key source: ${apiKeySource}${apiKeyTier ? ` (${apiKeyTier})` : ""}`);
5413
5592
  console.log(`Storage: ${storage}`);
5593
+ if (issue !== null)
5594
+ console.log(chalk2.yellow(`Issue: ${issue}`));
5414
5595
  if (failure !== undefined)
5415
5596
  console.log(chalk2.red(`Error: ${failure}`));
5416
5597
  if (counts) {
@@ -5482,19 +5663,19 @@ Add New Contact
5482
5663
  \u2713 Contact created: ${contact.display_name} (${contact.id})
5483
5664
  `));
5484
5665
  });
5485
- program2.command("list").description("List contacts").option("--tag <tag_id>", "Filter by tag ID").option("--company <id>", "Filter by company ID").option("--include-restricted", "Include restricted-sensitivity contacts").option("-l, --limit <n>", "Max results", "50").option("-o, --offset <n>", "Skip first N results", "0").option("--order-by <field>", "Sort field: display_name|created_at|updated_at|last_contacted_at|follow_up_at", "display_name").option("--order-dir <dir>", "Sort direction: asc|desc", "asc").option("-j, --json", "Output JSON").action(async (opts) => {
5666
+ program2.command("list").description("List contacts").option("--tag <tag_id>", "Filter by tag ID").option("--company <id>", "Filter by company ID").option("-l, --limit <n>", "Max results (default 20; --full defaults to 50)").option("-o, --offset <n>", "Skip first N results", "0").option("--status <status>", "Filter by hosted lifecycle status").option("-j, --json", "Output compact paged JSON").option("--full", "Return the legacy full-record list payload").action(async (opts) => {
5486
5667
  const store = getStore();
5668
+ const limit = normalizeContactsLimit(opts.limit, opts.full ? 50 : 20);
5669
+ const offset = normalizeContactsOffset(opts.offset);
5487
5670
  const result = await store.listContacts({
5488
5671
  tag_id: opts.tag,
5489
5672
  company_id: opts.company,
5490
- include_restricted: opts.includeRestricted,
5491
- limit: parseInt(opts.limit, 10),
5492
- offset: parseInt(opts.offset, 10),
5493
- order_by: opts.orderBy,
5494
- order_dir: opts.orderDir === "desc" ? "desc" : "asc"
5673
+ status: opts.status,
5674
+ limit,
5675
+ offset
5495
5676
  });
5496
5677
  if (opts.json) {
5497
- console.log(JSON.stringify(result, null, 2));
5678
+ console.log(JSON.stringify(opts.full ? result : compactContactsPage(result.contacts, { total: result.total, limit, offset }), null, 2));
5498
5679
  return;
5499
5680
  }
5500
5681
  if (result.contacts.length === 0) {
@@ -5835,33 +6016,22 @@ Invalid format: ${format}. Use csv, vcf, or json
5835
6016
  process.stdout.write(output);
5836
6017
  }
5837
6018
  });
5838
- program2.command("mcp").description("Print MCP server setup instructions").action(() => {
6019
+ program2.command("mcp").description("Print truthful MCP server setup instructions").action(() => {
5839
6020
  const config = JSON.stringify({ contacts: { command: "contacts-mcp", args: [], env: {} } }, null, 4);
6021
+ const coreCount = CONTACTS_CORE_TOOL_NAMES.length;
6022
+ const fullCount = CONTACTS_FULL_TOOL_COUNT;
5840
6023
  console.log(`
5841
6024
  ${chalk2.bold.blue("\u2501\u2501\u2501 Contacts MCP Server Setup \u2501\u2501\u2501")}
5842
6025
 
5843
- ${chalk2.bold("1. Install the package:")}
5844
- ${chalk2.cyan("npm install -g @hasna/contacts")}
5845
- ${chalk2.gray("or:")} ${chalk2.cyan("bun add -g @hasna/contacts")}
5846
-
5847
- ${chalk2.bold("2. Add to Claude Code (recommended):")}
5848
- ${chalk2.cyan("claude mcp add --transport stdio --scope user contacts -- contacts-mcp")}
6026
+ ${chalk2.bold("Install:")} ${chalk2.cyan("npm install -g @hasna/contacts")}
6027
+ ${chalk2.bold("Claude Code:")} ${chalk2.cyan("claude mcp add --transport stdio --scope user contacts -- contacts-mcp")}
6028
+ ${chalk2.bold("Manual config:")} ${chalk2.yellow(config)}
5849
6029
 
5850
- ${chalk2.bold("3. Or add manually to ~/.claude.json:")}
5851
- ${chalk2.yellow(config)}
6030
+ Default profile: core (${coreCount} tools). It includes routine contact/company/tag/relationship reads and writes plus search_tools and describe_tools for complete discovery across the ${fullCount}-tool full inventory.
6031
+ Full compatibility profile: set HASNA_CONTACTS_MCP_PROFILE=full or run contacts-mcp --mcp-profile full.
6032
+ List ordering: display_name asc, then id asc. Hosted list filters: company_id, tag_id, status.
5852
6033
 
5853
- ${chalk2.bold("4. Restart Claude Code and verify with")} ${chalk2.cyan("/mcp")}
5854
-
5855
- ${chalk2.bold("Available tools (24 total):")}
5856
- ${chalk2.gray("Contacts: ")}${chalk2.white("create_contact get_contact update_contact delete_contact")}
5857
- ${chalk2.gray(" ")}${chalk2.white("list_contacts search_contacts merge_contacts")}
5858
- ${chalk2.gray("Companies:")}${chalk2.white("create_company get_company update_company delete_company")}
5859
- ${chalk2.gray(" ")}${chalk2.white("list_companies search_companies")}
5860
- ${chalk2.gray("Tags: ")}${chalk2.white("create_tag list_tags delete_tag")}
5861
- ${chalk2.gray(" ")}${chalk2.white("add_tag_to_contact remove_tag_from_contact")}
5862
- ${chalk2.gray("Rels: ")}${chalk2.white("add_relationship list_relationships delete_relationship")}
5863
- ${chalk2.gray("I/O: ")}${chalk2.white("import_contacts export_contacts get_stats")}
5864
- `);
6034
+ Restart the client and verify the active profile in the MCP initialize instructions.`);
5865
6035
  });
5866
6036
  program2.command("recent").description("Show recently added or modified contacts").option("-l, --limit <n>", "Number to show", "10").option("-j, --json", "Output JSON").action(async (opts) => {
5867
6037
  const store = getStore();
@@ -7866,7 +8036,7 @@ Health data cleared for ${contact?.display_name ?? id}
7866
8036
  // src/cli/commands/audience.tsx
7867
8037
  import chalk5 from "chalk";
7868
8038
 
7869
- // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/external.js
8039
+ // node_modules/zod/v3/external.js
7870
8040
  var exports_external = {};
7871
8041
  __export(exports_external, {
7872
8042
  void: () => voidType,
@@ -7978,7 +8148,7 @@ __export(exports_external, {
7978
8148
  BRAND: () => BRAND
7979
8149
  });
7980
8150
 
7981
- // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/util.js
8151
+ // node_modules/zod/v3/helpers/util.js
7982
8152
  var util;
7983
8153
  (function(util2) {
7984
8154
  util2.assertEqual = (_) => {};
@@ -8109,7 +8279,7 @@ var getParsedType = (data) => {
8109
8279
  }
8110
8280
  };
8111
8281
 
8112
- // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/ZodError.js
8282
+ // node_modules/zod/v3/ZodError.js
8113
8283
  var ZodIssueCode = util.arrayToEnum([
8114
8284
  "invalid_type",
8115
8285
  "invalid_literal",
@@ -8228,7 +8398,7 @@ ZodError.create = (issues) => {
8228
8398
  return error;
8229
8399
  };
8230
8400
 
8231
- // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/locales/en.js
8401
+ // node_modules/zod/v3/locales/en.js
8232
8402
  var errorMap = (issue, _ctx) => {
8233
8403
  let message;
8234
8404
  switch (issue.code) {
@@ -8331,7 +8501,7 @@ var errorMap = (issue, _ctx) => {
8331
8501
  };
8332
8502
  var en_default = errorMap;
8333
8503
 
8334
- // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/errors.js
8504
+ // node_modules/zod/v3/errors.js
8335
8505
  var overrideErrorMap = en_default;
8336
8506
  function setErrorMap(map) {
8337
8507
  overrideErrorMap = map;
@@ -8339,7 +8509,7 @@ function setErrorMap(map) {
8339
8509
  function getErrorMap() {
8340
8510
  return overrideErrorMap;
8341
8511
  }
8342
- // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js
8512
+ // node_modules/zod/v3/helpers/parseUtil.js
8343
8513
  var makeIssue = (params) => {
8344
8514
  const { data, path, errorMaps, issueData } = params;
8345
8515
  const fullPath = [...path, ...issueData.path || []];
@@ -8445,14 +8615,14 @@ var isAborted = (x) => x.status === "aborted";
8445
8615
  var isDirty = (x) => x.status === "dirty";
8446
8616
  var isValid = (x) => x.status === "valid";
8447
8617
  var isAsync = (x) => typeof Promise !== "undefined" && x instanceof Promise;
8448
- // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/errorUtil.js
8618
+ // node_modules/zod/v3/helpers/errorUtil.js
8449
8619
  var errorUtil;
8450
8620
  (function(errorUtil2) {
8451
8621
  errorUtil2.errToObj = (message) => typeof message === "string" ? { message } : message || {};
8452
8622
  errorUtil2.toString = (message) => typeof message === "string" ? message : message?.message;
8453
8623
  })(errorUtil || (errorUtil = {}));
8454
8624
 
8455
- // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/types.js
8625
+ // node_modules/zod/v3/types.js
8456
8626
  class ParseInputLazyPath {
8457
8627
  constructor(parent, value, path, key) {
8458
8628
  this._cachedPath = [];
@@ -12169,82 +12339,73 @@ function registerStorageCommands(program2) {
12169
12339
  // src/cli/legacy.ts
12170
12340
  import { createHash as createHash2 } from "crypto";
12171
12341
  import fs, { constants, closeSync, existsSync as existsSync6, fstatSync, lstatSync, readSync, realpathSync, statSync as statSync2, writeSync } from "fs";
12172
- import { homedir as homedir2 } from "os";
12173
- import { basename, dirname, join as join3, resolve as resolve2 } from "path";
12342
+ import { homedir as homedir3 } from "os";
12343
+ import { basename, dirname, join as join4, resolve as resolve2 } from "path";
12174
12344
  import chalk7 from "chalk";
12175
12345
 
12176
12346
  // src/db/paths.ts
12177
- import { homedir as pathsResolverHomedir2 } from "os";
12178
- import { join as pathsResolverJoin2 } from "path";
12179
- var PATHS_RESOLVER_KIND_ENV2 = {
12180
- config: "HASNA_CONFIG_HOME",
12347
+ import { homedir as pathsResolverHomedir } from "os";
12348
+ import { join as pathsResolverJoin } from "path";
12349
+ var PATHS_RESOLVER_KIND_ENV = {
12181
12350
  data: "HASNA_DATA_HOME",
12182
- state: "HASNA_STATE_HOME",
12183
- cache: "HASNA_CACHE_HOME"
12351
+ state: "HASNA_STATE_HOME"
12184
12352
  };
12185
- var PATHS_RESOLVER_APP_SLUG_RE2 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
12186
- function pathsResolverAssertApp2(app) {
12353
+ var PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
12354
+ function pathsResolverAssertApp(app) {
12187
12355
  if (typeof app !== "string" || app.length === 0) {
12188
12356
  throw new TypeError("paths: app must be a non-empty string");
12189
12357
  }
12190
- if (!PATHS_RESOLVER_APP_SLUG_RE2.test(app)) {
12358
+ if (!PATHS_RESOLVER_APP_SLUG_RE.test(app)) {
12191
12359
  throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
12192
12360
  }
12193
12361
  }
12194
- function pathsResolverAssertKind2(kind) {
12195
- if (!Object.keys(PATHS_RESOLVER_KIND_ENV2).includes(kind)) {
12196
- throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV2).join(", ")}`);
12362
+ function pathsResolverAssertKind(kind) {
12363
+ if (!Object.keys(PATHS_RESOLVER_KIND_ENV).includes(kind)) {
12364
+ throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV).join(", ")}`);
12197
12365
  }
12198
12366
  }
12199
- function pathsResolverBaseDir2(kind, options) {
12200
- pathsResolverAssertKind2(kind);
12367
+ function pathsResolverBaseDir(kind, options) {
12368
+ pathsResolverAssertKind(kind);
12201
12369
  const env = options.env ?? process.env;
12202
- const override = env[PATHS_RESOLVER_KIND_ENV2[kind]];
12370
+ const override = env[PATHS_RESOLVER_KIND_ENV[kind]];
12203
12371
  if (typeof override === "string" && override.length > 0)
12204
12372
  return override;
12205
- const home = options.home ?? pathsResolverHomedir2();
12373
+ const home = options.home ?? pathsResolverHomedir();
12206
12374
  const platform = options.platform ?? process.platform;
12207
12375
  if (platform === "darwin") {
12208
12376
  switch (kind) {
12209
- case "config":
12210
12377
  case "data":
12211
- return pathsResolverJoin2(home, "Library", "Application Support", "Hasna");
12212
- case "cache":
12213
- return pathsResolverJoin2(home, "Library", "Caches", "Hasna");
12378
+ return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
12214
12379
  case "state":
12215
- return pathsResolverJoin2(home, "Library", "Logs", "Hasna");
12380
+ return pathsResolverJoin(home, "Library", "Logs", "Hasna");
12216
12381
  }
12217
12382
  }
12218
12383
  switch (kind) {
12219
- case "config":
12220
- return pathsResolverJoin2(home, ".config", "hasna");
12221
12384
  case "data":
12222
- return pathsResolverJoin2(home, ".local", "share", "hasna");
12385
+ return pathsResolverJoin(home, ".local", "share", "hasna");
12223
12386
  case "state":
12224
- return pathsResolverJoin2(home, ".local", "state", "hasna");
12225
- case "cache":
12226
- return pathsResolverJoin2(home, ".cache", "hasna");
12387
+ return pathsResolverJoin(home, ".local", "state", "hasna");
12227
12388
  }
12228
12389
  }
12229
- function pathsResolverResolve2(kind, options) {
12230
- pathsResolverAssertApp2(options.app);
12231
- const appSegment = options.internal === true ? pathsResolverJoin2("internal", options.app) : options.app;
12232
- return pathsResolverJoin2(pathsResolverBaseDir2(kind, options), appSegment);
12390
+ function pathsResolverResolve(kind, options) {
12391
+ pathsResolverAssertApp(options.app);
12392
+ const appSegment = options.internal === true ? pathsResolverJoin("internal", options.app) : options.app;
12393
+ return pathsResolverJoin(pathsResolverBaseDir(kind, options), appSegment);
12233
12394
  }
12234
12395
  function dataDir2(options) {
12235
- return pathsResolverResolve2("data", options);
12396
+ return pathsResolverResolve("data", options);
12236
12397
  }
12237
12398
 
12238
12399
  // src/cli/legacy.ts
12239
12400
  function home(env = process.env) {
12240
- return env.HOME || env.USERPROFILE || homedir2();
12401
+ return env.HOME || env.USERPROFILE || homedir3();
12241
12402
  }
12242
12403
  function legacyDatabaseCandidates(env = process.env) {
12243
12404
  const base = home(env);
12244
12405
  const paths = [
12245
- { source: "xdg", path: join3(dataDir2({ app: "contacts", home: base, env }), "contacts.db") },
12246
- { source: "legacy-hasna", path: join3(base, ".hasna", "contacts", "contacts.db") },
12247
- { source: "ancient", path: join3(base, ".contacts", "contacts.db") }
12406
+ { source: "xdg", path: join4(dataDir2({ app: "contacts", home: base, env }), "contacts.db") },
12407
+ { source: "legacy-hasna", path: join4(base, ".hasna", "contacts", "contacts.db") },
12408
+ { source: "ancient", path: join4(base, ".contacts", "contacts.db") }
12248
12409
  ];
12249
12410
  return paths.map(({ source, path }) => {
12250
12411
  const exists = existsSync6(path) && lstatSync(path).isFile();
@@ -12391,6 +12552,175 @@ function registerLegacyCommands(program2) {
12391
12552
  });
12392
12553
  }
12393
12554
 
12555
+ // src/browser/install.ts
12556
+ import { chmodSync, cpSync, existsSync as existsSync7, mkdirSync, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
12557
+ import { homedir as homedir5 } from "os";
12558
+ import { dirname as dirname2, join as join6, resolve as resolve3 } from "path";
12559
+ import { fileURLToPath } from "url";
12560
+
12561
+ // src/browser/identity.ts
12562
+ var EXTENSION_ID = "ceegpmbcoiomonopbhpifdalooccfgjj";
12563
+ var HOST_NAME = "com.hasna.contacts";
12564
+
12565
+ // src/browser/protocol.ts
12566
+ class BrowserError extends Error {
12567
+ code;
12568
+ constructor(code) {
12569
+ super(code);
12570
+ this.code = code;
12571
+ this.name = "BrowserError";
12572
+ }
12573
+ }
12574
+ function safeError(error) {
12575
+ return error instanceof BrowserError ? error.code : "CONTACTS_UNAVAILABLE";
12576
+ }
12577
+
12578
+ class NativeProtocol {
12579
+ handlers;
12580
+ buffer = Buffer.alloc(0);
12581
+ queued = 0;
12582
+ chain = Promise.resolve();
12583
+ constructor(handlers) {
12584
+ this.handlers = handlers;
12585
+ }
12586
+ accept(chunk) {
12587
+ this.buffer = Buffer.concat([this.buffer, chunk]);
12588
+ if (this.buffer.length > 1048576)
12589
+ return this.handlers.fail();
12590
+ while (this.buffer.length >= 4) {
12591
+ const length = this.buffer.readUInt32LE(0);
12592
+ if (!length || length > 65536)
12593
+ return this.handlers.fail();
12594
+ if (this.buffer.length < 4 + length)
12595
+ break;
12596
+ let message;
12597
+ try {
12598
+ message = JSON.parse(this.buffer.subarray(4, 4 + length).toString());
12599
+ } catch {
12600
+ return this.handlers.fail();
12601
+ }
12602
+ this.buffer = this.buffer.subarray(4 + length);
12603
+ if (!message || typeof message.id !== "string" || !/^[0-9a-f-]{36}$/.test(message.id))
12604
+ return this.handlers.fail();
12605
+ if (++this.queued > 32)
12606
+ return this.handlers.fail();
12607
+ this.chain = this.chain.then(async () => {
12608
+ try {
12609
+ const result = await this.handlers.handle(message);
12610
+ this.handlers.send({ id: message.id, ok: true, result });
12611
+ } catch (e) {
12612
+ this.handlers.send({ id: message.id, ok: false, error: safeError(e) });
12613
+ } finally {
12614
+ this.queued--;
12615
+ }
12616
+ });
12617
+ }
12618
+ }
12619
+ async idle() {
12620
+ await this.chain;
12621
+ }
12622
+ }
12623
+
12624
+ // src/browser/install.ts
12625
+ function packageRoot() {
12626
+ let dir = dirname2(fileURLToPath(import.meta.url));
12627
+ for (let i = 0;i < 5; i++, dir = dirname2(dir)) {
12628
+ const p = join6(dir, "package.json");
12629
+ if (existsSync7(p) && JSON.parse(readFileSync4(p, "utf8")).name === "@hasna/contacts")
12630
+ return dir;
12631
+ }
12632
+ throw new BrowserError("PACKAGE_ROOT_NOT_FOUND");
12633
+ }
12634
+ function hostDirectories(home2 = homedir5(), userDataDir) {
12635
+ const dirs = process.platform === "darwin" ? [
12636
+ "Library/Application Support/Google/Chrome/NativeMessagingHosts",
12637
+ "Library/Application Support/Chromium/NativeMessagingHosts",
12638
+ "Library/Application Support/Google/Chrome for Testing/NativeMessagingHosts"
12639
+ ] : [".config/google-chrome/NativeMessagingHosts", ".config/chromium/NativeMessagingHosts"];
12640
+ const out = dirs.map((d) => join6(home2, d));
12641
+ if (userDataDir)
12642
+ out.push(join6(resolve3(userDataDir), "NativeMessagingHosts"));
12643
+ return [...new Set(out)];
12644
+ }
12645
+ function installBrowser(options = {}) {
12646
+ if (!["darwin", "linux"].includes(process.platform))
12647
+ throw new BrowserError("UNSUPPORTED_PLATFORM");
12648
+ const root = options.root ?? packageRoot();
12649
+ const home2 = options.home ?? homedir5();
12650
+ const install = join6(home2, ".hasna", "contacts", "chrome");
12651
+ mkdirSync(install, { recursive: true, mode: 448 });
12652
+ const extension = join6(install, "extension");
12653
+ cpSync(join6(root, "extension"), extension, { recursive: true });
12654
+ const host = join6(install, "native-host");
12655
+ const entry = resolve3(root, "dist/browser/native.js");
12656
+ if (!existsSync7(entry))
12657
+ throw new BrowserError("BUILD_REQUIRED");
12658
+ writeFileSync2(host, `#!${options.execPath ?? process.execPath}
12659
+ import ${JSON.stringify(entry)};
12660
+ `, { mode: 448 });
12661
+ chmodSync(host, 448);
12662
+ const manifests = hostDirectories(home2, options.userDataDir).map((dir) => {
12663
+ mkdirSync(dir, { recursive: true, mode: 448 });
12664
+ const path = join6(dir, `${HOST_NAME}.json`);
12665
+ const manifest = {
12666
+ name: HOST_NAME,
12667
+ description: "Hasna Contacts autofill",
12668
+ path: host,
12669
+ type: "stdio",
12670
+ allowed_origins: [`chrome-extension://${EXTENSION_ID}/`]
12671
+ };
12672
+ if (existsSync7(path)) {
12673
+ const old = JSON.parse(readFileSync4(path, "utf8"));
12674
+ if (old.name !== HOST_NAME || old.path !== host)
12675
+ throw new BrowserError("EXISTING_HOST_CONFLICT");
12676
+ }
12677
+ writeFileSync2(path, `${JSON.stringify(manifest, null, 2)}
12678
+ `, { mode: 384 });
12679
+ return path;
12680
+ });
12681
+ return {
12682
+ ok: true,
12683
+ extensionId: EXTENSION_ID,
12684
+ extensionPath: extension,
12685
+ nativeHost: host,
12686
+ nativeManifests: manifests,
12687
+ next: "In the desired Chrome profile, open chrome://extensions, enable Developer mode, choose Load unpacked, and select extensionPath. Pin Contacts."
12688
+ };
12689
+ }
12690
+ function browserStatus(options = {}) {
12691
+ const home2 = options.home ?? homedir5();
12692
+ const host = join6(home2, ".hasna", "contacts", "chrome", "native-host");
12693
+ const extension = join6(home2, ".hasna", "contacts", "chrome", "extension");
12694
+ return {
12695
+ ok: true,
12696
+ extensionId: EXTENSION_ID,
12697
+ extensionInstalled: existsSync7(join6(extension, "manifest.json")),
12698
+ nativeHostInstalled: existsSync7(host),
12699
+ manifests: hostDirectories(home2, options.userDataDir).map((dir) => ({
12700
+ path: join6(dir, `${HOST_NAME}.json`),
12701
+ present: existsSync7(join6(dir, `${HOST_NAME}.json`))
12702
+ }))
12703
+ };
12704
+ }
12705
+
12706
+ // src/browser/cli.ts
12707
+ function registerBrowserCommands(program2) {
12708
+ const output = (fn) => {
12709
+ try {
12710
+ const r = fn();
12711
+ console.log(JSON.stringify(r));
12712
+ if (r?.ok === false)
12713
+ process.exitCode = 1;
12714
+ } catch (e) {
12715
+ console.log(JSON.stringify({ ok: false, error: safeError(e) }));
12716
+ process.exitCode = 1;
12717
+ }
12718
+ };
12719
+ const cmd = program2.command("browser").description("Chrome extension: inline contact autofill (hosted Contacts API)");
12720
+ cmd.command("install").description("Install the native host and unpacked extension assets; no browser restart").option("--user-data-dir <path>", "Also register the native host in an automated Chrome user-data directory").action((opts) => output(() => installBrowser({ userDataDir: opts.userDataDir })));
12721
+ cmd.command("status").description("Report where the native host manifests and extension assets are installed").option("--user-data-dir <path>", "Also check an automated Chrome user-data directory").action((opts) => output(() => browserStatus({ userDataDir: opts.userDataDir })));
12722
+ }
12723
+
12394
12724
  // src/cli/index.tsx
12395
12725
  import { createRequire } from "module";
12396
12726
  var { registerEventCommands: registerEventCommands2 } = exports_commander;
@@ -12404,13 +12734,13 @@ registerAdvancedCommands(program);
12404
12734
  registerAudienceCommands(program);
12405
12735
  registerStorageCommands(program);
12406
12736
  registerLegacyCommands(program);
12737
+ registerBrowserCommands(program);
12407
12738
  registerWebhookCommands2?.(program, { source: "contacts" });
12408
12739
  registerEventCommands2(program, { source: "contacts", eventsCommandName: "hasna-events" });
12409
12740
  program.parseAsync(process.argv).catch((err) => {
12410
12741
  const message = err instanceof Error ? err.message : String(err);
12411
12742
  const isApiUnavailable = err instanceof Error && err.name === "ApiUnavailableError";
12412
- console.error(`
12413
- ` + (isApiUnavailable ? chalk8.yellow(message) : chalk8.red(message)) + `
12743
+ console.error((isApiUnavailable ? chalk8.yellow(message) : chalk8.red(message)) + `
12414
12744
  `);
12415
12745
  process.exit(1);
12416
12746
  });