@hasna/contacts 0.6.36 → 0.8.1

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 (69) hide show
  1. package/README.md +123 -69
  2. package/dist/cli/commands/advanced.d.ts.map +1 -1
  3. package/dist/cli/commands/core.d.ts +1 -1
  4. package/dist/cli/commands/core.d.ts.map +1 -1
  5. package/dist/cli/commands/crm.d.ts.map +1 -1
  6. package/dist/cli/index.js +9206 -39957
  7. package/dist/cli/legacy.d.ts +18 -0
  8. package/dist/cli/legacy.d.ts.map +1 -0
  9. package/dist/cli/status-domain.preload.d.ts +2 -0
  10. package/dist/cli/status-domain.preload.d.ts.map +1 -0
  11. package/dist/cli/storage.d.ts.map +1 -1
  12. package/dist/cloud/client-config.d.ts +6 -0
  13. package/dist/cloud/client-config.d.ts.map +1 -0
  14. package/dist/cloud/http-storage.d.ts +49 -70
  15. package/dist/cloud/http-storage.d.ts.map +1 -1
  16. package/dist/cloud/resolver-inputs.d.ts +51 -0
  17. package/dist/cloud/resolver-inputs.d.ts.map +1 -0
  18. package/dist/db/paths.d.ts +13 -7
  19. package/dist/db/paths.d.ts.map +1 -1
  20. package/dist/generated/storage-kit/backend.d.ts +19 -0
  21. package/dist/generated/storage-kit/backend.d.ts.map +1 -0
  22. package/dist/generated/storage-kit/index.d.ts +2 -2
  23. package/dist/generated/storage-kit/index.d.ts.map +1 -1
  24. package/dist/generated/storage-kit/migrations.d.ts +21 -0
  25. package/dist/generated/storage-kit/migrations.d.ts.map +1 -1
  26. package/dist/generated/storage-kit/own.d.ts +11 -0
  27. package/dist/generated/storage-kit/own.d.ts.map +1 -0
  28. package/dist/generated/storage-kit/pool.d.ts +5 -7
  29. package/dist/generated/storage-kit/pool.d.ts.map +1 -1
  30. package/dist/generated/storage-kit/query.d.ts +1 -1
  31. package/dist/generated/storage-kit/query.d.ts.map +1 -1
  32. package/dist/generated/storage-kit/tls.d.ts +30 -3
  33. package/dist/generated/storage-kit/tls.d.ts.map +1 -1
  34. package/dist/index.d.ts +2 -2
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +454 -5713
  37. package/dist/lib/images.d.ts.map +1 -1
  38. package/dist/mcp/handlers/advanced.d.ts.map +1 -1
  39. package/dist/mcp/handlers/core.d.ts +1 -1
  40. package/dist/mcp/index.d.ts +9 -0
  41. package/dist/mcp/index.d.ts.map +1 -1
  42. package/dist/mcp/index.js +549 -5895
  43. package/dist/mcp/startup-gate.d.ts +48 -0
  44. package/dist/mcp/startup-gate.d.ts.map +1 -0
  45. package/dist/mcp/storage-tools.d.ts.map +1 -1
  46. package/dist/sdk/index.d.ts +80 -11
  47. package/dist/sdk/index.d.ts.map +1 -1
  48. package/dist/sdk/index.js +1028 -2
  49. package/dist/server/cloud.d.ts +6 -7
  50. package/dist/server/cloud.d.ts.map +1 -1
  51. package/dist/server/index.d.ts +2 -1
  52. package/dist/server/index.d.ts.map +1 -1
  53. package/dist/server/index.js +3892 -35295
  54. package/dist/server/security.d.ts +1 -1
  55. package/dist/server/security.d.ts.map +1 -1
  56. package/dist/server/serve.d.ts.map +1 -1
  57. package/dist/store/index.d.ts +66 -93
  58. package/dist/store/index.d.ts.map +1 -1
  59. package/dist/types/store-dto.d.ts +273 -0
  60. package/dist/types/store-dto.d.ts.map +1 -0
  61. package/hasna.contract.json +77 -10
  62. package/package.json +22 -13
  63. package/dashboard/dist/assets/index-0l6aQb1t.css +0 -1
  64. package/dashboard/dist/assets/index-opnZdkVD.js +0 -229
  65. package/dashboard/dist/index.html +0 -13
  66. package/dist/generated/storage-kit/mode.d.ts +0 -48
  67. package/dist/generated/storage-kit/mode.d.ts.map +0 -1
  68. package/dist/lib/config.d.ts +0 -7
  69. package/dist/lib/config.d.ts.map +0 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @hasna/contacts
2
2
 
3
- Contact management for AI coding agents — CLI + MCP + Web
3
+ Contact management for AI coding agents — CLI + MCP + authenticated HTTP API
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/@hasna/contacts)](https://www.npmjs.com/package/@hasna/contacts)
6
6
  [![License](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE)
@@ -11,12 +11,92 @@ Contact management for AI coding agents — CLI + MCP + Web
11
11
  npm install -g @hasna/contacts
12
12
  ```
13
13
 
14
+ ## Configure the client
15
+
16
+ Every CLI, MCP, and package data operation uses one authenticated HTTPS `/v1`
17
+ authority resolved by the shared `@hasna/contracts` client chain
18
+ (hasna/apps#1720), resolved fresh on every request. There is no local database
19
+ fallback and no per-app env chain of its own.
20
+
21
+ Once a contacts API key resolves from any tier, the authority defaults to the
22
+ fleet gateway `https://api.hasna.com/contacts` — no URL configuration is
23
+ needed on a station. The credential tiers, in order:
24
+
25
+ 1. explicit arguments / deliberate pointers — `HASNA_CONTACTS_API_KEY_OVERRIDE`, `HASNA_PROFILE`, `HASNA_CONTACTS_API_KEY_REF` (secrets vault)
26
+ 2. macOS Keychain — `hasna.credentials.contacts.api-key` / `.api-url`, account `HASNA_STATION` → `hostname -s` → `$USER`
27
+ 3. disk — `~/.hasna/contacts/config/credentials` (owner-only 0400/0600, `HASNA_CONTACTS_API_KEY=…` or the `HASNA_CONTACTS_API_URL=…` alias)
28
+ 4. environment — `HASNA_CONTACTS_API_KEY`
29
+
30
+ | Env var | Meaning |
31
+ |---|---|
32
+ | `HASNA_CONTACTS_API_URL` | Explicit API base URL (HTTPS; overrides the fleet gateway). Legacy alias: `CONTACTS_API_URL`. |
33
+ | `HASNA_CONTACTS_API_KEY` | API key (env tier). Legacy alias: `CONTACTS_API_KEY`. |
34
+ | `HASNA_CONTACTS_API_KEY_OVERRIDE` | Deliberate per-run override that outranks the Keychain and disk. |
35
+ | `HASNA_CONTACTS_API_KEY_REF` | Secrets-vault pointer (`namespace/app/live/api_key`); terminal when unresolvable. |
36
+ | `HASNA_PROFILE` | Selects which identity (`credentials-<profile>`) the disk tier reads. |
37
+ | `HASNA_STATION` | Keychain account when set; else short hostname, then `$USER`. |
38
+ | `HASNA_HOME` | Replaces `~` for the `~/.hasna/…` credential/disk root. |
39
+ | `HASNA_CONFIG_HOME` | Replaces `~/.hasna/<app>/config` entirely. |
40
+
41
+ ```bash
42
+ # Fully explicit:
43
+ export HASNA_CONTACTS_API_URL="https://contacts.example.com"
44
+ export HASNA_CONTACTS_API_KEY="…" # or configure the Keychain/disk tiers
45
+ contacts connection --json
46
+ ```
47
+
48
+ An absent URL and key fails closed: operations exit non-zero and never open a
49
+ local store; `contacts connection` reports `transport: "unconfigured"`.
50
+ The fail-closed message starts on the FIRST stderr line — it names what is
51
+ missing and where the credential should live (`HASNA_CONTACTS_API_KEY`, the
52
+ Keychain item, the credentials-file path), never a value.
53
+ `HASNA_CONTACTS_STORAGE_MODE`, `CONTACTS_STORAGE_MODE`, contacts DB-path
54
+ variables, and contacts database URLs are rejected in client processes.
55
+ PostgreSQL URLs belong only to `contacts-serve` and the migration task.
56
+
14
57
  ## CLI Usage
15
58
 
16
59
  ```bash
60
+ contacts status # CLI version, resolved /v1 authority + sources, storage mode, record counts
61
+ contacts status --json
17
62
  contacts --help
18
63
  ```
19
64
 
65
+ `contacts status` reports the authority the shared resolver actually decided
66
+ (`api`, the `/v1` base URL) and where each half came from — `api_url_source`,
67
+ `api_key_source`, `api_key_tier`: an env key name, a Keychain item reference,
68
+ a credentials-file path, or `default` for the fleet gateway; never a value.
69
+ It answers even on a box without an API key: an unconfigured client reports
70
+ storage `unconfigured` with the resolver's `issue` (a failed request on a
71
+ configured box reports storage `error` with the failure message) instead of
72
+ crashing, so agents can observe the configuration drift the command exists to
73
+ expose. `status` and `connection` are diagnostics and exit 0 with that report;
74
+ every data verb fails closed (non-zero exit, no local store).
75
+
76
+ ## SDK
77
+
78
+ ```ts
79
+ import { createContactsClient, ContactsV1Client } from "@hasna/contacts/sdk";
80
+
81
+ // Through the fleet resolver — the same @hasna/contracts chain the CLI and MCP
82
+ // server use: credential and authority resolved at construction, the key
83
+ // re-resolved on every request, the authority pinned. Nothing resolving throws.
84
+ const client = createContactsClient();
85
+ const { contacts } = await client.listContacts();
86
+
87
+ // Explicit pin: a caller-supplied baseUrl always requires a caller-supplied
88
+ // apiKey — the SDK never attaches an ambient credential to it.
89
+ const pinned = new ContactsV1Client({ baseUrl: "https://contacts.example.com", apiKey: "…" });
90
+ ```
91
+
92
+ `createContactsClient()` resolves the literal tiers only — an explicit
93
+ `apiKey`, `HASNA_CONTACTS_API_KEY_OVERRIDE`, the Keychain item, the
94
+ credentials file, `HASNA_CONTACTS_API_KEY`. The secrets-vault pointer
95
+ `HASNA_CONTACTS_API_KEY_REF` is refused with
96
+ `CONTACTS_CREDENTIAL_POINTER_UNSUPPORTED` (the SDK resolves synchronously per
97
+ request and cannot complete a vault lookup); the CLI and MCP server accept it.
98
+
99
+
20
100
  ## Audiences, consent, and suppression
21
101
 
22
102
  Audience segments implement the `hasna.audience.v1` contract (distribution
@@ -50,6 +130,22 @@ suppressed addresses; the audience `--policy` (`opt_in`, `opt_out`,
50
130
  contacts-mcp
51
131
  ```
52
132
 
133
+ `contacts-mcp` fails closed at startup: with no credential resolvable through
134
+ the chain above it exits non-zero BEFORE the stdio transport is connected or
135
+ the HTTP port is bound — an `initialize` request is never answered by an
136
+ unauthenticated server — and creates nothing under the app home. The first
137
+ stderr line names where the credential should live (the Keychain item, the
138
+ credentials-file path, `HASNA_CONTACTS_API_KEY`), never a value.
139
+ `--help` / `--version` answer ahead of the gate; every tool still re-resolves
140
+ the credential per request once the server is up.
141
+
142
+ The deliberate tiers are honoured at the gate as well. The secrets-vault
143
+ pointer `HASNA_CONTACTS_API_KEY_REF` is dereferenced once at startup (the
144
+ chain alone only checks the pointer's shape): a pointer the vault cannot
145
+ complete, a `HASNA_PROFILE` whose credentials file is missing, or an unsafe
146
+ (not owner-only) credentials file is a one-line refusal naming the pointer,
147
+ the file, or the item — never resolved around, never a stack trace.
148
+
53
149
  ## HTTP mode
54
150
 
55
151
  Long-lived Streamable HTTP transport (stateless, bind `127.0.0.1` only):
@@ -63,7 +159,7 @@ MCP_HTTP=1 contacts-mcp
63
159
  - Health: `GET http://127.0.0.1:8809/health`
64
160
  - MCP: `http://127.0.0.1:8809/mcp`
65
161
 
66
- The REST server (`contacts-serve`) also exposes `/health` and `/mcp` when running.
162
+ The REST server is a separate authenticated `/v1` surface.
67
163
 
68
164
  ## REST API
69
165
 
@@ -72,83 +168,41 @@ contacts-serve
72
168
  ```
73
169
 
74
170
  `contacts-serve` binds to `127.0.0.1` by default. Use `--host <host>` or
75
- `CONTACTS_HOST=<host>` only when intentionally exposing it beyond loopback.
76
- Shared binds still fail closed unless a valid contacts token is configured and
77
- sent with the request. The unauthenticated local development fallback is disabled
78
- by default and only activates when all of the following are true:
79
-
80
- - no contacts API token environment variable is configured
81
- - `CONTACTS_ALLOW_UNAUTHENTICATED_LOOPBACK=1`
82
- - the server is explicitly bound to a loopback host
83
-
84
- The fallback does not trust the HTTP `Host` header.
171
+ `CONTACTS_HOST=<host>` only when intentionally exposing it beyond loopback. The
172
+ server requires PostgreSQL configuration and API-key signing configuration;
173
+ readiness fails closed when either is unavailable.
85
174
 
86
- ## Storage
175
+ ## Storage and legacy data
87
176
 
88
- Contacts owns its local SQLite storage directly. It does not depend on shared
89
- cloud runtime commands or MCP tools.
177
+ The client never opens SQLite or PostgreSQL. The server owns PostgreSQL and all
178
+ authoritative data remains server-side. Inspect the value-free connection state
179
+ with:
90
180
 
91
181
  ```bash
92
- contacts storage status
93
- contacts storage status --json
94
- contacts storage push --tables contacts,companies
95
- contacts storage pull --tables contacts,companies
96
- contacts storage sync
182
+ contacts connection --json
97
183
  ```
98
184
 
99
- Optional cross-machine sync uses contacts-owned PostgreSQL storage. Set one of:
185
+ Retired local databases are never auto-adopted or silently ignored. The
186
+ explicit migration aid only inspects and copies them; it never opens, changes,
187
+ deletes, or selects one as the live store:
100
188
 
101
189
  ```bash
102
- export HASNA_CONTACTS_POSTGRES_URL="postgres://..."
103
- # or OPEN_CONTACTS_POSTGRES_URL / CONTACTS_POSTGRES_URL
190
+ contacts legacy inspect --json
191
+ contacts legacy preserve --source /exact/path/contacts.db \
192
+ --output /existing/directory/contacts.db.pre-https.20260901
104
193
  ```
105
194
 
106
- Remote PostgreSQL connections require verified TLS for non-local hosts. Local
107
- PostgreSQL development URLs can disable TLS explicitly.
108
-
109
- By default, remote sync covers contacts, companies, tags, and the other
110
- non-sensitive relationship tables. `webhooks`, `contact_documents`, and
111
- `contact_health` are excluded until explicitly requested with `--tables` and
112
- `HASNA_CONTACTS_ALLOW_SENSITIVE_SYNC=1`. Sync inserts or updates rows with
113
- timestamp conflict protection. Deletes for `contacts`, `companies`, and `tags`
114
- write `_contacts_tombstones`; push/pull sync carries those tombstones and pull
115
- applies them unless the local row has a newer `updated_at`.
116
-
117
- Shared REST/dashboard deployments must set one of
118
- `HASNA_CONTACTS_API_TOKENS`, `OPEN_CONTACTS_API_TOKENS`, or
119
- `CONTACTS_API_TOKENS`. Values are comma-separated `token=scope scope` records.
120
- Supported scopes include `contacts:read`, `contacts:write`,
121
- `contacts:import`, `contacts:export`, `contacts:export:full`,
122
- `documents:read`, `images:read`, `images:write`, `companies:*`, `tags:*`,
123
- `stats:read`, `dashboard:read`, and `mcp:access`. Loopback-only development
124
- without configured tokens remains allowed; shared hosts fail closed.
125
-
126
- Server exports are redacted by default: email addresses, phone numbers,
127
- addresses, notes, birthdays, social profiles, and custom fields are withheld.
128
- Full exports require both `contacts:export` and `contacts:export:full`. Export,
129
- import, document file read, image mutation, contact mutation, company mutation,
130
- and tag mutation routes write audit entries. Document attachments are served
131
- only from the managed contacts documents directory and use private `no-store`
132
- cache headers.
133
-
134
- `contacts cloud status`, `contacts cloud push`, `contacts cloud pull`, and
135
- `contacts cloud sync` remain compatibility aliases for the contacts-owned
136
- storage commands. They do not load or depend on the deprecated shared cloud
137
- runtime. `contacts cloud feedback` saves feedback locally in the contacts
138
- database.
139
-
140
- ## Data Directory
141
-
142
- On-box data resolves through the XDG dirs per `@hasna/paths`:
143
-
144
- - documents, the SQLite store, `config.json` and the vault config live in the
145
- **XDG data root**: `~/.local/share/hasna/contacts/` (or `$HASNA_DATA_HOME/contacts`).
146
- - the vault session state lives in the **XDG state root**:
147
- `~/.local/state/hasna/contacts/` (or `$HASNA_STATE_HOME/contacts`).
148
-
149
- The legacy `~/.hasna/contacts/` home is never written to; on first use any
150
- pre-existing legacy store is adopted into the XDG roots (never clobbering an
151
- existing XDG store).
195
+ If a SQLite WAL, journal, or shared-memory sidecar is present, preservation
196
+ refuses to proceed until every legacy process is stopped and the old client has checkpointed the database. To move
197
+ portable contact records, use a legacy release against the preserved copy to
198
+ export JSON, then run `contacts import exported.json` with the HTTPS client
199
+ configured. Existing files are never overwritten.
200
+
201
+ Preservation rejects source, ancestor, or output replacement races and verifies
202
+ the copied bytes with SHA-256 and stable output metadata before success. If a copy
203
+ fails after output creation, any private partial output is left untouched and
204
+ reported as unverified for manual inspection; the command never deletes a
205
+ pathname that another process might have replaced.
152
206
 
153
207
  ## License
154
208
 
@@ -1 +1 @@
1
- {"version":3,"file":"advanced.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/advanced.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAWzC,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAs5B/D"}
1
+ {"version":3,"file":"advanced.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/advanced.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAiBzC,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAq5B/D"}
@@ -1,3 +1,3 @@
1
1
  import type { Command } from "commander";
2
- export declare function registerCoreCommands(program: Command): void;
2
+ export declare function registerCoreCommands(program: Command, version: string): void;
3
3
  //# sourceMappingURL=core.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/core.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAgBzC,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA87B3D"}
1
+ {"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/core.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAqBzC,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAu5B5E"}
@@ -1 +1 @@
1
- {"version":3,"file":"crm.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/crm.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAezC,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAm+B1D"}
1
+ {"version":3,"file":"crm.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/crm.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAezC,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAi+B1D"}