@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
@@ -0,0 +1,166 @@
1
+ // Contacts popup — search, then one tap per contact.
2
+ //
3
+ // Shows the page it was opened on, lists contacts from the hosted Contacts API
4
+ // (names, company and email only) and fills the chosen contact through the
5
+ // background worker, which inspects exact document IDs before asking the
6
+ // native host for values. This script stores nothing.
7
+ const $ = (id) => document.getElementById(id);
8
+ let state = { contacts: [], page: null };
9
+ let busy = false;
10
+ let searchTimer = 0;
11
+ let searchSeq = 0;
12
+
13
+ function status(text, kind = "") {
14
+ $("status").textContent = text;
15
+ $("status").dataset.kind = kind;
16
+ }
17
+ function show(view) {
18
+ for (const id of ["view-connecting", "view-setup", "view-contacts"]) $(id).hidden = id !== view;
19
+ }
20
+ async function request(message) {
21
+ const r = await chrome.runtime.sendMessage(message);
22
+ if (!r?.ok) throw new Error(r?.error || "NATIVE_UNAVAILABLE");
23
+ return r.result;
24
+ }
25
+ function selection(contactId) {
26
+ if (!state.page) throw new Error("TAB_CHANGED");
27
+ return { contactId, ...state.page };
28
+ }
29
+ async function permission(origins) {
30
+ const ok = await chrome.permissions.request({ origins: [...new Set(origins)].map((o) => `${o}/*`) });
31
+ if (!ok) throw new Error("SITE_PERMISSION_DECLINED");
32
+ }
33
+ const HINTS = {
34
+ CONTACT_NOT_FOUND: "This contact is no longer available.",
35
+ NATIVE_UNAVAILABLE: "Contacts is not connected on this station.",
36
+ NATIVE_TIMEOUT: "Contacts took too long to answer. Try again.",
37
+ CONTACTS_UNAVAILABLE: "Cannot reach the hosted Contacts API.",
38
+ CONTACTS_NOT_CONFIGURED: "Contacts is not signed in on this station. Configure the contacts CLI credential first.",
39
+ SITE_PERMISSION_DECLINED: "Website access was not granted.",
40
+ TAB_CHANGED: "The page changed. Reopen Contacts and try again.",
41
+ FRAME_CHANGED: "An embedded frame changed. Reopen Contacts and try again.",
42
+ FILL_REVOKED: "The connection was lost before the fill completed.",
43
+ };
44
+ function failure(e) {
45
+ const code = e?.message || "FILL_FAILED";
46
+ if (["NATIVE_UNAVAILABLE", "CONTACTS_UNAVAILABLE", "CONTACTS_NOT_CONFIGURED"].includes(code) && !state.contacts.length) {
47
+ show("view-setup");
48
+ $("setup-reason").textContent = HINTS[code];
49
+ $("page").textContent = "Not connected";
50
+ return;
51
+ }
52
+ status(HINTS[code] || "Autofill did not complete. Reopen Contacts and try again.", "error");
53
+ if (code === "TAB_CHANGED") {
54
+ state.page = null;
55
+ render();
56
+ }
57
+ }
58
+ function initials(name) {
59
+ const parts = String(name || "").trim().split(/\s+/).filter(Boolean);
60
+ return (parts.length ? parts.slice(0, 2).map((p) => p[0]).join("") : "?").toUpperCase();
61
+ }
62
+ function pageLabel() {
63
+ const page = state.page;
64
+ if (!page) return { text: "Open a page with a form to fill", ready: false };
65
+ const host = new URL(page.origin).host;
66
+ if (page.fieldCount) return { text: `${host} · ${page.fieldCount} field${page.fieldCount === 1 ? "" : "s"} detected`, ready: true };
67
+ if (page.blockedOrigins.includes(page.origin)) return { text: `${host} · allow access to detect fields`, ready: false };
68
+ return { text: `${host} · no contact fields on this page`, ready: false };
69
+ }
70
+ function render() {
71
+ const label = pageLabel();
72
+ $("page").textContent = label.text;
73
+ $("page").dataset.ready = String(label.ready);
74
+ const page = state.page;
75
+ const blocked = !!page && page.blockedOrigins.length > 0;
76
+ $("page-actions").hidden = !blocked;
77
+ $("detect-frames").hidden = !blocked;
78
+ $("detect-frames").textContent = page?.blockedOrigins.includes(page.origin) ? "Detect fields on this page" : "Detect embedded fields";
79
+ const list = $("contacts");
80
+ list.replaceChildren();
81
+ $("empty").hidden = state.contacts.length > 0;
82
+ for (const contact of state.contacts) {
83
+ const li = document.createElement("li");
84
+ const btn = document.createElement("button");
85
+ btn.type = "button";
86
+ btn.className = "contact";
87
+ btn.dataset.contactId = contact.id;
88
+ btn.disabled = !label.ready || busy;
89
+ const avatar = document.createElement("span");
90
+ avatar.className = "avatar";
91
+ avatar.textContent = initials(contact.name);
92
+ const meta = document.createElement("span");
93
+ meta.className = "meta";
94
+ const name = document.createElement("span");
95
+ name.className = "name";
96
+ name.textContent = contact.name;
97
+ const sub = document.createElement("span");
98
+ sub.className = "sub";
99
+ sub.textContent = [contact.company, contact.email].filter(Boolean).join(" · ") || (label.ready ? "Tap to fill" : "Waiting for a form");
100
+ meta.append(name, sub);
101
+ const arrow = document.createElement("span");
102
+ arrow.className = "arrow";
103
+ arrow.textContent = "→";
104
+ btn.append(avatar, meta, arrow);
105
+ btn.addEventListener("click", () => fill(contact, btn).catch(failure));
106
+ li.appendChild(btn);
107
+ list.appendChild(li);
108
+ }
109
+ }
110
+ async function detect() {
111
+ state.page = await request({ op: "detect", input: selection("detect") });
112
+ render();
113
+ }
114
+ async function search(query) {
115
+ const seq = ++searchSeq;
116
+ const contacts = await request({ op: "search", query });
117
+ if (seq !== searchSeq) return;
118
+ state.contacts = contacts;
119
+ render();
120
+ }
121
+ async function fill(contact, btn) {
122
+ if (busy) return;
123
+ busy = true;
124
+ btn.classList.add("busy");
125
+ status(`Filling ${contact.name}…`);
126
+ try {
127
+ const r = await request({ op: "fill", input: selection(contact.id) });
128
+ if (!r.ok) {
129
+ status("No supported fields found. Check the form and try again.", "error");
130
+ return;
131
+ }
132
+ const skipped = r.results.flatMap((x) => x.fields || []).filter((x) => x.status !== "filled").length;
133
+ status(`Filled ${r.filled} field${r.filled === 1 ? "" : "s"}.${skipped ? ` ${skipped} need your review.` : ""} Review before submitting.`, r.filled ? "ok" : "error");
134
+ } finally {
135
+ busy = false;
136
+ btn.classList.remove("busy");
137
+ render();
138
+ }
139
+ }
140
+ async function init(tabId) {
141
+ show("view-connecting");
142
+ try {
143
+ state = await request({ op: "init", tabId });
144
+ show("view-contacts");
145
+ render();
146
+ status(state.page?.fieldCount ? "Pick a contact to fill this page." : "Contacts fills the page you are viewing.");
147
+ $("query").focus();
148
+ } catch (e) {
149
+ failure(e);
150
+ }
151
+ }
152
+ $("query").addEventListener("input", () => {
153
+ clearTimeout(searchTimer);
154
+ const query = $("query").value.trim().slice(0, 200);
155
+ searchTimer = setTimeout(() => search(query).catch(failure), 250);
156
+ });
157
+ $("detect-frames").addEventListener("click", async () => {
158
+ try {
159
+ await permission(state.page.blockedOrigins);
160
+ await detect();
161
+ status(state.page.fieldCount ? "Pick a contact to fill this page." : "No supported fields found on this page.");
162
+ } catch (e) {
163
+ failure(e);
164
+ }
165
+ });
166
+ init(Number(new URL(location.href).searchParams.get("tab")) || undefined);
@@ -4,8 +4,8 @@
4
4
  "name": "contacts",
5
5
  "class": "service",
6
6
  "contractVersion": "v1",
7
- "kitVersion": "0.14.2",
8
- "description": "Contact and company management for AI coding agents. Public CLI, MCP, and SDK clients use one explicitly configured authenticated HTTPS /v1 authority and never open SQLite or PostgreSQL. contacts-serve is the server-only PostgreSQL/API-key boundary.",
7
+ "kitVersion": "1.0.2",
8
+ "description": "Contact and company management for AI coding agents. Public CLI, MCP, and SDK clients use one authenticated HTTPS /v1 authority resolved by the @hasna/contracts client chain (fleet gateway default https://api.hasna.com/contacts) and never open SQLite or PostgreSQL. contacts-serve is the server-only PostgreSQL/API-key boundary.",
9
9
  "bins": [
10
10
  "contacts",
11
11
  "contacts-mcp",
@@ -22,9 +22,21 @@
22
22
  "status": "supported",
23
23
  "bin": "contacts-serve",
24
24
  "authMode": "api-key",
25
- "health": { "method": "GET", "path": "/health", "public": true },
26
- "readiness": { "method": "GET", "path": "/ready", "public": true },
27
- "version": { "method": "GET", "path": "/version", "public": true },
25
+ "health": {
26
+ "method": "GET",
27
+ "path": "/health",
28
+ "public": true
29
+ },
30
+ "readiness": {
31
+ "method": "GET",
32
+ "path": "/ready",
33
+ "public": true
34
+ },
35
+ "version": {
36
+ "method": "GET",
37
+ "path": "/version",
38
+ "public": true
39
+ },
28
40
  "apiBasePath": "/v1",
29
41
  "openApiPath": "/openapi.json"
30
42
  },
@@ -54,7 +66,9 @@
54
66
  ],
55
67
  "storage": {
56
68
  "backend": "postgresql",
57
- "engines": ["postgresql"],
69
+ "engines": [
70
+ "postgresql"
71
+ ],
58
72
  "envPrefix": "HASNA_CONTACTS_",
59
73
  "aliasEnvPrefix": "CONTACTS_",
60
74
  "pgTestGate": {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hasna/contacts",
3
- "version": "0.7.0",
4
- "description": "Contact management for AI coding agents \u2014 CLI + MCP + Web",
3
+ "version": "0.9.0",
4
+ "description": "Contact management for AI coding agents CLI + MCP + Web + Chrome autofill",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -23,6 +23,9 @@
23
23
  "files": [
24
24
  "dist",
25
25
  "migrations",
26
+ "extension",
27
+ "!extension/test",
28
+ "docs/chrome-autofill.md",
26
29
  "hasna.contract.json",
27
30
  "LICENSE",
28
31
  "THIRD_PARTY_NOTICES.md",
@@ -63,11 +66,12 @@
63
66
  },
64
67
  "repository": {
65
68
  "type": "git",
66
- "url": "git+https://github.com/hasna/contacts.git"
69
+ "url": "https://github.com/hasna/apps.git",
70
+ "directory": "apps/contacts"
67
71
  },
68
- "homepage": "https://github.com/hasna/contacts",
72
+ "homepage": "https://github.com/hasna/apps/tree/main/apps/contacts#readme",
69
73
  "bugs": {
70
- "url": "https://github.com/hasna/contacts/issues"
74
+ "url": "https://github.com/hasna/apps/issues"
71
75
  },
72
76
  "engines": {
73
77
  "bun": ">=1.0.0"
@@ -75,7 +79,7 @@
75
79
  "author": "Andrei Hasna <andrei@hasna.com>",
76
80
  "license": "Apache-2.0",
77
81
  "dependencies": {
78
- "@hasna/contracts": "0.14.2",
82
+ "@hasna/contracts": "1.0.2",
79
83
  "@hasna/events": "^0.1.6",
80
84
  "chalk": "^5.4.1",
81
85
  "commander": "^13.1.0",
@@ -1,7 +0,0 @@
1
- interface ContactsConfig {
2
- db_path?: string;
3
- }
4
- export declare function readConfig(): ContactsConfig;
5
- export declare function writeConfig(config: ContactsConfig): void;
6
- export {};
7
- //# sourceMappingURL=config.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAOA,UAAU,cAAc;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,UAAU,IAAI,cAAc,CAG3C;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,CAGxD"}