@hraness/peopleblade 0.1.2 → 0.2.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.
@@ -3,8 +3,9 @@
3
3
  var __require = import.meta.require;
4
4
 
5
5
  // src/cli/main.ts
6
- import { existsSync as existsSync5 } from "fs";
6
+ import { closeSync as closeSync9, constants as constants9, existsSync as existsSync5, fstatSync as fstatSync8, openSync as openSync9, readSync as readSync3 } from "fs";
7
7
  import { resolve as resolve6 } from "path";
8
+ import { ZodError } from "zod";
8
9
 
9
10
  // src/local/contacts.ts
10
11
  import { closeSync, constants, fstatSync, openSync, readFileSync } from "fs";
@@ -363,6 +364,7 @@ var serverCliEnrichmentStartSchema = z.object({
363
364
  var cliEnrichmentStatusSchema = z.object({
364
365
  jobId: z.uuid()
365
366
  }).strict();
367
+ var cliEnrichmentPublicEmailsRequestSchema = z.object({}).strict();
366
368
  var cliEnrichmentRevalidationSchema = z.object({
367
369
  limit: z.number().int().min(1).max(100)
368
370
  }).strict();
@@ -2857,8 +2859,8 @@ function exactProfileSupersessionConflictsWithClaim(subject, output, clause, fie
2857
2859
  if (clause.tokens.some((token) => LOCATION_SUPERSESSION_MARKERS.has(token))) {
2858
2860
  return true;
2859
2861
  }
2860
- const location = normalizedEvidenceText(output.location).split(" ").filter(Boolean);
2861
- return location.length === 0 || normalizedPhraseStarts(clause.tokens, location).length > 0;
2862
+ const location2 = normalizedEvidenceText(output.location).split(" ").filter(Boolean);
2863
+ return location2.length === 0 || normalizedPhraseStarts(clause.tokens, location2).length > 0;
2862
2864
  }
2863
2865
  if (!exactProfileClauseSupersedesEmployment(subject, clause))
2864
2866
  return false;
@@ -3836,6 +3838,26 @@ var researchFileSchema = z3.object({
3836
3838
  researcher: z3.enum(["agent-public-web", "human-public-web"]),
3837
3839
  result: manualResearchSchema
3838
3840
  }).strict();
3841
+ var researchInstructions = "Search only public pages. A same-name page is never enough: one identity-evidence row must use a trusted profile URL from identityAnchors, or contain the exact full name plus a stored organization or title or a trusted email or phone from identityAnchors. providerHandles are display/search hints only. Do not guess emails. Cite every non-null field with evidence indexes.";
3842
+ var researchSubjectSchema = z3.object({
3843
+ name: z3.string(),
3844
+ organization: z3.string().nullable(),
3845
+ title: z3.string().nullable(),
3846
+ emails: z3.array(z3.string()).max(100),
3847
+ phones: z3.array(z3.string()).max(100),
3848
+ identityAnchors: z3.object({
3849
+ emails: z3.array(z3.string()).max(100),
3850
+ phones: z3.array(z3.string()).max(100),
3851
+ profileUrls: z3.array(z3.string()).max(100)
3852
+ }).strict(),
3853
+ providerHandles: z3.record(z3.string(), z3.string())
3854
+ }).strict();
3855
+ var preparedResearchSchema = researchFileSchema.extend({
3856
+ schemaVersion: z3.literal("peopleblade.research.v1"),
3857
+ inputSha256: z3.string().regex(/^[a-f0-9]{64}$/u),
3858
+ subject: researchSubjectSchema,
3859
+ instructions: z3.literal(researchInstructions)
3860
+ }).strict();
3839
3861
  var MAX_RESEARCH_FILE_BYTES = 2 * 1024 * 1024;
3840
3862
  var MAX_RESEARCH_EMAILS = 100;
3841
3863
  var MAX_RESEARCH_EMAIL_CANDIDATES = 1e4;
@@ -3980,53 +4002,582 @@ function readResearchFile(path) {
3980
4002
  }
3981
4003
  }
3982
4004
  function applyManualResearch(database, path) {
3983
- const parsed = researchFileSchema.parse(readResearchFile(path));
3984
- const { evidence, ...output } = parsed.result;
3985
- const context = currentResearchContext(database, parsed.personId);
3986
- validateEnrichmentIdentityBinding({
3987
- displayName: context.displayName,
4005
+ const parsed = z3.union([preparedResearchSchema, researchFileSchema]).parse(readResearchFile(path));
4006
+ return database.transaction(() => {
4007
+ const { evidence, ...output } = parsed.result;
4008
+ const context = currentResearchContext(database, parsed.personId);
4009
+ if ("schemaVersion" in parsed) {
4010
+ const current = preparedResearchContext(database, context);
4011
+ if (parsed.personId !== context.personId || parsed.inputSha256 !== current.inputSha256 || canonicalJson(parsed.subject) !== canonicalJson(current.subject)) {
4012
+ throw new Error("Research preparation is stale or belongs to a different database. Prepare this contact again.");
4013
+ }
4014
+ }
4015
+ validateEnrichmentIdentityBinding({
4016
+ displayName: context.displayName,
4017
+ emails: context.emails,
4018
+ phones: context.phones,
4019
+ identityAnchors: {
4020
+ state: "available",
4021
+ emails: [...context.emails],
4022
+ phones: [...context.phones],
4023
+ profileUrls: [...context.profileUrls]
4024
+ },
4025
+ organization: context.organization,
4026
+ title: context.title,
4027
+ providerHandles: context.providerHandles
4028
+ }, output, evidence);
4029
+ const evidenceSha = sha256(canonicalJson(parsed.result.evidence));
4030
+ const existing = database.query("SELECT id,output_json FROM public_enrichments WHERE person_id=? AND provider=? AND input_sha256=? AND evidence_sha256=?").get(context.personId, parsed.researcher, context.inputSha256, evidenceSha);
4031
+ if (existing !== null) {
4032
+ if (canonicalJson(JSON.parse(existing.output_json)) !== canonicalJson(parsed.result)) {
4033
+ throw new Error("Research evidence already has a different reviewed result. Existing evidence is immutable; do not treat this as a successful replay.");
4034
+ }
4035
+ return { enrichmentId: existing.id, cached: true };
4036
+ }
4037
+ const result = database.query(`INSERT INTO public_enrichments(person_id,provider,input_sha256,evidence_sha256,evidence_json,output_json)
4038
+ VALUES (?,?,?,?,?,?)`).run(context.personId, parsed.researcher, context.inputSha256, evidenceSha, canonicalJson(parsed.result.evidence), canonicalJson(parsed.result));
4039
+ return { enrichmentId: Number(result.lastInsertRowid), cached: false };
4040
+ }).immediate();
4041
+ }
4042
+ function preparedResearchContext(database, context) {
4043
+ const subject = {
4044
+ name: context.displayName,
4045
+ organization: context.organization,
4046
+ title: context.title,
3988
4047
  emails: context.emails,
3989
4048
  phones: context.phones,
3990
4049
  identityAnchors: {
3991
- state: "available",
3992
- emails: [...context.emails],
3993
- phones: [...context.phones],
3994
- profileUrls: [...context.profileUrls]
4050
+ emails: context.emails,
4051
+ phones: context.phones,
4052
+ profileUrls: context.profileUrls
3995
4053
  },
3996
- organization: context.organization,
3997
- title: context.title,
3998
4054
  providerHandles: context.providerHandles
3999
- }, output, evidence);
4000
- const evidenceSha = sha256(canonicalJson(parsed.result.evidence));
4001
- const existing = database.query("SELECT id FROM public_enrichments WHERE person_id=? AND provider=? AND input_sha256=? AND evidence_sha256=?").get(context.personId, parsed.researcher, context.inputSha256, evidenceSha);
4002
- if (existing !== null)
4003
- return { enrichmentId: existing.id, cached: true };
4004
- const result = database.query(`INSERT INTO public_enrichments(person_id,provider,input_sha256,evidence_sha256,evidence_json,output_json)
4005
- VALUES (?,?,?,?,?,?)`).run(context.personId, parsed.researcher, context.inputSha256, evidenceSha, canonicalJson(parsed.result.evidence), canonicalJson(parsed.result));
4006
- return { enrichmentId: Number(result.lastInsertRowid), cached: false };
4055
+ };
4056
+ return {
4057
+ subject,
4058
+ inputSha256: sha256(canonicalJson({
4059
+ schemaVersion: "peopleblade.research.v1",
4060
+ databaseInstanceId: localDatabaseInstanceId(database),
4061
+ personId: context.personId,
4062
+ contextSha256: context.inputSha256,
4063
+ subject,
4064
+ instructions: researchInstructions
4065
+ }))
4066
+ };
4007
4067
  }
4008
4068
  function researchTemplate(database, personId) {
4009
- const context = currentResearchContext(database, personId);
4069
+ return database.transaction(() => {
4070
+ const context = currentResearchContext(database, personId);
4071
+ return {
4072
+ schemaVersion: "peopleblade.research.v1",
4073
+ personId: context.personId,
4074
+ researcher: "agent-public-web",
4075
+ ...preparedResearchContext(database, context),
4076
+ instructions: researchInstructions,
4077
+ result: { identityMatch: "insufficient", identityEvidenceIndexes: [], confidence: 0, headline: null, organization: null, role: null, location: null, website: null, publicEmail: null, publicEmailEvidenceIndex: null, notes: "", claims: [], evidence: [] }
4078
+ };
4079
+ }).deferred();
4080
+ }
4081
+
4082
+ // src/local/contact-query.ts
4083
+ import { z as z4 } from "zod";
4084
+ var contactQueryInputSchema = z4.object({
4085
+ search: z4.string().max(200).default(""),
4086
+ source: z4.string().regex(/^[a-z][a-z0-9-]{0,63}$/u).optional(),
4087
+ doNotContact: z4.enum(["all", "exclude", "only"]).default("all"),
4088
+ hasEmail: z4.boolean().optional(),
4089
+ hasPhone: z4.boolean().optional(),
4090
+ sort: z4.enum(["name", "organization", "interactions", "last-contact"]).default("interactions"),
4091
+ direction: z4.enum(["asc", "desc"]).default("desc"),
4092
+ limit: z4.number().int().min(1).max(100).default(50),
4093
+ offset: z4.number().int().min(0).max(1e6).default(0)
4094
+ }).strict();
4095
+ var contactCtes = `
4096
+ -- Resolve the existing canonical view once, including reviewed representatives.
4097
+ -- The schema qualifier avoids a recursive reference to this local CTE.
4098
+ person_identity_components AS MATERIALIZED (
4099
+ SELECT * FROM main.person_identity_components
4100
+ ),
4101
+ ${canonicalInteractionRollupCtesSql},
4102
+ active_resources AS MATERIALIZED (
4103
+ SELECT resource.*, member.canonical_person_id, realm.service
4104
+ FROM provider_resources resource
4105
+ JOIN person_identity_components member ON member.person_id=resource.person_id
4106
+ LEFT JOIN source_realms realm ON realm.id=resource.source_realm_id
4107
+ WHERE resource.active=1 AND (realm.id IS NULL OR realm.active=1)
4108
+ ),
4109
+ active_methods AS MATERIALIZED (
4110
+ SELECT method.*,member.canonical_person_id
4111
+ FROM contact_methods method
4112
+ JOIN person_identity_components member ON member.person_id=method.person_id
4113
+ LEFT JOIN provider_resources resource ON resource.id=method.provider_resource_id
4114
+ LEFT JOIN source_realms realm ON realm.id=resource.source_realm_id
4115
+ WHERE method.active=1 AND (method.provider_resource_id IS NULL OR (
4116
+ resource.active=1 AND (realm.id IS NULL OR realm.active=1)
4117
+ ))
4118
+ ),
4119
+ component_rollup AS (
4120
+ SELECT member.canonical_person_id AS person_id,max(person.do_not_contact) AS do_not_contact,
4121
+ CASE WHEN count(DISTINCT person.birthday)=1 THEN min(person.birthday) ELSE NULL END AS birthday
4122
+ FROM person_identity_components member JOIN people person ON person.id=member.person_id
4123
+ GROUP BY member.canonical_person_id
4124
+ ),
4125
+ source_values AS (
4126
+ SELECT canonical_person_id AS person_id,provider FROM active_resources
4127
+ UNION SELECT person_id,provider FROM canonical_interaction_inputs
4128
+ ),
4129
+ ranked_methods AS (
4130
+ SELECT canonical_person_id,kind,value,
4131
+ row_number() OVER (
4132
+ PARTITION BY canonical_person_id,kind ORDER BY is_primary DESC,id
4133
+ ) AS preference
4134
+ FROM active_methods WHERE kind IN ('email','phone')
4135
+ ),
4136
+ primary_methods AS (
4137
+ SELECT canonical_person_id,
4138
+ max(CASE WHEN kind='email' THEN value END) AS primary_email,
4139
+ max(CASE WHEN kind='phone' THEN value END) AS primary_phone
4140
+ FROM ranked_methods WHERE preference=1 GROUP BY canonical_person_id
4141
+ ),
4142
+ source_rollup AS (
4143
+ SELECT person_id,json_group_array(provider) AS sources_json
4144
+ FROM source_values GROUP BY person_id
4145
+ ),
4146
+ -- Recursive-view cardinality estimates can make SQLite repeatedly scan each
4147
+ -- aggregated LEFT JOIN for every contact. Union the independent fact lanes
4148
+ -- and group once instead; each lane contributes at most one row per person.
4149
+ summary_facts(person_id,birthday,do_not_contact,interaction_count,reciprocal,
4150
+ last_interaction_at,primary_email,primary_phone,sources_json) AS (
4151
+ SELECT person_id,birthday,do_not_contact,NULL,NULL,NULL,NULL,NULL,NULL FROM component_rollup
4152
+ UNION ALL
4153
+ SELECT person_id,NULL,NULL,interaction_count,reciprocal,last_interaction_at,NULL,NULL,NULL
4154
+ FROM canonical_interaction_rollup
4155
+ UNION ALL
4156
+ SELECT canonical_person_id,NULL,NULL,NULL,NULL,NULL,primary_email,primary_phone,NULL
4157
+ FROM primary_methods
4158
+ UNION ALL
4159
+ SELECT person_id,NULL,NULL,NULL,NULL,NULL,NULL,NULL,sources_json FROM source_rollup
4160
+ ),
4161
+ summary_rollup AS (
4162
+ SELECT person_id,max(birthday) AS birthday,max(do_not_contact) AS do_not_contact,
4163
+ coalesce(max(interaction_count),0) AS interaction_count,
4164
+ coalesce(max(reciprocal),0) AS reciprocal,max(last_interaction_at) AS last_interaction_at,
4165
+ max(primary_email) AS primary_email,max(primary_phone) AS primary_phone,
4166
+ coalesce(max(sources_json),'[]') AS sources_json
4167
+ FROM summary_facts GROUP BY person_id
4168
+ ),
4169
+ summaries AS (
4170
+ SELECT person.id,coalesce(person.display_name,'Unnamed contact') AS display_name,
4171
+ person.organization,person.title,contact.birthday,contact.do_not_contact,
4172
+ contact.interaction_count,contact.reciprocal,contact.last_interaction_at,
4173
+ contact.primary_email,contact.primary_phone,contact.sources_json
4174
+ FROM people person
4175
+ JOIN summary_rollup contact ON contact.person_id=person.id
4176
+ )
4177
+ `;
4178
+ function summary(row) {
4179
+ const sources = JSON.parse(row.sources_json);
4010
4180
  return {
4011
- personId: context.personId,
4012
- researcher: "agent-public-web",
4013
- subject: {
4014
- name: context.displayName,
4015
- organization: context.organization,
4016
- title: context.title,
4017
- emails: context.emails,
4018
- phones: context.phones,
4019
- identityAnchors: {
4020
- emails: context.emails,
4021
- phones: context.phones,
4022
- profileUrls: context.profileUrls
4023
- },
4024
- providerHandles: context.providerHandles
4025
- },
4026
- instructions: "Search only public pages. A same-name page is never enough: one identity-evidence row must use a trusted profile URL from identityAnchors, or contain the exact full name plus a stored organization or title or a trusted email or phone from identityAnchors. providerHandles are display/search hints only. Do not guess emails. Cite every non-null field with evidence indexes.",
4027
- result: { identityMatch: "insufficient", identityEvidenceIndexes: [], confidence: 0, headline: null, organization: null, role: null, location: null, website: null, publicEmail: null, publicEmailEvidenceIndex: null, notes: "", claims: [], evidence: [] }
4181
+ id: row.id,
4182
+ displayName: row.display_name,
4183
+ organization: row.organization,
4184
+ title: row.title,
4185
+ birthday: row.birthday,
4186
+ primaryEmail: row.primary_email,
4187
+ primaryPhone: row.primary_phone,
4188
+ interactionCount: row.interaction_count,
4189
+ reciprocal: row.reciprocal === 1,
4190
+ lastInteractionAt: row.last_interaction_at,
4191
+ doNotContact: row.do_not_contact === 1,
4192
+ sources: z4.array(z4.string().max(64)).parse(sources).sort((left, right) => Buffer.compare(Buffer.from(left, "utf8"), Buffer.from(right, "utf8")))
4193
+ };
4194
+ }
4195
+ function filter(input) {
4196
+ const clauses = [];
4197
+ const bindings = [];
4198
+ const search = input.search.trim();
4199
+ if (search.length > 0) {
4200
+ const pattern = `%${search.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_")}%`;
4201
+ clauses.push(`contact.id IN (
4202
+ SELECT member.canonical_person_id FROM people alias
4203
+ JOIN person_identity_components member ON member.person_id=alias.id
4204
+ WHERE (
4205
+ coalesce(alias.display_name,'') LIKE ? ESCAPE '\\' COLLATE NOCASE OR
4206
+ coalesce(alias.organization,'') LIKE ? ESCAPE '\\' COLLATE NOCASE OR
4207
+ coalesce(alias.title,'') LIKE ? ESCAPE '\\' COLLATE NOCASE)
4208
+ UNION SELECT method.canonical_person_id FROM active_methods method
4209
+ WHERE method.value LIKE ? ESCAPE '\\' COLLATE NOCASE
4210
+ UNION SELECT resource.canonical_person_id FROM active_resources resource
4211
+ WHERE (coalesce(resource.username,'') LIKE ? ESCAPE '\\' COLLATE NOCASE OR
4212
+ coalesce(resource.profile_url,'') LIKE ? ESCAPE '\\' COLLATE NOCASE))`);
4213
+ bindings.push(pattern, pattern, pattern, pattern, pattern, pattern);
4214
+ }
4215
+ if (input.source !== undefined) {
4216
+ clauses.push("contact.id IN (SELECT person_id FROM source_values WHERE provider=?)");
4217
+ bindings.push(input.source);
4218
+ }
4219
+ if (input.doNotContact !== "all") {
4220
+ clauses.push("contact.do_not_contact=?");
4221
+ bindings.push(input.doNotContact === "only" ? 1 : 0);
4222
+ }
4223
+ if (input.hasEmail !== undefined)
4224
+ clauses.push(`contact.primary_email IS ${input.hasEmail ? "NOT " : ""}NULL`);
4225
+ if (input.hasPhone !== undefined)
4226
+ clauses.push(`contact.primary_phone IS ${input.hasPhone ? "NOT " : ""}NULL`);
4227
+ return { sql: clauses.length === 0 ? "" : `WHERE ${clauses.join(" AND ")}`, bindings };
4228
+ }
4229
+ function queryLocalContacts(database, value = {}) {
4230
+ const input = contactQueryInputSchema.parse(value);
4231
+ const where = filter(input);
4232
+ const sortColumns = {
4233
+ name: "display_name COLLATE NOCASE",
4234
+ organization: "organization COLLATE NOCASE",
4235
+ interactions: "interaction_count",
4236
+ "last-contact": "last_interaction_at"
4237
+ };
4238
+ const column = sortColumns[input.sort];
4239
+ const direction = input.direction === "asc" ? "ASC" : "DESC";
4240
+ return database.transaction(() => {
4241
+ const total = database.query(`WITH ${contactCtes}
4242
+ SELECT count(*) AS count FROM summaries contact ${where.sql}`).get(...where.bindings)?.count ?? 0;
4243
+ const rows = database.query(`WITH ${contactCtes}
4244
+ SELECT contact.* FROM summaries contact ${where.sql}
4245
+ ORDER BY ${column} ${direction},id ASC LIMIT ? OFFSET ?
4246
+ `).all(...where.bindings, input.limit, input.offset);
4247
+ const sources = database.query(`WITH ${contactCtes}
4248
+ SELECT provider AS source,count(*) AS count FROM source_values GROUP BY provider ORDER BY provider`).all();
4249
+ return {
4250
+ schemaVersion: "peopleblade.contact-query.v1",
4251
+ consistency: "live",
4252
+ items: rows.map(summary),
4253
+ total,
4254
+ limit: input.limit,
4255
+ offset: input.offset,
4256
+ nextOffset: input.offset + rows.length < total ? input.offset + rows.length : null,
4257
+ facets: { sources }
4258
+ };
4259
+ }).deferred();
4260
+ }
4261
+ function getLocalContactDetail(database, requestedPersonId) {
4262
+ const id = z4.number().int().positive().max(Number.MAX_SAFE_INTEGER).parse(requestedPersonId);
4263
+ return database.transaction(() => {
4264
+ const canonical = database.query("SELECT canonical_person_id FROM person_identity_components WHERE person_id=?").get(id)?.canonical_person_id;
4265
+ if (canonical === undefined)
4266
+ throw new Error("Contact not found.");
4267
+ const row = database.query(`WITH ${contactCtes}
4268
+ SELECT * FROM summaries WHERE id=?`).get(canonical);
4269
+ if (row === null)
4270
+ throw new Error("Contact not found.");
4271
+ const totals = database.query(`WITH ${contactCtes} SELECT
4272
+ (SELECT count(*) FROM person_identity_components WHERE canonical_person_id=?) AS members,
4273
+ (SELECT count(*) FROM active_methods WHERE canonical_person_id=?) AS methods,
4274
+ (SELECT count(*) FROM active_resources WHERE canonical_person_id=?) AS sources`).get(canonical, canonical, canonical);
4275
+ const methods = database.query(`WITH ${contactCtes} SELECT id,kind,value,label,is_primary,identity_eligible,confidence
4276
+ FROM active_methods WHERE canonical_person_id=? ORDER BY is_primary DESC,kind,id LIMIT 100`).all(canonical);
4277
+ const sources = database.query(`WITH ${contactCtes} SELECT provider,service,resource_type,profile_url,profile_url_identity_eligible
4278
+ FROM active_resources WHERE canonical_person_id=? ORDER BY provider,id LIMIT 100`).all(canonical);
4279
+ return {
4280
+ schemaVersion: "peopleblade.contact-detail.v1",
4281
+ consistency: "live",
4282
+ requestedPersonId: id,
4283
+ canonicalPersonId: canonical,
4284
+ contact: summary(row),
4285
+ memberCount: totals.members,
4286
+ methods: { total: totals.methods, truncated: totals.methods > methods.length, items: methods.map((method) => ({
4287
+ id: method.id,
4288
+ kind: method.kind,
4289
+ value: method.value,
4290
+ label: method.label,
4291
+ primary: method.is_primary === 1,
4292
+ identityEligible: method.identity_eligible === 1 && method.confidence === "exact",
4293
+ confidence: method.confidence
4294
+ })) },
4295
+ sources: { total: totals.sources, truncated: totals.sources > sources.length, items: sources.map((source) => ({
4296
+ provider: source.provider,
4297
+ service: source.service,
4298
+ resourceType: source.resource_type,
4299
+ profileUrl: source.profile_url,
4300
+ identityEligible: source.profile_url_identity_eligible === 1
4301
+ })) }
4302
+ };
4303
+ }).deferred();
4304
+ }
4305
+
4306
+ // src/local/capabilities.ts
4307
+ var peoplebladeCapabilities = Object.freeze({
4308
+ schemaVersion: "peopleblade.capabilities.v1",
4309
+ availability: "declarative-not-configured",
4310
+ localAuthority: true,
4311
+ discoveryPerformsNetworkRequests: false,
4312
+ sources: [
4313
+ { id: "apple-contacts", command: "contacts sync", status: "supported", transport: "local-database", requirement: "macOS Contacts access" },
4314
+ { id: "imessage", command: "imessage sync", status: "supported", transport: "local-metadata", requirement: "macOS Messages access; no message bodies" },
4315
+ { id: "google-contacts", command: "google sync", status: "supported", transport: "wrench", requirement: "Reviewed Gmail/People contacts.list and contact scopes" },
4316
+ { id: "beeper", command: "beeper sync", status: "supported", transport: "wrench", requirement: "Wrench 0.16.8, Beeper adapter 2.4.0, CLI 0.6.2; body-free relationships require macOS arm64" },
4317
+ { id: "whatsapp", command: "whatsapp sync", status: "supported", transport: "wrench", requirement: "Reviewed linked-device contacts.list v2" },
4318
+ { id: "linkedin", command: "linkedin import", status: "supported", transport: "official-archive", requirement: "Owner-selected official ZIP; no authenticated browser collection" },
4319
+ { id: "instagram", command: "instagram import", status: "supported", transport: "official-archive", requirement: "Owner-selected JSON ZIP and exact owner username" },
4320
+ { id: "x-archive", command: "x import", status: "bounded", transport: "official-archive", requirement: "Owner-selected ZIP; unresolved IDs remain inert" },
4321
+ { id: "x-live", command: "x mutuals sync", status: "blocked", transport: "unavailable", requirement: "A reviewed bounded Wrench mutuals capability is not yet available" },
4322
+ { id: "telegram", command: "telegram status", status: "planned", transport: "unavailable", requirement: "First real export and importer validation remain pending" }
4323
+ ],
4324
+ enrichment: [
4325
+ { id: "local-anchors", priority: 1, status: "supported", commands: ["identity audit", "identity suggest", "identity decide"], paid: false, boundary: "Exact source evidence first; joins require reversible evidence-bound decisions" },
4326
+ { id: "manual-public-research", priority: 2, status: "supported", commands: ["research prepare", "research apply"], paid: false, boundary: "Agent or human supplies cited public evidence; PeopleBlade performs no provider calls" },
4327
+ { id: "cloud-professional-research", priority: 3, status: "optional", commands: ["cloud enrich --person-id ID", "cloud enrich --confirm TOKEN"], paid: true, boundary: "Exa exact profiles or bounded search, then cited model extraction; device-bound preview approval required" },
4328
+ { id: "cloud-email-fallback", priority: 4, status: "optional", commands: ["cloud enrich --person-id ID"], paid: true, boundary: "Hunter is a separately configured narrow fallback, not a substitute for identity evidence" }
4329
+ ],
4330
+ interfaces: {
4331
+ workspace: { command: "ui [--port N]", availability: "included-in-this-build", binding: "127.0.0.1", defaultPort: "random", authentication: "per-process capability file", remoteAssets: false, noteUpload: false },
4332
+ query: { command: "query", schemaVersion: "peopleblade.contact-query.v1", consistency: "live", defaultLimit: 50, maximumLimit: 100, pagination: "offset", sorts: ["name", "organization", "interactions", "last-contact"] },
4333
+ detail: { command: "people show ID", schemaVersion: "peopleblade.contact-detail.v1", maximumMethods: 100, maximumSources: 100 },
4334
+ notes: { commands: ["notes add", "notes show ID", "notes update ID", "notes history ID", "notes list", "notes search"], cloudSync: false, edits: "append-only-compare-and-swap" },
4335
+ research: { schemaVersion: "peopleblade.research.v1", commands: ["research prepare ID", "research apply FILE"], legacyInputAccepted: true },
4336
+ errors: { schemaVersion: "peopleblade.error.v1", stream: "stderr", enabledBy: "--json" }
4337
+ },
4338
+ boundaries: [
4339
+ "Supported does not mean configured or authorized; discovery does not inspect private configuration.",
4340
+ "Notes, archives, credentials, provider payloads, and message bodies are not cloud-sync inputs.",
4341
+ "Provider availability never authorizes contact, outreach, account automation, or a paid request.",
4342
+ "Query pagination is a live view; concurrent imports and identity decisions can move rows between pages."
4343
+ ]
4344
+ });
4345
+
4346
+ // src/local/workspace.ts
4347
+ import { randomBytes } from "crypto";
4348
+ import { chmodSync, lstatSync, mkdtempSync, rmdirSync, unlinkSync, writeFileSync } from "fs";
4349
+ import { join } from "path";
4350
+ import { tmpdir } from "os";
4351
+ import { z as z5 } from "zod";
4352
+
4353
+ // src/local/markdown.ts
4354
+ function previewNoteMarkdown(source) {
4355
+ if (Buffer.byteLength(source, "utf8") > 65536)
4356
+ throw new Error("Markdown exceeds 64 KiB.");
4357
+ return Bun.markdown.render(source, {
4358
+ text: (text) => Bun.escapeHTML(text),
4359
+ html: (text) => Bun.escapeHTML(text),
4360
+ paragraph: (children) => `<p>${children}</p>`,
4361
+ heading: (children, { level }) => `<h${level}>${children}</h${level}>`,
4362
+ strong: (children) => `<strong>${children}</strong>`,
4363
+ emphasis: (children) => `<em>${children}</em>`,
4364
+ strikethrough: (children) => `<del>${children}</del>`,
4365
+ codespan: (children) => `<code>${children}</code>`,
4366
+ code: (children) => `<pre><code>${children}</code></pre>`,
4367
+ blockquote: (children) => `<blockquote>${children}</blockquote>`,
4368
+ list: (children, meta) => meta.ordered ? `<ol>${children}</ol>` : `<ul>${children}</ul>`,
4369
+ listItem: (children, meta) => `<li>${meta.checked === undefined ? "" : meta.checked ? "\u2611 " : "\u2610 "}${children}</li>`,
4370
+ hr: () => "<hr>",
4371
+ table: (children) => `<table>${children}</table>`,
4372
+ thead: (children) => `<thead>${children}</thead>`,
4373
+ tbody: (children) => `<tbody>${children}</tbody>`,
4374
+ tr: (children) => `<tr>${children}</tr>`,
4375
+ th: (children) => `<th>${children}</th>`,
4376
+ td: (children) => `<td>${children}</td>`,
4377
+ image: (children) => `<span>[Image omitted${children ? `: ${children}` : ""}]</span>`,
4378
+ link: (children, { href }) => {
4379
+ try {
4380
+ const url = new URL(href);
4381
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password)
4382
+ return children;
4383
+ return `<a href="${Bun.escapeHTML(url.href)}" target="_blank" rel="noopener noreferrer">${children}</a>`;
4384
+ } catch {
4385
+ return children;
4386
+ }
4387
+ }
4388
+ }, { tables: true, strikethrough: true, tasklists: true, noHtmlBlocks: true, noHtmlSpans: true, autolinks: false });
4389
+ }
4390
+
4391
+ // src/local/note-revisions.ts
4392
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
4393
+ var SHA256 = /^[0-9a-f]{64}$/u;
4394
+ var UNSUPPORTED_CONTROLS = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/u;
4395
+
4396
+ class NoteRevisionError extends Error {
4397
+ code;
4398
+ constructor(code, message) {
4399
+ super(message);
4400
+ this.name = "NoteRevisionError";
4401
+ this.code = code;
4402
+ }
4403
+ }
4404
+ function invalid(message) {
4405
+ throw new NoteRevisionError("invalid_input", message);
4406
+ }
4407
+ function positiveInteger(value, label) {
4408
+ if (!Number.isSafeInteger(value) || value < 1)
4409
+ invalid(`${label} must be a positive safe integer.`);
4410
+ }
4411
+ function validatePersonNoteMutationRequest(requestId, expectedContextSha256) {
4412
+ if (typeof requestId !== "string" || !UUID.test(requestId))
4413
+ invalid("Request ID must be a canonical lowercase UUID.");
4414
+ if (typeof expectedContextSha256 !== "string" || !SHA256.test(expectedContextSha256))
4415
+ invalid("Expected context must be a SHA-256 digest.");
4416
+ }
4417
+ function personNoteCreationContext(database, personId) {
4418
+ positiveInteger(personId, "Person ID");
4419
+ const row = database.query(`
4420
+ WITH components AS MATERIALIZED (SELECT * FROM person_identity_components)
4421
+ SELECT target.canonical_person_id,
4422
+ (SELECT instance_id FROM local_state WHERE singleton=1) AS database_instance_id,
4423
+ (SELECT identity_revision FROM local_revisions WHERE singleton=1) AS identity_revision,
4424
+ (SELECT json_group_array(person_id) FROM (
4425
+ SELECT person_id FROM components WHERE canonical_person_id=target.canonical_person_id ORDER BY person_id
4426
+ )) AS component_person_ids
4427
+ FROM components target WHERE target.person_id=?
4428
+ `).get(personId);
4429
+ if (row === null)
4430
+ throw new NoteRevisionError("not_found", "Person not found.");
4431
+ const members = JSON.parse(row.component_person_ids);
4432
+ if (!Array.isArray(members) || !members.every((id) => typeof id === "number" && Number.isSafeInteger(id) && id > 0)) {
4433
+ throw new Error("Stored note person context is malformed.");
4434
+ }
4435
+ const canonicalPersonId = Number(row.canonical_person_id);
4436
+ const contextJson = canonicalJson({
4437
+ schemaVersion: 1,
4438
+ databaseInstanceId: row.database_instance_id,
4439
+ personId,
4440
+ canonicalPersonId,
4441
+ componentPersonIds: members,
4442
+ identityRevision: Number(row.identity_revision)
4443
+ });
4444
+ return { personId, canonicalPersonId, contextJson, contextSha256: sha256(contextJson) };
4445
+ }
4446
+ function validatePersonNoteContent(title, body) {
4447
+ if (typeof body !== "string" || body.trim().length === 0)
4448
+ invalid("Note body must not be empty.");
4449
+ if (Buffer.byteLength(body, "utf8") > 65536)
4450
+ invalid("Note body exceeds 64 KiB.");
4451
+ if (UNSUPPORTED_CONTROLS.test(body) || Buffer.from(body, "utf8").toString("utf8") !== body) {
4452
+ invalid("Note body contains unsupported control characters or malformed Unicode.");
4453
+ }
4454
+ if (title !== null && (typeof title !== "string" || title.trim().length === 0 || [...title].length > 1024 || UNSUPPORTED_CONTROLS.test(title) || /[\r\n]/u.test(title) || Buffer.from(title, "utf8").toString("utf8") !== title)) {
4455
+ invalid("Note title must be null or nonblank text of at most 1024 characters.");
4456
+ }
4457
+ }
4458
+ function personNoteContexts(database, noteIds) {
4459
+ if (noteIds.length > 1001)
4460
+ invalid("Note context request exceeds its bounded page limit.");
4461
+ for (const noteId of noteIds)
4462
+ positiveInteger(noteId, "Note ID");
4463
+ if (noteIds.length === 0)
4464
+ return new Map;
4465
+ const rows = database.query(`
4466
+ WITH components AS MATERIALIZED (SELECT * FROM person_identity_components)
4467
+ SELECT note.id,note.revision,original.person_id AS original_person_id,
4468
+ (SELECT instance_id FROM local_state WHERE singleton=1) AS database_instance_id,
4469
+ note.person_id AS attributed_person_id,
4470
+ coalesce((SELECT max(id) FROM person_note_attribution_revisions WHERE note_id=note.id),0) AS attribution_revision_id,
4471
+ (SELECT identity_revision FROM local_revisions WHERE singleton=1) AS identity_revision,
4472
+ target.canonical_person_id,
4473
+ (SELECT json_group_array(person_id) FROM (
4474
+ SELECT member.person_id FROM components member
4475
+ WHERE member.canonical_person_id=target.canonical_person_id ORDER BY member.person_id
4476
+ )) AS component_person_ids
4477
+ FROM person_notes_effective note JOIN person_notes original ON original.id=note.id
4478
+ JOIN components target ON target.person_id=note.person_id
4479
+ WHERE note.id IN (${noteIds.map(() => "?").join(",")})
4480
+ `).all(...noteIds);
4481
+ return new Map(rows.map((row) => {
4482
+ const members = JSON.parse(row.component_person_ids);
4483
+ if (!Array.isArray(members) || !members.every((id) => typeof id === "number" && Number.isSafeInteger(id) && id > 0)) {
4484
+ throw new Error("Stored note component context is malformed.");
4485
+ }
4486
+ const contextJson = canonicalJson({
4487
+ schemaVersion: 1,
4488
+ databaseInstanceId: row.database_instance_id,
4489
+ noteId: Number(row.id),
4490
+ revision: Number(row.revision),
4491
+ originalPersonId: Number(row.original_person_id),
4492
+ attributedPersonId: Number(row.attributed_person_id),
4493
+ attributionRevisionId: Number(row.attribution_revision_id),
4494
+ identityRevision: Number(row.identity_revision),
4495
+ canonicalPersonId: Number(row.canonical_person_id),
4496
+ componentPersonIds: members
4497
+ });
4498
+ return [Number(row.id), { revision: Number(row.revision), contextJson, contextSha256: sha256(contextJson) }];
4499
+ }));
4500
+ }
4501
+ function mapRevision(row) {
4502
+ return {
4503
+ noteId: Number(row.note_id),
4504
+ revision: Number(row.revision),
4505
+ expectedRevision: row.expected_revision === null ? null : Number(row.expected_revision),
4506
+ expectedContextSha256: row.expected_context_sha256,
4507
+ requestId: row.request_id,
4508
+ provenance: Number(row.revision) === 0 ? "original" : "user-edit",
4509
+ title: row.title,
4510
+ body: row.body,
4511
+ createdAt: row.created_at
4028
4512
  };
4029
4513
  }
4514
+ function revisePersonNote(database, input) {
4515
+ positiveInteger(input.noteId, "Note ID");
4516
+ if (!Number.isSafeInteger(input.expectedRevision) || input.expectedRevision < 0 || input.expectedRevision >= Number.MAX_SAFE_INTEGER)
4517
+ invalid("Expected revision must be a nonnegative safe integer below the revision ceiling.");
4518
+ validatePersonNoteMutationRequest(input.requestId, input.expectedContextSha256);
4519
+ validatePersonNoteContent(input.title, input.body);
4520
+ database.exec("BEGIN IMMEDIATE");
4521
+ try {
4522
+ const prior = database.query("SELECT * FROM person_note_content_revisions WHERE request_id=?").get(input.requestId);
4523
+ if (prior !== null) {
4524
+ if (Number(prior.note_id) !== input.noteId || Number(prior.expected_revision) !== input.expectedRevision || prior.expected_context_sha256 !== input.expectedContextSha256 || prior.title !== input.title || prior.body !== input.body) {
4525
+ throw new NoteRevisionError("request_conflict", "Request ID already belongs to a different note edit. Use its original payload or a new request ID.");
4526
+ }
4527
+ database.exec("COMMIT");
4528
+ return { kind: "replayed", revision: mapRevision(prior) };
4529
+ }
4530
+ const context = personNoteContexts(database, [input.noteId]).get(input.noteId);
4531
+ if (context === undefined)
4532
+ throw new NoteRevisionError("not_found", "Note not found.");
4533
+ if (context.revision !== input.expectedRevision) {
4534
+ throw new NoteRevisionError("revision_conflict", "Note content changed. Reload the note and review your draft before saving again.");
4535
+ }
4536
+ if (context.contextSha256 !== input.expectedContextSha256) {
4537
+ throw new NoteRevisionError("context_conflict", "Note attribution or identity context changed. Reload the note and review its owner before saving again.");
4538
+ }
4539
+ const revision = input.expectedRevision + 1;
4540
+ database.query(`INSERT INTO person_note_content_revisions(
4541
+ note_id,revision,expected_revision,request_id,expected_context_sha256,context_json,title,body
4542
+ ) VALUES (?,?,?,?,?,?,?,?)`).run(input.noteId, revision, input.expectedRevision, input.requestId, input.expectedContextSha256, context.contextJson, input.title, input.body);
4543
+ const inserted = database.query("SELECT * FROM person_note_content_revisions WHERE note_id=? AND revision=?").get(input.noteId, revision);
4544
+ if (inserted === null)
4545
+ throw new Error("Note revision was not recorded.");
4546
+ database.exec("COMMIT");
4547
+ return { kind: "created", revision: mapRevision(inserted) };
4548
+ } catch (error) {
4549
+ database.exec("ROLLBACK");
4550
+ throw error;
4551
+ }
4552
+ }
4553
+ function listPersonNoteRevisions(database, input) {
4554
+ positiveInteger(input.noteId, "Note ID");
4555
+ const limit = input.limit ?? 50;
4556
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1000)
4557
+ invalid("Note history limit must be between 1 and 1000.");
4558
+ if (input.beforeRevision !== undefined && (!Number.isSafeInteger(input.beforeRevision) || input.beforeRevision < 1)) {
4559
+ invalid("Before revision must be a positive safe integer.");
4560
+ }
4561
+ return database.transaction(() => {
4562
+ const original = database.query("SELECT id FROM person_notes WHERE id=?").get(input.noteId);
4563
+ if (original === null)
4564
+ throw new NoteRevisionError("not_found", "Note not found.");
4565
+ const current = database.query("SELECT coalesce(max(revision),0) AS revision FROM person_note_content_revisions WHERE note_id=?").get(input.noteId);
4566
+ const before = input.beforeRevision ?? null;
4567
+ const rows = database.query(`
4568
+ SELECT note_id,revision,expected_revision,expected_context_sha256,request_id,title,body,created_at
4569
+ FROM person_note_content_revisions WHERE note_id=? AND (? IS NULL OR revision<?)
4570
+ UNION ALL SELECT id,0,NULL,NULL,NULL,title,body,created_at FROM person_notes WHERE id=?
4571
+ ORDER BY revision DESC LIMIT ?
4572
+ `).all(input.noteId, before, before, input.noteId, limit + 1);
4573
+ const items = rows.slice(0, limit).map(mapRevision);
4574
+ return {
4575
+ items,
4576
+ currentRevision: Number(current.revision),
4577
+ nextBeforeRevision: rows.length > limit ? items.at(-1).revision : null
4578
+ };
4579
+ }).deferred();
4580
+ }
4030
4581
 
4031
4582
  // src/local/notes.ts
4032
4583
  import { closeSync as closeSync2, constants as constants2, fstatSync as fstatSync2, openSync as openSync2, readFileSync as readFileSync2 } from "fs";
@@ -4896,24 +5447,6 @@ function parseLimit(value) {
4896
5447
  }
4897
5448
  return limit;
4898
5449
  }
4899
- function boundedBody(value, label) {
4900
- const body = value.normalize("NFKC").trim();
4901
- if (body.length === 0)
4902
- fail(`${label} must not be empty.`);
4903
- if (/[\u0000\r]/u.test(body))
4904
- fail(`${label} contains an unsupported control character.`);
4905
- if (Buffer.byteLength(body, "utf8") > MAX_BODY_BYTES)
4906
- fail(`${label} exceeds 64 KiB.`);
4907
- if (body.length > MAX_BODY_BYTES)
4908
- fail(`${label} exceeds the stored character limit.`);
4909
- return body;
4910
- }
4911
- function boundedTitle(value) {
4912
- if (value === undefined)
4913
- return null;
4914
- const title = cleanText(value, MAX_TITLE_CHARS);
4915
- return title;
4916
- }
4917
5450
  function metadataObject(value) {
4918
5451
  if (value === undefined)
4919
5452
  return {};
@@ -4931,7 +5464,7 @@ function canonicalPersonId2(database, personId) {
4931
5464
  fail("Person not found.");
4932
5465
  return Number(row.canonical_person_id);
4933
5466
  }
4934
- function mapNote(row) {
5467
+ function mapNote(row, context) {
4935
5468
  let metadata = {};
4936
5469
  try {
4937
5470
  const parsed = JSON.parse(row.metadata_json);
@@ -4950,9 +5483,90 @@ function mapNote(row) {
4950
5483
  title: row.title === null ? null : String(row.title),
4951
5484
  body: String(row.body),
4952
5485
  metadata,
4953
- createdAt: String(row.created_at)
5486
+ createdAt: String(row.created_at),
5487
+ revision: context.revision,
5488
+ revisedAt: row.revised_at ?? null,
5489
+ contextSha256: context.contextSha256
4954
5490
  };
4955
5491
  }
5492
+ function mapEffectiveNotes(database, rows) {
5493
+ const contexts = personNoteContexts(database, rows.map((row) => Number(row.id)));
5494
+ return rows.map((row) => {
5495
+ const context = contexts.get(Number(row.id));
5496
+ if (context === undefined || context.revision !== Number(row.revision ?? 0)) {
5497
+ throw new Error("Stored note content and identity context disagree.");
5498
+ }
5499
+ return mapNote(row, context);
5500
+ });
5501
+ }
5502
+ function getPersonNote(database, noteId) {
5503
+ parsePersonId(noteId, "Note ID");
5504
+ return database.transaction(() => {
5505
+ const row = database.query("SELECT * FROM person_notes_effective WHERE id=?").get(noteId);
5506
+ if (row === null)
5507
+ throw new NoteRevisionError("not_found", "Note not found.");
5508
+ return mapEffectiveNotes(database, [row])[0];
5509
+ }).deferred();
5510
+ }
5511
+ function personNoteContext(database, personId) {
5512
+ const context = personNoteCreationContext(database, personId);
5513
+ return { personId: context.personId, canonicalPersonId: context.canonicalPersonId, contextSha256: context.contextSha256 };
5514
+ }
5515
+ function workspaceOccurredAt(value) {
5516
+ if (typeof value !== "string" || value.length > 80) {
5517
+ throw new NoteRevisionError("invalid_input", "Occurred-at must be an ISO date or canonical UTC datetime.");
5518
+ }
5519
+ const date = new Date(DATE_ONLY.test(value) ? `${value}T00:00:00.000Z` : value);
5520
+ if (!Number.isFinite(date.valueOf()) || (DATE_ONLY.test(value) ? date.toISOString().slice(0, 10) !== value : date.toISOString() !== value)) {
5521
+ throw new NoteRevisionError("invalid_input", "Occurred-at must be an ISO date or canonical UTC datetime.");
5522
+ }
5523
+ return date.toISOString();
5524
+ }
5525
+ function createWorkspaceNote(database, input) {
5526
+ if (!Number.isSafeInteger(input.personId) || input.personId < 1) {
5527
+ throw new NoteRevisionError("invalid_input", "Person ID must be a positive safe integer.");
5528
+ }
5529
+ validatePersonNoteMutationRequest(input.requestId, input.expectedPersonContextSha256);
5530
+ validatePersonNoteContent(input.title, input.body);
5531
+ const occurredAt = workspaceOccurredAt(input.occurredAt);
5532
+ database.exec("BEGIN IMMEDIATE");
5533
+ try {
5534
+ const prior = database.query(`SELECT receipt.note_id,receipt.requested_person_id,
5535
+ receipt.expected_context_sha256,receipt.occurred_at_input,note.title,note.body
5536
+ FROM person_note_creation_requests receipt JOIN person_notes note ON note.id=receipt.note_id
5537
+ WHERE receipt.request_id=?`).get(input.requestId);
5538
+ if (prior !== null) {
5539
+ if (Number(prior.requested_person_id) !== input.personId || prior.expected_context_sha256 !== input.expectedPersonContextSha256 || prior.occurred_at_input !== input.occurredAt || prior.title !== input.title || prior.body !== input.body) {
5540
+ throw new NoteRevisionError("request_conflict", "Request ID already belongs to a different note creation.");
5541
+ }
5542
+ database.exec("COMMIT");
5543
+ return { kind: "replayed", noteId: Number(prior.note_id) };
5544
+ }
5545
+ const context = personNoteCreationContext(database, input.personId);
5546
+ if (context.contextSha256 !== input.expectedPersonContextSha256) {
5547
+ throw new NoteRevisionError("context_conflict", "Person identity context changed. Reload and review the person before creating a note.");
5548
+ }
5549
+ const inserted = insertNote(database, {
5550
+ personId: context.canonicalPersonId,
5551
+ occurredAt,
5552
+ source: "manual",
5553
+ sourceId: input.requestId,
5554
+ title: input.title,
5555
+ body: input.body,
5556
+ metadata: {}
5557
+ });
5558
+ if (!inserted.inserted)
5559
+ throw new NoteRevisionError("request_conflict", "A source note already uses this request ID without a workspace creation receipt.");
5560
+ database.query(`INSERT INTO person_note_creation_requests(
5561
+ request_id,note_id,requested_person_id,expected_context_sha256,context_json,occurred_at_input
5562
+ ) VALUES (?,?,?,?,?,?)`).run(input.requestId, inserted.id, input.personId, input.expectedPersonContextSha256, context.contextJson, input.occurredAt);
5563
+ database.exec("COMMIT");
5564
+ return { kind: "created", noteId: inserted.id };
5565
+ } catch (error) {
5566
+ database.exec("ROLLBACK");
5567
+ throw error;
5568
+ }
5569
+ }
4956
5570
  function storedGranolaAttendeeIdentity(metadataJson) {
4957
5571
  let metadata;
4958
5572
  try {
@@ -5179,8 +5793,9 @@ function addPersonNote(database, args) {
5179
5793
  const occurredAt = parseOccurredAt(args.occurredAt, "occurred-at");
5180
5794
  const source = parseSource(args.source);
5181
5795
  const sourceId = args.sourceId === undefined ? null : cleanText(args.sourceId, 256);
5182
- const title = boundedTitle(args.title);
5183
- const body = boundedBody(args.body, "Note body");
5796
+ const title = args.title ?? null;
5797
+ const body = args.body;
5798
+ validatePersonNoteContent(title, body);
5184
5799
  const metadata = metadataObject(args.metadata);
5185
5800
  database.exec("BEGIN IMMEDIATE");
5186
5801
  try {
@@ -5196,9 +5811,10 @@ function addPersonNote(database, args) {
5196
5811
  });
5197
5812
  if (!inserted.inserted)
5198
5813
  fail("A note for this source identity already exists.");
5199
- const row = database.query("SELECT * FROM person_notes_current WHERE id=?").get(inserted.id);
5814
+ const row = database.query("SELECT * FROM person_notes_effective WHERE id=?").get(inserted.id);
5815
+ const note = mapEffectiveNotes(database, [row])[0];
5200
5816
  database.exec("COMMIT");
5201
- return mapNote(row);
5817
+ return note;
5202
5818
  } catch (error) {
5203
5819
  database.exec("ROLLBACK");
5204
5820
  throw error;
@@ -5235,16 +5851,18 @@ function noteFiltersSql(args) {
5235
5851
  };
5236
5852
  }
5237
5853
  function listPersonNotes(database, args) {
5238
- canonicalPersonId2(database, parsePersonId(args.personId));
5239
5854
  const filters = noteFiltersSql(args);
5240
5855
  const limit = parseLimit(args.limit);
5241
- const rows = database.query(`
5242
- SELECT note.* FROM person_notes_current note
5243
- ${filters.sql}
5244
- ORDER BY note.occurred_at DESC, note.id DESC
5245
- LIMIT ?
5246
- `).all(...filters.bindings, limit);
5247
- return rows.map(mapNote);
5856
+ return database.transaction(() => {
5857
+ canonicalPersonId2(database, parsePersonId(args.personId));
5858
+ const rows = database.query(`
5859
+ SELECT note.* FROM person_notes_effective note
5860
+ ${filters.sql}
5861
+ ORDER BY note.occurred_at DESC, note.id DESC
5862
+ LIMIT ?
5863
+ `).all(...filters.bindings, limit);
5864
+ return mapEffectiveNotes(database, rows);
5865
+ }).deferred();
5248
5866
  }
5249
5867
  function ftsMatch(query) {
5250
5868
  const tokens = query.normalize("NFKC").trim().slice(0, MAX_QUERY_CHARS).split(/[^\p{L}\p{N}]+/u).map((token) => token.toLocaleLowerCase("en-US")).filter((token) => token.length > 0);
@@ -5256,8 +5874,6 @@ function searchPersonNotes(database, args) {
5256
5874
  if (typeof args.query !== "string" || args.query.trim().length === 0) {
5257
5875
  fail("Search query is required.");
5258
5876
  }
5259
- if (args.personId !== undefined)
5260
- canonicalPersonId2(database, parsePersonId(args.personId));
5261
5877
  const match = ftsMatch(args.query);
5262
5878
  const filters = noteFiltersSql({
5263
5879
  ...args.personId === undefined ? {} : { personId: args.personId },
@@ -5266,15 +5882,19 @@ function searchPersonNotes(database, args) {
5266
5882
  ...args.source === undefined ? {} : { source: args.source }
5267
5883
  });
5268
5884
  const limit = parseLimit(args.limit);
5269
- const where = filters.sql.length === 0 ? "WHERE person_notes_fts MATCH ?" : `${filters.sql} AND person_notes_fts MATCH ?`;
5270
- const rows = database.query(`
5271
- SELECT note.* FROM person_notes_fts
5272
- JOIN person_notes_current note ON note.id=person_notes_fts.rowid
5273
- ${where}
5274
- ORDER BY note.occurred_at DESC, note.id DESC
5275
- LIMIT ?
5276
- `).all(...filters.bindings, match, limit);
5277
- return rows.map(mapNote);
5885
+ const where = filters.sql.length === 0 ? "WHERE person_notes_effective_fts MATCH ?" : `${filters.sql} AND person_notes_effective_fts MATCH ?`;
5886
+ return database.transaction(() => {
5887
+ if (args.personId !== undefined)
5888
+ canonicalPersonId2(database, parsePersonId(args.personId));
5889
+ const rows = database.query(`
5890
+ SELECT note.* FROM person_notes_effective_fts
5891
+ JOIN person_notes_effective note ON note.id=person_notes_effective_fts.rowid
5892
+ ${where}
5893
+ ORDER BY note.occurred_at DESC, note.id DESC
5894
+ LIMIT ?
5895
+ `).all(...filters.bindings, match, limit);
5896
+ return mapEffectiveNotes(database, rows);
5897
+ }).deferred();
5278
5898
  }
5279
5899
  function recordValue(record, ...keys) {
5280
5900
  for (const key of keys) {
@@ -5360,8 +5980,8 @@ function firstCleanText(record, keys, maximum) {
5360
5980
  }
5361
5981
  function granolaBody(record) {
5362
5982
  const privateNotes = firstCleanText(record, ["privateNotes", "private_notes", "notes"], MAX_BODY_BYTES);
5363
- const summary = firstCleanText(record, ["summary_markdown", "summary", "summary_text", "aiSummary", "ai_summary"], MAX_BODY_BYTES);
5364
- const parts = [privateNotes, summary].filter((part) => part !== null);
5983
+ const summary2 = firstCleanText(record, ["summary_markdown", "summary", "summary_text", "aiSummary", "ai_summary"], MAX_BODY_BYTES);
5984
+ const parts = [privateNotes, summary2].filter((part) => part !== null);
5365
5985
  if (parts.length === 0)
5366
5986
  return null;
5367
5987
  const joined = parts.join(`
@@ -5371,8 +5991,8 @@ function granolaBody(record) {
5371
5991
  return joined;
5372
5992
  if (privateNotes !== null && Buffer.byteLength(privateNotes, "utf8") <= MAX_BODY_BYTES)
5373
5993
  return privateNotes;
5374
- if (summary !== null && Buffer.byteLength(summary, "utf8") <= MAX_BODY_BYTES)
5375
- return summary;
5994
+ if (summary2 !== null && Buffer.byteLength(summary2, "utf8") <= MAX_BODY_BYTES)
5995
+ return summary2;
5376
5996
  return joined.slice(0, MAX_BODY_BYTES);
5377
5997
  }
5378
5998
  function inviteeEmailByName(record) {
@@ -5788,55 +6408,774 @@ function readGranolaImportFile(path) {
5788
6408
  closeSync2(descriptor);
5789
6409
  }
5790
6410
  }
5791
- function parseGranolaImportJson(raw) {
5792
- if (Buffer.byteLength(raw, "utf8") > MAX_IMPORT_BYTES) {
5793
- fail("Granola import exceeds the bounded size limit.");
6411
+ function parseGranolaImportJson(raw) {
6412
+ if (Buffer.byteLength(raw, "utf8") > MAX_IMPORT_BYTES) {
6413
+ fail("Granola import exceeds the bounded size limit.");
6414
+ }
6415
+ try {
6416
+ return JSON.parse(raw);
6417
+ } catch {
6418
+ fail("Granola import is not valid JSON.");
6419
+ }
6420
+ }
6421
+
6422
+ // src/local/workspace-assets.ts
6423
+ var workspaceHtml = `<!doctype html>
6424
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
6425
+ <title>PeopleBlade \xB7 Local workspace</title><link rel="stylesheet" href="/workspace.css"><script src="/workspace.js" defer></script></head>
6426
+ <body><a class="skip" href="#workspace">Skip to contacts</a><header><a class="brand" href="/">PeopleBlade<span>LOCAL WORKSPACE</span></a><p>Your people. Your machine.</p><span class="privacy">No cloud sync \xB7 no enrichment calls</span></header>
6427
+ <main id="workspace"><section id="unlock" class="unlock"><span class="eyebrow">PRIVATE BY DEFAULT</span><h1>Open your local contact book.</h1><p>Select the access file printed by <code>peopleblade ui</code>. It connects this tab to that running process only. The file is not uploaded to a remote service.</p><label class="file-label">Workspace access file<input id="access-file" type="file" accept="application/json,.json"></label><p class="muted">Keep the terminal running. Closing it locks the workspace. Notes never leave this machine.</p></section>
6428
+ <section id="book" hidden><div class="page-heading"><div><span class="eyebrow">YOUR RELATIONSHIPS</span><h1>Contact book</h1></div><button id="lock" type="button">Lock workspace</button></div>
6429
+ <form id="filters" class="filters"><label class="search">Search contacts<input id="search" name="search" type="search" placeholder="Name, company, title, email, phone\u2026" maxlength="200"></label><label>Source<select id="source"><option value="">All sources</option><option>apple</option><option>beeper</option><option>google</option><option>imessage</option><option>linkedin</option><option>instagram</option><option>whatsapp</option><option>x</option></select></label><label>Email<select id="email"><option value="">Any email</option><option value="yes">Has email</option><option value="no">Needs email</option></select></label><label>Contact preference<select id="dnc"><option value="all">All contacts</option><option value="exclude">May contact</option><option value="only">Do not contact</option></select></label><button type="submit">Search</button><button id="reset" type="button">Reset</button></form>
6430
+ <div class="workspace-grid"><section class="records" aria-label="Contacts"><div class="table-toolbar"><span id="range" aria-live="polite"></span><div><label>Sort <select id="sort"><option value="interactions">Interactions</option><option value="name">Name</option><option value="organization">Company</option><option value="last-contact">Last interaction</option></select></label><button id="direction" type="button" aria-label="Toggle sort direction">Descending \u2193</button></div></div><div class="table-scroll" tabindex="0" aria-label="Scrollable contacts"><table><caption class="sr-only">Local contacts; open a person to inspect details and notes.</caption><thead><tr><th scope="col">Person</th><th scope="col">Company</th><th scope="col">Interactions</th></tr></thead><tbody id="rows"></tbody></table></div><div id="empty" class="empty" hidden>No matches. Try another search or import a source with the CLI.</div><nav class="pagination" aria-label="Contact pages"><button id="previous" type="button">\u2190 Previous</button><span id="page"></span><button id="next" type="button">Next \u2192</button></nav><p class="live-note">Live view: imports and identity reviews can move rows between pages. Refresh after changes.</p></section>
6431
+ <aside id="detail" class="detail" aria-label="Contact details"><div class="empty"><h2>A little context goes a long way.</h2><p>Open a person to see their contact methods, sources and private notes.</p></div></aside></div></section>
6432
+ <p id="status" role="status" aria-live="polite"></p></main><footer>PeopleBlade \xB7 Local authority, reviewable identity, private notes.</footer></body></html>`;
6433
+ var workspaceCss = `
6434
+ :root{color-scheme:light dark;--bg:#f6f5f1;--panel:#fffefa;--text:#232721;--muted:#647064;--line:#dedfd6;--accent:#375a42;--soft:#edf1e8;--danger:#9c342d}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:14px/1.55 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}button,input,select,textarea{font:inherit;color:inherit}button,input,select,textarea{border:1px solid var(--line);border-radius:7px;background:var(--panel);padding:9px 12px}button{cursor:pointer;font-weight:550}button:hover{border-color:var(--accent);background:var(--soft)}button:disabled{opacity:.45;cursor:default}a{color:var(--accent)}:focus-visible{outline:3px solid var(--accent);outline-offset:3px}header{display:flex;align-items:center;gap:30px;padding:22px 32px;border-bottom:1px solid var(--line);background:var(--panel)}header p{color:var(--muted);margin:0}.brand{color:var(--text);font-size:22px;line-height:1.1;text-decoration:none;font-weight:650;letter-spacing:-.7px}.brand span{display:block;font-size:9px;letter-spacing:1.8px;color:var(--muted);margin-top:8px}.privacy{margin-left:auto;border:1px solid var(--line);border-radius:99px;padding:6px 12px;font-size:11px;color:var(--muted)}main{max-width:1600px;margin:auto;padding:36px 32px}h1{font-size:34px;line-height:1.2;letter-spacing:-1px;font-weight:550;margin:8px 0 24px}h2{font-size:21px;line-height:1.3;letter-spacing:-.5px;margin:0 0 12px}.eyebrow{font-size:10px;letter-spacing:2px;color:var(--muted);font-weight:650}.unlock{max-width:600px;margin:7vh auto;padding:36px;background:var(--panel);border:1px solid var(--line);border-radius:12px}.unlock p{color:var(--muted)}.file-label{display:grid;gap:8px;margin:25px 0}.page-heading{display:flex;align-items:center;justify-content:space-between}.page-heading button{margin-bottom:20px}.filters{display:flex;gap:10px;align-items:end;margin-bottom:20px;flex-wrap:wrap}.filters label{display:grid;gap:5px;font-size:11px;color:var(--muted)}.filters input,.filters select{font-size:13px;color:var(--text);min-height:42px}.filters .search{flex:1;min-width:220px}.workspace-grid{display:grid;grid-template-columns:minmax(420px,1.4fr) minmax(320px,1fr);gap:20px;align-items:start}.records,.detail{border:1px solid var(--line);background:var(--panel);border-radius:10px;overflow:hidden}.table-toolbar{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:13px 16px;border-bottom:1px solid var(--line);color:var(--muted);font-size:12px;flex-wrap:wrap}.table-toolbar>div{display:flex;align-items:center;gap:6px}.table-toolbar select,.table-toolbar button{padding:5px 8px;font-size:12px}.table-scroll{overflow:auto;max-height:65vh}table{width:100%;border-collapse:collapse;text-align:left}th{position:sticky;top:0;background:var(--soft);font-size:10px;text-transform:uppercase;letter-spacing:1px;color:var(--muted);z-index:1}th,td{padding:11px 16px;border-bottom:1px solid var(--line);vertical-align:top}td{font-size:12px}td small{display:block;color:var(--muted);margin-top:2px}td button{padding:0;border:0;background:none;text-align:left;font-size:13px;color:var(--text)}tr.selected{background:var(--soft)}tr:hover{background:var(--soft)}.pagination{display:flex;justify-content:space-between;align-items:center;padding:14px 16px;font-size:12px}.pagination button{font-size:12px;padding:6px 10px}.live-note{font-size:10px;color:var(--muted);padding:0 16px 12px;margin:0}.detail{padding:24px;position:sticky;top:20px;max-height:83vh;overflow:auto}.detail h2{font-size:25px}.detail dl{display:grid;grid-template-columns:90px 1fr;gap:8px;margin:22px 0}.detail dt{font-size:11px;color:var(--muted)}.detail dd{margin:0;font-size:12px;overflow-wrap:anywhere}.detail dd span{display:block}.pill{display:inline-block;border:1px solid var(--line);border-radius:99px;padding:2px 8px;margin:0 4px 5px 0;color:var(--muted);font-size:10px}.notes-heading{border-top:1px solid var(--line);padding-top:22px;margin-top:24px;display:flex;justify-content:space-between;align-items:center}.notes-heading h3{margin:0;font-weight:550}.note-card{border-bottom:1px solid var(--line);padding:16px 0}.note-card>button{font-size:12px;padding:5px 9px;margin:8px 6px 0 0}.note-meta{font-size:10px;color:var(--muted);margin:4px 0 10px}.note-title{font-size:14px;font-weight:600;margin:0}.markdown{font-size:13px;overflow-wrap:anywhere}.markdown h1,.markdown h2,.markdown h3{font-size:17px;margin:12px 0}.markdown pre{overflow:auto;background:var(--soft);padding:12px;border-radius:6px}.markdown img{display:none}.markdown blockquote{margin-left:0;padding-left:12px;border-left:3px solid var(--line);color:var(--muted)}.markdown p{margin:8px 0}.markdown td,.markdown th{padding:5px;position:static}.editor{display:grid;gap:10px}.editor label{display:grid;gap:4px;font-size:11px;color:var(--muted)}.editor textarea{min-height:230px;resize:vertical;font-family:ui-monospace,SFMono-Regular,monospace;font-size:12px;tab-size:2}.editor-actions{display:flex;gap:6px;flex-wrap:wrap}.editor-actions button{font-size:12px}.primary{background:var(--accent);color:var(--panel)}.primary:hover{background:var(--accent);color:var(--panel)}.empty{padding:44px 22px;color:var(--muted);text-align:center}.empty h2{font-size:21px}.muted{color:var(--muted);font-size:12px}.danger{color:var(--danger)}#status{background:var(--panel);border:1px solid var(--line);padding:12px 16px;border-radius:8px}#status:empty{display:none}footer{color:var(--muted);font-size:11px;text-align:center;padding:20px}.skip{position:absolute;left:12px;top:-100px;padding:8px;background:var(--panel);z-index:5}.skip:focus{top:8px}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}[hidden]{display:none!important}
6435
+ @media(prefers-color-scheme:dark){:root{--bg:#151a17;--panel:#1c231e;--text:#e1e7dc;--muted:#a3b19e;--line:#354136;--accent:#a9c4a3;--soft:#28342b;--danger:#ffb4a9}.primary{color:#182019}}
6436
+ @media(max-width:1000px){.workspace-grid{grid-template-columns:1fr}.detail{position:static;max-height:none}.privacy{display:none}}@media(max-width:600px){header{padding:18px;gap:18px}header p{font-size:11px}main{padding:26px 14px}.unlock{padding:22px;margin:25px 0}h1{font-size:28px}.filters{gap:8px}.filters label{flex:1;min-width:125px}.filters .search{flex-basis:100%}th,td{padding:10px}.detail{padding:18px}.table-toolbar{padding:10px}.page-heading{align-items:start}.page-heading button{font-size:11px}.table-scroll{max-height:50vh}}
6437
+ `;
6438
+ function workspaceClient() {
6439
+ const el = (id) => document.getElementById(id);
6440
+ let capability = "";
6441
+ let offset = 0;
6442
+ let direction = "desc";
6443
+ let selectedPerson = null;
6444
+ let dirty = false;
6445
+ let activeSave = false;
6446
+ let activeEditor = null;
6447
+ let editorGeneration = 0;
6448
+ let detailLoading = false;
6449
+ let generation = 0;
6450
+ let detailGeneration = 0;
6451
+ let page = null;
6452
+ const status = (message) => {
6453
+ el("status").textContent = message;
6454
+ };
6455
+ const node = (tag, text = "", className = "") => {
6456
+ const element = document.createElement(tag);
6457
+ element.textContent = text;
6458
+ element.className = className;
6459
+ return element;
6460
+ };
6461
+ async function api(payload) {
6462
+ const response = await fetch("/api", { method: "POST", cache: "no-store", signal: AbortSignal.timeout(15000), headers: { "content-type": "application/json", authorization: `Bearer ${capability}` }, body: JSON.stringify(payload) });
6463
+ const envelope = await response.json();
6464
+ if (!response.ok || envelope.ok !== true) {
6465
+ const error = new Error(typeof envelope.message === "string" ? envelope.message : response.status === 401 ? "Access expired. Load the current access file again." : "The request failed.");
6466
+ Object.assign(error, { status: response.status });
6467
+ throw error;
6468
+ }
6469
+ return envelope.result;
6470
+ }
6471
+ function mayDiscard() {
6472
+ if (activeSave) {
6473
+ status("Wait for the current save outcome before switching notes or locking.");
6474
+ return false;
6475
+ }
6476
+ if (detailLoading) {
6477
+ status("Wait for the contact to finish loading before switching notes or locking.");
6478
+ return false;
6479
+ }
6480
+ return !dirty || window.confirm("Discard your unsaved note draft?");
6481
+ }
6482
+ function query() {
6483
+ const email = el("email").value;
6484
+ return { search: el("search").value, ...el("source").value ? { source: el("source").value } : {}, ...email ? { hasEmail: email === "yes" } : {}, doNotContact: el("dnc").value, sort: el("sort").value, direction, limit: 50, offset };
6485
+ }
6486
+ async function load() {
6487
+ const current = ++generation;
6488
+ status("Loading contacts\u2026");
6489
+ try {
6490
+ const result = await api({ action: "query", query: query() });
6491
+ if (current !== generation || !capability)
6492
+ return;
6493
+ page = result;
6494
+ const sourceSelect = el("source");
6495
+ const selectedSource = sourceSelect.value;
6496
+ sourceSelect.replaceChildren(new Option("All sources", ""));
6497
+ for (const facet of result.facets.sources)
6498
+ sourceSelect.add(new Option(`${facet.source} (${facet.count})`, facet.source));
6499
+ if (selectedSource && !result.facets.sources.some((facet) => facet.source === selectedSource))
6500
+ sourceSelect.add(new Option(selectedSource, selectedSource));
6501
+ sourceSelect.value = selectedSource;
6502
+ el("rows").replaceChildren();
6503
+ for (const person of result.items) {
6504
+ const row = node("tr", "", selectedPerson === person.id ? "selected" : "");
6505
+ const name = node("td");
6506
+ const open = node("button", person.displayName);
6507
+ open.type = "button";
6508
+ open.addEventListener("click", () => {
6509
+ if (mayDiscard()) {
6510
+ dirty = false;
6511
+ showPerson(person.id);
6512
+ }
6513
+ });
6514
+ name.append(open, node("small", person.title ?? person.primaryEmail ?? ""));
6515
+ const company = node("td", person.organization ?? "\u2014");
6516
+ company.append(node("small", person.sources.join(" \xB7 ")));
6517
+ const interactions = node("td", person.interactionCount.toLocaleString());
6518
+ interactions.append(node("small", person.doNotContact ? "Do not contact" : person.reciprocal ? "Reciprocal" : "One-way / unknown"));
6519
+ row.append(name, company, interactions);
6520
+ el("rows").append(row);
6521
+ }
6522
+ el("range").textContent = result.items.length === 0 ? `0 shown \xB7 ${result.total.toLocaleString()} matching contacts` : `${offset + 1}\u2013${offset + result.items.length} of ${result.total.toLocaleString()} contacts`;
6523
+ el("empty").hidden = result.items.length !== 0;
6524
+ el("page").textContent = `Page ${Math.floor(offset / 50) + 1}`;
6525
+ el("previous").disabled = offset === 0;
6526
+ el("next").disabled = result.nextOffset === null;
6527
+ status("");
6528
+ } catch (error) {
6529
+ if (current === generation)
6530
+ status(error instanceof Error ? error.message : "Contact query failed.");
6531
+ }
6532
+ }
6533
+ function renderNote(note, target) {
6534
+ const card = node("article", "", "note-card");
6535
+ card.dataset.noteId = String(note.id);
6536
+ card.append(node("h4", note.title ?? "Untitled note", "note-title"), node("p", `${note.occurredAt.slice(0, 10)} \xB7 ${note.source} original \xB7 revision ${note.revision}`, "note-meta"));
6537
+ const preview = node("div", "", "markdown");
6538
+ preview.innerHTML = note.previewHtml ?? "";
6539
+ card.append(preview);
6540
+ const edit = node("button", "Edit markdown");
6541
+ edit.type = "button";
6542
+ edit.dataset.noteAction = "edit";
6543
+ edit.addEventListener("click", () => {
6544
+ if (mayDiscard())
6545
+ editNote(note, card);
6546
+ });
6547
+ const history = node("button", "History");
6548
+ history.type = "button";
6549
+ history.dataset.noteAction = "history";
6550
+ history.addEventListener("click", () => {
6551
+ showHistory(note, card);
6552
+ });
6553
+ card.append(edit, history);
6554
+ target.append(card);
6555
+ }
6556
+ async function showHistory(note, card, beforeRevision, existing) {
6557
+ try {
6558
+ const history = await api({ action: "note-history", noteId: note.id, ...beforeRevision === undefined ? {} : { beforeRevision } });
6559
+ const container = existing ?? node("details");
6560
+ if (!existing) {
6561
+ container.setAttribute("open", "");
6562
+ container.append(node("summary", "Revision history"));
6563
+ }
6564
+ for (const item of history.items) {
6565
+ const entry = node("details");
6566
+ entry.append(node("summary", `${item.revision === 0 ? "Original imported content" : `User revision ${item.revision}`} \xB7 ${item.createdAt}`), node("pre", item.body));
6567
+ const restore = node("button", "Use this as a new draft");
6568
+ restore.type = "button";
6569
+ restore.addEventListener("click", () => {
6570
+ if (mayDiscard())
6571
+ editNote(note, card, item);
6572
+ });
6573
+ entry.append(restore);
6574
+ container.append(entry);
6575
+ }
6576
+ if (history.nextBeforeRevision !== null) {
6577
+ const older = node("button", "Older revisions");
6578
+ older.type = "button";
6579
+ older.dataset.noteAction = "older";
6580
+ older.addEventListener("click", () => {
6581
+ older.remove();
6582
+ showHistory(note, card, history.nextBeforeRevision ?? undefined, container);
6583
+ });
6584
+ container.append(older);
6585
+ }
6586
+ if (!existing)
6587
+ card.append(container);
6588
+ } catch (error) {
6589
+ status(error instanceof Error ? error.message : "History could not be loaded.");
6590
+ }
6591
+ }
6592
+ function editNote(note, card, restore) {
6593
+ if (activeSave || detailLoading)
6594
+ return;
6595
+ editorGeneration++;
6596
+ if (activeEditor && activeEditor.card !== card) {
6597
+ if (activeEditor.note.id === 0)
6598
+ activeEditor.card.remove();
6599
+ else {
6600
+ const replacement = node("div");
6601
+ renderNote(activeEditor.note, replacement);
6602
+ activeEditor.card.replaceWith(...replacement.childNodes);
6603
+ }
6604
+ }
6605
+ activeEditor = { note, card };
6606
+ card.dataset.noteId = String(note.id);
6607
+ const original = { title: note.title ?? "", body: note.body };
6608
+ card.replaceChildren();
6609
+ const editor = node("form", "", "editor");
6610
+ const titleLabel = node("label", "Title");
6611
+ const title = document.createElement("input");
6612
+ title.value = restore ? restore.title ?? "" : original.title;
6613
+ title.maxLength = 1024;
6614
+ title.spellcheck = false;
6615
+ title.autocomplete = "off";
6616
+ titleLabel.append(title);
6617
+ const bodyLabel = node("label", "Markdown note \xB7 private to this machine");
6618
+ const body = document.createElement("textarea");
6619
+ body.value = restore?.body ?? original.body;
6620
+ body.spellcheck = false;
6621
+ body.autocomplete = "off";
6622
+ body.setAttribute("autocorrect", "off");
6623
+ body.setAttribute("autocapitalize", "off");
6624
+ bodyLabel.append(body);
6625
+ const preview = node("div", "", "markdown");
6626
+ preview.hidden = true;
6627
+ const actions = node("div", "", "editor-actions");
6628
+ const save = node("button", "Save revision", "primary");
6629
+ save.type = "submit";
6630
+ const toggle = node("button", "Preview");
6631
+ toggle.type = "button";
6632
+ toggle.dataset.noteAction = "preview";
6633
+ toggle.addEventListener("click", async () => {
6634
+ try {
6635
+ preview.hidden = !preview.hidden;
6636
+ if (!preview.hidden) {
6637
+ const result = await api({ action: "preview", body: body.value });
6638
+ preview.innerHTML = result.html;
6639
+ }
6640
+ toggle.textContent = preview.hidden ? "Preview" : "Hide preview";
6641
+ } catch {
6642
+ status("Preview is unavailable. Your draft is unchanged.");
6643
+ }
6644
+ });
6645
+ const cancel = node("button", "Cancel");
6646
+ cancel.type = "button";
6647
+ cancel.dataset.noteAction = "cancel";
6648
+ cancel.addEventListener("click", () => {
6649
+ if (mayDiscard()) {
6650
+ dirty = false;
6651
+ if (selectedPerson !== null)
6652
+ showPerson(selectedPerson);
6653
+ }
6654
+ });
6655
+ const download = node("button", "Download draft");
6656
+ download.type = "button";
6657
+ download.addEventListener("click", () => {
6658
+ const url = URL.createObjectURL(new Blob([body.value], { type: "text/markdown;charset=utf-8" }));
6659
+ const link = document.createElement("a");
6660
+ link.href = url;
6661
+ link.download = `peopleblade-note-${note.id}.md`;
6662
+ link.click();
6663
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
6664
+ });
6665
+ actions.append(save, toggle, cancel, download);
6666
+ editor.append(titleLabel, bodyLabel, preview, actions);
6667
+ card.append(editor);
6668
+ let pending = null;
6669
+ let saving = false;
6670
+ let acknowledged = false;
6671
+ const changed = () => {
6672
+ editorGeneration++;
6673
+ dirty = title.value !== original.title || body.value !== original.body;
6674
+ };
6675
+ title.addEventListener("input", changed);
6676
+ body.addEventListener("input", changed);
6677
+ changed();
6678
+ editor.addEventListener("submit", async (event) => {
6679
+ event.preventDefault();
6680
+ if (saving || acknowledged || detailLoading)
6681
+ return;
6682
+ pending ??= {
6683
+ ...note.id === 0 ? { action: "note-create", personId: note.personId, expectedPersonContextSha256: note.contextSha256, occurredAt: note.occurredAt } : { action: "note-update", noteId: note.id, expectedRevision: note.revision, expectedContextSha256: note.contextSha256 },
6684
+ requestId: crypto.randomUUID(),
6685
+ title: title.value.trim() === "" ? null : title.value,
6686
+ body: body.value
6687
+ };
6688
+ saving = true;
6689
+ activeSave = true;
6690
+ save.disabled = true;
6691
+ body.readOnly = true;
6692
+ title.readOnly = true;
6693
+ status("Saving an immutable revision\u2026");
6694
+ try {
6695
+ await api(pending);
6696
+ acknowledged = true;
6697
+ pending = null;
6698
+ dirty = false;
6699
+ save.textContent = "Saved";
6700
+ status("Note saved. Original content and history are preserved.");
6701
+ const refreshed = selectedPerson !== null && await showPerson(selectedPerson);
6702
+ if (!refreshed) {
6703
+ cancel.textContent = "Reload saved note";
6704
+ status("Note saved. Details could not be refreshed. Reload the saved note; it will not be submitted again.");
6705
+ }
6706
+ } catch (error) {
6707
+ const responseStatus = error.status;
6708
+ const conflict = responseStatus === 409;
6709
+ const rejected = responseStatus !== undefined && responseStatus >= 400 && responseStatus < 500;
6710
+ status(conflict ? "This note or its contact identity changed. Download your draft, then Cancel to reload before applying it." : rejected ? error instanceof Error ? error.message : "The note was rejected; correct the draft and try again." : "Save outcome is uncertain. Retry sends the same request safely; keep this tab open or download your draft.");
6711
+ if (rejected) {
6712
+ pending = null;
6713
+ body.readOnly = false;
6714
+ title.readOnly = false;
6715
+ }
6716
+ save.textContent = rejected ? "Save revision" : "Retry same save";
6717
+ } finally {
6718
+ saving = false;
6719
+ activeSave = false;
6720
+ save.disabled = acknowledged;
6721
+ }
6722
+ });
6723
+ body.addEventListener("keydown", (event) => {
6724
+ if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
6725
+ event.preventDefault();
6726
+ editor.dispatchEvent(new Event("submit", { cancelable: true }));
6727
+ }
6728
+ });
6729
+ body.focus();
6730
+ }
6731
+ async function showPerson(id, noteSearch = "") {
6732
+ const current = ++detailGeneration;
6733
+ const editorAtRequest = editorGeneration;
6734
+ const target = el("detail");
6735
+ detailLoading = true;
6736
+ target.inert = true;
6737
+ status("Loading person and notes\u2026");
6738
+ try {
6739
+ const result = await api({ action: "detail", personId: id, ...noteSearch ? { noteSearch } : {} });
6740
+ const notes = result.notes;
6741
+ if (current !== detailGeneration || !capability)
6742
+ return false;
6743
+ if (editorAtRequest !== editorGeneration) {
6744
+ status("Contact loading was canceled because the note draft changed.");
6745
+ return false;
6746
+ }
6747
+ activeEditor = null;
6748
+ selectedPerson = id;
6749
+ target.replaceChildren();
6750
+ target.append(node("span", `PERSON ${id} \xB7 ${result.memberCount} SOURCE RECORD${result.memberCount === 1 ? "" : "S"}`, "eyebrow"), node("h2", result.contact.displayName));
6751
+ const pills = node("div");
6752
+ result.contact.sources.forEach((source) => pills.append(node("span", source, "pill")));
6753
+ target.append(pills);
6754
+ const fields = node("dl");
6755
+ for (const [label, value] of [["Company", result.contact.organization], ["Title", result.contact.title], ["Preference", result.contact.doNotContact ? "Do not contact" : "No do-not-contact flag"], ["Interactions", result.contact.interactionCount.toLocaleString()]]) {
6756
+ fields.append(node("dt", label ?? ""), node("dd", value ?? "\u2014"));
6757
+ }
6758
+ fields.append(node("dt", "Methods"));
6759
+ const methods = node("dd");
6760
+ result.methods.items.forEach((method) => methods.append(node("span", `${method.kind}: ${method.value}`)));
6761
+ if (result.methods.truncated)
6762
+ methods.append(node("span", `Showing ${result.methods.items.length} of ${result.methods.total}. Use the CLI for a narrower query.`));
6763
+ fields.append(methods);
6764
+ target.append(fields);
6765
+ const heading = node("div", "", "notes-heading");
6766
+ heading.append(node("h3", "Private notes"), node("span", "LOCAL ONLY", "eyebrow"));
6767
+ target.append(heading);
6768
+ target.append(node("p", "Latest 20 matching notes. Search older notes below. Edits preserve source content; identity review and joins remain explicit CLI actions.", "muted"));
6769
+ const noteTools = node("form", "", "editor-actions");
6770
+ const searchLabel = node("label", "Search notes ");
6771
+ const noteQuery = document.createElement("input");
6772
+ noteQuery.type = "search";
6773
+ noteQuery.maxLength = 200;
6774
+ noteQuery.value = noteSearch;
6775
+ searchLabel.append(noteQuery);
6776
+ const searchNotes = node("button", "Find");
6777
+ searchNotes.type = "submit";
6778
+ const add = node("button", "New note");
6779
+ add.type = "button";
6780
+ add.dataset.noteAction = "create";
6781
+ noteTools.append(searchLabel, searchNotes, add);
6782
+ target.append(noteTools);
6783
+ noteTools.addEventListener("submit", (event) => {
6784
+ event.preventDefault();
6785
+ if (mayDiscard()) {
6786
+ dirty = false;
6787
+ showPerson(id, noteQuery.value);
6788
+ }
6789
+ });
6790
+ const noteContainer = node("div");
6791
+ target.append(noteContainer);
6792
+ add.addEventListener("click", () => {
6793
+ if (!mayDiscard())
6794
+ return;
6795
+ dirty = false;
6796
+ const draft = node("article", "", "note-card");
6797
+ noteContainer.prepend(draft);
6798
+ editNote({ id: 0, personId: id, title: null, body: "", occurredAt: new Date().toISOString(), source: "manual", revision: 0, contextSha256: result.noteContext.contextSha256 }, draft);
6799
+ });
6800
+ if (notes.length === 0)
6801
+ noteContainer.append(node("p", "No notes yet.", "muted"));
6802
+ for (const note of notes)
6803
+ renderNote(note, noteContainer);
6804
+ status("");
6805
+ return true;
6806
+ } catch (error) {
6807
+ if (current === detailGeneration)
6808
+ status(error instanceof Error ? error.message : "Contact details could not be loaded.");
6809
+ return false;
6810
+ } finally {
6811
+ if (current === detailGeneration) {
6812
+ detailLoading = false;
6813
+ target.inert = false;
6814
+ }
6815
+ }
6816
+ }
6817
+ el("access-file").addEventListener("change", async (event) => {
6818
+ try {
6819
+ const input = event.target;
6820
+ const file = input.files?.[0];
6821
+ input.value = "";
6822
+ if (!file || file.size > 2048)
6823
+ throw new Error("Choose the small access file printed by the running CLI.");
6824
+ const access = JSON.parse(await file.text());
6825
+ if (access.schemaVersion !== "peopleblade.workspace-access.v1" || access.origin !== location.origin || typeof access.capability !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(access.capability))
6826
+ throw new Error("This access file belongs to another workspace. Use the current CLI file and URL.");
6827
+ capability = access.capability;
6828
+ await api({ action: "ping" });
6829
+ el("unlock").hidden = true;
6830
+ el("book").hidden = false;
6831
+ await load();
6832
+ el("search").focus();
6833
+ } catch (error) {
6834
+ capability = "";
6835
+ status(error instanceof Error ? error.message : "Workspace could not be unlocked.");
6836
+ }
6837
+ });
6838
+ el("lock").addEventListener("click", () => {
6839
+ if (!mayDiscard())
6840
+ return;
6841
+ capability = "";
6842
+ dirty = false;
6843
+ generation++;
6844
+ detailGeneration++;
6845
+ activeEditor = null;
6846
+ selectedPerson = null;
6847
+ el("rows").replaceChildren();
6848
+ el("detail").replaceChildren();
6849
+ el("book").hidden = true;
6850
+ el("unlock").hidden = false;
6851
+ status("Workspace locked.");
6852
+ });
6853
+ el("filters").addEventListener("submit", (event) => {
6854
+ event.preventDefault();
6855
+ offset = 0;
6856
+ load();
6857
+ });
6858
+ el("reset").addEventListener("click", () => {
6859
+ HTMLFormElement.prototype.reset.call(el("filters"));
6860
+ offset = 0;
6861
+ load();
6862
+ });
6863
+ el("sort").addEventListener("change", () => {
6864
+ offset = 0;
6865
+ load();
6866
+ });
6867
+ el("direction").addEventListener("click", () => {
6868
+ direction = direction === "asc" ? "desc" : "asc";
6869
+ el("direction").textContent = direction === "asc" ? "Ascending \u2191" : "Descending \u2193";
6870
+ offset = 0;
6871
+ load();
6872
+ });
6873
+ el("previous").addEventListener("click", () => {
6874
+ offset = Math.max(0, offset - 50);
6875
+ load();
6876
+ });
6877
+ el("next").addEventListener("click", () => {
6878
+ if (page?.nextOffset !== null && page?.nextOffset !== undefined) {
6879
+ offset = page.nextOffset;
6880
+ load();
6881
+ }
6882
+ });
6883
+ window.addEventListener("beforeunload", (event) => {
6884
+ if (dirty || activeSave) {
6885
+ event.preventDefault();
6886
+ event.returnValue = "";
6887
+ }
6888
+ });
6889
+ }
6890
+ var workspaceScript = `(${workspaceClient.toString()})();`;
6891
+
6892
+ // src/local/workspace-security.ts
6893
+ import { timingSafeEqual } from "crypto";
6894
+ var securityHeaders = {
6895
+ "cache-control": "no-store",
6896
+ "content-security-policy": "default-src 'none'; script-src 'self'; style-src 'self'; connect-src 'self'; img-src 'none'; font-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'; object-src 'none'",
6897
+ "referrer-policy": "no-referrer",
6898
+ "x-content-type-options": "nosniff",
6899
+ "x-frame-options": "DENY",
6900
+ "cross-origin-resource-policy": "same-origin",
6901
+ "permissions-policy": "camera=(), microphone=(), geolocation=()"
6902
+ };
6903
+
6904
+ class WorkspaceRequestError extends Error {
6905
+ code;
6906
+ constructor(code, message) {
6907
+ super(message);
6908
+ this.code = code;
6909
+ }
6910
+ }
6911
+ function json(value, status = 200) {
6912
+ return Response.json(value, { status, headers: securityHeaders });
6913
+ }
6914
+ async function readPayload(request) {
6915
+ const maximum = 256 * 1024;
6916
+ const length = request.headers.get("content-length");
6917
+ if (length !== null && (!/^(0|[1-9][0-9]*)$/u.test(length) || Number(length) > maximum)) {
6918
+ request.body?.cancel().catch(() => {
6919
+ return;
6920
+ });
6921
+ throw new WorkspaceRequestError("invalid_request", "Request exceeds 256 KiB.");
6922
+ }
6923
+ if (request.body === null)
6924
+ throw new WorkspaceRequestError("invalid_request", "A JSON body is required.");
6925
+ const reader = request.body.getReader();
6926
+ const decoder = new TextDecoder("utf-8", { fatal: true });
6927
+ let size = 0;
6928
+ let text = "";
6929
+ try {
6930
+ for (;; ) {
6931
+ const chunk = await reader.read();
6932
+ if (chunk.done)
6933
+ break;
6934
+ size += chunk.value.byteLength;
6935
+ if (size > maximum)
6936
+ throw new Error("oversized");
6937
+ text += decoder.decode(chunk.value, { stream: true });
6938
+ }
6939
+ text += decoder.decode();
6940
+ return JSON.parse(text);
6941
+ } catch {
6942
+ reader.cancel().catch(() => {
6943
+ return;
6944
+ });
6945
+ throw new WorkspaceRequestError("invalid_request", "Request must be valid UTF-8 JSON within 256 KiB.");
6946
+ } finally {
6947
+ reader.releaseLock();
6948
+ }
6949
+ }
6950
+ function createWorkspaceHandler(input) {
6951
+ const expected = new URL(input.origin);
6952
+ if (expected.protocol !== "http:" || expected.hostname !== "127.0.0.1" || !expected.port || expected.origin !== input.origin || !/^[A-Za-z0-9_-]{43}$/u.test(input.capability)) {
6953
+ throw new Error("Invalid local workspace authority.");
6954
+ }
6955
+ const secret = Buffer.from(input.capability);
6956
+ return async (request) => {
6957
+ const url = new URL(request.url);
6958
+ if (url.origin !== input.origin || request.headers.get("host") !== expected.host || url.search !== "" || request.headers.get("sec-fetch-site") === "cross-site" || request.headers.has("origin") && request.headers.get("origin") !== input.origin) {
6959
+ request.body?.cancel().catch(() => {
6960
+ return;
6961
+ });
6962
+ return json({ error: "forbidden" }, 403);
6963
+ }
6964
+ if (request.method === "GET") {
6965
+ const asset = url.pathname === "/" ? [input.assets.html, "text/html; charset=utf-8"] : url.pathname === "/workspace.js" ? [input.assets.script, "text/javascript; charset=utf-8"] : url.pathname === "/workspace.css" ? [input.assets.css, "text/css; charset=utf-8"] : null;
6966
+ return asset === null ? json({ error: "not_found" }, 404) : new Response(asset[0], { headers: { ...securityHeaders, "content-type": asset[1] } });
6967
+ }
6968
+ if (request.method !== "POST" || url.pathname !== "/api")
6969
+ return json({ error: "not_found" }, 404);
6970
+ const supplied = request.headers.get("authorization")?.replace(/^Bearer /u, "") ?? "";
6971
+ if (request.headers.get("origin") !== input.origin || !/^[A-Za-z0-9_-]{43}$/u.test(supplied) || !timingSafeEqual(Buffer.from(supplied), secret)) {
6972
+ request.body?.cancel().catch(() => {
6973
+ return;
6974
+ });
6975
+ return json({ error: "unauthorized" }, 401);
6976
+ }
6977
+ if (request.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase() !== "application/json") {
6978
+ return json({ error: "unsupported_media_type" }, 415);
6979
+ }
6980
+ try {
6981
+ return json({ ok: true, result: await input.dispatch(await readPayload(request)) });
6982
+ } catch (error) {
6983
+ if (error instanceof WorkspaceRequestError) {
6984
+ return json({ ok: false, error: error.code, message: error.message }, error.code === "not_found" ? 404 : error.code === "conflict" ? 409 : 400);
6985
+ }
6986
+ return json({ ok: false, error: "operation_failed", message: "The operation failed. Your existing data is preserved." }, 500);
6987
+ }
6988
+ };
6989
+ }
6990
+
6991
+ // src/local/workspace.ts
6992
+ var id = z5.number().int().positive().max(Number.MAX_SAFE_INTEGER);
6993
+ var requestSchema = z5.discriminatedUnion("action", [
6994
+ z5.object({ action: z5.literal("ping") }).strict(),
6995
+ z5.object({ action: z5.literal("query"), query: contactQueryInputSchema }).strict(),
6996
+ z5.object({ action: z5.literal("detail"), personId: id, noteSearch: z5.string().max(200).optional() }).strict(),
6997
+ z5.object({ action: z5.literal("notes"), personId: id, search: z5.string().max(200).optional() }).strict(),
6998
+ z5.object({ action: z5.literal("note"), noteId: id }).strict(),
6999
+ z5.object({ action: z5.literal("note-history"), noteId: id, beforeRevision: z5.number().int().min(1).optional() }).strict(),
7000
+ z5.object({ action: z5.literal("preview"), body: z5.string().max(65536) }).strict(),
7001
+ z5.object({
7002
+ action: z5.literal("note-update"),
7003
+ noteId: id,
7004
+ expectedRevision: z5.number().int().min(0).max(Number.MAX_SAFE_INTEGER - 1),
7005
+ expectedContextSha256: z5.string().regex(/^[0-9a-f]{64}$/u),
7006
+ requestId: z5.uuid(),
7007
+ title: z5.string().max(4096).nullable(),
7008
+ body: z5.string().max(65536)
7009
+ }).strict(),
7010
+ z5.object({
7011
+ action: z5.literal("note-create"),
7012
+ personId: id,
7013
+ expectedPersonContextSha256: z5.string().regex(/^[0-9a-f]{64}$/u),
7014
+ requestId: z5.uuid(),
7015
+ occurredAt: z5.string().max(64),
7016
+ title: z5.string().max(4096).nullable(),
7017
+ body: z5.string().max(65536)
7018
+ }).strict()
7019
+ ]);
7020
+ function dispatchWorkspaceRequest(database, payload) {
7021
+ const parsed = requestSchema.safeParse(payload);
7022
+ if (!parsed.success)
7023
+ throw new WorkspaceRequestError("invalid_request", "Invalid workspace request. Check the action and its fields.");
7024
+ const input = parsed.data;
7025
+ try {
7026
+ switch (input.action) {
7027
+ case "ping":
7028
+ return { schemaVersion: "peopleblade.workspace.v1", localOnly: true };
7029
+ case "query":
7030
+ return queryLocalContacts(database, input.query);
7031
+ case "detail":
7032
+ return database.transaction(() => ({
7033
+ ...getLocalContactDetail(database, input.personId),
7034
+ noteContext: personNoteContext(database, input.personId),
7035
+ notes: ((input.noteSearch?.trim()) ? searchPersonNotes(database, { personId: input.personId, query: input.noteSearch, limit: 20 }) : listPersonNotes(database, { personId: input.personId, limit: 20 })).map((note) => ({ ...note, previewHtml: previewNoteMarkdown(note.body) }))
7036
+ })).deferred();
7037
+ case "notes": {
7038
+ const notes = input.search?.trim() ? searchPersonNotes(database, { personId: input.personId, query: input.search, limit: 20 }) : listPersonNotes(database, { personId: input.personId, limit: 20 });
7039
+ return notes.map((note) => ({ ...note, previewHtml: previewNoteMarkdown(note.body) }));
7040
+ }
7041
+ case "note":
7042
+ return getPersonNote(database, input.noteId);
7043
+ case "note-history":
7044
+ return listPersonNoteRevisions(database, { noteId: input.noteId, beforeRevision: input.beforeRevision, limit: 50 });
7045
+ case "note-update":
7046
+ return revisePersonNote(database, input);
7047
+ case "note-create":
7048
+ return createWorkspaceNote(database, input);
7049
+ case "preview":
7050
+ return { html: previewNoteMarkdown(input.body) };
7051
+ }
7052
+ } catch (error) {
7053
+ if (error instanceof NoteRevisionError) {
7054
+ throw new WorkspaceRequestError(error.code === "not_found" ? "not_found" : error.code === "invalid_input" ? "invalid_request" : "conflict", error.message);
7055
+ }
7056
+ if (input.action === "preview")
7057
+ throw new WorkspaceRequestError("invalid_request", "Markdown preview requires at most 64 KiB of UTF-8 text.");
7058
+ throw error;
7059
+ }
7060
+ }
7061
+ function startLocalWorkspace(database, port = 0) {
7062
+ if (!Number.isSafeInteger(port) || port < 0 || port > 65535)
7063
+ throw new Error("Workspace port must be 0\u201365535.");
7064
+ const capability = randomBytes(32).toString("base64url");
7065
+ let handler = null;
7066
+ const server = Bun.serve({
7067
+ hostname: "127.0.0.1",
7068
+ port,
7069
+ maxRequestBodySize: 256 * 1024,
7070
+ idleTimeout: 20,
7071
+ fetch(request) {
7072
+ return handler === null ? new Response(null, { status: 503 }) : handler(request);
7073
+ },
7074
+ error() {
7075
+ return new Response("Workspace request failed.", { status: 500, headers: { "cache-control": "no-store" } });
7076
+ }
7077
+ });
7078
+ const origin = `http://127.0.0.1:${server.port}`;
7079
+ let directory;
7080
+ let accessFile;
7081
+ try {
7082
+ directory = mkdtempSync(join(tmpdir(), "peopleblade-workspace-"));
7083
+ chmodSync(directory, 448);
7084
+ accessFile = join(directory, "access.json");
7085
+ writeFileSync(accessFile, JSON.stringify({ schemaVersion: "peopleblade.workspace-access.v1", origin, capability }), { flag: "wx", mode: 384 });
7086
+ } catch (error) {
7087
+ server.stop(true);
7088
+ throw error;
5794
7089
  }
7090
+ const fileIdentity = lstatSync(accessFile);
7091
+ handler = createWorkspaceHandler({
7092
+ origin,
7093
+ capability,
7094
+ assets: { html: workspaceHtml, css: workspaceCss, script: workspaceScript },
7095
+ dispatch: (payload) => dispatchWorkspaceRequest(database, payload)
7096
+ });
7097
+ let stopped = false;
7098
+ const stop = async () => {
7099
+ if (stopped)
7100
+ return;
7101
+ stopped = true;
7102
+ handler = null;
7103
+ await server.stop(true);
7104
+ try {
7105
+ const current = lstatSync(accessFile);
7106
+ if (current.isFile() && !current.isSymbolicLink() && current.ino === fileIdentity.ino && current.dev === fileIdentity.dev && current.nlink === 1)
7107
+ unlinkSync(accessFile);
7108
+ rmdirSync(directory);
7109
+ } catch {}
7110
+ };
7111
+ return { schemaVersion: "peopleblade.workspace.v1", origin, accessFile, localOnly: true, stop };
7112
+ }
7113
+ async function runLocalWorkspace(database, options = {}) {
7114
+ const workspace = startLocalWorkspace(database, options.port);
7115
+ const { stop: _stop, ...receipt } = workspace;
7116
+ if (options.json)
7117
+ console.log(JSON.stringify(receipt));
7118
+ else
7119
+ console.log(`PeopleBlade local workspace
7120
+ ${workspace.origin}
7121
+ Select this access file in the page: ${workspace.accessFile}
7122
+ Keep this terminal open. Press Ctrl+C to lock and stop.`);
7123
+ let terminate = () => {
7124
+ return;
7125
+ };
5795
7126
  try {
5796
- return JSON.parse(raw);
5797
- } catch {
5798
- fail("Granola import is not valid JSON.");
7127
+ await new Promise((resolve) => {
7128
+ terminate = () => resolve();
7129
+ process.once("SIGINT", terminate);
7130
+ process.once("SIGTERM", terminate);
7131
+ process.once("SIGHUP", terminate);
7132
+ });
7133
+ } finally {
7134
+ process.removeListener("SIGINT", terminate);
7135
+ process.removeListener("SIGTERM", terminate);
7136
+ process.removeListener("SIGHUP", terminate);
7137
+ await workspace.stop();
5799
7138
  }
5800
7139
  }
5801
7140
 
5802
7141
  // src/local/cloud-client.ts
5803
- import { createHash as createHash2, randomBytes } from "crypto";
5804
- import { z as z6 } from "zod";
7142
+ import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
7143
+ import { z as z8 } from "zod";
5805
7144
 
5806
7145
  // src/local/cloud-configuration.ts
5807
- import { z as z5 } from "zod";
7146
+ import { z as z7 } from "zod";
5808
7147
 
5809
7148
  // src/local/config.ts
5810
- import { closeSync as closeSync3, constants as constants3, existsSync, mkdirSync, openSync as openSync3, readFileSync as readFileSync3, renameSync, writeFileSync } from "fs";
7149
+ import { closeSync as closeSync3, constants as constants3, existsSync, mkdirSync, openSync as openSync3, readFileSync as readFileSync3, renameSync, writeFileSync as writeFileSync2 } from "fs";
5811
7150
  import { dirname } from "path";
5812
- import { z as z4 } from "zod";
7151
+ import { z as z6 } from "zod";
5813
7152
 
5814
7153
  // src/local/paths.ts
5815
7154
  import { homedir, hostname, platform } from "os";
5816
- import { join } from "path";
7155
+ import { join as join2 } from "path";
5817
7156
  function peoplebladeDirectory() {
5818
7157
  if (platform() === "darwin")
5819
- return join(homedir(), "Library", "Application Support", "PeopleBlade");
7158
+ return join2(homedir(), "Library", "Application Support", "PeopleBlade");
5820
7159
  if (platform() === "win32")
5821
- return join(process.env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local"), "PeopleBlade");
5822
- return join(process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share"), "peopleblade");
7160
+ return join2(process.env.LOCALAPPDATA ?? join2(homedir(), "AppData", "Local"), "PeopleBlade");
7161
+ return join2(process.env.XDG_DATA_HOME ?? join2(homedir(), ".local", "share"), "peopleblade");
5823
7162
  }
5824
7163
  function peoplebladeDatabasePath() {
5825
- return process.env.PEOPLEBLADE_DATABASE ?? join(peoplebladeDirectory(), "peopleblade.sqlite3");
7164
+ return process.env.PEOPLEBLADE_DATABASE ?? join2(peoplebladeDirectory(), "peopleblade.sqlite3");
5826
7165
  }
5827
7166
  function peoplebladeConfigPath() {
5828
- return join(peoplebladeDirectory(), "config.json");
7167
+ return join2(peoplebladeDirectory(), "config.json");
5829
7168
  }
5830
7169
  function defaultDeviceName() {
5831
7170
  return `${hostname() || "Mac"} \xB7 PeopleBlade CLI`.slice(0, 128);
5832
7171
  }
5833
7172
 
5834
7173
  // src/local/config.ts
5835
- var configSchema = z4.object({
5836
- cloud: z4.object({
5837
- baseUrl: z4.url().max(2048),
5838
- deviceId: z4.uuid(),
5839
- token: z4.string().min(20).max(512)
7174
+ var configSchema = z6.object({
7175
+ cloud: z6.object({
7176
+ baseUrl: z6.url().max(2048),
7177
+ deviceId: z6.uuid(),
7178
+ token: z6.string().min(20).max(512)
5840
7179
  }).strict().nullable()
5841
7180
  }).strict();
5842
7181
  function readLocalConfig() {
@@ -5852,7 +7191,7 @@ function writeLocalConfig(config) {
5852
7191
  const temporary = `${path}.stage-${process.pid}`;
5853
7192
  const descriptor = openSync3(temporary, constants3.O_WRONLY | constants3.O_CREAT | constants3.O_EXCL, 384);
5854
7193
  try {
5855
- writeFileSync(descriptor, `${JSON.stringify(configSchema.parse(config), null, 2)}
7194
+ writeFileSync2(descriptor, `${JSON.stringify(configSchema.parse(config), null, 2)}
5856
7195
  `);
5857
7196
  } finally {
5858
7197
  closeSync3(descriptor);
@@ -5861,10 +7200,10 @@ function writeLocalConfig(config) {
5861
7200
  }
5862
7201
 
5863
7202
  // src/local/cloud-configuration.ts
5864
- var cloudConfigurationSchema = z5.object({
5865
- baseUrl: z5.url().max(2048),
5866
- deviceId: z5.uuid(),
5867
- token: z5.string().min(20).max(512)
7203
+ var cloudConfigurationSchema = z7.object({
7204
+ baseUrl: z7.url().max(2048),
7205
+ deviceId: z7.uuid(),
7206
+ token: z7.string().min(20).max(512)
5868
7207
  }).strict();
5869
7208
  function cloudConfiguration(override) {
5870
7209
  const config = override ?? readLocalConfig().cloud;
@@ -5970,7 +7309,14 @@ function iso(value) {
5970
7309
  function localDatabaseFingerprint(database) {
5971
7310
  return localDatabaseInstanceId(database);
5972
7311
  }
5973
- function projectCloudContacts(database) {
7312
+ function projectCloudContacts(database, options = {}) {
7313
+ const selected = options.personIds === undefined ? undefined : [...new Set(options.personIds)];
7314
+ if (selected !== undefined && (selected.length > 100 || selected.some((id2) => !Number.isSafeInteger(id2) || id2 < 1))) {
7315
+ throw new Error("Projection selection requires at most 100 positive canonical person IDs.");
7316
+ }
7317
+ if (selected?.length === 0)
7318
+ return [];
7319
+ const selectionSql = selected === undefined ? "" : `AND p.id IN (${selected.map(() => "?").join(",")})`;
5974
7320
  const rows = database.query(`
5975
7321
  WITH
5976
7322
  ${canonicalInteractionRollupCtesSql},
@@ -6137,11 +7483,12 @@ function projectCloudContacts(database) {
6137
7483
  LEFT JOIN source_rollup ON source_rollup.person_id = p.id
6138
7484
  LEFT JOIN handle_rollup ON handle_rollup.person_id = p.id
6139
7485
  LEFT JOIN resource_eligibility ON resource_eligibility.person_id = p.id
6140
- WHERE resource_eligibility.person_id IS NULL
7486
+ WHERE (resource_eligibility.person_id IS NULL
6141
7487
  OR resource_eligibility.has_active_resource = 1
6142
- OR canonical_interaction_rollup.person_id IS NOT NULL
7488
+ OR canonical_interaction_rollup.person_id IS NOT NULL)
7489
+ ${selectionSql}
6143
7490
  ORDER BY p.id
6144
- `).all();
7491
+ `).all(...selected ?? []);
6145
7492
  const instance = localDatabaseFingerprint(database);
6146
7493
  return rows.map((row) => {
6147
7494
  const sources = sourceLabelSchema.array().parse(parseStringArray(row.sources_json).filter((source) => sourceLabelSchema.safeParse(source).success));
@@ -6182,49 +7529,49 @@ function projectCloudContacts(database) {
6182
7529
  }
6183
7530
 
6184
7531
  // src/local/cloud-client.ts
6185
- var deviceStartResponse = z6.object({
7532
+ var deviceStartResponse = z8.object({
6186
7533
  deviceCode: deviceCodeSchema,
6187
- userCode: z6.string().regex(/^[23456789A-HJ-NP-Z]{8}$/u),
6188
- verificationUri: z6.url().max(2048),
6189
- expiresIn: z6.number().int().min(60).max(3600),
6190
- interval: z6.number().int().min(1).max(30)
7534
+ userCode: z8.string().regex(/^[23456789A-HJ-NP-Z]{8}$/u),
7535
+ verificationUri: z8.url().max(2048),
7536
+ expiresIn: z8.number().int().min(60).max(3600),
7537
+ interval: z8.number().int().min(1).max(30)
6191
7538
  }).strict();
6192
- var deviceStatusResponse = z6.discriminatedUnion("status", [
6193
- z6.object({ status: z6.literal("pending") }).strict(),
6194
- z6.object({ status: z6.literal("expired") }).strict(),
6195
- z6.object({ status: z6.literal("authorized"), deviceId: z6.uuid() }).strict()
7539
+ var deviceStatusResponse = z8.discriminatedUnion("status", [
7540
+ z8.object({ status: z8.literal("pending") }).strict(),
7541
+ z8.object({ status: z8.literal("expired") }).strict(),
7542
+ z8.object({ status: z8.literal("authorized"), deviceId: z8.uuid() }).strict()
6196
7543
  ]);
6197
- var boundedModelCoordinateSchema = z6.string().min(1).max(256).regex(/^[A-Za-z0-9][A-Za-z0-9._:/-]*$/u);
6198
- var usageOperationSchema = z6.string().regex(/^[a-z][a-z0-9_]{0,63}$/u);
6199
- var usageProviderSchema = z6.string().regex(/^[a-z][a-z0-9_-]{0,63}$/u);
6200
- var usageUnitSchema = z6.string().regex(/^[a-z][a-z0-9_-]{0,63}$/u);
6201
- var cliEnrichmentUsageLimitResponse = z6.object({
7544
+ var boundedModelCoordinateSchema = z8.string().min(1).max(256).regex(/^[A-Za-z0-9][A-Za-z0-9._:/-]*$/u);
7545
+ var usageOperationSchema = z8.string().regex(/^[a-z][a-z0-9_]{0,63}$/u);
7546
+ var usageProviderSchema = z8.string().regex(/^[a-z][a-z0-9_-]{0,63}$/u);
7547
+ var usageUnitSchema = z8.string().regex(/^[a-z][a-z0-9_-]{0,63}$/u);
7548
+ var cliEnrichmentUsageLimitResponse = z8.object({
6202
7549
  operation: usageOperationSchema,
6203
7550
  provider: usageProviderSchema,
6204
7551
  unit: usageUnitSchema,
6205
- maximum: z6.number().int().min(0).max(100)
7552
+ maximum: z8.number().int().min(0).max(100)
6206
7553
  }).strict();
6207
- var cliEnrichmentPreviewResponse = z6.object({
6208
- previewId: z6.uuid(),
7554
+ var cliEnrichmentPreviewResponse = z8.object({
7555
+ previewId: z8.uuid(),
6209
7556
  confirmationToken: cliEnrichmentConfirmationTokenSchema,
6210
- selectedCount: z6.number().int().min(1).max(100),
6211
- availableCredits: z6.number().int().min(0),
6212
- usageLimits: z6.array(cliEnrichmentUsageLimitResponse).min(1).max(16),
6213
- policyVersion: z6.literal(enrichmentPolicyVersion),
7557
+ selectedCount: z8.number().int().min(1).max(100),
7558
+ availableCredits: z8.number().int().min(0),
7559
+ usageLimits: z8.array(cliEnrichmentUsageLimitResponse).min(1).max(16),
7560
+ policyVersion: z8.literal(enrichmentPolicyVersion),
6214
7561
  model: boundedModelCoordinateSchema,
6215
- expiresAt: z6.iso.datetime({ offset: true })
7562
+ expiresAt: z8.iso.datetime({ offset: true })
6216
7563
  }).strict();
6217
- var cliPrioritizedEnrichmentContactResponse = z6.object({
6218
- localPersonId: z6.string().regex(/^[1-9][0-9]{0,18}$/u),
6219
- enrichmentInputVersion: z6.literal(2),
6220
- enrichmentInputSha256: z6.string().regex(/^[a-f0-9]{64}$/u),
6221
- rank: z6.number().int().min(1).max(100),
6222
- score: z6.number().int().min(0).max(1e4),
6223
- components: z6.object({
6224
- engagement: z6.number().int().min(0).max(5500),
6225
- reciprocity: z6.number().int().min(0).max(1000),
6226
- dataGaps: z6.number().int().min(0).max(2000),
6227
- identityReadiness: z6.number().int().min(0).max(1500)
7564
+ var cliPrioritizedEnrichmentContactResponse = z8.object({
7565
+ localPersonId: z8.string().regex(/^[1-9][0-9]{0,18}$/u),
7566
+ enrichmentInputVersion: z8.literal(2),
7567
+ enrichmentInputSha256: z8.string().regex(/^[a-f0-9]{64}$/u),
7568
+ rank: z8.number().int().min(1).max(100),
7569
+ score: z8.number().int().min(0).max(1e4),
7570
+ components: z8.object({
7571
+ engagement: z8.number().int().min(0).max(5500),
7572
+ reciprocity: z8.number().int().min(0).max(1000),
7573
+ dataGaps: z8.number().int().min(0).max(2000),
7574
+ identityReadiness: z8.number().int().min(0).max(1500)
6228
7575
  }).strict()
6229
7576
  }).strict().superRefine((value, context) => {
6230
7577
  const total = value.components.engagement + value.components.reciprocity + value.components.dataGaps + value.components.identityReadiness;
@@ -6232,39 +7579,39 @@ var cliPrioritizedEnrichmentContactResponse = z6.object({
6232
7579
  context.addIssue({ code: "custom", path: ["score"], message: "Priority score components drifted" });
6233
7580
  });
6234
7581
  var cliPrioritizedEnrichmentPreviewResponse = cliEnrichmentPreviewResponse.extend({
6235
- contacts: z6.array(cliPrioritizedEnrichmentContactResponse).min(1).max(100),
6236
- priority: z6.object({
6237
- policyVersion: z6.literal(enrichmentPriorityPolicyVersion),
6238
- asOf: z6.iso.datetime({ offset: true }),
6239
- receiptSha256: z6.string().regex(/^[a-f0-9]{64}$/u),
6240
- eligibleCount: z6.number().int().min(1).max(1e6),
6241
- executionCapacity: z6.tuple([z6.object({
6242
- operation: z6.literal("enrich_email"),
6243
- maximum: z6.number().int().min(0).max(100)
7582
+ contacts: z8.array(cliPrioritizedEnrichmentContactResponse).min(1).max(100),
7583
+ priority: z8.object({
7584
+ policyVersion: z8.literal(enrichmentPriorityPolicyVersion),
7585
+ asOf: z8.iso.datetime({ offset: true }),
7586
+ receiptSha256: z8.string().regex(/^[a-f0-9]{64}$/u),
7587
+ eligibleCount: z8.number().int().min(1).max(1e6),
7588
+ executionCapacity: z8.tuple([z8.object({
7589
+ operation: z8.literal("enrich_email"),
7590
+ maximum: z8.number().int().min(0).max(100)
6244
7591
  }).strict()])
6245
7592
  }).strict()
6246
7593
  }).strict();
6247
- var cliEnrichmentDispatchResponse = z6.object({
6248
- jobId: z6.uuid(),
6249
- dispatchStatus: z6.enum(["dispatching", "launched", "indeterminate"]),
6250
- replayed: z6.boolean()
7594
+ var cliEnrichmentDispatchResponse = z8.object({
7595
+ jobId: z8.uuid(),
7596
+ dispatchStatus: z8.enum(["dispatching", "launched", "indeterminate"]),
7597
+ replayed: z8.boolean()
6251
7598
  }).strict();
6252
- var enrichmentFieldSchema = z6.enum(["headline", "organization", "role", "location", "website", "publicEmail"]);
6253
- var usageBillingSurfaceSchema = z6.string().regex(/^[a-z][a-z0-9_-]{0,63}$/u);
7599
+ var enrichmentFieldSchema = z8.enum(["headline", "organization", "role", "location", "website", "publicEmail"]);
7600
+ var usageBillingSurfaceSchema = z8.string().regex(/^[a-z][a-z0-9_-]{0,63}$/u);
6254
7601
  var usageModelSchema = boundedModelCoordinateSchema.nullable();
6255
- var usageBillingUnitSchema = z6.string().regex(/^[a-z][a-z0-9_-]{0,63}$/u).nullable();
6256
- var usageBillingCertaintySchema = z6.enum(["unknown", "reported", "contractual", "conservative"]);
6257
- var usageOutcomeSchema = z6.string().regex(/^[a-z][a-z0-9_]{0,63}$/u);
6258
- var usageErrorCodeSchema = z6.string().regex(/^[A-Z0-9][A-Z0-9_:.-]{2,159}$/u).nullable();
6259
- var usageCostCertaintySchema = z6.enum(["reported", "contractual", "estimated", "unknown", "reconciled"]);
6260
- var usageExecutionRouteSchema = z6.string().regex(/^[a-z][a-z0-9_]{0,63}$/u);
6261
- var usageExecutionProfileSha256Schema = z6.string().regex(/^[0-9a-f]{64}$/u).nullable();
6262
- var boundedUsageCount = z6.number().int().min(0).max(Number.MAX_SAFE_INTEGER);
6263
- var cliEnrichmentUsageCoverageResponse = z6.object({
6264
- ledgerRuns: z6.number().int().min(0).max(100),
6265
- legacyRuns: z6.number().int().min(0).max(100)
7602
+ var usageBillingUnitSchema = z8.string().regex(/^[a-z][a-z0-9_-]{0,63}$/u).nullable();
7603
+ var usageBillingCertaintySchema = z8.enum(["unknown", "reported", "contractual", "conservative"]);
7604
+ var usageOutcomeSchema = z8.string().regex(/^[a-z][a-z0-9_]{0,63}$/u);
7605
+ var usageErrorCodeSchema = z8.string().regex(/^[A-Z0-9][A-Z0-9_:.-]{2,159}$/u).nullable();
7606
+ var usageCostCertaintySchema = z8.enum(["reported", "contractual", "estimated", "unknown", "reconciled"]);
7607
+ var usageExecutionRouteSchema = z8.string().regex(/^[a-z][a-z0-9_]{0,63}$/u);
7608
+ var usageExecutionProfileSha256Schema = z8.string().regex(/^[0-9a-f]{64}$/u).nullable();
7609
+ var boundedUsageCount = z8.number().int().min(0).max(Number.MAX_SAFE_INTEGER);
7610
+ var cliEnrichmentUsageCoverageResponse = z8.object({
7611
+ ledgerRuns: z8.number().int().min(0).max(100),
7612
+ legacyRuns: z8.number().int().min(0).max(100)
6266
7613
  }).strict();
6267
- var cliEnrichmentProviderUsageResponse = z6.object({
7614
+ var cliEnrichmentProviderUsageResponse = z8.object({
6268
7615
  operation: usageOperationSchema,
6269
7616
  executionRoute: usageExecutionRouteSchema,
6270
7617
  executionProfileSha256: usageExecutionProfileSha256Schema,
@@ -6282,57 +7629,57 @@ var cliEnrichmentProviderUsageResponse = z6.object({
6282
7629
  inputTokens: boundedUsageCount,
6283
7630
  outputTokens: boundedUsageCount,
6284
7631
  billingUnit: usageBillingUnitSchema,
6285
- billedUnits: z6.number().min(0).max(Number.MAX_SAFE_INTEGER),
7632
+ billedUnits: z8.number().min(0).max(Number.MAX_SAFE_INTEGER),
6286
7633
  billingCertainty: usageBillingCertaintySchema,
6287
- billingUnknown: z6.boolean(),
6288
- knownCostUsd: z6.number().min(0).max(Number.MAX_SAFE_INTEGER),
7634
+ billingUnknown: z8.boolean(),
7635
+ knownCostUsd: z8.number().min(0).max(Number.MAX_SAFE_INTEGER),
6289
7636
  costCertainty: usageCostCertaintySchema,
6290
7637
  unknownCostAttempts: boundedUsageCount
6291
7638
  }).strict();
6292
- var cliEnrichmentAggregateFieldSourceResponse = z6.object({
7639
+ var cliEnrichmentAggregateFieldSourceResponse = z8.object({
6293
7640
  field: enrichmentFieldSchema,
6294
7641
  operation: usageOperationSchema,
6295
7642
  provider: usageProviderSchema,
6296
- contacts: z6.number().int().min(0).max(100)
7643
+ contacts: z8.number().int().min(0).max(100)
6297
7644
  }).strict();
6298
- var cliEnrichmentFailureResponse = z6.object({
7645
+ var cliEnrichmentFailureResponse = z8.object({
6299
7646
  operation: usageOperationSchema,
6300
- count: z6.number().int().min(1).max(100)
7647
+ count: z8.number().int().min(1).max(100)
6301
7648
  }).strict();
6302
- var cliEnrichmentJobStatusV4Response = z6.object({
6303
- id: z6.uuid(),
6304
- status: z6.enum(["queued", "running", "complete", "partial", "failed"]),
6305
- dispatchStatus: z6.enum(["dispatching", "launched", "indeterminate"]),
6306
- selectedCount: z6.number().int().min(1).max(100),
6307
- completedCount: z6.number().int().min(0).max(100),
6308
- failedCount: z6.number().int().min(0).max(100),
6309
- productCreditsSpent: z6.number().int().min(0).max(100),
7649
+ var cliEnrichmentJobStatusV4Response = z8.object({
7650
+ id: z8.uuid(),
7651
+ status: z8.enum(["queued", "running", "complete", "partial", "failed"]),
7652
+ dispatchStatus: z8.enum(["dispatching", "launched", "indeterminate"]),
7653
+ selectedCount: z8.number().int().min(1).max(100),
7654
+ completedCount: z8.number().int().min(0).max(100),
7655
+ failedCount: z8.number().int().min(0).max(100),
7656
+ productCreditsSpent: z8.number().int().min(0).max(100),
6310
7657
  usageCoverage: cliEnrichmentUsageCoverageResponse,
6311
- providerUsage: z6.array(cliEnrichmentProviderUsageResponse).max(256),
6312
- fieldSources: z6.array(cliEnrichmentAggregateFieldSourceResponse).max(600),
6313
- failures: z6.array(cliEnrichmentFailureResponse).max(9),
6314
- createdAt: z6.iso.datetime({ offset: true }),
6315
- completedAt: z6.iso.datetime({ offset: true }).nullable()
7658
+ providerUsage: z8.array(cliEnrichmentProviderUsageResponse).max(256),
7659
+ fieldSources: z8.array(cliEnrichmentAggregateFieldSourceResponse).max(600),
7660
+ failures: z8.array(cliEnrichmentFailureResponse).max(9),
7661
+ createdAt: z8.iso.datetime({ offset: true }),
7662
+ completedAt: z8.iso.datetime({ offset: true }).nullable()
6316
7663
  }).strict();
6317
- var cliEnrichmentRunDetailResponse = z6.object({
6318
- localPersonId: z6.string().regex(/^[1-9][0-9]{0,18}$/u),
6319
- status: z6.enum(["pending", "running", "complete", "failed"]),
6320
- errorCode: z6.string().min(1).max(200).nullable(),
7664
+ var cliEnrichmentRunDetailResponse = z8.object({
7665
+ localPersonId: z8.string().regex(/^[1-9][0-9]{0,18}$/u),
7666
+ status: z8.enum(["pending", "running", "complete", "failed"]),
7667
+ errorCode: z8.string().min(1).max(200).nullable(),
6321
7668
  failureOperation: usageOperationSchema.nullable(),
6322
- identityMatch: z6.enum(["confirmed", "possible", "insufficient"]).nullable(),
6323
- confidence: z6.number().int().min(0).max(100).nullable(),
6324
- fieldsPresent: z6.array(enrichmentFieldSchema).max(6),
6325
- claimFields: z6.array(enrichmentFieldSchema).max(6)
7669
+ identityMatch: z8.enum(["confirmed", "possible", "insufficient"]).nullable(),
7670
+ confidence: z8.number().int().min(0).max(100).nullable(),
7671
+ fieldsPresent: z8.array(enrichmentFieldSchema).max(6),
7672
+ claimFields: z8.array(enrichmentFieldSchema).max(6)
6326
7673
  }).strict();
6327
- var cliEnrichmentRunUsageResponse = z6.object({
7674
+ var cliEnrichmentRunUsageResponse = z8.object({
6328
7675
  operation: usageOperationSchema,
6329
7676
  executionRoute: usageExecutionRouteSchema,
6330
7677
  executionProfileSha256: usageExecutionProfileSha256Schema,
6331
- attempt: z6.number().int().min(1).max(16),
7678
+ attempt: z8.number().int().min(1).max(16),
6332
7679
  billingSurface: usageBillingSurfaceSchema,
6333
7680
  provider: usageProviderSchema,
6334
7681
  model: usageModelSchema,
6335
- status: z6.enum(["open", "succeeded", "failed", "indeterminate"]),
7682
+ status: z8.enum(["open", "succeeded", "failed", "indeterminate"]),
6336
7683
  outcome: usageOutcomeSchema,
6337
7684
  errorCode: usageErrorCodeSchema,
6338
7685
  results: boundedUsageCount,
@@ -6340,10 +7687,10 @@ var cliEnrichmentRunUsageResponse = z6.object({
6340
7687
  inputTokens: boundedUsageCount,
6341
7688
  outputTokens: boundedUsageCount,
6342
7689
  billingUnit: usageBillingUnitSchema,
6343
- billedUnits: z6.number().min(0).max(Number.MAX_SAFE_INTEGER),
7690
+ billedUnits: z8.number().min(0).max(Number.MAX_SAFE_INTEGER),
6344
7691
  billingCertainty: usageBillingCertaintySchema,
6345
- billingUnknown: z6.boolean(),
6346
- costUsd: z6.number().min(0).max(Number.MAX_SAFE_INTEGER).nullable(),
7692
+ billingUnknown: z8.boolean(),
7693
+ costUsd: z8.number().min(0).max(Number.MAX_SAFE_INTEGER).nullable(),
6347
7694
  costCertainty: usageCostCertaintySchema
6348
7695
  }).strict().superRefine((value, context) => {
6349
7696
  if ((value.status === "open" || value.status === "succeeded") && value.errorCode !== null) {
@@ -6354,22 +7701,31 @@ var cliEnrichmentRunUsageResponse = z6.object({
6354
7701
  });
6355
7702
  }
6356
7703
  });
6357
- var cliEnrichmentRunFieldSourceResponse = z6.object({
7704
+ var cliEnrichmentRunFieldSourceResponse = z8.object({
6358
7705
  field: enrichmentFieldSchema,
6359
- sources: z6.array(z6.object({
7706
+ sources: z8.array(z8.object({
6360
7707
  operation: usageOperationSchema,
6361
7708
  provider: usageProviderSchema,
6362
- evidenceRows: z6.number().int().min(1).max(5)
7709
+ evidenceRows: z8.number().int().min(1).max(5)
6363
7710
  }).strict()).max(5)
6364
7711
  }).strict();
6365
- var cliEnrichmentJobDetailsV4Response = z6.object({
6366
- jobId: z6.uuid(),
6367
- runs: z6.array(cliEnrichmentRunDetailResponse.extend({
6368
- usageCoverage: z6.enum(["ledger", "legacy"]),
6369
- usage: z6.array(cliEnrichmentRunUsageResponse).max(512),
6370
- fieldSources: z6.array(cliEnrichmentRunFieldSourceResponse).max(6)
7712
+ var cliEnrichmentJobDetailsV4Response = z8.object({
7713
+ jobId: z8.uuid(),
7714
+ runs: z8.array(cliEnrichmentRunDetailResponse.extend({
7715
+ usageCoverage: z8.enum(["ledger", "legacy"]),
7716
+ usage: z8.array(cliEnrichmentRunUsageResponse).max(512),
7717
+ fieldSources: z8.array(cliEnrichmentRunFieldSourceResponse).max(6)
6371
7718
  }).strict()).max(100)
6372
7719
  }).strict();
7720
+ var cliEnrichmentPublicEmailLeftoverResponse = z8.object({
7721
+ localPersonId: z8.string().regex(/^[1-9][0-9]{0,18}$/u),
7722
+ runId: z8.uuid(),
7723
+ publicEmail: z8.email().max(1024),
7724
+ completedAt: z8.iso.datetime({ offset: true })
7725
+ }).strict();
7726
+ var cliEnrichmentPublicEmailsResponse = z8.object({
7727
+ publicEmails: z8.array(cliEnrichmentPublicEmailLeftoverResponse).max(1e4)
7728
+ }).strict();
6373
7729
  function baseUrl(value) {
6374
7730
  const parsed = new URL(value);
6375
7731
  if (parsed.protocol !== "https:" && !(parsed.protocol === "http:" && ["localhost", "127.0.0.1"].includes(parsed.hostname))) {
@@ -6393,7 +7749,7 @@ async function openBrowser(url) {
6393
7749
  async function signInCloud(origin = "https://peopleblade.com", options = {}) {
6394
7750
  const fetcher = options.fetcher ?? fetch;
6395
7751
  const root = baseUrl(origin);
6396
- const rawToken = randomBytes(32).toString("base64url");
7752
+ const rawToken = randomBytes2(32).toString("base64url");
6397
7753
  const tokenSha256 = createHash2("sha256").update(rawToken).digest("hex");
6398
7754
  const started = deviceStartResponse.parse(await postJson(fetcher, `${root}/api/cli/device/start`, { deviceName: defaultDeviceName(), tokenSha256 }));
6399
7755
  options.onCode?.(started.userCode, started.verificationUri);
@@ -6464,7 +7820,7 @@ function selectedLocalContacts(database, personIds, config) {
6464
7820
  WHERE member.canonical_person_id IN (${placeholders}) AND person.do_not_contact = 1`).get(...unique);
6465
7821
  if (doNotContact.count > 0)
6466
7822
  throw new Error("Selection contains a do-not-contact identity.");
6467
- const contacts = projectCloudContacts(database);
7823
+ const contacts = projectCloudContacts(database, { personIds: unique });
6468
7824
  const byId = new Map(contacts.map((contact) => [Number(contact.localPersonId), contact]));
6469
7825
  const selected = unique.map((personId) => byId.get(personId));
6470
7826
  if (selected.some((contact) => contact === undefined)) {
@@ -6553,6 +7909,11 @@ async function cloudEnrichmentDetails(jobId, options = {}) {
6553
7909
  const raw = await postJson(options.fetcher ?? fetch, `${config.baseUrl}/api/cli/enrich/details?v=${cliEnrichmentReadContractVersion}`, input, config.token);
6554
7910
  return cliEnrichmentJobDetailsV4Response.parse(raw);
6555
7911
  }
7912
+ async function cloudEnrichmentPublicEmails(options = {}) {
7913
+ const config = cloudConfiguration(options.configuration);
7914
+ const raw = await postJson(options.fetcher ?? fetch, `${config.baseUrl}/api/cli/enrich/public-emails?v=${cliEnrichmentReadContractVersion}`, {}, config.token);
7915
+ return cliEnrichmentPublicEmailsResponse.parse(raw);
7916
+ }
6556
7917
  var cloudEnrichmentRevalidationBatchLimit = 10;
6557
7918
  var maximumCloudEnrichmentRevalidationBatches = 1000;
6558
7919
  function addBoundedRevalidationCount(left, right) {
@@ -6601,6 +7962,20 @@ async function revalidateHistoricalCloudEnrichment(options = {}) {
6601
7962
  }
6602
7963
  throw new Error("PeopleBlade historical enrichment revalidation exceeded its batch limit.");
6603
7964
  }
7965
+
7966
+ class CloudEnrichmentReadError extends Error {
7967
+ jobId;
7968
+ dispatchStatus;
7969
+ nextCommand;
7970
+ constructor(jobId, dispatchStatus, cause) {
7971
+ const nextCommand = `peopleblade cloud enrich --status ${jobId} --json`;
7972
+ super(`Enrichment dispatch was accepted as ${jobId}, but status could not be read. Do not repeat dispatch; run ${nextCommand}.`, { cause });
7973
+ this.jobId = jobId;
7974
+ this.dispatchStatus = dispatchStatus;
7975
+ this.name = "CloudEnrichmentReadError";
7976
+ this.nextCommand = nextCommand;
7977
+ }
7978
+ }
6604
7979
  async function startCloudEnrichment(database, confirmation, personIds, options = {}) {
6605
7980
  const match = /^([0-9a-f-]{36})\.([A-Za-z0-9_-]{43})$/u.exec(confirmation);
6606
7981
  if (match === null)
@@ -6617,7 +7992,14 @@ async function startCloudEnrichment(database, confirmation, personIds, options =
6617
7992
  enrichmentInputSha256: contact.enrichmentInputSha256
6618
7993
  }))
6619
7994
  }, config.token));
6620
- let status = await cloudEnrichmentStatus(dispatch.jobId, { fetcher, configuration: config });
7995
+ const readStatus = async () => {
7996
+ try {
7997
+ return await cloudEnrichmentStatus(dispatch.jobId, { fetcher, configuration: config });
7998
+ } catch (cause) {
7999
+ throw new CloudEnrichmentReadError(dispatch.jobId, dispatch.dispatchStatus, cause);
8000
+ }
8001
+ };
8002
+ let status = await readStatus();
6621
8003
  if (options.wait === false || dispatch.dispatchStatus !== "launched")
6622
8004
  return { dispatch, status };
6623
8005
  const pollIntervalMs = Math.max(250, Math.min(30000, options.pollIntervalMs ?? 2000));
@@ -6626,16 +8008,16 @@ async function startCloudEnrichment(database, confirmation, personIds, options =
6626
8008
  if (["complete", "partial", "failed"].includes(status.status))
6627
8009
  return { dispatch, status };
6628
8010
  await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
6629
- status = await cloudEnrichmentStatus(dispatch.jobId, { fetcher, configuration: config });
8011
+ status = await readStatus();
6630
8012
  }
6631
8013
  throw new Error(`Enrichment job ${dispatch.jobId} is still running; inspect it with cloud enrich --status.`);
6632
8014
  }
6633
8015
 
6634
8016
  // src/local/database.ts
6635
8017
  import { Database } from "bun:sqlite";
6636
- import crypto from "crypto";
8018
+ import crypto2 from "crypto";
6637
8019
  import {
6638
- chmodSync,
8020
+ chmodSync as chmodSync2,
6639
8021
  closeSync as closeSync4,
6640
8022
  constants as constants4,
6641
8023
  existsSync as existsSync2,
@@ -6643,17 +8025,17 @@ import {
6643
8025
  fstatSync as fstatSync3,
6644
8026
  fsyncSync,
6645
8027
  linkSync,
6646
- lstatSync,
8028
+ lstatSync as lstatSync2,
6647
8029
  mkdirSync as mkdirSync2,
6648
8030
  openSync as openSync4,
6649
8031
  readFileSync as readFileSync4,
6650
8032
  realpathSync,
6651
8033
  readdirSync,
6652
8034
  statSync,
6653
- unlinkSync,
6654
- writeFileSync as writeFileSync2
8035
+ unlinkSync as unlinkSync2,
8036
+ writeFileSync as writeFileSync3
6655
8037
  } from "fs";
6656
- import { basename, dirname as dirname2, join as join2, resolve } from "path";
8038
+ import { basename, dirname as dirname2, join as join3, resolve } from "path";
6657
8039
  import { fileURLToPath } from "url";
6658
8040
 
6659
8041
  // src/local/phone-identity-migration.ts
@@ -7150,7 +8532,7 @@ function convergeLegacyProviderAccounts(database) {
7150
8532
  }
7151
8533
 
7152
8534
  // src/local/database.ts
7153
- var migrationsDirectory = join2(dirname2(fileURLToPath(import.meta.url)), "migrations");
8535
+ var migrationsDirectory = join3(dirname2(fileURLToPath(import.meta.url)), "migrations");
7154
8536
  function migrationNames() {
7155
8537
  return readdirSync(migrationsDirectory).filter((name) => name.endsWith(".sql")).sort();
7156
8538
  }
@@ -7160,18 +8542,18 @@ function secureDirectory(path) {
7160
8542
  const standardRoot = resolve(peoplebladeDirectory());
7161
8543
  const physical = resolve(path);
7162
8544
  if (!existed || physical === standardRoot || physical.startsWith(`${standardRoot}/`))
7163
- chmodSync(path, 448);
8545
+ chmodSync2(path, 448);
7164
8546
  }
7165
8547
  function assertSecureBackupDirectory(path) {
7166
8548
  const existed = existsSync2(path);
7167
8549
  if (existed)
7168
- assertSecureBackupDirectoryIdentity(lstatSync(path), true);
8550
+ assertSecureBackupDirectoryIdentity(lstatSync2(path), true);
7169
8551
  secureDirectory(path);
7170
8552
  const physical = realpathSync(path);
7171
- assertSecureBackupDirectoryIdentity(lstatSync(physical), true);
8553
+ assertSecureBackupDirectoryIdentity(lstatSync2(physical), true);
7172
8554
  let ancestor = dirname2(physical);
7173
8555
  while (true) {
7174
- const identity = lstatSync(ancestor);
8556
+ const identity = lstatSync2(ancestor);
7175
8557
  assertSecureBackupDirectoryIdentity(identity, false);
7176
8558
  const parent = dirname2(ancestor);
7177
8559
  if (parent === ancestor)
@@ -7209,12 +8591,12 @@ function assertSecureFileIdentity(identity) {
7209
8591
  }
7210
8592
  }
7211
8593
  function secureFile(path) {
7212
- const identity = lstatSync(path);
8594
+ const identity = lstatSync2(path);
7213
8595
  assertSecureFileIdentity(identity);
7214
- chmodSync(path, 384);
8596
+ chmodSync2(path, 384);
7215
8597
  }
7216
8598
  function assertSameTemporaryFile(path, expected) {
7217
- const current = lstatSync(path);
8599
+ const current = lstatSync2(path);
7218
8600
  assertSecureFileIdentity(current);
7219
8601
  if (current.dev !== expected.dev || current.ino !== expected.ino) {
7220
8602
  throw new Error("PeopleBlade backup temporary file was replaced.");
@@ -7225,7 +8607,7 @@ function temporarySnapshotSidecars(path) {
7225
8607
  }
7226
8608
  function pathEntryExists(path) {
7227
8609
  try {
7228
- lstatSync(path);
8610
+ lstatSync2(path);
7229
8611
  return true;
7230
8612
  } catch (error) {
7231
8613
  if (error.code === "ENOENT")
@@ -7235,7 +8617,7 @@ function pathEntryExists(path) {
7235
8617
  }
7236
8618
  function unlinkIfPresent(path) {
7237
8619
  try {
7238
- unlinkSync(path);
8620
+ unlinkSync2(path);
7239
8621
  } catch (error) {
7240
8622
  if (error.code !== "ENOENT")
7241
8623
  throw error;
@@ -7244,7 +8626,7 @@ function unlinkIfPresent(path) {
7244
8626
  function unlinkTemporarySidecarIfPresent(path) {
7245
8627
  let identity;
7246
8628
  try {
7247
- identity = lstatSync(path);
8629
+ identity = lstatSync2(path);
7248
8630
  } catch (error) {
7249
8631
  if (error.code === "ENOENT")
7250
8632
  return;
@@ -7255,7 +8637,7 @@ function unlinkTemporarySidecarIfPresent(path) {
7255
8637
  } catch {
7256
8638
  throw new Error("PeopleBlade backup temporary SQLite sidecar was not a safe owned file.");
7257
8639
  }
7258
- unlinkSync(path);
8640
+ unlinkSync2(path);
7259
8641
  }
7260
8642
  function assertNoTemporarySidecars(path) {
7261
8643
  for (const sidecar of temporarySnapshotSidecars(path)) {
@@ -7321,7 +8703,7 @@ function migrateLocalDatabase(database) {
7321
8703
  continue;
7322
8704
  database.exec("BEGIN IMMEDIATE");
7323
8705
  try {
7324
- database.exec(readFileSync4(join2(migrationsDirectory, filename), "utf8"));
8706
+ database.exec(readFileSync4(join3(migrationsDirectory, filename), "utf8"));
7325
8707
  database.query("INSERT INTO schema_migrations(version) VALUES (?)").run(filename);
7326
8708
  database.exec("COMMIT");
7327
8709
  } catch (error) {
@@ -7378,7 +8760,7 @@ function backupLocalDatabase(sourcePath, destinationPath) {
7378
8760
  if (source === requestedDestination)
7379
8761
  throw new Error("Backup destination cannot be the source database.");
7380
8762
  const destinationDirectory = assertSecureBackupDirectory(dirname2(requestedDestination));
7381
- const destination = join2(destinationDirectory, basename(requestedDestination));
8763
+ const destination = join3(destinationDirectory, basename(requestedDestination));
7382
8764
  if (existsSync2(destination))
7383
8765
  throw new Error(`Backup already exists: ${destination}`);
7384
8766
  if (source === destination)
@@ -7397,14 +8779,14 @@ function backupLocalDatabase(sourcePath, destinationPath) {
7397
8779
  } finally {
7398
8780
  database.close();
7399
8781
  }
7400
- const temporary = join2(destinationDirectory, `.peopleblade-backup-${process.pid}-${crypto.randomBytes(8).toString("hex")}`);
8782
+ const temporary = join3(destinationDirectory, `.peopleblade-backup-${process.pid}-${crypto2.randomBytes(8).toString("hex")}`);
7401
8783
  const descriptor = openSync4(temporary, constants4.O_WRONLY | constants4.O_CREAT | constants4.O_EXCL | (constants4.O_NOFOLLOW ?? 0), 384);
7402
8784
  try {
7403
8785
  try {
7404
8786
  fchmodSync(descriptor, 384);
7405
8787
  const identity = fstatSync3(descriptor);
7406
8788
  assertNoTemporarySidecars(temporary);
7407
- writeFileSync2(descriptor, bytes);
8789
+ writeFileSync3(descriptor, bytes);
7408
8790
  fsyncSync(descriptor);
7409
8791
  normalizeAndValidateSnapshot(temporary, identity);
7410
8792
  fsyncSync(descriptor);
@@ -7412,7 +8794,7 @@ function backupLocalDatabase(sourcePath, destinationPath) {
7412
8794
  closeSync4(descriptor);
7413
8795
  }
7414
8796
  linkSync(temporary, destination);
7415
- unlinkSync(temporary);
8797
+ unlinkSync2(temporary);
7416
8798
  } catch (error) {
7417
8799
  unlinkIfPresent(temporary);
7418
8800
  for (const sidecar of temporarySnapshotSidecars(temporary))
@@ -7431,11 +8813,11 @@ function standardBackupPath(label, databasePath = peoplebladeDatabasePath()) {
7431
8813
  if (!/^[a-z0-9][a-z0-9-]{0,63}$/u.test(label))
7432
8814
  throw new Error("Backup label is invalid.");
7433
8815
  const timestamp2 = new Date().toISOString().replaceAll(/[-:.]/gu, "").replace("Z", "Z");
7434
- return join2(dirname2(databasePath), "backups", `${label}-${timestamp2}.sqlite3`);
8816
+ return join3(dirname2(databasePath), "backups", `${label}-${timestamp2}.sqlite3`);
7435
8817
  }
7436
8818
 
7437
8819
  // src/local/ensoul.ts
7438
- import { randomBytes as randomBytes2 } from "crypto";
8820
+ import { randomBytes as randomBytes3 } from "crypto";
7439
8821
  import {
7440
8822
  closeSync as closeSync5,
7441
8823
  constants as constants5,
@@ -7443,25 +8825,25 @@ import {
7443
8825
  fstatSync as fstatSync4,
7444
8826
  fsyncSync as fsyncSync2,
7445
8827
  linkSync as linkSync2,
7446
- lstatSync as lstatSync2,
8828
+ lstatSync as lstatSync3,
7447
8829
  openSync as openSync5,
7448
8830
  realpathSync as realpathSync2,
7449
- unlinkSync as unlinkSync2,
7450
- writeFileSync as writeFileSync3
8831
+ unlinkSync as unlinkSync3,
8832
+ writeFileSync as writeFileSync4
7451
8833
  } from "fs";
7452
- import { basename as basename2, dirname as dirname3, join as join3, resolve as resolve2 } from "path";
8834
+ import { basename as basename2, dirname as dirname3, join as join4, resolve as resolve2 } from "path";
7453
8835
 
7454
8836
  // src/lib/ensoul-contracts.ts
7455
- import { z as z7 } from "zod";
8837
+ import { z as z9 } from "zod";
7456
8838
  var ensoulSourcePacketVersion = "ensoul.source-packet.v1";
7457
8839
  var peoplebladeEnsoulAdapter = "peopleblade";
7458
8840
  var peoplebladeEnsoulPayloadSchema = "ensoul.public-enrichment-source.v1";
7459
8841
  var ensoulDigestCanonicalization = "JCS-RFC8785";
7460
- var digestSchema = z7.string().regex(/^[a-f0-9]{64}$/u);
7461
- var prefixedDigestSchema = z7.string().regex(/^sha256:[a-f0-9]{64}$/u);
7462
- var timestampSchema = z7.iso.datetime({ offset: true });
7463
- var boundedText2 = (maximum) => z7.string().trim().min(1).max(maximum);
7464
- var positiveSafeInteger = z7.number().int().positive().max(Number.MAX_SAFE_INTEGER);
8842
+ var digestSchema = z9.string().regex(/^[a-f0-9]{64}$/u);
8843
+ var prefixedDigestSchema = z9.string().regex(/^sha256:[a-f0-9]{64}$/u);
8844
+ var timestampSchema = z9.iso.datetime({ offset: true });
8845
+ var boundedText2 = (maximum) => z9.string().trim().min(1).max(maximum);
8846
+ var positiveSafeInteger = z9.number().int().positive().max(Number.MAX_SAFE_INTEGER);
7465
8847
  function assertWellFormedUnicode(value) {
7466
8848
  for (let index = 0;index < value.length; index += 1) {
7467
8849
  const code = value.charCodeAt(index);
@@ -7503,7 +8885,7 @@ function ensoulJcsCanonicalJson(value) {
7503
8885
  }
7504
8886
  throw new Error("JCS input is not valid JSON.");
7505
8887
  }
7506
- var ensoulSourceClassSchema = z7.enum([
8888
+ var ensoulSourceClassSchema = z9.enum([
7507
8889
  "private_capture",
7508
8890
  "polished_self_presentation",
7509
8891
  "observed_behavior",
@@ -7512,24 +8894,24 @@ var ensoulSourceClassSchema = z7.enum([
7512
8894
  "metadata",
7513
8895
  "public_web_evidence"
7514
8896
  ]);
7515
- var ensoulSourceRecordSemanticSchema = z7.object({
8897
+ var ensoulSourceRecordSemanticSchema = z9.object({
7516
8898
  id: boundedText2(200),
7517
8899
  kind: boundedText2(100),
7518
8900
  occurredAt: timestampSchema.optional(),
7519
8901
  observedAt: timestampSchema.optional(),
7520
- authorRole: z7.enum(["subject", "counterpart", "third_party", "mixed", "unknown"]),
7521
- contentRole: z7.enum(["original", "quoted", "forwarded", "summary", "ai_assisted", "mixed", "unknown"]),
7522
- authorshipConfidence: z7.enum(["verified", "strong", "weak", "unknown"]),
7523
- sentStatus: z7.enum(["sent", "draft", "received", "published", "unknown"]),
7524
- visibility: z7.enum(["public", "private"]),
8902
+ authorRole: z9.enum(["subject", "counterpart", "third_party", "mixed", "unknown"]),
8903
+ contentRole: z9.enum(["original", "quoted", "forwarded", "summary", "ai_assisted", "mixed", "unknown"]),
8904
+ authorshipConfidence: z9.enum(["verified", "strong", "weak", "unknown"]),
8905
+ sentStatus: z9.enum(["sent", "draft", "received", "published", "unknown"]),
8906
+ visibility: z9.enum(["public", "private"]),
7525
8907
  sourceClass: ensoulSourceClassSchema,
7526
- content: z7.object({
7527
- text: z7.string().max(50000).optional(),
7528
- title: z7.string().max(1000).optional(),
8908
+ content: z9.object({
8909
+ text: z9.string().max(50000).optional(),
8910
+ title: z9.string().max(1000).optional(),
7529
8911
  url: httpUrlSchema.optional(),
7530
- truncated: z7.boolean().optional()
8912
+ truncated: z9.boolean().optional()
7531
8913
  }).strict().refine((content) => content.text !== undefined || content.title !== undefined || content.url !== undefined, "Record content requires text, title, or URL"),
7532
- provenance: z7.object({
8914
+ provenance: z9.object({
7533
8915
  provider: boundedText2(100),
7534
8916
  operation: boundedText2(160).optional(),
7535
8917
  sourceId: boundedText2(300).optional(),
@@ -7542,17 +8924,17 @@ var ensoulSourceRecordSemanticSchema = z7.object({
7542
8924
  var ensoulSourceRecordSchema = ensoulSourceRecordSemanticSchema.extend({
7543
8925
  digest: prefixedDigestSchema
7544
8926
  }).strict();
7545
- var ensoulClaimSchema = z7.object({
8927
+ var ensoulClaimSchema = z9.object({
7546
8928
  id: boundedText2(200),
7547
8929
  text: boundedText2(4000),
7548
- recordIds: z7.array(boundedText2(200)).min(1).max(50).refine((values) => new Set(values).size === values.length, "Claim record IDs must be unique"),
7549
- status: z7.enum(["source_reported", "adapter_structured", "contested"]),
7550
- claimantRole: z7.enum(["subject", "counterpart", "third_party", "institutional", "adapter", "unknown"]),
7551
- claimKind: z7.enum(["fact", "stated_belief", "reported_observation", "derived_index"]),
8930
+ recordIds: z9.array(boundedText2(200)).min(1).max(50).refine((values) => new Set(values).size === values.length, "Claim record IDs must be unique"),
8931
+ status: z9.enum(["source_reported", "adapter_structured", "contested"]),
8932
+ claimantRole: z9.enum(["subject", "counterpart", "third_party", "institutional", "adapter", "unknown"]),
8933
+ claimKind: z9.enum(["fact", "stated_belief", "reported_observation", "derived_index"]),
7552
8934
  subjectLocalId: boundedText2(200),
7553
- sensitivity: z7.enum(["ordinary", "sensitive_explicit"])
8935
+ sensitivity: z9.enum(["ordinary", "sensitive_explicit"])
7554
8936
  }).strict();
7555
- var ensoulLimitationSchema = z7.enum([
8937
+ var ensoulLimitationSchema = z9.enum([
7556
8938
  "Public-web evidence plus bounded local subject background only; this packet is not a complete account of the person.",
7557
8939
  "Web authorship is unknown; no record is a direct subject voice sample.",
7558
8940
  "Private messages and CRM notes are excluded.",
@@ -7560,41 +8942,41 @@ var ensoulLimitationSchema = z7.enum([
7560
8942
  "Raw provider payloads, credentials, and unrelated contacts are excluded.",
7561
8943
  "Do not infer sensitive traits, diagnoses, consent, or permission to act from this packet."
7562
8944
  ]);
7563
- var peoplebladeEnsoulPacketBodySchema = z7.object({
7564
- schemaVersion: z7.literal(ensoulSourcePacketVersion),
7565
- digestCanonicalization: z7.literal(ensoulDigestCanonicalization),
8945
+ var peoplebladeEnsoulPacketBodySchema = z9.object({
8946
+ schemaVersion: z9.literal(ensoulSourcePacketVersion),
8947
+ digestCanonicalization: z9.literal(ensoulDigestCanonicalization),
7566
8948
  generatedAt: timestampSchema,
7567
- subject: z7.object({
8949
+ subject: z9.object({
7568
8950
  localId: boundedText2(200),
7569
- kind: z7.literal("person"),
8951
+ kind: z9.literal("person"),
7570
8952
  displayName: boundedText2(300).optional(),
7571
8953
  identityBasis: boundedText2(1000)
7572
8954
  }).strict(),
7573
- scope: z7.object({
7574
- adapter: z7.literal(peoplebladeEnsoulAdapter),
7575
- payloadSchema: z7.literal(peoplebladeEnsoulPayloadSchema),
8955
+ scope: z9.object({
8956
+ adapter: z9.literal(peoplebladeEnsoulAdapter),
8957
+ payloadSchema: z9.literal(peoplebladeEnsoulPayloadSchema),
7576
8958
  asOf: timestampSchema,
7577
8959
  sourceCutoff: timestampSchema.optional(),
7578
- completeness: z7.enum(["complete", "sampled", "bounded", "unknown"]),
8960
+ completeness: z9.enum(["complete", "sampled", "bounded", "unknown"]),
7579
8961
  sourceRevision: boundedText2(300),
7580
- limits: z7.object({
8962
+ limits: z9.object({
7581
8963
  maxCurrentEnrichments: positiveSafeInteger,
7582
8964
  maxRecords: positiveSafeInteger,
7583
8965
  maxClaims: positiveSafeInteger,
7584
8966
  maxPacketBytes: positiveSafeInteger,
7585
8967
  oldestObservedAt: timestampSchema.nullable(),
7586
- exactContactCoordinates: z7.literal("excluded-or-redacted"),
7587
- notes: z7.literal("excluded"),
7588
- messages: z7.literal("excluded"),
7589
- rawProviderPayloads: z7.literal("excluded")
8968
+ exactContactCoordinates: z9.literal("excluded-or-redacted"),
8969
+ notes: z9.literal("excluded"),
8970
+ messages: z9.literal("excluded"),
8971
+ rawProviderPayloads: z9.literal("excluded")
7590
8972
  }).strict()
7591
8973
  }).strict(),
7592
- records: z7.array(ensoulSourceRecordSchema).max(2000),
7593
- claims: z7.array(ensoulClaimSchema).max(500),
7594
- limitations: z7.array(ensoulLimitationSchema).min(1).max(32)
8974
+ records: z9.array(ensoulSourceRecordSchema).max(2000),
8975
+ claims: z9.array(ensoulClaimSchema).max(500),
8976
+ limitations: z9.array(ensoulLimitationSchema).min(1).max(32)
7595
8977
  }).strict();
7596
8978
  var peoplebladeEnsoulPacketSemanticSchema = peoplebladeEnsoulPacketBodySchema.extend({
7597
- packetId: z7.string().trim().min(8).max(160)
8979
+ packetId: z9.string().trim().min(8).max(160)
7598
8980
  }).strict();
7599
8981
  var ensoulSourcePacketSchema = peoplebladeEnsoulPacketSemanticSchema.extend({
7600
8982
  packetDigest: prefixedDigestSchema
@@ -8042,7 +9424,7 @@ function buildPeoplebladeEnsoulSourcePacket(database, requestedPersonId, options
8042
9424
  }
8043
9425
  function pathEntryExists2(path) {
8044
9426
  try {
8045
- lstatSync2(path);
9427
+ lstatSync3(path);
8046
9428
  return true;
8047
9429
  } catch (error) {
8048
9430
  if (error.code === "ENOENT")
@@ -8051,7 +9433,7 @@ function pathEntryExists2(path) {
8051
9433
  }
8052
9434
  }
8053
9435
  function assertPrivateOutputDirectory(path) {
8054
- const identity = lstatSync2(path);
9436
+ const identity = lstatSync3(path);
8055
9437
  const uid = typeof process.getuid === "function" ? process.getuid() : null;
8056
9438
  if (!identity.isDirectory() || identity.isSymbolicLink() || uid === null || identity.uid !== uid || (identity.mode & 18) !== 0)
8057
9439
  throw new Error("Ensoul output directory must be a private, owned directory and not a symlink.");
@@ -8061,9 +9443,9 @@ function unlinkExpectedInode(path, expected) {
8061
9443
  if (expected === null)
8062
9444
  return;
8063
9445
  try {
8064
- const current = lstatSync2(path);
9446
+ const current = lstatSync3(path);
8065
9447
  if (current.dev === expected.dev && current.ino === expected.ino)
8066
- unlinkSync2(path);
9448
+ unlinkSync3(path);
8067
9449
  } catch (error) {
8068
9450
  if (error.code !== "ENOENT")
8069
9451
  throw error;
@@ -8077,10 +9459,10 @@ function writePrivatePacket(path, contents) {
8077
9459
  if (filename === "." || filename === "..")
8078
9460
  throw new Error("Ensoul output requires a file path.");
8079
9461
  const directory = assertPrivateOutputDirectory(dirname3(requested));
8080
- const destination = join3(directory, filename);
9462
+ const destination = join4(directory, filename);
8081
9463
  if (pathEntryExists2(destination))
8082
9464
  throw new Error("Ensoul output already exists; refusing to overwrite it.");
8083
- const temporary = join3(directory, `.peopleblade-ensoul-${process.pid}-${randomBytes2(12).toString("hex")}`);
9465
+ const temporary = join4(directory, `.peopleblade-ensoul-${process.pid}-${randomBytes3(12).toString("hex")}`);
8084
9466
  let identity = null;
8085
9467
  const descriptor = openSync5(temporary, constants5.O_WRONLY | constants5.O_CREAT | constants5.O_EXCL | (constants5.O_NOFOLLOW ?? 0), 384);
8086
9468
  try {
@@ -8090,7 +9472,7 @@ function writePrivatePacket(path, contents) {
8090
9472
  if (!identity.isFile() || identity.nlink !== 1 || uid === null || identity.uid !== uid) {
8091
9473
  throw new Error("Ensoul output staging file is not a private, owned regular file.");
8092
9474
  }
8093
- writeFileSync3(descriptor, contents, "utf8");
9475
+ writeFileSync4(descriptor, contents, "utf8");
8094
9476
  fsyncSync2(descriptor);
8095
9477
  } catch (error) {
8096
9478
  unlinkExpectedInode(temporary, identity);
@@ -8103,8 +9485,8 @@ function writePrivatePacket(path, contents) {
8103
9485
  try {
8104
9486
  linkSync2(temporary, destination);
8105
9487
  destinationLinked = true;
8106
- unlinkSync2(temporary);
8107
- const published = lstatSync2(destination);
9488
+ unlinkSync3(temporary);
9489
+ const published = lstatSync3(destination);
8108
9490
  const uid = typeof process.getuid === "function" ? process.getuid() : null;
8109
9491
  if (!published.isFile() || published.isSymbolicLink() || identity === null || published.dev !== identity.dev || published.ino !== identity.ino || published.nlink !== 1 || uid === null || published.uid !== uid || (published.mode & 511) !== 384) {
8110
9492
  throw new Error("Ensoul output was not published as one private, owned regular file.");
@@ -8145,7 +9527,7 @@ function preparePeoplebladeEnsoulSource(database, requestedPersonId, outputPath,
8145
9527
 
8146
9528
  // src/local/legacy-rolodex.ts
8147
9529
  import { Database as Database2 } from "bun:sqlite";
8148
- import { lstatSync as lstatSync3, realpathSync as realpathSync3 } from "fs";
9530
+ import { lstatSync as lstatSync4, realpathSync as realpathSync3 } from "fs";
8149
9531
  function tableExists2(database, name) {
8150
9532
  return database.query("SELECT 1 FROM sqlite_master WHERE type IN ('table','view') AND name=?").get(name) !== null;
8151
9533
  }
@@ -8167,7 +9549,7 @@ function binding(value) {
8167
9549
  return value;
8168
9550
  throw new Error("Legacy database value is not a supported SQLite binding.");
8169
9551
  }
8170
- function json(value) {
9552
+ function json2(value) {
8171
9553
  return canonicalJson(value);
8172
9554
  }
8173
9555
  function providerForKind(kind) {
@@ -8193,7 +9575,7 @@ function maxDate(left, right) {
8193
9575
  }
8194
9576
  function migrateLegacyRolodex(sourcePath, destinationPath) {
8195
9577
  const sourcePhysical = realpathSync3(sourcePath);
8196
- const identity = lstatSync3(sourcePhysical);
9578
+ const identity = lstatSync4(sourcePhysical);
8197
9579
  if (!identity.isFile() || identity.isSymbolicLink() || identity.nlink !== 1) {
8198
9580
  throw new Error("Legacy Rolodex must be one regular database file.");
8199
9581
  }
@@ -8243,7 +9625,7 @@ function migrateLegacyRolodex(sourcePath, destinationPath) {
8243
9625
  notes, metadata_json, created_at, updated_at
8244
9626
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
8245
9627
  for (const [canonicalId, memberIds] of [...members].sort(([a], [b]) => a - b)) {
8246
- const candidates = memberIds.map((id) => legacyPeople.get(id)).filter((item) => item !== undefined);
9628
+ const candidates = memberIds.map((id2) => legacyPeople.get(id2)).filter((item) => item !== undefined);
8247
9629
  const root = candidates.find((person) => person.id === canonicalId) ?? candidates[0];
8248
9630
  if (root === undefined)
8249
9631
  throw new Error("Legacy identity component has no person.");
@@ -8253,7 +9635,7 @@ function migrateLegacyRolodex(sourcePath, destinationPath) {
8253
9635
  return rootValue;
8254
9636
  return candidates.map((item) => item[key]).find((value) => value !== null && value !== "") ?? rootValue;
8255
9637
  };
8256
- const inserted = insertPerson.run(`rolodex-v1:${canonicalId}`, preferred("display_name"), preferred("given_name"), preferred("family_name"), preferred("organization"), preferred("title"), candidates.some((person) => person.do_not_contact === 1) ? 1 : 0, preferred("notes"), json({ legacy_person_ids: memberIds }), candidates.map((person) => person.created_at).sort()[0], candidates.map((person) => person.updated_at).sort().at(-1));
9638
+ const inserted = insertPerson.run(`rolodex-v1:${canonicalId}`, preferred("display_name"), preferred("given_name"), preferred("family_name"), preferred("organization"), preferred("title"), candidates.some((person) => person.do_not_contact === 1) ? 1 : 0, preferred("notes"), json2({ legacy_person_ids: memberIds }), candidates.map((person) => person.created_at).sort()[0], candidates.map((person) => person.updated_at).sort().at(-1));
8257
9639
  targetByCanonical.set(canonicalId, number(inserted.lastInsertRowid, "person id"));
8258
9640
  }
8259
9641
  const personFor = (legacyPersonId) => {
@@ -8278,7 +9660,7 @@ function migrateLegacyRolodex(sourcePath, destinationPath) {
8278
9660
  const fileHash = text(row.file_sha256, "source hash");
8279
9661
  const importedAt = text(row.imported_at, "source imported_at");
8280
9662
  const locator = nullableText2(row.file_path);
8281
- const result2 = insertRun.run(providerForKind(kind), `${kind}:${legacyId}`, fileHash, locator === null ? null : sha256(locator), json({ legacy_source_id: legacyId, kind, name: row.name, row_count: row.row_count, metadata: JSON.parse(text(row.metadata_json, "metadata_json")) }), importedAt, importedAt);
9663
+ const result2 = insertRun.run(providerForKind(kind), `${kind}:${legacyId}`, fileHash, locator === null ? null : sha256(locator), json2({ legacy_source_id: legacyId, kind, name: row.name, row_count: row.row_count, metadata: JSON.parse(text(row.metadata_json, "metadata_json")) }), importedAt, importedAt);
8282
9664
  sourceMap.set(legacyId, number(result2.lastInsertRowid, "source run id"));
8283
9665
  sourceRuns += 1;
8284
9666
  }
@@ -8289,7 +9671,7 @@ function migrateLegacyRolodex(sourcePath, destinationPath) {
8289
9671
  for (const row of source.query("SELECT * FROM emails ORDER BY id").all()) {
8290
9672
  const targetPerson = personFor(number(row.person_id, "email person"));
8291
9673
  const run = row.source_id === null ? null : sourceMap.get(number(row.source_id, "email source")) ?? null;
8292
- const result2 = insertMethod.run(...[targetPerson, "email", row.address, row.normalized_address, "", row.is_primary, row.confidence, run, run, json({ legacy_email_id: row.id, source_row: row.source_row }), row.created_at].map(binding));
9674
+ const result2 = insertMethod.run(...[targetPerson, "email", row.address, row.normalized_address, "", row.is_primary, row.confidence, run, run, json2({ legacy_email_id: row.id, source_row: row.source_row }), row.created_at].map(binding));
8293
9675
  if (result2.changes > 0)
8294
9676
  contactMethods += 1;
8295
9677
  }
@@ -8297,7 +9679,7 @@ function migrateLegacyRolodex(sourcePath, destinationPath) {
8297
9679
  for (const row of source.query("SELECT * FROM phone_numbers ORDER BY id").all()) {
8298
9680
  const targetPerson = personFor(number(row.person_id, "phone person"));
8299
9681
  const run = row.source_id === null ? null : sourceMap.get(number(row.source_id, "phone source")) ?? null;
8300
- const result2 = insertMethod.run(...[targetPerson, "phone", row.value, row.normalized_value, nullableText2(row.type) ?? "", 0, "exact", run, run, json({ legacy_phone_id: row.id, source_row: row.source_row }), row.created_at].map(binding));
9682
+ const result2 = insertMethod.run(...[targetPerson, "phone", row.value, row.normalized_value, nullableText2(row.type) ?? "", 0, "exact", run, run, json2({ legacy_phone_id: row.id, source_row: row.source_row }), row.created_at].map(binding));
8301
9683
  if (result2.changes > 0)
8302
9684
  contactMethods += 1;
8303
9685
  }
@@ -8307,12 +9689,12 @@ function migrateLegacyRolodex(sourcePath, destinationPath) {
8307
9689
  metadata_json, created_at, updated_at
8308
9690
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, coalesce(?, CURRENT_TIMESTAMP), coalesce(?, CURRENT_TIMESTAMP))`);
8309
9691
  const resourceSpecs = [
8310
- { table: "google_contacts", query: "SELECT * FROM google_contacts ORDER BY id", convert: (r) => ["google", r.account_subject, r.collection, r.resource_name, personFor(number(r.person_id, "google person")), null, null, 0, r.collection === "contacts" || r.collection === "other-contacts" ? 1 : 0, null, r.deleted === 1 ? 0 : 1, r.first_seen_source_id === null ? null : sourceMap.get(number(r.first_seen_source_id, "source")), r.last_seen_source_id === null ? null : sourceMap.get(number(r.last_seen_source_id, "source")), json({ etag: r.etag, legacy_id: r.id }), r.created_at, r.updated_at] },
8311
- { table: "apple_contacts", query: "SELECT * FROM apple_contacts ORDER BY id", convert: (r) => ["apple", r.account_key, "contact", `${r.store_key}:${r.contact_identifier}`, personFor(number(r.person_id, "apple person")), null, null, 0, 1, null, r.removed === 1 ? 0 : 1, sourceMap.get(number(r.first_seen_source_id, "source")), sourceMap.get(number(r.last_seen_source_id, "source")), json({ store_key: r.store_key, contact_identifier: r.contact_identifier, legacy_id: r.id }), r.created_at, r.updated_at] },
8312
- { table: "linkedin_connections", query: "SELECT * FROM linkedin_connections ORDER BY id", convert: (r) => ["linkedin", r.account_key, "profile", r.profile_url, personFor(number(r.person_id, "linkedin person")), null, r.profile_url, 1, 1, [r.first_name, r.last_name].filter(Boolean).join(" ") || null, r.removed === 1 ? 0 : 1, sourceMap.get(number(r.first_seen_source_id, "source")), sourceMap.get(number(r.last_seen_source_id, "source")), json({ company: r.company, position: r.position, connected_on: r.connected_on, legacy_id: r.id }), r.created_at, r.updated_at] },
8313
- { table: "telegram_contacts", query: "SELECT * FROM telegram_contacts ORDER BY id", convert: (r) => ["telegram", r.account_key, "contact", r.contact_key, personFor(number(r.person_id, "telegram person")), null, null, 0, 0, null, 1, sourceMap.get(number(r.first_seen_source_id, "source")), sourceMap.get(number(r.last_seen_source_id, "source")), json({ telegram_user_id: r.telegram_user_id, phone: r.phone_normalized, legacy_id: r.id }), r.created_at, r.updated_at] },
8314
- { table: "whatsapp_contacts", query: "SELECT * FROM whatsapp_contacts ORDER BY id", convert: (r) => ["whatsapp", r.account_subject, "jid", r.contact_jid, personFor(number(r.person_id, "whatsapp person")), null, null, 0, 0, r.display_name, 1, null, null, json({ jid_kind: r.jid_kind, phone: r.normalized_phone, display_name_basis: r.display_name_basis, legacy_id: r.id }), r.created_at, r.updated_at] },
8315
- { table: "instagram_identities", query: "SELECT * FROM instagram_identities ORDER BY id", convert: (r) => ["instagram", r.account_key, r.provider_user_id === null ? "profile" : "user", r.provider_user_id ?? r.profile_href, personFor(number(r.person_id, "instagram person")), r.current_username, r.profile_href, 1, 0, r.current_username, 1, sourceMap.get(number(r.first_seen_source_id, "source")), sourceMap.get(number(r.last_seen_source_id, "source")), json({ legacy_id: r.id }), r.created_at, r.updated_at] }
9692
+ { table: "google_contacts", query: "SELECT * FROM google_contacts ORDER BY id", convert: (r) => ["google", r.account_subject, r.collection, r.resource_name, personFor(number(r.person_id, "google person")), null, null, 0, r.collection === "contacts" || r.collection === "other-contacts" ? 1 : 0, null, r.deleted === 1 ? 0 : 1, r.first_seen_source_id === null ? null : sourceMap.get(number(r.first_seen_source_id, "source")), r.last_seen_source_id === null ? null : sourceMap.get(number(r.last_seen_source_id, "source")), json2({ etag: r.etag, legacy_id: r.id }), r.created_at, r.updated_at] },
9693
+ { table: "apple_contacts", query: "SELECT * FROM apple_contacts ORDER BY id", convert: (r) => ["apple", r.account_key, "contact", `${r.store_key}:${r.contact_identifier}`, personFor(number(r.person_id, "apple person")), null, null, 0, 1, null, r.removed === 1 ? 0 : 1, sourceMap.get(number(r.first_seen_source_id, "source")), sourceMap.get(number(r.last_seen_source_id, "source")), json2({ store_key: r.store_key, contact_identifier: r.contact_identifier, legacy_id: r.id }), r.created_at, r.updated_at] },
9694
+ { table: "linkedin_connections", query: "SELECT * FROM linkedin_connections ORDER BY id", convert: (r) => ["linkedin", r.account_key, "profile", r.profile_url, personFor(number(r.person_id, "linkedin person")), null, r.profile_url, 1, 1, [r.first_name, r.last_name].filter(Boolean).join(" ") || null, r.removed === 1 ? 0 : 1, sourceMap.get(number(r.first_seen_source_id, "source")), sourceMap.get(number(r.last_seen_source_id, "source")), json2({ company: r.company, position: r.position, connected_on: r.connected_on, legacy_id: r.id }), r.created_at, r.updated_at] },
9695
+ { table: "telegram_contacts", query: "SELECT * FROM telegram_contacts ORDER BY id", convert: (r) => ["telegram", r.account_key, "contact", r.contact_key, personFor(number(r.person_id, "telegram person")), null, null, 0, 0, null, 1, sourceMap.get(number(r.first_seen_source_id, "source")), sourceMap.get(number(r.last_seen_source_id, "source")), json2({ telegram_user_id: r.telegram_user_id, phone: r.phone_normalized, legacy_id: r.id }), r.created_at, r.updated_at] },
9696
+ { table: "whatsapp_contacts", query: "SELECT * FROM whatsapp_contacts ORDER BY id", convert: (r) => ["whatsapp", r.account_subject, "jid", r.contact_jid, personFor(number(r.person_id, "whatsapp person")), null, null, 0, 0, r.display_name, 1, null, null, json2({ jid_kind: r.jid_kind, phone: r.normalized_phone, display_name_basis: r.display_name_basis, legacy_id: r.id }), r.created_at, r.updated_at] },
9697
+ { table: "instagram_identities", query: "SELECT * FROM instagram_identities ORDER BY id", convert: (r) => ["instagram", r.account_key, r.provider_user_id === null ? "profile" : "user", r.provider_user_id ?? r.profile_href, personFor(number(r.person_id, "instagram person")), r.current_username, r.profile_href, 1, 0, r.current_username, 1, sourceMap.get(number(r.first_seen_source_id, "source")), sourceMap.get(number(r.last_seen_source_id, "source")), json2({ legacy_id: r.id }), r.created_at, r.updated_at] }
8316
9698
  ];
8317
9699
  for (const spec of resourceSpecs)
8318
9700
  if (tableExists2(source, spec.table))
@@ -8357,7 +9739,7 @@ function migrateLegacyRolodex(sourcePath, destinationPath) {
8357
9739
  addMetric("instagram", text(r.account_key, "account"), number(r.person_id, "person"), number(r.outgoing_messages, "sent"), number(r.incoming_messages, "received"), number(r.outgoing_messages, "sent") + number(r.incoming_messages, "received"), number(r.conversation_count, "conversations"), nullableText2(r.first_message_at), nullableText2(r.last_message_at), number(r.outgoing_messages, "sent") > 0 && number(r.incoming_messages, "received") > 0, "unknown");
8358
9740
  const insertMetric = target.query(`INSERT INTO interaction_metrics(provider,account_key,person_id,sent_count,received_count,interaction_count,conversation_count,first_interaction_at,last_interaction_at,reciprocal,completeness,metadata_json) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`);
8359
9741
  for (const metric of metrics.values()) {
8360
- insertMetric.run(metric.provider, metric.account, metric.person, metric.sent, metric.received, metric.count, metric.conversations, metric.first, metric.last, metric.reciprocal ? 1 : 0, metric.completeness, json(metric.metadata));
9742
+ insertMetric.run(metric.provider, metric.account, metric.person, metric.sent, metric.received, metric.count, metric.conversations, metric.first, metric.last, metric.reciprocal ? 1 : 0, metric.completeness, json2(metric.metadata));
8361
9743
  interactionMetrics += 1;
8362
9744
  }
8363
9745
  if (tableExists2(source, "tags") && tableExists2(source, "person_tags")) {
@@ -8373,16 +9755,16 @@ function migrateLegacyRolodex(sourcePath, destinationPath) {
8373
9755
  if (tableExists2(source, "publications") && tableExists2(source, "publication_memberships")) {
8374
9756
  const collectionMap = new Map;
8375
9757
  for (const r of source.query("SELECT * FROM publications ORDER BY id").all()) {
8376
- const result2 = target.query("INSERT INTO collections(provider,account_key,resource_id,name,metadata_json) VALUES (?,?,?,?,?)").run(text(r.platform, "platform"), "legacy", text(r.slug, "slug"), text(r.name, "name"), json({ legacy_publication_id: r.id }));
9758
+ const result2 = target.query("INSERT INTO collections(provider,account_key,resource_id,name,metadata_json) VALUES (?,?,?,?,?)").run(text(r.platform, "platform"), "legacy", text(r.slug, "slug"), text(r.name, "name"), json2({ legacy_publication_id: r.id }));
8377
9759
  collectionMap.set(number(r.id, "publication"), number(result2.lastInsertRowid, "collection"));
8378
9760
  collections += 1;
8379
9761
  }
8380
9762
  for (const r of source.query("SELECT * FROM publication_memberships").all())
8381
- target.query("INSERT INTO collection_memberships(collection_id,person_id,state,metadata_json,updated_at) VALUES (?,?,?,?,?)").run(collectionMap.get(number(r.publication_id, "publication")) ?? null, personFor(number(r.person_id, "person")), text(r.state, "state"), json(r), text(r.updated_at, "membership timestamp"));
9763
+ target.query("INSERT INTO collection_memberships(collection_id,person_id,state,metadata_json,updated_at) VALUES (?,?,?,?,?)").run(collectionMap.get(number(r.publication_id, "publication")) ?? null, personFor(number(r.person_id, "person")), text(r.state, "state"), json2(r), text(r.updated_at, "membership timestamp"));
8382
9764
  }
8383
9765
  renormalizePhoneIdentities(target, { transaction: false });
8384
9766
  const result = { people: targetByCanonical.size, contactMethods, providerResources, sourceRuns, sourceRecords, interactionMetrics, tags, collections };
8385
- target.query("INSERT INTO legacy_migrations(source_format,source_sha256,source_locator_sha256,result_json) VALUES ('rolodex-v1',?,?,?)").run(sourceSha256, sha256(sourcePhysical), json(result));
9767
+ target.query("INSERT INTO legacy_migrations(source_format,source_sha256,source_locator_sha256,result_json) VALUES ('rolodex-v1',?,?,?)").run(sourceSha256, sha256(sourcePhysical), json2(result));
8386
9768
  target.exec("COMMIT");
8387
9769
  return { ...result, cached: false };
8388
9770
  } catch (error) {
@@ -8423,8 +9805,9 @@ function selfStatus(metadataJson) {
8423
9805
  throw new Error("Provider resource identity metadata is malformed.");
8424
9806
  }
8425
9807
  }
8426
- function profiles(database) {
8427
- const rows = database.query(`SELECT p.id, component.canonical_person_id, p.display_name, p.organization,
9808
+ function profiles(database, index, personIds) {
9809
+ const scoped = personIds !== undefined;
9810
+ const rows = database.query(`SELECT p.id, p.display_name, p.organization,
8428
9811
  coalesce((SELECT json_group_array(normalized_value) FROM (
8429
9812
  SELECT DISTINCT method.normalized_value
8430
9813
  FROM contact_methods method
@@ -8458,19 +9841,37 @@ function profiles(database) {
8458
9841
  AND (realm.id IS NULL OR realm.active=1)
8459
9842
  ORDER BY value
8460
9843
  )),'[]') AS resources
8461
- FROM people p JOIN person_identity_components component ON component.person_id=p.id ORDER BY p.id`).all();
8462
- return new Map(rows.map((row) => {
8463
- const profile = {
8464
- id: Number(row.id),
8465
- canonicalId: Number(row.canonical_person_id),
9844
+ FROM people p
9845
+ ${scoped ? "WHERE p.id IN (SELECT value FROM json_each(?))" : ""} ORDER BY p.id`).all(...scoped ? [JSON.stringify(personIds)] : []);
9846
+ const result = new Map;
9847
+ for (const row of rows) {
9848
+ const id2 = Number(row.id);
9849
+ const canonicalId = index.canonicalOf.get(id2);
9850
+ if (canonicalId === undefined)
9851
+ continue;
9852
+ result.set(id2, {
9853
+ id: id2,
9854
+ canonicalId,
8466
9855
  displayName: nullableString(row.display_name),
8467
9856
  organization: nullableString(row.organization),
8468
9857
  emails: stringArray(String(row.emails)),
8469
9858
  phones: stringArray(String(row.phones)),
8470
9859
  resources: stringArray(String(row.resources))
8471
- };
8472
- return [profile.id, profile];
8473
- }));
9860
+ });
9861
+ }
9862
+ return result;
9863
+ }
9864
+ function personNamesLookup(database) {
9865
+ const statement = database.query("SELECT display_name,organization FROM people WHERE id=?");
9866
+ const cache = new Map;
9867
+ return (personId) => {
9868
+ if (cache.has(personId))
9869
+ return cache.get(personId);
9870
+ const row = statement.get(personId);
9871
+ const names = row === null ? undefined : { displayName: nullableString(row.display_name), organization: nullableString(row.organization) };
9872
+ cache.set(personId, names);
9873
+ return names;
9874
+ };
8474
9875
  }
8475
9876
  function legacyEvidence(left, right, kind, signals, warnings) {
8476
9877
  return {
@@ -8533,7 +9934,7 @@ function reviewedEvidenceDigest(decision) {
8533
9934
  }
8534
9935
  }
8535
9936
  function suggestLegacyIdentities(database, kind, limit) {
8536
- const values = [...profiles(database).values()];
9937
+ const values = [...profiles(database, componentIndex(database)).values()];
8537
9938
  const suggestions = [];
8538
9939
  const candidates = new Map;
8539
9940
  const priority = { "exact-email": 3, "exact-phone": 2, "name-organization": 1 };
@@ -8703,10 +10104,29 @@ function eligibleOwners(database) {
8703
10104
  }
8704
10105
  return buckets;
8705
10106
  }
8706
- function componentSnapshot(database, canonicalId) {
10107
+ function componentIndex(database) {
10108
+ const rows = database.query(`SELECT person_id,canonical_person_id FROM person_identity_components
10109
+ ORDER BY canonical_person_id,person_id`).all();
10110
+ const canonicalOf = new Map;
10111
+ const membersOf = new Map;
10112
+ for (const row of rows) {
10113
+ const personId = Number(row.person_id);
10114
+ const canonicalId = Number(row.canonical_person_id);
10115
+ canonicalOf.set(personId, canonicalId);
10116
+ const members = membersOf.get(canonicalId) ?? [];
10117
+ members.push(personId);
10118
+ membersOf.set(canonicalId, members);
10119
+ }
10120
+ return { canonicalOf, membersOf };
10121
+ }
10122
+ function memberList(index, canonicalId) {
10123
+ return JSON.stringify(index.membersOf.get(canonicalId) ?? []);
10124
+ }
10125
+ function componentSnapshot(database, canonicalId, index) {
10126
+ const memberIds = memberList(index, canonicalId);
8707
10127
  const members = database.query(`SELECT person.id,person.display_name,person.organization
8708
- FROM person_identity_components component JOIN people person ON person.id=component.person_id
8709
- WHERE component.canonical_person_id=? ORDER BY person.id`).all(canonicalId).map((row) => ({
10128
+ FROM people person
10129
+ WHERE person.id IN (SELECT value FROM json_each(?)) ORDER BY person.id`).all(memberIds).map((row) => ({
8710
10130
  id: Number(row.id),
8711
10131
  displayName: nullableString(row.display_name),
8712
10132
  organization: nullableString(row.organization),
@@ -8719,11 +10139,10 @@ function componentSnapshot(database, canonicalId) {
8719
10139
  realm.identity_namespace AS realm_identity_namespace,
8720
10140
  realm.external_id_sha256 AS realm_external_id_sha256
8721
10141
  FROM provider_resources resource
8722
- JOIN person_identity_components component ON component.person_id=resource.person_id
8723
10142
  LEFT JOIN source_realms realm ON realm.id=resource.source_realm_id
8724
- WHERE component.canonical_person_id=? AND resource.active=1
10143
+ WHERE resource.person_id IN (SELECT value FROM json_each(?)) AND resource.active=1
8725
10144
  AND (realm.id IS NULL OR realm.active=1)
8726
- ORDER BY resource.id`).all(canonicalId).map((row) => ({
10145
+ ORDER BY resource.id`).all(memberIds).map((row) => ({
8727
10146
  id: Number(row.id),
8728
10147
  personId: Number(row.person_id),
8729
10148
  provider: String(row.provider),
@@ -8740,14 +10159,13 @@ function componentSnapshot(database, canonicalId) {
8740
10159
  method.value,method.normalized_value,method.label,method.metadata_json,
8741
10160
  method.provider_resource_id
8742
10161
  FROM contact_methods method
8743
- JOIN person_identity_components component ON component.person_id=method.person_id
8744
10162
  LEFT JOIN provider_resources resource ON resource.id=method.provider_resource_id
8745
10163
  LEFT JOIN source_realms realm ON realm.id=resource.source_realm_id
8746
- WHERE component.canonical_person_id=? AND method.active=1
10164
+ WHERE method.person_id IN (SELECT value FROM json_each(?)) AND method.active=1
8747
10165
  AND method.identity_eligible=1 AND method.confidence='exact'
8748
10166
  AND (resource.id IS NULL OR resource.active=1)
8749
10167
  AND (realm.id IS NULL OR realm.active=1)
8750
- ORDER BY method.kind,method.normalized_value,method.person_id,method.id`).all(canonicalId).map((row) => ({
10168
+ ORDER BY method.kind,method.normalized_value,method.person_id,method.id`).all(memberIds).map((row) => ({
8751
10169
  id: Number(row.id),
8752
10170
  personId: Number(row.person_id),
8753
10171
  kind: String(row.kind),
@@ -8800,13 +10218,12 @@ function componentRealmContradictions(left, right) {
8800
10218
  }
8801
10219
  return [...realms.values()].filter((group) => group.values.size > 1).map((group) => `distinct exact resources in ${group.label}`).sort();
8802
10220
  }
8803
- function componentInteractionLanes(database, canonicalId) {
10221
+ function componentInteractionLanes(database, canonicalId, index) {
8804
10222
  const rows = database.query(`SELECT metric.provider,realm.service
8805
10223
  FROM interaction_metrics metric
8806
- JOIN person_identity_components component ON component.person_id=metric.person_id
8807
10224
  LEFT JOIN source_realms realm ON realm.authority=metric.provider AND realm.account_key=metric.account_key
8808
- WHERE component.canonical_person_id=? AND (realm.id IS NULL OR realm.active=1)
8809
- ORDER BY metric.provider,realm.service`).all(canonicalId);
10225
+ WHERE metric.person_id IN (SELECT value FROM json_each(?)) AND (realm.id IS NULL OR realm.active=1)
10226
+ ORDER BY metric.provider,realm.service`).all(memberList(index, canonicalId));
8810
10227
  const result = new Map;
8811
10228
  for (const row of rows) {
8812
10229
  const provider = String(row.provider);
@@ -8887,14 +10304,15 @@ function eligibleOwnerSemantic(owner) {
8887
10304
  function observedCandidates(database, options) {
8888
10305
  const allObservations = observedMethods(database);
8889
10306
  const ownersBySignal = eligibleOwners(database);
8890
- const profileMap = profiles(database);
10307
+ const index = options.index ?? componentIndex(database);
10308
+ const names = personNamesLookup(database);
8891
10309
  const snapshots = new Map;
8892
10310
  const laneCache = new Map;
8893
10311
  const snapshot = (canonicalId) => {
8894
10312
  const cached = snapshots.get(canonicalId);
8895
10313
  if (cached !== undefined)
8896
10314
  return cached;
8897
- const created = componentSnapshot(database, canonicalId);
10315
+ const created = componentSnapshot(database, canonicalId, index);
8898
10316
  snapshots.set(canonicalId, created);
8899
10317
  return created;
8900
10318
  };
@@ -8902,7 +10320,7 @@ function observedCandidates(database, options) {
8902
10320
  const cached = laneCache.get(canonicalId);
8903
10321
  if (cached !== undefined)
8904
10322
  return cached;
8905
- const created = componentInteractionLanes(database, canonicalId);
10323
+ const created = componentInteractionLanes(database, canonicalId, index);
8906
10324
  laneCache.set(canonicalId, created);
8907
10325
  return created;
8908
10326
  };
@@ -8923,7 +10341,7 @@ function observedCandidates(database, options) {
8923
10341
  }
8924
10342
  targetComponentsByPerson.set(observation.personId, targets);
8925
10343
  }
8926
- const selected = allObservations.filter((observation) => observation.kind === (options.kind === "observed-email" ? "email" : "phone") && (options.source === undefined || observation.provider === options.source) && (options.realmId === undefined || observation.realmId === options.realmId));
10344
+ const selected = allObservations.filter((observation) => observation.kind === (options.kind === "observed-email" ? "email" : "phone") && (options.source === undefined || observation.provider === options.source) && (options.realmId === undefined || observation.realmId === options.realmId) && (options.observationPersonIds === undefined || options.observationPersonIds.includes(observation.personId)));
8927
10345
  const deduplicated = new Map;
8928
10346
  for (const observation of selected) {
8929
10347
  if (selfStatus(observation.resourceMetadataJson) === "true")
@@ -8955,10 +10373,10 @@ function observedCandidates(database, options) {
8955
10373
  continue;
8956
10374
  const leftId = Math.min(observation.personId, anchor.personId);
8957
10375
  const rightId = Math.max(observation.personId, anchor.personId);
8958
- const leftProfile = profileMap.get(leftId);
8959
- const rightProfile = profileMap.get(rightId);
8960
- const observationProfile = profileMap.get(observation.personId);
8961
- const targetProfile = profileMap.get(anchor.personId);
10376
+ const leftProfile = names(leftId);
10377
+ const rightProfile = names(rightId);
10378
+ const observationProfile = names(observation.personId);
10379
+ const targetProfile = names(anchor.personId);
8962
10380
  if (leftProfile === undefined || rightProfile === undefined || observationProfile === undefined || targetProfile === undefined)
8963
10381
  continue;
8964
10382
  const candidateComponentIds = [...new Set(item.owners.map((owner) => owner.canonicalId))].sort((a, b) => a - b);
@@ -9108,9 +10526,9 @@ function assertReviewTransitionAllowed(current, digest) {
9108
10526
  return;
9109
10527
  throw new Error("This identity evidence has already been reviewed; request a fresh suggestion after evidence changes.");
9110
10528
  }
9111
- function assertMergeAllowed(database, leftCanonicalId, rightCanonicalId, supersededDecisionId = null) {
9112
- const leftMembers = database.query("SELECT person_id FROM person_identity_components WHERE canonical_person_id=? ORDER BY person_id").all(leftCanonicalId).map((row) => row.person_id);
9113
- const rightMembers = database.query("SELECT person_id FROM person_identity_components WHERE canonical_person_id=? ORDER BY person_id").all(rightCanonicalId).map((row) => row.person_id);
10529
+ function assertMergeAllowed(database, index, leftCanonicalId, rightCanonicalId, supersededDecisionId = null) {
10530
+ const leftMembers = index.membersOf.get(leftCanonicalId) ?? [];
10531
+ const rightMembers = index.membersOf.get(rightCanonicalId) ?? [];
9114
10532
  const leftMarks = leftMembers.map(() => "?").join(",");
9115
10533
  const rightMarks = rightMembers.map(() => "?").join(",");
9116
10534
  const blockers = database.query(`SELECT count(*) AS count FROM identity_current_decisions
@@ -9130,7 +10548,8 @@ function insertDecision(database, left, right, action, digest, evidence, priorDe
9130
10548
  function decideLegacyIdentity(database, parsed, action, note) {
9131
10549
  if (action === "separate")
9132
10550
  throw new Error("Use identity separate with the accepted decision ID.");
9133
- const map = profiles(database);
10551
+ const index = componentIndex(database);
10552
+ const map = profiles(database, index, [parsed.left, parsed.right]);
9134
10553
  const left = map.get(parsed.left);
9135
10554
  const right = map.get(parsed.right);
9136
10555
  if (left === undefined || right === undefined)
@@ -9149,17 +10568,20 @@ function decideLegacyIdentity(database, parsed, action, note) {
9149
10568
  assertReviewTransitionAllowed(current, parsed.digest);
9150
10569
  if (action === "accept") {
9151
10570
  const superseded = current !== null && ["reject", "separate"].includes(current.action) && reviewedEvidenceDigest(current) !== parsed.digest ? current.id : null;
9152
- assertMergeAllowed(database, left.canonicalId, right.canonicalId, superseded);
10571
+ assertMergeAllowed(database, index, left.canonicalId, right.canonicalId, superseded);
9153
10572
  }
9154
10573
  return insertDecision(database, parsed.left, parsed.right, action, parsed.digest, evidence, current?.id ?? null, note);
9155
10574
  }
9156
10575
  function decideObservedIdentity(database, parsed, action, note) {
9157
10576
  if (action === "separate")
9158
10577
  throw new Error("Use identity separate with the accepted decision ID.");
10578
+ const index = componentIndex(database);
9159
10579
  const candidates = observedCandidates(database, {
9160
10580
  kind: parsed.kind,
9161
10581
  ambiguity: "all",
9162
- suppressDecisions: false
10582
+ suppressDecisions: false,
10583
+ observationPersonIds: [parsed.left, parsed.right],
10584
+ index
9163
10585
  });
9164
10586
  const candidate = candidates.find((item) => item.suggestion.token === `id2.${parsed.left}.${parsed.right}.${parsed.kind}.${parsed.observationMethodId}.${item.digest}`);
9165
10587
  if (candidate === undefined || candidate.digest !== parsed.digest) {
@@ -9168,16 +10590,16 @@ function decideObservedIdentity(database, parsed, action, note) {
9168
10590
  if (action === "accept" && candidate.realmContradictions.length > 0) {
9169
10591
  throw new Error("Distinct exact resources in one source realm block this identity merge.");
9170
10592
  }
9171
- const left = database.query("SELECT canonical_person_id FROM person_identity_components WHERE person_id=?").get(parsed.left);
9172
- const right = database.query("SELECT canonical_person_id FROM person_identity_components WHERE person_id=?").get(parsed.right);
9173
- if (left === null || right === null || left.canonical_person_id === right.canonical_person_id) {
10593
+ const left = index.canonicalOf.get(parsed.left);
10594
+ const right = index.canonicalOf.get(parsed.right);
10595
+ if (left === undefined || right === undefined || left === right) {
9174
10596
  throw new Error("Identity evidence changed; request a fresh suggestion.");
9175
10597
  }
9176
10598
  const current = currentDecision(database, parsed.left, parsed.right);
9177
10599
  assertReviewTransitionAllowed(current, parsed.digest);
9178
10600
  if (action === "accept") {
9179
10601
  const superseded = current !== null && ["reject", "separate"].includes(current.action) && reviewedEvidenceDigest(current) !== parsed.digest ? current.id : null;
9180
- assertMergeAllowed(database, left.canonical_person_id, right.canonical_person_id, superseded);
10602
+ assertMergeAllowed(database, index, left, right, superseded);
9181
10603
  }
9182
10604
  return insertDecision(database, parsed.left, parsed.right, action, parsed.digest, candidate.evidence, current?.id ?? null, note);
9183
10605
  }
@@ -9226,6 +10648,275 @@ function separateIdentityDecision(database, acceptedDecisionId, note) {
9226
10648
  throw error;
9227
10649
  }
9228
10650
  }
10651
+ var IDENTITY_AUTO_ACCEPT_NOTE = "auto:high-precision";
10652
+ var IDENTITY_AUTO_ACCEPT_KINDS = [
10653
+ "exact-email",
10654
+ "exact-phone",
10655
+ "observed-email",
10656
+ "observed-phone"
10657
+ ];
10658
+ var DEFAULT_IDENTITY_AUTO_ACCEPT_KINDS = [
10659
+ "exact-email",
10660
+ "exact-phone",
10661
+ "observed-phone"
10662
+ ];
10663
+ var DEFAULT_IDENTITY_AUTO_ACCEPT_LIMIT = 100;
10664
+ var MAX_IDENTITY_AUTO_ACCEPT_LIMIT = 500;
10665
+ var AUTO_ACCEPT_SUGGESTION_PAGE = 500;
10666
+ var REALM_CONTRADICTION_PREFIX = "distinct exact resources in";
10667
+ var CONFLICTING_TARGETS_WARNING = "this provider identity has observed methods pointing to different identity components";
10668
+ var OBSERVED_BASELINE_WARNING = /^unverified (?:email|phone) observation; review is required$/u;
10669
+ var OBSERVED_TOLERATED_WARNINGS = [
10670
+ OBSERVED_BASELINE_WARNING,
10671
+ /^normalized display names differ$/u,
10672
+ /^interaction history overlaps direct and Beeper /u
10673
+ ];
10674
+ var PHONE_NAME_MISMATCH_DETAIL = "both records carry different display names; a shared phone joins only matching names or a nameless stub";
10675
+ var PHONE_COMPONENT_NAME_MISMATCH_DETAIL = "identity components carry different display names; a shared phone requires compatible names across both components";
10676
+ function isAutoAcceptKind(kind) {
10677
+ return IDENTITY_AUTO_ACCEPT_KINDS.includes(kind);
10678
+ }
10679
+ function displayNameTokens(displayName) {
10680
+ const raw = (displayName ?? "").trim();
10681
+ if (raw.length === 0 || raw.includes("@") && !/\s/u.test(raw))
10682
+ return [];
10683
+ const normalized = raw.normalize("NFKC").toLocaleLowerCase("en-US").replaceAll(/[^\p{L}\p{M}\p{N}]+/gu, " ").trim();
10684
+ if (normalized.length === 0 || /^[\p{N} ]+$/u.test(normalized))
10685
+ return [];
10686
+ return [...new Set(normalized.split(" "))];
10687
+ }
10688
+ function compatibleDisplayNames(left, right) {
10689
+ const leftTokens = displayNameTokens(left);
10690
+ const rightTokens = displayNameTokens(right);
10691
+ if (leftTokens.length === 0 || rightTokens.length === 0)
10692
+ return true;
10693
+ const [shorter, longer] = leftTokens.length <= rightTokens.length ? [leftTokens, rightTokens] : [rightTokens, leftTokens];
10694
+ const longerSet = new Set(longer);
10695
+ return shorter.every((token) => longerSet.has(token));
10696
+ }
10697
+ function phoneNameBlocker(suggestion) {
10698
+ if (suggestion.kind !== "exact-phone" && suggestion.kind !== "observed-phone")
10699
+ return null;
10700
+ if (compatibleDisplayNames(suggestion.left.displayName, suggestion.right.displayName))
10701
+ return null;
10702
+ return { reason: "name-mismatch", detail: PHONE_NAME_MISMATCH_DETAIL };
10703
+ }
10704
+ function identityAutoAcceptBlocker(suggestion) {
10705
+ if (!isAutoAcceptKind(suggestion.kind)) {
10706
+ return { reason: "ineligible-kind", detail: `${suggestion.kind} evidence always requires manual review` };
10707
+ }
10708
+ const realm = suggestion.warnings.find((warning) => warning.startsWith(REALM_CONTRADICTION_PREFIX));
10709
+ if (realm !== undefined)
10710
+ return { reason: "realm-contradiction", detail: realm };
10711
+ const observation = suggestion.observation;
10712
+ if (observation === undefined) {
10713
+ if (suggestion.warnings.length > 0 || suggestion.confidence !== "strong") {
10714
+ return { reason: "unreviewed-warning", detail: suggestion.warnings[0] ?? "suggestion is not strong" };
10715
+ }
10716
+ return phoneNameBlocker(suggestion);
10717
+ }
10718
+ if (observation.ambiguity !== "unique" || observation.candidateComponentCount !== 1) {
10719
+ return {
10720
+ reason: "ambiguous-candidates",
10721
+ detail: `observed value has ${observation.candidateComponentCount} eligible identity candidates`
10722
+ };
10723
+ }
10724
+ if (observation.observationComponentCount > 1) {
10725
+ return {
10726
+ reason: "multi-observation-cluster",
10727
+ detail: `observed value appears in ${observation.observationComponentCount} provider identity components`
10728
+ };
10729
+ }
10730
+ if (suggestion.warnings.includes(CONFLICTING_TARGETS_WARNING)) {
10731
+ return { reason: "conflicting-targets", detail: CONFLICTING_TARGETS_WARNING };
10732
+ }
10733
+ if (observation.selfStatus !== "false") {
10734
+ return { reason: "self-status-unknown", detail: `source self status is ${observation.selfStatus}` };
10735
+ }
10736
+ const unexpected = suggestion.warnings.find((warning) => !OBSERVED_TOLERATED_WARNINGS.some((pattern) => pattern.test(warning)));
10737
+ if (unexpected !== undefined)
10738
+ return { reason: "unreviewed-warning", detail: unexpected };
10739
+ return phoneNameBlocker(suggestion);
10740
+ }
10741
+ function componentLevelRealmBlocker(database, index, suggestion) {
10742
+ if (suggestion.observation !== undefined)
10743
+ return null;
10744
+ const left = index.canonicalOf.get(suggestion.leftPersonId);
10745
+ const right = index.canonicalOf.get(suggestion.rightPersonId);
10746
+ if (left === undefined || right === undefined || left === right) {
10747
+ return { reason: "stale-evidence", detail: "identity components changed during this run" };
10748
+ }
10749
+ const contradictions = componentRealmContradictions(componentSnapshot(database, left, index), componentSnapshot(database, right, index));
10750
+ const first = contradictions[0];
10751
+ return first === undefined ? null : { reason: "realm-contradiction", detail: first };
10752
+ }
10753
+ function componentLevelPhoneNameBlocker(database, index, suggestion) {
10754
+ if (suggestion.kind !== "exact-phone" && suggestion.kind !== "observed-phone")
10755
+ return null;
10756
+ const left = index.canonicalOf.get(suggestion.leftPersonId);
10757
+ const right = index.canonicalOf.get(suggestion.rightPersonId);
10758
+ const stale = {
10759
+ reason: "stale-evidence",
10760
+ detail: "identity components changed during this run"
10761
+ };
10762
+ if (left === undefined || right === undefined || left === right)
10763
+ return stale;
10764
+ const names = personNamesLookup(database);
10765
+ const componentNames = (canonicalId) => {
10766
+ const members = index.membersOf.get(canonicalId);
10767
+ if (members === undefined || members.length === 0)
10768
+ return null;
10769
+ const result = [];
10770
+ for (const personId of members) {
10771
+ const person = names(personId);
10772
+ if (person === undefined)
10773
+ return null;
10774
+ result.push(person.displayName);
10775
+ }
10776
+ return result;
10777
+ };
10778
+ const leftNames = componentNames(left);
10779
+ const rightNames = componentNames(right);
10780
+ if (leftNames === null || rightNames === null)
10781
+ return stale;
10782
+ if (leftNames.some((leftName) => rightNames.some((rightName) => !compatibleDisplayNames(leftName, rightName)))) {
10783
+ return { reason: "name-mismatch", detail: PHONE_COMPONENT_NAME_MISMATCH_DETAIL };
10784
+ }
10785
+ return null;
10786
+ }
10787
+ function normalizeAutoAcceptKinds(kinds) {
10788
+ const requested = kinds === undefined || kinds.length === 0 ? DEFAULT_IDENTITY_AUTO_ACCEPT_KINDS : kinds;
10789
+ const result = [];
10790
+ for (const kind of requested) {
10791
+ if (kind === "name-organization") {
10792
+ throw new Error("name-organization evidence is never eligible for automatic acceptance.");
10793
+ }
10794
+ if (!isAutoAcceptKind(kind))
10795
+ throw new Error("Identity auto-accept kinds must be exact-email, exact-phone, observed-email, or observed-phone.");
10796
+ if (!result.includes(kind))
10797
+ result.push(kind);
10798
+ }
10799
+ return result;
10800
+ }
10801
+ function autoAcceptIdentities(database, options = {}) {
10802
+ const kinds = normalizeAutoAcceptKinds(options.kinds);
10803
+ if (options.ambiguity !== undefined && options.ambiguity !== "unique") {
10804
+ throw new Error("Identity auto-accept reviews unique candidates only.");
10805
+ }
10806
+ const limit = options.limit ?? DEFAULT_IDENTITY_AUTO_ACCEPT_LIMIT;
10807
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_IDENTITY_AUTO_ACCEPT_LIMIT) {
10808
+ throw new Error(`Identity auto-accept limit must be between 1 and ${MAX_IDENTITY_AUTO_ACCEPT_LIMIT}.`);
10809
+ }
10810
+ if (options.source !== undefined && !/^[a-z][a-z0-9-]{0,63}$/u.test(options.source)) {
10811
+ throw new Error("Identity observation source is invalid.");
10812
+ }
10813
+ const dryRun = options.dryRun === true;
10814
+ const operatorNote = options.note?.trim() ?? "";
10815
+ const noteFor = (kind) => `${IDENTITY_AUTO_ACCEPT_NOTE} ${kind} unique${operatorNote.length > 0 ? `; ${operatorNote}` : ""}`;
10816
+ const accepted = [];
10817
+ const skipped = [];
10818
+ const resolved = new Set;
10819
+ const stale = new Map;
10820
+ const pairKey = (suggestion) => `${suggestion.leftPersonId}.${suggestion.rightPersonId}`;
10821
+ const skip = (suggestion, blocker) => {
10822
+ resolved.add(pairKey(suggestion));
10823
+ stale.delete(pairKey(suggestion));
10824
+ skipped.push({
10825
+ token: suggestion.token,
10826
+ kind: suggestion.kind,
10827
+ leftPersonId: suggestion.leftPersonId,
10828
+ rightPersonId: suggestion.rightPersonId,
10829
+ reason: blocker.reason,
10830
+ detail: blocker.detail,
10831
+ warnings: suggestion.warnings
10832
+ });
10833
+ };
10834
+ let index = null;
10835
+ const components = () => {
10836
+ index ??= componentIndex(database);
10837
+ return index;
10838
+ };
10839
+ let rounds = 0;
10840
+ let progress = true;
10841
+ while (progress && accepted.length < limit && rounds <= limit) {
10842
+ rounds += 1;
10843
+ progress = false;
10844
+ for (const kind of kinds) {
10845
+ if (accepted.length >= limit)
10846
+ break;
10847
+ const observed = kind === "observed-email" || kind === "observed-phone";
10848
+ const suggestions = suggestIdentities(database, {
10849
+ kind,
10850
+ limit: AUTO_ACCEPT_SUGGESTION_PAGE,
10851
+ ...observed ? { ambiguity: "unique" } : {},
10852
+ ...observed && options.source !== undefined ? { source: options.source } : {}
10853
+ });
10854
+ for (const suggestion of suggestions) {
10855
+ if (accepted.length >= limit)
10856
+ break;
10857
+ const key = pairKey(suggestion);
10858
+ if (resolved.has(key))
10859
+ continue;
10860
+ const blocker = identityAutoAcceptBlocker(suggestion) ?? (currentDecision(database, suggestion.leftPersonId, suggestion.rightPersonId) === null ? null : { reason: "prior-decision", detail: "a current decision already exists for this pair" }) ?? componentLevelRealmBlocker(database, components(), suggestion) ?? componentLevelPhoneNameBlocker(database, components(), suggestion);
10861
+ if (blocker !== null) {
10862
+ skip(suggestion, blocker);
10863
+ continue;
10864
+ }
10865
+ const entry = {
10866
+ token: suggestion.token,
10867
+ kind,
10868
+ leftPersonId: suggestion.leftPersonId,
10869
+ rightPersonId: suggestion.rightPersonId,
10870
+ score: suggestion.score
10871
+ };
10872
+ if (dryRun) {
10873
+ resolved.add(key);
10874
+ accepted.push({ decisionId: null, ...entry });
10875
+ continue;
10876
+ }
10877
+ try {
10878
+ const decision = decideIdentity(database, suggestion.token, "accept", noteFor(kind));
10879
+ index = null;
10880
+ resolved.add(key);
10881
+ stale.delete(key);
10882
+ accepted.push({ decisionId: decision.decisionId, ...entry });
10883
+ progress = true;
10884
+ } catch (error) {
10885
+ const message = error instanceof Error ? error.message : "unexpected failure";
10886
+ if (message === "Identity evidence changed; request a fresh suggestion.") {
10887
+ stale.set(key, suggestion);
10888
+ continue;
10889
+ }
10890
+ skip(suggestion, { reason: "decision-refused", detail: message });
10891
+ }
10892
+ }
10893
+ }
10894
+ if (dryRun)
10895
+ break;
10896
+ }
10897
+ for (const suggestion of stale.values()) {
10898
+ skip(suggestion, { reason: "stale-evidence", detail: "evidence changed during this run; rerun to re-evaluate" });
10899
+ }
10900
+ const byReason = {};
10901
+ for (const item of skipped)
10902
+ byReason[item.reason] = (byReason[item.reason] ?? 0) + 1;
10903
+ return {
10904
+ dryRun,
10905
+ policy: {
10906
+ kinds,
10907
+ ambiguity: "unique",
10908
+ limit,
10909
+ source: options.source ?? null,
10910
+ note: IDENTITY_AUTO_ACCEPT_NOTE
10911
+ },
10912
+ rounds,
10913
+ reviewed: resolved.size,
10914
+ limitReached: accepted.length >= limit,
10915
+ accepted,
10916
+ skipped,
10917
+ counts: { accepted: accepted.length, skipped: skipped.length, byReason }
10918
+ };
10919
+ }
9229
10920
  function currentAcceptedObservedDecisions(database, observationsByMethod, source) {
9230
10921
  const decisions = database.query(`SELECT id,evidence_json,created_at FROM identity_current_decisions
9231
10922
  WHERE action='accept' AND json_extract(evidence_json,'$.version')=2
@@ -9348,7 +11039,7 @@ function auditObservedIdentityLinks(database, options = {}) {
9348
11039
  }
9349
11040
 
9350
11041
  // src/local/people-create.ts
9351
- import { randomBytes as randomBytes3 } from "crypto";
11042
+ import { randomBytes as randomBytes4 } from "crypto";
9352
11043
  var MODE2 = "confirmed-person-v1";
9353
11044
  var PROVIDER3 = "manual";
9354
11045
  var ACCOUNT_KEY2 = "local";
@@ -9400,7 +11091,7 @@ function nameStableKey(displayName) {
9400
11091
  schemaVersion: 1,
9401
11092
  kind: "confirmed-name",
9402
11093
  displayName,
9403
- entropy: randomBytes3(32).toString("hex")
11094
+ entropy: randomBytes4(32).toString("hex")
9404
11095
  }))}`;
9405
11096
  }
9406
11097
  function explicitAttestationMetadata(note) {
@@ -9598,12 +11289,12 @@ function addConfirmedPerson(database, args) {
9598
11289
  import { Database as Database3 } from "bun:sqlite";
9599
11290
  import { createHash as createHash3 } from "crypto";
9600
11291
  import {
9601
- lstatSync as lstatSync4,
11292
+ lstatSync as lstatSync5,
9602
11293
  readdirSync as readdirSync2,
9603
11294
  realpathSync as realpathSync4
9604
11295
  } from "fs";
9605
11296
  import { homedir as homedir2 } from "os";
9606
- import { join as join4, resolve as resolve3 } from "path";
11297
+ import { join as join5, resolve as resolve3 } from "path";
9607
11298
  var EMAIL_PATTERN2 = /^(?=[\x21-\x7E]+$)[^@\s]+@[^@\s]+\.[^@\s]+$/u;
9608
11299
  function clean(value) {
9609
11300
  if (typeof value !== "string")
@@ -9614,7 +11305,7 @@ function clean(value) {
9614
11305
  function normalizeEmail(value) {
9615
11306
  return value.trim().toLocaleLowerCase("en-US");
9616
11307
  }
9617
- var DEFAULT_APPLE_CONTACTS_DIRECTORY = join4(homedir2(), "Library", "Application Support", "AddressBook");
11308
+ var DEFAULT_APPLE_CONTACTS_DIRECTORY = join5(homedir2(), "Library", "Application Support", "AddressBook");
9618
11309
  var DEFAULT_APPLE_CONTACTS_ACCOUNT = "apple-contacts-main";
9619
11310
  var MAX_SOURCE_DATABASES = 64;
9620
11311
  var MAX_SOURCE_DATABASE_BYTES = 512 * 1024 * 1024;
@@ -9807,8 +11498,8 @@ function rowsByOwner(database, table, names) {
9807
11498
  }
9808
11499
  function sourceDatabasePaths(directory) {
9809
11500
  const base = realpathSync4(resolve3(directory));
9810
- const sourceDirectory = join4(base, "Sources");
9811
- const sourceIdentity = lstatSync4(sourceDirectory);
11501
+ const sourceDirectory = join5(base, "Sources");
11502
+ const sourceIdentity = lstatSync5(sourceDirectory);
9812
11503
  if (!sourceIdentity.isDirectory() || sourceIdentity.isSymbolicLink()) {
9813
11504
  throw new Error("Apple Contacts Sources must be a real directory");
9814
11505
  }
@@ -9819,7 +11510,7 @@ function sourceDatabasePaths(directory) {
9819
11510
  if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(entry.name)) {
9820
11511
  throw new Error("Apple Contacts source directory name is invalid");
9821
11512
  }
9822
- const candidateDirectory = join4(sourceDirectory, entry.name);
11513
+ const candidateDirectory = join5(sourceDirectory, entry.name);
9823
11514
  const databases = readdirSync2(candidateDirectory, { withFileTypes: true }).filter((item) => item.isFile() && /^AddressBook-v\d+\.abcddb$/u.test(item.name));
9824
11515
  if (databases.length === 0)
9825
11516
  continue;
@@ -9828,8 +11519,8 @@ function sourceDatabasePaths(directory) {
9828
11519
  const databaseName = databases[0]?.name;
9829
11520
  if (databaseName === undefined)
9830
11521
  throw new Error("Apple Contacts database name is missing");
9831
- const candidate = join4(candidateDirectory, databaseName);
9832
- const identity = lstatSync4(candidate);
11522
+ const candidate = join5(candidateDirectory, databaseName);
11523
+ const identity = lstatSync5(candidate);
9833
11524
  const uid = typeof process.getuid === "function" ? process.getuid() : null;
9834
11525
  if (!identity.isFile() || identity.isSymbolicLink() || identity.size < 1 || identity.size > MAX_SOURCE_DATABASE_BYTES || uid !== null && identity.uid !== uid) {
9835
11526
  throw new Error(`Apple Contacts store ${entry.name} is not a supported private database`);
@@ -9933,9 +11624,9 @@ function readStore(storeKey, path) {
9933
11624
  values[child.name] = rows.map((row) => stableObject(row, metrics));
9934
11625
  }
9935
11626
  const serviceRows = childMaps.get("ZABCDMESSAGINGADDRESS")?.get(primaryKey) ?? [];
9936
- values.ZABCDSERVICE = [...new Set(serviceRows.map((row) => row.ZSERVICE))].filter((id) => typeof id === "number" && Number.isSafeInteger(id)).map((id) => serviceById.get(id)).filter((row) => row !== undefined).map((row) => stableObject(row, metrics));
11627
+ values.ZABCDSERVICE = [...new Set(serviceRows.map((row) => row.ZSERVICE))].filter((id2) => typeof id2 === "number" && Number.isSafeInteger(id2)).map((id2) => serviceById.get(id2)).filter((row) => row !== undefined).map((row) => stableObject(row, metrics));
9937
11628
  const customRows = childMaps.get("ZABCDCUSTOMPROPERTYVALUE")?.get(primaryKey) ?? [];
9938
- values.ZABCDCUSTOMPROPERTY = [...new Set(customRows.map((row) => row.ZCUSTOMPROPERTY))].filter((id) => typeof id === "number" && Number.isSafeInteger(id)).map((id) => customPropertyById.get(id)).filter((row) => row !== undefined).map((row) => stableObject(row, metrics));
11629
+ values.ZABCDCUSTOMPROPERTY = [...new Set(customRows.map((row) => row.ZCUSTOMPROPERTY))].filter((id2) => typeof id2 === "number" && Number.isSafeInteger(id2)).map((id2) => customPropertyById.get(id2)).filter((row) => row !== undefined).map((row) => stableObject(row, metrics));
9939
11630
  values.ZABCDGROUPS = (groups.get(primaryKey) ?? []).map((row) => stableObject(row, metrics));
9940
11631
  const rawJson = stableJson({
9941
11632
  schemaVersion: 1,
@@ -10170,15 +11861,15 @@ function syncAppleContacts(database, options = {}) {
10170
11861
  }
10171
11862
 
10172
11863
  // src/local/source-ingestion.ts
10173
- import { z as z8 } from "zod";
10174
- var providerSchema = z8.string().regex(/^[a-z][a-z0-9-]{0,63}$/u);
10175
- var boundedText3 = (maximum) => z8.string().min(1).max(maximum).refine((value) => !/[\u0000\r]/u.test(value), "Text contains an unsupported control character");
11864
+ import { z as z10 } from "zod";
11865
+ var providerSchema = z10.string().regex(/^[a-z][a-z0-9-]{0,63}$/u);
11866
+ var boundedText3 = (maximum) => z10.string().min(1).max(maximum).refine((value) => !/[\u0000\r]/u.test(value), "Text contains an unsupported control character");
10176
11867
  var nullableText3 = (maximum) => boundedText3(maximum).nullable();
10177
- var digestSchema2 = z8.string().regex(/^[a-f0-9]{64}$/u);
10178
- var timestampSchema2 = z8.iso.datetime({ offset: true });
11868
+ var digestSchema2 = z10.string().regex(/^[a-f0-9]{64}$/u);
11869
+ var timestampSchema2 = z10.iso.datetime({ offset: true });
10179
11870
  var sqliteNoCase = (value) => value.replace(/[A-Z]/gu, (character) => character.toLowerCase());
10180
- var jsonObjectSchema = z8.record(z8.string(), z8.json()).refine((value) => Buffer.byteLength(canonicalJson(value), "utf8") <= 64 * 1024, "Metadata exceeds 64 KiB");
10181
- var personSchema = z8.object({
11871
+ var jsonObjectSchema = z10.record(z10.string(), z10.json()).refine((value) => Buffer.byteLength(canonicalJson(value), "utf8") <= 64 * 1024, "Metadata exceeds 64 KiB");
11872
+ var personSchema = z10.object({
10182
11873
  displayName: nullableText3(1024),
10183
11874
  givenName: nullableText3(1024),
10184
11875
  middleName: nullableText3(1024),
@@ -10191,16 +11882,16 @@ var personSchema = z8.object({
10191
11882
  title: nullableText3(1024),
10192
11883
  birthday: birthdaySchema,
10193
11884
  observationBasis: boundedText3(128),
10194
- observationPriority: z8.number().int().min(0).max(1000),
11885
+ observationPriority: z10.number().int().min(0).max(1000),
10195
11886
  metadata: jsonObjectSchema
10196
11887
  }).strict();
10197
- var resourceSchema = z8.object({
11888
+ var resourceSchema = z10.object({
10198
11889
  type: boundedText3(64),
10199
11890
  id: boundedText3(4096),
10200
11891
  username: nullableText3(2048),
10201
11892
  profileUrl: nullableText3(4096),
10202
- profileUrlIdentityEligible: z8.boolean(),
10203
- nameIdentityEligible: z8.boolean(),
11893
+ profileUrlIdentityEligible: z10.boolean(),
11894
+ nameIdentityEligible: z10.boolean(),
10204
11895
  displayName: nullableText3(2048),
10205
11896
  metadata: jsonObjectSchema
10206
11897
  }).strict().superRefine((resource, context) => {
@@ -10211,20 +11902,20 @@ var resourceSchema = z8.object({
10211
11902
  message: "An identity-eligible profile URL must be a stored HTTP(S) profile URL"
10212
11903
  });
10213
11904
  });
10214
- var methodSchema = z8.object({
10215
- kind: z8.enum(["email", "phone", "url", "address", "date", "social", "other"]),
11905
+ var methodSchema = z10.object({
11906
+ kind: z10.enum(["email", "phone", "url", "address", "date", "social", "other"]),
10216
11907
  value: boundedText3(8192),
10217
11908
  normalizedValue: boundedText3(8192),
10218
- label: z8.string().max(256),
10219
- primary: z8.boolean(),
10220
- confidence: z8.enum(["exact", "likely", "possible", "unknown"]),
10221
- identityEligible: z8.boolean(),
11909
+ label: z10.string().max(256),
11910
+ primary: z10.boolean(),
11911
+ confidence: z10.enum(["exact", "likely", "possible", "unknown"]),
11912
+ identityEligible: z10.boolean(),
10222
11913
  metadata: jsonObjectSchema
10223
11914
  }).strict();
10224
- var contactSchema = z8.object({
11915
+ var contactSchema = z10.object({
10225
11916
  person: personSchema,
10226
11917
  resource: resourceSchema,
10227
- methods: z8.array(methodSchema).max(1000)
11918
+ methods: z10.array(methodSchema).max(1000)
10228
11919
  }).strict().superRefine((contact, context) => {
10229
11920
  const coordinates = new Set;
10230
11921
  contact.methods.forEach((method, index) => {
@@ -10235,24 +11926,24 @@ var contactSchema = z8.object({
10235
11926
  coordinates.add(coordinate);
10236
11927
  });
10237
11928
  });
10238
- var completenessSchema = z8.enum(["complete", "partial", "lower-bound", "unknown"]);
10239
- var coverageSchema = z8.object({
10240
- localEnumerationComplete: z8.boolean(),
10241
- remoteSetComplete: z8.boolean(),
10242
- truncated: z8.boolean(),
10243
- absencePolicy: z8.enum(["reconcile-observed", "preserve"]),
10244
- warnings: z8.array(boundedText3(128)).max(32)
11929
+ var completenessSchema = z10.enum(["complete", "partial", "lower-bound", "unknown"]);
11930
+ var coverageSchema = z10.object({
11931
+ localEnumerationComplete: z10.boolean(),
11932
+ remoteSetComplete: z10.boolean(),
11933
+ truncated: z10.boolean(),
11934
+ absencePolicy: z10.enum(["reconcile-observed", "preserve"]),
11935
+ warnings: z10.array(boundedText3(128)).max(32)
10245
11936
  }).strict();
10246
- var interactionSchema = z8.object({
11937
+ var interactionSchema = z10.object({
10247
11938
  resourceType: boundedText3(64),
10248
11939
  resourceId: boundedText3(4096),
10249
- sentCount: z8.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
10250
- receivedCount: z8.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
10251
- interactionCount: z8.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
10252
- conversationCount: z8.number().int().min(0).max(Number.MAX_SAFE_INTEGER).nullable(),
11940
+ sentCount: z10.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
11941
+ receivedCount: z10.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
11942
+ interactionCount: z10.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
11943
+ conversationCount: z10.number().int().min(0).max(Number.MAX_SAFE_INTEGER).nullable(),
10253
11944
  firstInteractionAt: timestampSchema2.nullable(),
10254
11945
  lastInteractionAt: timestampSchema2.nullable(),
10255
- reciprocal: z8.boolean(),
11946
+ reciprocal: z10.boolean(),
10256
11947
  completeness: completenessSchema,
10257
11948
  metadata: jsonObjectSchema
10258
11949
  }).strict().superRefine((interaction, context) => {
@@ -10265,32 +11956,32 @@ var interactionSchema = z8.object({
10265
11956
  if (interaction.interactionCount === 0 && (interaction.firstInteractionAt !== null || interaction.lastInteractionAt !== null) || interaction.interactionCount > 0 && (interaction.firstInteractionAt === null || interaction.lastInteractionAt === null) || interaction.firstInteractionAt !== null && interaction.lastInteractionAt !== null && Date.parse(interaction.firstInteractionAt) > Date.parse(interaction.lastInteractionAt))
10266
11957
  context.addIssue({ code: "custom", path: ["firstInteractionAt"], message: "Interaction timestamps do not match the observed count" });
10267
11958
  });
10268
- var sourceBindingSchema = z8.object({
11959
+ var sourceBindingSchema = z10.object({
10269
11960
  authId: boundedText3(128),
10270
11961
  authSha256: digestSchema2,
10271
11962
  subjectSha256: digestSchema2,
10272
11963
  metadata: jsonObjectSchema
10273
11964
  }).strict();
10274
- var sourceRealmSchema = z8.object({
11965
+ var sourceRealmSchema = z10.object({
10275
11966
  service: boundedText3(64),
10276
11967
  identityNamespace: boundedText3(128),
10277
11968
  externalIdSha256: digestSchema2,
10278
11969
  metadata: jsonObjectSchema
10279
11970
  }).strict();
10280
- var sourceRealmInventoryEntrySchema = z8.object({
11971
+ var sourceRealmInventoryEntrySchema = z10.object({
10281
11972
  accountKey: boundedText3(256),
10282
11973
  sourceRealm: sourceRealmSchema
10283
11974
  }).strict();
10284
- var sourceRealmInventorySchema = z8.object({
10285
- schemaVersion: z8.literal(1),
11975
+ var sourceRealmInventorySchema = z10.object({
11976
+ schemaVersion: z10.literal(1),
10286
11977
  provider: providerSchema,
10287
11978
  observedAt: timestampSchema2,
10288
11979
  sourceBinding: sourceBindingSchema,
10289
- localEnumerationComplete: z8.boolean(),
10290
- truncated: z8.boolean(),
10291
- absencePolicy: z8.enum(["reconcile-observed", "preserve"]),
10292
- realms: z8.array(sourceRealmInventoryEntrySchema).max(128),
10293
- warnings: z8.array(boundedText3(128)).max(32)
11980
+ localEnumerationComplete: z10.boolean(),
11981
+ truncated: z10.boolean(),
11982
+ absencePolicy: z10.enum(["reconcile-observed", "preserve"]),
11983
+ realms: z10.array(sourceRealmInventoryEntrySchema).max(128),
11984
+ warnings: z10.array(boundedText3(128)).max(32)
10294
11985
  }).strict().superRefine((inventory, context) => {
10295
11986
  if (inventory.absencePolicy === "reconcile-observed" && (!inventory.localEnumerationComplete || inventory.truncated)) {
10296
11987
  context.addIssue({
@@ -10313,8 +12004,8 @@ var sourceRealmInventorySchema = z8.object({
10313
12004
  externalIds.add(entry.sourceRealm.externalIdSha256);
10314
12005
  });
10315
12006
  });
10316
- var contactSourceSnapshotSchema = z8.object({
10317
- schemaVersion: z8.literal(1),
12007
+ var contactSourceSnapshotSchema = z10.object({
12008
+ schemaVersion: z10.literal(1),
10318
12009
  provider: providerSchema,
10319
12010
  accountKey: boundedText3(256),
10320
12011
  mode: boundedText3(64),
@@ -10325,8 +12016,8 @@ var contactSourceSnapshotSchema = z8.object({
10325
12016
  observedAt: timestampSchema2,
10326
12017
  sourceBinding: sourceBindingSchema,
10327
12018
  sourceRealm: sourceRealmSchema,
10328
- contacts: z8.array(contactSchema).max(250000),
10329
- interactions: z8.array(interactionSchema).max(250000),
12019
+ contacts: z10.array(contactSchema).max(250000),
12020
+ interactions: z10.array(interactionSchema).max(250000),
10330
12021
  metadata: jsonObjectSchema
10331
12022
  }).strict().superRefine((snapshot, context) => {
10332
12023
  if (snapshot.coverage.absencePolicy === "reconcile-observed" && (!snapshot.coverage.localEnumerationComplete || snapshot.coverage.truncated)) {
@@ -10366,19 +12057,19 @@ function resourceStableKey(snapshot, resource) {
10366
12057
  return `source:${sha256(canonicalJson([snapshot.provider, snapshot.accountKey, resource.type, resource.id]))}`;
10367
12058
  }
10368
12059
  function parseCachedResult(value) {
10369
- return z8.object({
10370
- account_key: z8.string(),
10371
- source_rows: z8.number().int(),
10372
- people_created: z8.number().int(),
10373
- people_matched: z8.number().int(),
10374
- methods_touched: z8.number().int(),
10375
- interactions_touched: z8.number().int(),
10376
- resources_removed: z8.number().int(),
10377
- reconciled: z8.boolean(),
12060
+ return z10.object({
12061
+ account_key: z10.string(),
12062
+ source_rows: z10.number().int(),
12063
+ people_created: z10.number().int(),
12064
+ people_matched: z10.number().int(),
12065
+ methods_touched: z10.number().int(),
12066
+ interactions_touched: z10.number().int(),
12067
+ resources_removed: z10.number().int(),
12068
+ reconciled: z10.boolean(),
10378
12069
  completeness: completenessSchema,
10379
- local_enumeration_complete: z8.boolean(),
10380
- remote_set_complete: z8.boolean(),
10381
- truncated: z8.boolean()
12070
+ local_enumeration_complete: z10.boolean(),
12071
+ remote_set_complete: z10.boolean(),
12072
+ truncated: z10.boolean()
10382
12073
  }).strict().transform((result) => ({ ...result, cached: true })).parse(JSON.parse(value));
10383
12074
  }
10384
12075
  function bindSourceRealm(database, snapshot) {
@@ -16485,7 +18176,7 @@ function plainDataObject(value, label) {
16485
18176
  }
16486
18177
  return descriptors;
16487
18178
  }
16488
- function positiveInteger(value, label, maximum) {
18179
+ function positiveInteger2(value, label, maximum) {
16489
18180
  if (value === undefined)
16490
18181
  return;
16491
18182
  if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1 || value > maximum)
@@ -16507,9 +18198,9 @@ function prepareRequest2(value) {
16507
18198
  const authId = descriptors.authId?.value;
16508
18199
  if (typeof authId !== "string" || !/^[a-z][a-z0-9-]{0,127}$/u.test(authId))
16509
18200
  return fail22("authId must be lowercase kebab text");
16510
- const limitChats = positiveInteger(descriptors.limitChats?.value, "limitChats", 1e5);
16511
- const limitMessages = positiveInteger(descriptors.limitMessages?.value, "limitMessages", 1e6);
16512
- const maxParticipants = positiveInteger(descriptors.maxParticipants?.value, "maxParticipants", 2000);
18201
+ const limitChats = positiveInteger2(descriptors.limitChats?.value, "limitChats", 1e5);
18202
+ const limitMessages = positiveInteger2(descriptors.limitMessages?.value, "limitMessages", 1e6);
18203
+ const maxParticipants = positiveInteger2(descriptors.maxParticipants?.value, "maxParticipants", 2000);
16513
18204
  return Object.freeze({
16514
18205
  authId,
16515
18206
  ...limitChats === undefined ? {} : { limitChats },
@@ -16631,7 +18322,7 @@ function timestamp4(value, label) {
16631
18322
  throw new Error(`${label} must be an ISO timestamp.`);
16632
18323
  return new Date(milliseconds).toISOString();
16633
18324
  }
16634
- function json2(value, label) {
18325
+ function json3(value, label) {
16635
18326
  const encoded = canonicalJson(value);
16636
18327
  if (Buffer.byteLength(encoded, "utf8") > 64 * 1024)
16637
18328
  throw new Error(`${label} exceeds its byte limit.`);
@@ -16657,9 +18348,9 @@ function recordSourceExecutionReceipt(database, input) {
16657
18348
  const completedAt = timestamp4(input.completedAt, "Execution completion");
16658
18349
  if (completedAt !== null && completedAt < startedAt)
16659
18350
  throw new Error("Execution timing is invalid.");
16660
- const usageJson = json2(input.usage, "Execution usage");
16661
- const costJson = json2(input.cost, "Execution cost");
16662
- const metadataJson = json2(input.metadata, "Execution metadata");
18351
+ const usageJson = json3(input.usage, "Execution usage");
18352
+ const costJson = json3(input.cost, "Execution cost");
18353
+ const metadataJson = json3(input.metadata, "Execution metadata");
16663
18354
  const existing = database.query(`SELECT id,provider,account_key,capability,operation,transport,
16664
18355
  implementation_id,implementation_version,implementation_sha256,contract_sha256,
16665
18356
  external_run_id_sha256,input_sha256,outcome,usage_json,cost_json,metadata_json,
@@ -16834,9 +18525,9 @@ var SUMMARY_CONTRACT_SHA256 = sha256(canonicalJson({
16834
18525
  sourceVersion: "1.1.0"
16835
18526
  }
16836
18527
  }));
16837
- function validateBeeperContactInteractionAccounts(summary, connectedAccounts) {
18528
+ function validateBeeperContactInteractionAccounts(summary2, connectedAccounts) {
16838
18529
  const expected = [...connectedAccounts].map((account) => ({ accountId: account.accountId, service: account.service })).sort((left, right) => left.accountId.localeCompare(right.accountId));
16839
- const observed = summary.accounts.map((account) => ({ accountId: account.accountId, service: normalizeBeeperService(account.network) })).sort((left, right) => left.accountId.localeCompare(right.accountId));
18530
+ const observed = summary2.accounts.map((account) => ({ accountId: account.accountId, service: normalizeBeeperService(account.network) })).sort((left, right) => left.accountId.localeCompare(right.accountId));
16840
18531
  if (canonicalJson(observed) !== canonicalJson(expected)) {
16841
18532
  throw new Error("Beeper interaction summary connected-account inventory drifted");
16842
18533
  }
@@ -16909,7 +18600,7 @@ function ledgerBeeperContactInteractionExport(database, accountKey2, result) {
16909
18600
  completedAt: receipt.finishedAt
16910
18601
  });
16911
18602
  }
16912
- function projectBeeperContactInteractions(summary, coordinates) {
18603
+ function projectBeeperContactInteractions(summary2, coordinates) {
16913
18604
  const accounts = new Map;
16914
18605
  for (const coordinate2 of coordinates) {
16915
18606
  const prior = accounts.get(coordinate2.rawAccountId);
@@ -16918,7 +18609,7 @@ function projectBeeperContactInteractions(summary, coordinates) {
16918
18609
  }
16919
18610
  accounts.set(coordinate2.rawAccountId, { accountKey: coordinate2.accountKey, service: coordinate2.service });
16920
18611
  }
16921
- const summaryAccounts = new Map(summary.accounts.map((account) => [
18612
+ const summaryAccounts = new Map(summary2.accounts.map((account) => [
16922
18613
  account.accountId,
16923
18614
  normalizeBeeperService(account.network)
16924
18615
  ]));
@@ -16933,7 +18624,7 @@ function projectBeeperContactInteractions(summary, coordinates) {
16933
18624
  ]));
16934
18625
  const grouped = new Map;
16935
18626
  let matched = 0;
16936
- for (const interaction of summary.interactions) {
18627
+ for (const interaction of summary2.interactions) {
16937
18628
  const coordinate2 = contacts.get(`${interaction.accountId}\x00${interaction.contactId}`);
16938
18629
  if (coordinate2 === undefined)
16939
18630
  continue;
@@ -16954,11 +18645,11 @@ function projectBeeperContactInteractions(summary, coordinates) {
16954
18645
  completeness: interaction.completeness,
16955
18646
  metadata: {
16956
18647
  basis: "wrench-contact-interaction-summary-v1",
16957
- summarySha256: summary.integrity.summarySha256,
18648
+ summarySha256: summary2.integrity.summarySha256,
16958
18649
  sourceVersion: interaction.provenance.sourceVersion,
16959
18650
  providerVersion: interaction.provenance.providerVersion,
16960
- conversationScope: summary.scope.conversations,
16961
- messageScope: summary.scope.messages
18651
+ conversationScope: summary2.scope.conversations,
18652
+ messageScope: summary2.scope.messages
16962
18653
  }
16963
18654
  });
16964
18655
  grouped.set(coordinate2.accountKey, items);
@@ -16969,7 +18660,7 @@ function projectBeeperContactInteractions(summary, coordinates) {
16969
18660
  return {
16970
18661
  accountKey: accountKey2,
16971
18662
  relationshipSourceSha256: sha256(canonicalJson({
16972
- summarySha256: summary.integrity.summarySha256,
18663
+ summarySha256: summary2.integrity.summarySha256,
16973
18664
  accountKey: accountKey2,
16974
18665
  interactions
16975
18666
  })),
@@ -16979,10 +18670,10 @@ function projectBeeperContactInteractions(summary, coordinates) {
16979
18670
  };
16980
18671
  });
16981
18672
  return {
16982
- summarySha256: summary.integrity.summarySha256,
16983
- directRelationships: summary.interactions.length,
18673
+ summarySha256: summary2.integrity.summarySha256,
18674
+ directRelationships: summary2.interactions.length,
16984
18675
  matchedRelationships: matched,
16985
- unmatchedRelationships: summary.interactions.length - matched,
18676
+ unmatchedRelationships: summary2.interactions.length - matched,
16986
18677
  contactReferences: coordinates.length,
16987
18678
  unmatchedContactReferences: coordinates.length - matched,
16988
18679
  groups
@@ -18022,7 +19713,7 @@ var MESSAGING_SEARCH_CONTRACT_SHA256 = BEEPER_WRENCH_COMPATIBILITY.operations["m
18022
19713
  var FINAL_ORIGIN2 = BEEPER_WRENCH_COMPATIBILITY.adapter.origin;
18023
19714
  var MAX_QUERIES = 50;
18024
19715
  var MAX_RESULTS = 20;
18025
- var SHA256 = /^[a-f0-9]{64}$/u;
19716
+ var SHA2562 = /^[a-f0-9]{64}$/u;
18026
19717
  var AUTH_ID2 = /^[a-z][a-z0-9-]{0,127}$/u;
18027
19718
  function record4(value, label) {
18028
19719
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
@@ -18076,7 +19767,7 @@ function integer3(value, label, maximum) {
18076
19767
  }
18077
19768
  function digest4(value, label) {
18078
19769
  const parsed = text3(value, label, 64);
18079
- if (!SHA256.test(parsed))
19770
+ if (!SHA2562.test(parsed))
18080
19771
  throw new Error(`${label} must be a SHA-256 digest`);
18081
19772
  return parsed;
18082
19773
  }
@@ -18305,8 +19996,8 @@ function searchCandidates(account, contacts, conversations) {
18305
19996
  mergeCandidate(candidates, peers[0], "messaging.search");
18306
19997
  }
18307
19998
  return {
18308
- candidates: [...candidates.entries()].map(([id, candidate]) => ({
18309
- id,
19999
+ candidates: [...candidates.entries()].map(([id2, candidate]) => ({
20000
+ id: id2,
18310
20001
  displayName: candidate.displayName,
18311
20002
  username: candidate.username,
18312
20003
  origins: [...candidate.origins].sort()
@@ -19540,10 +21231,10 @@ function googleContactStats(database) {
19540
21231
  // src/local/providers/imessage.ts
19541
21232
  import { Database as Database4 } from "bun:sqlite";
19542
21233
  import { createHash as createHash6 } from "crypto";
19543
- import { lstatSync as lstatSync5, realpathSync as realpathSync5 } from "fs";
21234
+ import { lstatSync as lstatSync6, realpathSync as realpathSync5 } from "fs";
19544
21235
  import { homedir as homedir3 } from "os";
19545
- import { join as join5, resolve as resolve4 } from "path";
19546
- var DEFAULT_IMESSAGE_DATABASE = join5(homedir3(), "Library", "Messages", "chat.db");
21236
+ import { join as join6, resolve as resolve4 } from "path";
21237
+ var DEFAULT_IMESSAGE_DATABASE = join6(homedir3(), "Library", "Messages", "chat.db");
19547
21238
  var DEFAULT_IMESSAGE_ACCOUNT = "imessage-main";
19548
21239
  var DEFAULT_IMESSAGE_PAGE_SIZE = 1000;
19549
21240
  var MAX_DATABASE_BYTES = 4 * 1024 * 1024 * 1024;
@@ -19657,11 +21348,11 @@ function loadHandles(database) {
19657
21348
  const result = new Map;
19658
21349
  for (const row of allRows3(database, "SELECT ROWID,id,service FROM handle ORDER BY ROWID")) {
19659
21350
  const rowid = safeInteger2(row.ROWID, "iMessage handle ROWID");
19660
- const id = boundedText4(row.id, "iMessage handle identity");
21351
+ const id2 = boundedText4(row.id, "iMessage handle identity");
19661
21352
  const service = boundedText4(row.service, "iMessage handle service");
19662
21353
  if (result.has(rowid))
19663
21354
  throw new Error("iMessage handle ROWIDs are duplicated");
19664
- result.set(rowid, { hash: sha2566(id), service, identity: exactMessageHandleIdentity(id) });
21355
+ result.set(rowid, { hash: sha2566(id2), service, identity: exactMessageHandleIdentity(id2) });
19665
21356
  }
19666
21357
  return result;
19667
21358
  }
@@ -19987,7 +21678,7 @@ function syncIMessageRelationships(database, options = {}) {
19987
21678
  if (!Number.isSafeInteger(pageSize) || pageSize < 1 || pageSize > MAX_PAGE_SIZE)
19988
21679
  throw new Error(`iMessage page size must be between 1 and ${MAX_PAGE_SIZE}`);
19989
21680
  const requestedPath = resolve4(options.messagesDatabase ?? DEFAULT_IMESSAGE_DATABASE);
19990
- const identity = lstatSync5(requestedPath);
21681
+ const identity = lstatSync6(requestedPath);
19991
21682
  const uid = typeof process.getuid === "function" ? process.getuid() : null;
19992
21683
  if (!identity.isFile() || identity.isSymbolicLink() || identity.size < 1 || identity.size > MAX_DATABASE_BYTES || uid !== null && identity.uid !== uid) {
19993
21684
  throw new Error("iMessage source must be a supported database owned by the current user");
@@ -20154,12 +21845,12 @@ function validateExtra(bytes, label) {
20154
21845
  while (position < bytes.length) {
20155
21846
  if (bytes.length - position < 4)
20156
21847
  throw new Error(`ZIP ${label} contains a truncated extra field`);
20157
- const id = bytes.readUInt16LE(position);
21848
+ const id2 = bytes.readUInt16LE(position);
20158
21849
  const length = bytes.readUInt16LE(position + 2);
20159
21850
  const next = end(position + 4, length, `${label} extra field`);
20160
21851
  if (next > bytes.length)
20161
21852
  throw new Error(`ZIP ${label} contains a truncated extra field`);
20162
- if (id === ZIP64_EXTRA)
21853
+ if (id2 === ZIP64_EXTRA)
20163
21854
  throw new Error("ZIP64 archives are not supported");
20164
21855
  position = next;
20165
21856
  }
@@ -20603,11 +22294,11 @@ function providerId2(value, label) {
20603
22294
  return String(value);
20604
22295
  }
20605
22296
  function oneProviderId(value, label) {
20606
- const id = providerId2(value.id, `${label}.id`);
22297
+ const id2 = providerId2(value.id, `${label}.id`);
20607
22298
  const userId = providerId2(value.user_id, `${label}.user_id`);
20608
- if (id !== null && userId !== null && id !== userId)
22299
+ if (id2 !== null && userId !== null && id2 !== userId)
20609
22300
  throw new Error(`${label} contains conflicting exact provider IDs`);
20610
- return id ?? userId;
22301
+ return id2 ?? userId;
20611
22302
  }
20612
22303
  function profileHref(value, handle, label) {
20613
22304
  const text5 = bounded(value, label, 2048, true);
@@ -20716,12 +22407,12 @@ function messageItems(member) {
20716
22407
  const handle = optionalUsername(name);
20717
22408
  if (handle !== null)
20718
22409
  names.add(handle);
20719
- const id = oneProviderId(participant, label);
20720
- if (id !== null && ids.has(id))
22410
+ const id2 = oneProviderId(participant, label);
22411
+ if (id2 !== null && ids.has(id2))
20721
22412
  throw new Error(`${label} repeats an ID`);
20722
- if (id !== null)
20723
- ids.add(id);
20724
- participants.push({ providerUserId: id, username: handle });
22413
+ if (id2 !== null)
22414
+ ids.add(id2);
22415
+ participants.push({ providerUserId: id2, username: handle });
20725
22416
  }
20726
22417
  return dense(root.messages, `${member.memberName}.messages`, MAX_MESSAGES).map((value, index) => {
20727
22418
  const label = `${member.memberName}.messages[${index}]`;
@@ -20902,33 +22593,33 @@ function importInstagramArchive(database, path, options) {
20902
22593
  const values = aliases.get(handle);
20903
22594
  return values?.size === 1 ? [...values.values()][0] : null;
20904
22595
  };
20905
- const ensure = (id, href, handle, display) => {
22596
+ const ensure = (id2, href, handle, display) => {
20906
22597
  if (handle === owner)
20907
22598
  return null;
20908
22599
  const canonicalHref = href ?? (handle === null ? null : `https://www.instagram.com/${handle}/`);
20909
- if (id === null && canonicalHref === null)
22600
+ if (id2 === null && canonicalHref === null)
20910
22601
  return null;
20911
- const byId = id === null ? null : getRow6(database, "SELECT id,person_id,username,profile_url FROM provider_resources WHERE provider=? AND account_key=? AND resource_type='user' AND resource_id=?", PROVIDER8, account, id);
22602
+ const byId = id2 === null ? null : getRow6(database, "SELECT id,person_id,username,profile_url FROM provider_resources WHERE provider=? AND account_key=? AND resource_type='user' AND resource_id=?", PROVIDER8, account, id2);
20912
22603
  const byHref = canonicalHref === null ? null : getRow6(database, "SELECT id,person_id,username,profile_url FROM provider_resources WHERE provider=? AND account_key=? AND profile_url=? ORDER BY id LIMIT 1", PROVIDER8, account, canonicalHref);
20913
22604
  if (byId !== null && byHref !== null && byId.person_id !== byHref.person_id)
20914
22605
  throw new Error("Instagram ID and profile URL identify different people");
20915
22606
  let personId = byId?.person_id ?? byHref?.person_id ?? null;
20916
22607
  if (personId === null) {
20917
- personId = insertedId6(run5(database, "INSERT INTO people(stable_key,display_name,metadata_json) VALUES (?,?,?)", `instagram:${sha2567(`${account}\x00${id ?? canonicalHref}`)}`, display ?? handle, stableJson4({ createdBy: "instagram-exact-resource" })));
22608
+ personId = insertedId6(run5(database, "INSERT INTO people(stable_key,display_name,metadata_json) VALUES (?,?,?)", `instagram:${sha2567(`${account}\x00${id2 ?? canonicalHref}`)}`, display ?? handle, stableJson4({ createdBy: "instagram-exact-resource" })));
20918
22609
  peopleCreated += 1;
20919
22610
  } else if (display !== null || handle !== null)
20920
22611
  run5(database, "UPDATE people SET display_name=coalesce(display_name,?) WHERE id=?", display ?? handle, personId);
20921
22612
  peopleTouched.add(personId);
20922
- if (id !== null && byId === null) {
22613
+ if (id2 !== null && byId === null) {
20923
22614
  const prior = allRows4(database, "SELECT resource_id FROM provider_resources WHERE provider=? AND account_key=? AND resource_type='user' AND person_id=?", PROVIDER8, account, personId);
20924
22615
  if (prior.length > 0)
20925
22616
  throw new Error("Instagram profile is bound to conflicting numeric IDs");
20926
22617
  }
20927
22618
  let primary = byId?.id ?? byHref?.id ?? null;
20928
- if (id !== null && byId === null) {
20929
- primary = insertedId6(run5(database, "INSERT INTO provider_resources(provider,account_key,resource_type,resource_id,person_id,username,profile_url,display_name,metadata_json) VALUES (?,?,'user',?,?,?,?,?,?)", PROVIDER8, account, id, personId, handle, canonicalHref, display ?? handle, "{}"));
22619
+ if (id2 !== null && byId === null) {
22620
+ primary = insertedId6(run5(database, "INSERT INTO provider_resources(provider,account_key,resource_type,resource_id,person_id,username,profile_url,display_name,metadata_json) VALUES (?,?,'user',?,?,?,?,?,?)", PROVIDER8, account, id2, personId, handle, canonicalHref, display ?? handle, "{}"));
20930
22621
  identitiesCreated += 1;
20931
- } else if (id === null && canonicalHref !== null && byHref === null) {
22622
+ } else if (id2 === null && canonicalHref !== null && byHref === null) {
20932
22623
  primary = insertedId6(run5(database, "INSERT INTO provider_resources(provider,account_key,resource_type,resource_id,person_id,username,profile_url,display_name,metadata_json) VALUES (?,?,'profile',?,?,?,?,?,?)", PROVIDER8, account, canonicalHref, personId, handle, canonicalHref, display ?? handle, "{}"));
20933
22624
  identitiesCreated += 1;
20934
22625
  }
@@ -20936,7 +22627,7 @@ function importInstagramArchive(database, path, options) {
20936
22627
  throw new Error("Instagram identity was not materialized");
20937
22628
  run5(database, "UPDATE provider_resources SET username=coalesce(?,username),profile_url=coalesce(?,profile_url),profile_url_identity_eligible=CASE WHEN ? IS NULL THEN profile_url_identity_eligible ELSE 1 END,display_name=coalesce(?,display_name),active=1 WHERE id=?", handle, canonicalHref, canonicalHref, display ?? handle, primary);
20938
22629
  touchedResources.add(primary);
20939
- const identity = { personId, primaryResourceId: primary, username: handle, profileHref: canonicalHref, providerUserId: id };
22630
+ const identity = { personId, primaryResourceId: primary, username: handle, profileHref: canonicalHref, providerUserId: id2 };
20940
22631
  observe(handle, identity);
20941
22632
  return identity;
20942
22633
  };
@@ -21233,14 +22924,14 @@ function extraFields(bytes, label) {
21233
22924
  while (position < bytes.length) {
21234
22925
  if (bytes.length - position < 4)
21235
22926
  throw new Error(`X ZIP ${label} contains a truncated extra field`);
21236
- const id = bytes.readUInt16LE(position);
22927
+ const id2 = bytes.readUInt16LE(position);
21237
22928
  const length = bytes.readUInt16LE(position + 2);
21238
22929
  const next = checkedEnd(position + 4, length, `${label} extra field`);
21239
22930
  if (next > bytes.length)
21240
22931
  throw new Error(`X ZIP ${label} contains a truncated extra field`);
21241
- if (fields.has(id))
21242
- throw new Error(`X ZIP ${label} contains duplicate extra field ${id}`);
21243
- fields.set(id, bytes.subarray(position + 4, next));
22932
+ if (fields.has(id2))
22933
+ throw new Error(`X ZIP ${label} contains duplicate extra field ${id2}`);
22934
+ fields.set(id2, bytes.subarray(position + 4, next));
21244
22935
  position = next;
21245
22936
  }
21246
22937
  return fields;
@@ -21663,7 +23354,7 @@ function timestamp5(value, label) {
21663
23354
  throw new Error(`${label} is not a timestamp`);
21664
23355
  return new Date(milliseconds2).toISOString();
21665
23356
  }
21666
- function userLink(value, id, label) {
23357
+ function userLink(value, id2, label) {
21667
23358
  const raw = text5(value, label, 2048, true);
21668
23359
  let parsed;
21669
23360
  try {
@@ -21671,7 +23362,7 @@ function userLink(value, id, label) {
21671
23362
  } catch (error) {
21672
23363
  throw new Error(`${label} is not an X user link`, { cause: error });
21673
23364
  }
21674
- if (parsed.protocol !== "https:" || parsed.username !== "" || parsed.password !== "" || parsed.port !== "" || parsed.hash !== "" || !["twitter.com", "www.twitter.com", "x.com", "www.x.com"].includes(parsed.hostname) || parsed.pathname !== "/intent/user" || parsed.searchParams.size !== 1 || parsed.searchParams.get("user_id") !== id) {
23365
+ if (parsed.protocol !== "https:" || parsed.username !== "" || parsed.password !== "" || parsed.port !== "" || parsed.hash !== "" || !["twitter.com", "www.twitter.com", "x.com", "www.x.com"].includes(parsed.hostname) || parsed.pathname !== "/intent/user" || parsed.searchParams.size !== 1 || parsed.searchParams.get("user_id") !== id2) {
21675
23366
  throw new Error(`${label} does not identify the exported X user ID`);
21676
23367
  }
21677
23368
  }
@@ -21844,11 +23535,11 @@ function parseTweetIdentities(member) {
21844
23535
  const mentionLabel = `${label}.tweet.entities.user_mentions[${mentionIndex}]`;
21845
23536
  const mention = plain2(value2, mentionLabel);
21846
23537
  exactKeys5(mention, REVIEWED_MENTION_KEYS, mentionLabel);
21847
- const id = optionalOpaqueProviderId(mention.id, `${mentionLabel}.id`);
23538
+ const id2 = optionalOpaqueProviderId(mention.id, `${mentionLabel}.id`);
21848
23539
  const idString = optionalOpaqueProviderId(mention.id_str, `${mentionLabel}.id_str`);
21849
- if (id === null || idString === null)
23540
+ if (id2 === null || idString === null)
21850
23541
  throw new Error(`${mentionLabel} must contain both X user ID coordinates`);
21851
- if (id !== idString)
23542
+ if (id2 !== idString)
21852
23543
  throw new Error(`${mentionLabel} X user IDs disagree`);
21853
23544
  if (!PROVIDER_ID2.test(idString))
21854
23545
  continue;
@@ -21928,12 +23619,12 @@ function parseMessages(member, selfId, group, seenMessageIds, conversationMember
21928
23619
  state.participants.add(selfId);
21929
23620
  let directPeerId = null;
21930
23621
  if (!group) {
21931
- const ids = conversationId.split("-").map((id, part) => providerId3(id, `${label}.conversationId[${part}]`));
23622
+ const ids = conversationId.split("-").map((id2, part) => providerId3(id2, `${label}.conversationId[${part}]`));
21932
23623
  if (ids.length !== 2 || !ids.includes(selfId))
21933
23624
  throw new Error(`${label}.conversationId is not bound to the archive account`);
21934
- for (const id of ids)
21935
- state.participants.add(id);
21936
- directPeerId = ids.find((id) => id !== selfId) ?? null;
23625
+ for (const id2 of ids)
23626
+ state.participants.add(id2);
23627
+ directPeerId = ids.find((id2) => id2 !== selfId) ?? null;
21937
23628
  if (directPeerId === null)
21938
23629
  throw new Error(`${label}.conversationId does not identify a direct-message peer`);
21939
23630
  }
@@ -21956,7 +23647,7 @@ function parseMessages(member, selfId, group, seenMessageIds, conversationMember
21956
23647
  state.participants.add(message.senderId);
21957
23648
  if (message.recipientId !== null)
21958
23649
  state.participants.add(message.recipientId);
21959
- if (!group && [...state.participants].some((id) => !conversationId.split("-").includes(id)))
23650
+ if (!group && [...state.participants].some((id2) => !conversationId.split("-").includes(id2)))
21960
23651
  throw new Error(`${eventLabel} names a user outside its direct conversation`);
21961
23652
  let providerUserId = null;
21962
23653
  let direction = null;
@@ -21987,8 +23678,8 @@ function parseMessages(member, selfId, group, seenMessageIds, conversationMember
21987
23678
  occurredAt = message.createdAt;
21988
23679
  state.messageCount += 1;
21989
23680
  } else if (["joinConversation", "participantsJoin", "participantsLeave", "conversationNameUpdate"].includes(eventKind)) {
21990
- for (const id of validateMembershipEvent(event[eventKind], `${eventLabel}.${eventKind}`, eventKind))
21991
- state.participants.add(id);
23681
+ for (const id2 of validateMembershipEvent(event[eventKind], `${eventLabel}.${eventKind}`, eventKind))
23682
+ state.participants.add(id2);
21992
23683
  } else
21993
23684
  throw new Error(`${eventLabel} contains unreviewed event ${eventKind}`);
21994
23685
  if (occurredAt !== null) {
@@ -22005,7 +23696,7 @@ function parseMessages(member, selfId, group, seenMessageIds, conversationMember
22005
23696
  const evidence = [];
22006
23697
  for (const [conversationId, state] of [...combined.entries()].sort(([left], [right]) => left.localeCompare(right))) {
22007
23698
  const conversationSha256 = sha2568(conversationId);
22008
- for (const providerUserId of [...state.participants].filter((id) => id !== selfId).sort()) {
23699
+ for (const providerUserId of [...state.participants].filter((id2) => id2 !== selfId).sort()) {
22009
23700
  const safe = { kind: "dm-participant", memberName: member.memberName, memberRecord: state.memberRecord, providerUserId, conversationSha256, group, messageCount: state.messageCount, firstMessageAt: state.first, lastMessageAt: state.last };
22010
23701
  evidence.push({ ...safe, hash: sha2568(stableJson5(safe)) });
22011
23702
  }
@@ -23293,7 +24984,7 @@ var DEFAULT_WHATSAPP_INTERACTION_PAGE_SIZE = 1000;
23293
24984
  var MAX_PAGES2 = 1e5;
23294
24985
  var MAX_CONTACTS5 = 250000;
23295
24986
  var MAX_INTERACTIONS = 1e6;
23296
- var SHA2562 = /^[a-f0-9]{64}$/u;
24987
+ var SHA2563 = /^[a-f0-9]{64}$/u;
23297
24988
  var DECIMAL = /^(?:0|[1-9][0-9]{0,18})$/u;
23298
24989
  var SUBJECT2 = /^whatsapp:(?:pn:[0-9]{5,20}|lid:[0-9]{5,32})$/u;
23299
24990
  var USER_JID = /^([0-9]{5,20})@s\.whatsapp\.net$/u;
@@ -23351,7 +25042,7 @@ function bool3(value, label) {
23351
25042
  }
23352
25043
  function digest5(value, label) {
23353
25044
  const parsed = text6(value, label, 64);
23354
- if (!SHA2562.test(parsed))
25045
+ if (!SHA2563.test(parsed))
23355
25046
  throw new Error(`${label} is invalid`);
23356
25047
  return parsed;
23357
25048
  }
@@ -23988,7 +25679,7 @@ function syncWhatsAppRelationships(database, options = {}) {
23988
25679
  }
23989
25680
 
23990
25681
  // src/cli/version.ts
23991
- var peoplebladeVersion = "0.1.2";
25682
+ var peoplebladeVersion = "0.2.1";
23992
25683
 
23993
25684
  // src/cli/main.ts
23994
25685
  var usage = `PeopleBlade \u2014 local-first contact intelligence
@@ -24000,6 +25691,13 @@ Core:
24000
25691
  backup [PATH] Create a consistent private SQLite backup
24001
25692
  migrate rolodex --from PATH Back up and migrate a legacy Rolodex database once
24002
25693
  list [--search TEXT] [--limit N] Search the local contact book
25694
+ query [--search TEXT] Page the live contact book (default 50, maximum 100)
25695
+ [--source NAME] [--has-email yes|no] [--has-phone yes|no]
25696
+ [--do-not-contact all|exclude|only] [--sort name|organization|interactions|last-contact]
25697
+ [--direction asc|desc] [--limit N] [--offset N]
25698
+ people show ID Read bounded canonical contact details
25699
+ capabilities Discover supported sources and contracts without a database
25700
+ ui [--port N] Open the local-only CRM workspace (default: random loopback port)
24003
25701
  people add --display-name NAME --confirm
24004
25702
  Create one user-confirmed local person
24005
25703
  [--email EMAIL] [--label NAME] [--note TEXT]
@@ -24009,6 +25707,9 @@ Core:
24009
25707
  List reversible accepted observed decisions
24010
25708
  identity suggest [--kind KIND] Review one evidence-bound possible duplicate
24011
25709
  identity decide TOKEN ACTION Accept, reject, or defer one fresh suggestion
25710
+ identity auto-accept [--kinds LIST] [--source NAME] [--limit N] [--dry-run] [--note TEXT]
25711
+ Accept only unique, warning-free exact evidence and report every skip
25712
+ Default kinds: exact-email,exact-phone,observed-phone
24012
25713
  identity separate DECISION_ID Reverse one current accepted identity decision
24013
25714
  identity attest-email PERSON_ID EMAIL
24014
25715
  Add one explicit, reversible exact email attestation
@@ -24016,7 +25717,7 @@ Core:
24016
25717
  identity retract-email METHOD_ID Retract one explicit email attestation
24017
25718
  identity accept-candidate TOKEN Materialize one reviewed staged provider candidate
24018
25719
 
24019
- Sources:
25720
+ Sources (append --auto-accept to run identity auto-accept after a successful import):
24020
25721
  contacts sync [--directory PATH] Sync macOS Contacts from the local AddressBook database
24021
25722
  beeper sync [--auth ID] Sync connected Beeper accounts sequentially through Wrench
24022
25723
  [--with-relationships] Add body-free lower-bound direct interaction counts
@@ -24048,6 +25749,7 @@ Cloud (optional):
24048
25749
  cloud enrich --confirm TOKEN Launch a preview with the same --person-id values
24049
25750
  cloud enrich --status JOB_ID Read one CLI-owned aggregate enrichment receipt
24050
25751
  cloud enrich --details JOB_ID Read privacy-safe per-run tuning diagnostics
25752
+ cloud enrich --public-emails Read deduplicated publicEmail leftovers from completed runs
24051
25753
  cloud enrich --revalidate-historical
24052
25754
  Revalidate stored results under the current policy
24053
25755
 
@@ -24055,6 +25757,11 @@ Notes (local JSON; not synced to cloud):
24055
25757
  notes add --person-id ID --occurred-at ISO --body TEXT
24056
25758
  Append one dated note [--title TEXT] [--source NAME] [--source-id ID]
24057
25759
  notes list --person-id ID List notes [--since ISO] [--until ISO] [--source NAME]
25760
+ notes show ID Read effective markdown and revision/context fences
25761
+ notes update ID --body-file PATH|- --expected-revision N
25762
+ --expected-context SHA256 --request-id UUID [--title TEXT | --clear-title]
25763
+ Append an idempotent, compare-and-swap note edit
25764
+ notes history ID Read immutable history [--before-revision N] [--limit N]
24058
25765
  notes search QUERY Search note text [--person-id ID] [--since ISO] [--until ISO]
24059
25766
  notes import --from granola.json Attach Granola summaries by exact email/phone, then unique name search; apply a new calendar email only to that unique match
24060
25767
 
@@ -24067,8 +25774,11 @@ Ensoul:
24067
25774
  Write a private, source-bounded person packet
24068
25775
 
24069
25776
  The default database is ${peoplebladeDatabasePath()}`;
25777
+
25778
+ class CliUsageError extends Error {
25779
+ }
24070
25780
  function fail3(message) {
24071
- throw new Error(message);
25781
+ throw new CliUsageError(message);
24072
25782
  }
24073
25783
  function valueAfter(args, name) {
24074
25784
  const index = args.indexOf(name);
@@ -24099,6 +25809,76 @@ function positive(value, label, fallback, maximum) {
24099
25809
  fail3(`${label} is too large.`);
24100
25810
  return parsed;
24101
25811
  }
25812
+ function nonnegative(value, label, fallback, maximum) {
25813
+ if (value === undefined)
25814
+ return fallback;
25815
+ if (!/^(?:0|[1-9][0-9]*)$/u.test(value))
25816
+ fail3(`${label} must be a nonnegative integer.`);
25817
+ const number2 = Number(value);
25818
+ if (!Number.isSafeInteger(number2) || number2 > maximum)
25819
+ fail3(`${label} is too large.`);
25820
+ return number2;
25821
+ }
25822
+ function yesNo(value, label) {
25823
+ if (value === undefined)
25824
+ return;
25825
+ if (value !== "yes" && value !== "no")
25826
+ fail3(`${label} must be yes or no.`);
25827
+ return value === "yes";
25828
+ }
25829
+ async function readNoteMarkdown(path) {
25830
+ const maximum = 65536;
25831
+ if (path !== "-") {
25832
+ const descriptor = openSync9(resolve6(path), constants9.O_RDONLY | constants9.O_NONBLOCK | (constants9.O_NOFOLLOW ?? 0));
25833
+ try {
25834
+ const before = fstatSync8(descriptor);
25835
+ if (!before.isFile() || before.nlink !== 1 || before.size > maximum || typeof process.getuid === "function" && before.uid !== process.getuid()) {
25836
+ fail3("Markdown input must be one owned regular file of at most 65536 bytes.");
25837
+ }
25838
+ const bytes2 = new Uint8Array(maximum + 1);
25839
+ let length2 = 0;
25840
+ for (;; ) {
25841
+ const count = readSync3(descriptor, bytes2, length2, bytes2.length - length2, null);
25842
+ length2 += count;
25843
+ if (length2 > maximum)
25844
+ fail3("Markdown input exceeds 65536 bytes.");
25845
+ if (count === 0)
25846
+ break;
25847
+ }
25848
+ const after = fstatSync8(descriptor);
25849
+ if (after.size !== before.size || after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs || after.ino !== before.ino || after.dev !== before.dev || length2 !== before.size) {
25850
+ fail3("Markdown input changed while it was being read.");
25851
+ }
25852
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes2.subarray(0, length2));
25853
+ } finally {
25854
+ closeSync9(descriptor);
25855
+ }
25856
+ }
25857
+ const reader = Bun.stdin.stream().getReader();
25858
+ const bytes = new Uint8Array(maximum);
25859
+ let length = 0;
25860
+ let done = false;
25861
+ try {
25862
+ for (;; ) {
25863
+ const next = await reader.read();
25864
+ if (next.done) {
25865
+ done = true;
25866
+ break;
25867
+ }
25868
+ if (next.value.byteLength > maximum - length)
25869
+ fail3("Markdown input exceeds 65536 bytes.");
25870
+ bytes.set(next.value, length);
25871
+ length += next.value.byteLength;
25872
+ }
25873
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(0, length));
25874
+ } finally {
25875
+ if (!done)
25876
+ reader.cancel().catch(() => {
25877
+ return;
25878
+ });
25879
+ reader.releaseLock();
25880
+ }
25881
+ }
24102
25882
  function print(value, asJson) {
24103
25883
  if (asJson)
24104
25884
  console.log(JSON.stringify(value, null, 2));
@@ -24107,12 +25887,29 @@ function print(value, asJson) {
24107
25887
  else
24108
25888
  console.log(JSON.stringify(value, null, 2));
24109
25889
  }
25890
+ function flag(args, name) {
25891
+ const index = args.indexOf(name);
25892
+ if (index < 0)
25893
+ return false;
25894
+ args.splice(index, 1);
25895
+ return true;
25896
+ }
25897
+ var AUTO_ACCEPT_SOURCE_COMMANDS = new Set([
25898
+ "contacts sync",
25899
+ "beeper sync",
25900
+ "google sync",
25901
+ "imessage sync",
25902
+ "whatsapp sync",
25903
+ "linkedin import",
25904
+ "x import",
25905
+ "instagram import",
25906
+ "notes import"
25907
+ ]);
24110
25908
  async function main(argv) {
24111
25909
  const args = [...argv];
24112
25910
  const databasePath = valueAfter(args, "--db") ?? peoplebladeDatabasePath();
24113
- const asJson = args.includes("--json");
24114
- if (asJson)
24115
- args.splice(args.indexOf("--json"), 1);
25911
+ const asJson = flag(args, "--json");
25912
+ const autoAccept = flag(args, "--auto-accept");
24116
25913
  if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
24117
25914
  console.log(usage);
24118
25915
  return;
@@ -24124,6 +25921,15 @@ async function main(argv) {
24124
25921
  return;
24125
25922
  }
24126
25923
  const [command, subcommand, ...rest] = args;
25924
+ if (autoAccept && !AUTO_ACCEPT_SOURCE_COMMANDS.has(`${command} ${subcommand}`)) {
25925
+ fail3("--auto-accept is valid only with a local source sync or import command; use `identity auto-accept` directly otherwise.");
25926
+ }
25927
+ if (command === "capabilities") {
25928
+ if (args.length !== 1)
25929
+ fail3("capabilities takes no arguments.");
25930
+ print(peoplebladeCapabilities, true);
25931
+ return;
25932
+ }
24127
25933
  if (command === "init") {
24128
25934
  if (args.length !== 1)
24129
25935
  fail3("init takes no arguments.");
@@ -24146,7 +25952,7 @@ async function main(argv) {
24146
25952
  if (command === "cloud" && subcommand === "signin") {
24147
25953
  if (rest.length)
24148
25954
  fail3(`Unknown argument: ${rest[0]}`);
24149
- const result = await signInCloud(undefined, { onCode: (code, verification) => console.log(`Authorize code ${code}
25955
+ const result = await signInCloud(undefined, { onCode: (code, verification) => console.error(`Authorize code ${code}
24150
25956
  ${verification}`) });
24151
25957
  print(result, asJson);
24152
25958
  return;
@@ -24183,7 +25989,22 @@ ${verification}`) });
24183
25989
  }
24184
25990
  initializeLocalDatabase(databasePath);
24185
25991
  const database = connectLocalDatabase(databasePath);
25992
+ const printImport = (result, json4) => {
25993
+ if (!autoAccept) {
25994
+ print(result, json4);
25995
+ return;
25996
+ }
25997
+ print({ ...result, identityAutoAccept: autoAcceptIdentities(database) }, json4);
25998
+ };
24186
25999
  try {
26000
+ if (command === "ui") {
26001
+ const options = [subcommand, ...rest].filter((item) => item !== undefined);
26002
+ const port = nonnegative(valueAfter(options, "--port"), "--port", 0, 65535);
26003
+ if (options.length)
26004
+ fail3(`Unknown argument: ${options[0]}`);
26005
+ await runLocalWorkspace(database, { port, json: asJson });
26006
+ return;
26007
+ }
24187
26008
  if (command === "list") {
24188
26009
  const options = [subcommand, ...rest].filter((item) => item !== undefined);
24189
26010
  const search = valueAfter(options, "--search") ?? "";
@@ -24193,9 +26014,32 @@ ${verification}`) });
24193
26014
  print(listLocalContacts(database, search, limit), asJson);
24194
26015
  return;
24195
26016
  }
26017
+ if (command === "query") {
26018
+ const options = [subcommand, ...rest].filter((item) => item !== undefined);
26019
+ const search = valueAfter(options, "--search") ?? "";
26020
+ const source = valueAfter(options, "--source");
26021
+ const doNotContact = valueAfter(options, "--do-not-contact") ?? "all";
26022
+ const sort = valueAfter(options, "--sort") ?? "interactions";
26023
+ const direction = valueAfter(options, "--direction") ?? "desc";
26024
+ const hasEmail = yesNo(valueAfter(options, "--has-email"), "--has-email");
26025
+ const hasPhone = yesNo(valueAfter(options, "--has-phone"), "--has-phone");
26026
+ const limit = positive(valueAfter(options, "--limit"), "--limit", 50, 100);
26027
+ const offset = nonnegative(valueAfter(options, "--offset"), "--offset", 0, 1e6);
26028
+ if (options.length)
26029
+ fail3(`Unknown argument: ${options[0]}`);
26030
+ const input = contactQueryInputSchema.parse({ search, source, doNotContact, sort, direction, hasEmail, hasPhone, limit, offset });
26031
+ print(queryLocalContacts(database, input), true);
26032
+ return;
26033
+ }
24196
26034
  if (command === "people") {
26035
+ if (subcommand === "show") {
26036
+ if (rest.length !== 1)
26037
+ fail3("people show requires one ID.");
26038
+ print(getLocalContactDetail(database, positive(rest[0], "ID", 0, Number.MAX_SAFE_INTEGER)), true);
26039
+ return;
26040
+ }
24197
26041
  if (subcommand !== "add")
24198
- fail3("people supports add.");
26042
+ fail3("people supports add or show.");
24199
26043
  const options = [...rest];
24200
26044
  const displayName2 = valueAfter(options, "--display-name") ?? fail3("people add requires --display-name.");
24201
26045
  const email2 = valueAfter(options, "--email");
@@ -24288,6 +26132,32 @@ ${verification}`) });
24288
26132
  print(decideIdentity(database, token3, action, note), asJson);
24289
26133
  return;
24290
26134
  }
26135
+ if (command === "identity" && subcommand === "auto-accept") {
26136
+ const options = [...rest];
26137
+ const kindsValue = valueAfter(options, "--kinds");
26138
+ const ambiguity = valueAfter(options, "--ambiguity");
26139
+ const source = valueAfter(options, "--source");
26140
+ const limit = positive(valueAfter(options, "--limit"), "--limit", 100, 500);
26141
+ const note = valueAfter(options, "--note");
26142
+ const dryRun = flag(options, "--dry-run");
26143
+ if (ambiguity !== undefined && ambiguity !== "unique")
26144
+ fail3("identity auto-accept reviews unique candidates only; --ambiguity must be unique.");
26145
+ if (source !== undefined && !/^[a-z][a-z0-9-]{0,63}$/u.test(source))
26146
+ fail3("--source must be a lowercase provider name.");
26147
+ const kinds = kindsValue === undefined ? undefined : kindsValue.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
26148
+ if (kinds !== undefined && kinds.length === 0)
26149
+ fail3("--kinds requires a comma-separated list of evidence kinds.");
26150
+ if (options.length)
26151
+ fail3(`Unknown argument: ${options[0]}`);
26152
+ print(autoAcceptIdentities(database, {
26153
+ ...kinds === undefined ? {} : { kinds },
26154
+ limit,
26155
+ dryRun,
26156
+ ...source === undefined ? {} : { source },
26157
+ ...note === undefined ? {} : { note }
26158
+ }), asJson);
26159
+ return;
26160
+ }
24291
26161
  if (command === "identity" && subcommand === "separate") {
24292
26162
  const options = [...rest];
24293
26163
  const decisionIdValue = options.shift();
@@ -24362,6 +26232,9 @@ ${verification}`) });
24362
26232
  const statusJobId = valueAfter(options, "--status");
24363
26233
  const detailsJobId = valueAfter(options, "--details");
24364
26234
  const prioritizeValue = valueAfter(options, "--prioritize");
26235
+ const publicEmails = options.includes("--public-emails");
26236
+ if (publicEmails)
26237
+ options.splice(options.indexOf("--public-emails"), 1);
24365
26238
  const revalidateHistorical = options.includes("--revalidate-historical");
24366
26239
  if (revalidateHistorical)
24367
26240
  options.splice(options.indexOf("--revalidate-historical"), 1);
@@ -24371,11 +26244,14 @@ ${verification}`) });
24371
26244
  const personIdValues = valuesAfter(options, "--person-id");
24372
26245
  if (options.length)
24373
26246
  fail3(`Unknown argument: ${options[0]}`);
24374
- if (statusJobId !== undefined && (detailsJobId !== undefined || revalidateHistorical || confirmation !== undefined || prioritizeValue !== undefined || personIdValues.length > 0 || noWait)) {
24375
- fail3("--status cannot be combined with --details, --revalidate-historical, --confirm, --prioritize, --person-id, or --no-wait.");
26247
+ if (statusJobId !== undefined && (detailsJobId !== undefined || publicEmails || revalidateHistorical || confirmation !== undefined || prioritizeValue !== undefined || personIdValues.length > 0 || noWait)) {
26248
+ fail3("--status cannot be combined with --details, --public-emails, --revalidate-historical, --confirm, --prioritize, --person-id, or --no-wait.");
24376
26249
  }
24377
- if (detailsJobId !== undefined && (revalidateHistorical || confirmation !== undefined || prioritizeValue !== undefined || personIdValues.length > 0 || noWait)) {
24378
- fail3("--details cannot be combined with --revalidate-historical, --confirm, --prioritize, --person-id, or --no-wait.");
26250
+ if (detailsJobId !== undefined && (publicEmails || revalidateHistorical || confirmation !== undefined || prioritizeValue !== undefined || personIdValues.length > 0 || noWait)) {
26251
+ fail3("--details cannot be combined with --public-emails, --revalidate-historical, --confirm, --prioritize, --person-id, or --no-wait.");
26252
+ }
26253
+ if (publicEmails && (revalidateHistorical || confirmation !== undefined || prioritizeValue !== undefined || personIdValues.length > 0 || noWait)) {
26254
+ fail3("--public-emails cannot be combined with --revalidate-historical, --confirm, --prioritize, --person-id, or --no-wait.");
24379
26255
  }
24380
26256
  if (revalidateHistorical && (confirmation !== undefined || prioritizeValue !== undefined || personIdValues.length > 0 || noWait)) {
24381
26257
  fail3("--revalidate-historical cannot be combined with --confirm, --prioritize, --person-id, or --no-wait.");
@@ -24388,6 +26264,15 @@ ${verification}`) });
24388
26264
  print(await cloudEnrichmentDetails(detailsJobId), asJson);
24389
26265
  return;
24390
26266
  }
26267
+ if (publicEmails) {
26268
+ const leftovers = await cloudEnrichmentPublicEmails();
26269
+ if (asJson) {
26270
+ print(leftovers, true);
26271
+ return;
26272
+ }
26273
+ print({ count: leftovers.publicEmails.length, sample: leftovers.publicEmails.slice(0, 5) }, false);
26274
+ return;
26275
+ }
24391
26276
  if (revalidateHistorical) {
24392
26277
  print(await revalidateHistoricalCloudEnrichment(), asJson);
24393
26278
  return;
@@ -24416,6 +26301,55 @@ ${verification}`) });
24416
26301
  print({ ...preview, nextCommand: `peopleblade cloud enrich --confirm ${preview.confirmation} ${selectedFlags}` }, asJson);
24417
26302
  return;
24418
26303
  }
26304
+ if (command === "notes" && subcommand === "show") {
26305
+ if (rest.length !== 1)
26306
+ fail3("notes show requires one ID.");
26307
+ print(getPersonNote(database, positive(rest[0], "ID", 0, Number.MAX_SAFE_INTEGER)), true);
26308
+ return;
26309
+ }
26310
+ if (command === "notes" && subcommand === "update") {
26311
+ const options = [...rest];
26312
+ const noteId = positive(options.shift(), "ID", 0, Number.MAX_SAFE_INTEGER);
26313
+ const path = valueAfter(options, "--body-file") ?? fail3("notes update requires --body-file PATH or -.");
26314
+ const revisionValue = valueAfter(options, "--expected-revision") ?? fail3("notes update requires --expected-revision.");
26315
+ const expectedRevision = nonnegative(revisionValue, "--expected-revision", 0, Number.MAX_SAFE_INTEGER);
26316
+ const expectedContextSha256 = valueAfter(options, "--expected-context") ?? fail3("notes update requires --expected-context.");
26317
+ const requestId = valueAfter(options, "--request-id") ?? fail3("notes update requires --request-id.");
26318
+ const title = valueAfter(options, "--title");
26319
+ const clearTitle = flag(options, "--clear-title");
26320
+ if (title !== undefined && clearTitle)
26321
+ fail3("--title and --clear-title are mutually exclusive.");
26322
+ if (options.length)
26323
+ fail3(`Unknown argument: ${options[0]}`);
26324
+ let currentTitle;
26325
+ if (title !== undefined)
26326
+ currentTitle = title;
26327
+ else if (clearTitle)
26328
+ currentTitle = null;
26329
+ else {
26330
+ if (expectedRevision === Number.MAX_SAFE_INTEGER)
26331
+ fail3("Expected note revision is unavailable.");
26332
+ const expected = listPersonNoteRevisions(database, { noteId, beforeRevision: expectedRevision + 1, limit: 1 }).items[0];
26333
+ if (expected?.revision !== expectedRevision)
26334
+ throw new NoteRevisionError("revision_conflict", "Expected note revision is unavailable. Reload the note before saving.");
26335
+ currentTitle = expected.title;
26336
+ }
26337
+ const body = await readNoteMarkdown(path);
26338
+ print(revisePersonNote(database, { noteId, expectedRevision, expectedContextSha256, requestId, title: currentTitle, body }), true);
26339
+ return;
26340
+ }
26341
+ if (command === "notes" && subcommand === "history") {
26342
+ const options = [...rest];
26343
+ const noteId = positive(options.shift(), "ID", 0, Number.MAX_SAFE_INTEGER);
26344
+ const before = valueAfter(options, "--before-revision");
26345
+ const limit = positive(valueAfter(options, "--limit"), "--limit", 50, 100);
26346
+ if (options.length)
26347
+ fail3(`Unknown argument: ${options[0]}`);
26348
+ print(listPersonNoteRevisions(database, { noteId, limit, ...before === undefined ? {} : {
26349
+ beforeRevision: positive(before, "--before-revision", 0, Number.MAX_SAFE_INTEGER)
26350
+ } }), true);
26351
+ return;
26352
+ }
24419
26353
  if (command === "notes" && subcommand === "add") {
24420
26354
  const options = [...rest];
24421
26355
  const personIdValue = valueAfter(options, "--person-id") ?? fail3("notes add requires --person-id.");
@@ -24482,14 +26416,14 @@ ${verification}`) });
24482
26416
  if (options.length)
24483
26417
  fail3(`Unknown argument: ${options[0]}`);
24484
26418
  const payload = from === "-" ? parseGranolaImportJson(await new Response(Bun.stdin).text()) : readGranolaImportFile(resolve6(from));
24485
- print(importGranolaMeetings(database, payload), true);
26419
+ printImport(importGranolaMeetings(database, payload), true);
24486
26420
  return;
24487
26421
  }
24488
26422
  if (command === "research" && subcommand === "prepare") {
24489
- const id = positive(rest[0], "PERSON_ID", 0, Number.MAX_SAFE_INTEGER);
26423
+ const id2 = positive(rest[0], "PERSON_ID", 0, Number.MAX_SAFE_INTEGER);
24490
26424
  if (rest.length !== 1)
24491
26425
  fail3("research prepare requires one person ID.");
24492
- print(researchTemplate(database, id), true);
26426
+ print(researchTemplate(database, id2), true);
24493
26427
  return;
24494
26428
  }
24495
26429
  if (command === "research" && subcommand === "apply") {
@@ -24519,7 +26453,7 @@ ${verification}`) });
24519
26453
  options.splice(options.indexOf("--preserve"), 1);
24520
26454
  if (options.length)
24521
26455
  fail3(`Unknown argument: ${options[0]}`);
24522
- print(syncAppleContacts(database, {
26456
+ printImport(syncAppleContacts(database, {
24523
26457
  ...contactsDirectory === undefined ? {} : { contactsDirectory },
24524
26458
  ...accountKey5 === undefined ? {} : { accountKey: accountKey5 },
24525
26459
  reconcile: !preserve
@@ -24539,7 +26473,7 @@ ${verification}`) });
24539
26473
  if (withRelationships)
24540
26474
  console.error("Beeper relationships \xB7 deriving a body-free interaction summary through Wrench");
24541
26475
  console.error("Beeper contacts \xB7 discovering connected accounts through Wrench");
24542
- print(syncBeeperContacts(database, {
26476
+ printImport(syncBeeperContacts(database, {
24543
26477
  authId: effectiveAuthId,
24544
26478
  contactLimit,
24545
26479
  withRelationships,
@@ -24593,7 +26527,7 @@ ${verification}`) });
24593
26527
  const pageSize = positive(valueAfter(options, "--page-size"), "--page-size", 100, 100);
24594
26528
  if (options.length)
24595
26529
  fail3(`Unknown argument: ${options[0]}`);
24596
- print(syncGoogleContacts(database, { ...authId3 === undefined ? {} : { authId: authId3 }, pageSize }), asJson);
26530
+ printImport(syncGoogleContacts(database, { ...authId3 === undefined ? {} : { authId: authId3 }, pageSize }), asJson);
24597
26531
  return;
24598
26532
  }
24599
26533
  if (command === "google" && subcommand === "stats") {
@@ -24609,7 +26543,7 @@ ${verification}`) });
24609
26543
  const pageSize = positive(valueAfter(options, "--page-size"), "--page-size", 1000, 5000);
24610
26544
  if (options.length)
24611
26545
  fail3(`Unknown argument: ${options[0]}`);
24612
- print(syncIMessageRelationships(database, {
26546
+ printImport(syncIMessageRelationships(database, {
24613
26547
  ...messagesDatabase === undefined ? {} : { messagesDatabase },
24614
26548
  ...accountKey5 === undefined ? {} : { accountKey: accountKey5 },
24615
26549
  pageSize
@@ -24623,7 +26557,7 @@ ${verification}`) });
24623
26557
  const interactionPageSize = positive(valueAfter(options, "--interaction-page-size"), "--interaction-page-size", 1000, 1000);
24624
26558
  if (options.length)
24625
26559
  fail3(`Unknown argument: ${options[0]}`);
24626
- print(syncWhatsAppRelationships(database, {
26560
+ printImport(syncWhatsAppRelationships(database, {
24627
26561
  ...authId3 === undefined ? {} : { authId: authId3 },
24628
26562
  contactPageSize,
24629
26563
  interactionPageSize
@@ -24639,7 +26573,7 @@ ${verification}`) });
24639
26573
  const ownerProfileUrl = valueAfter(options, "--owner-profile-url");
24640
26574
  if (options.length)
24641
26575
  fail3(`Unknown argument: ${options[0]}`);
24642
- print(importLinkedInArchive(database, archive, {
26576
+ printImport(importLinkedInArchive(database, archive, {
24643
26577
  ...accountKey5 === undefined ? {} : { accountKey: accountKey5 },
24644
26578
  ...ownerProfileUrl === undefined ? {} : { ownerProfileUrl }
24645
26579
  }), asJson);
@@ -24659,7 +26593,7 @@ ${verification}`) });
24659
26593
  const accountKey5 = valueAfter(options, "--account");
24660
26594
  if (options.length)
24661
26595
  fail3(`Unknown argument: ${options[0]}`);
24662
- print(await importXArchive(database, archive, { ...accountKey5 === undefined ? {} : { accountKey: accountKey5 } }), asJson);
26596
+ printImport(await importXArchive(database, archive, { ...accountKey5 === undefined ? {} : { accountKey: accountKey5 } }), asJson);
24663
26597
  return;
24664
26598
  }
24665
26599
  if (command === "x" && subcommand === "stats") {
@@ -24677,7 +26611,7 @@ ${verification}`) });
24677
26611
  const accountKey5 = valueAfter(options, "--account");
24678
26612
  if (options.length)
24679
26613
  fail3(`Unknown argument: ${options[0]}`);
24680
- print(importInstagramArchive(database, archive, { ownerUsername, ...accountKey5 === undefined ? {} : { accountKey: accountKey5 } }), asJson);
26614
+ printImport(importInstagramArchive(database, archive, { ownerUsername, ...accountKey5 === undefined ? {} : { accountKey: accountKey5 } }), asJson);
24681
26615
  return;
24682
26616
  }
24683
26617
  if (command === "instagram" && subcommand === "stats") {
@@ -24700,6 +26634,22 @@ ${usage}`);
24700
26634
  }
24701
26635
  }
24702
26636
  main(process.argv.slice(2)).catch((error) => {
24703
- console.error(`peopleblade: ${error instanceof Error ? error.message : "unexpected failure"}`);
26637
+ const message = error instanceof Error ? error.message.slice(0, 2000) : "Unexpected failure.";
26638
+ if (process.argv.slice(2).includes("--json")) {
26639
+ console.error(JSON.stringify({
26640
+ schemaVersion: "peopleblade.error.v1",
26641
+ error: {
26642
+ code: error instanceof CliUsageError ? "invalid_arguments" : error instanceof ZodError ? "invalid_input" : error instanceof NoteRevisionError ? error.code : error instanceof CloudEnrichmentReadError ? "enrichment_status_unavailable" : "operation_failed",
26643
+ message,
26644
+ ...error instanceof CloudEnrichmentReadError ? {
26645
+ jobId: error.jobId,
26646
+ dispatchStatus: error.dispatchStatus,
26647
+ nextCommand: error.nextCommand,
26648
+ dispatchRetrySafe: false
26649
+ } : {}
26650
+ }
26651
+ }));
26652
+ } else
26653
+ console.error(`peopleblade: ${message}`);
24704
26654
  process.exitCode = 1;
24705
26655
  });