@hraness/peopleblade 0.3.4 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,7 +15,7 @@ Installation includes the pinned runtime dependencies Zod 4.4.3 and Effect 3.22.
15
15
  ## Install
16
16
 
17
17
  ```bash
18
- bun add --global @hraness/peopleblade@0.3.4
18
+ bun add --global @hraness/peopleblade@0.3.5
19
19
  peopleblade --version
20
20
  peopleblade init
21
21
  peopleblade stats --json
@@ -51,7 +51,7 @@ execution runtime.
51
51
  path. When both variables are set, they must name the same executable. The
52
52
  bridge still requires exact Ghostget 0.17.3 before invoking a provider operation.
53
53
 
54
- PeopleBlade 0.3.4 contains the reviewed Ghostget-backed Beeper, Google, and WhatsApp
54
+ PeopleBlade 0.3.5 contains the reviewed Ghostget-backed Beeper, Google, and WhatsApp
55
55
  live sync commands. Running them requires the separately installed public
56
56
  `@hraness/ghostget` 0.17.3 package. Beeper support is bounded to account-aware
57
57
  `contacts.list@3` reads, `contacts.search@1`, `messaging.search@2`, and the separate
@@ -59,7 +59,9 @@ body-free interaction exporter. `contacts.search@1` executes the official Beeper
59
59
  CLI 0.6.2 executable pinned by Ghostget; `contacts.list@3` and `messaging.search@2`
60
60
  use Ghostget's bounded Desktop-loopback read paths. The exporter is macOS arm64 only
61
61
  and reports lower-bound interaction counts from complete one-to-one conversations
62
- only. See
62
+ only. If a history participant is absent from the current contact page, PeopleBlade
63
+ preserves its exact account-scoped coordinate as a relationship-only resource; it
64
+ does not join that resource by name or another fuzzy field. See
63
65
  [peopleblade.com/sources](https://peopleblade.com/sources) for the complete provider
64
66
  guide. A database previously synced by PeopleBlade 0.1.1 through Wrench 0.16.7
65
67
  must run `peopleblade beeper rebind --auth beeper-main --confirm --json` once after
@@ -3,8 +3,8 @@
3
3
  var __require = import.meta.require;
4
4
 
5
5
  // src/cli/main.ts
6
- import { closeSync as closeSync9, constants as constants9, existsSync as existsSync5, fstatSync as fstatSync8, openSync as openSync9, readSync as readSync3 } from "fs";
7
- import { resolve as resolve6 } from "path";
6
+ import { closeSync as closeSync9, constants as constants9, existsSync as existsSync6, fstatSync as fstatSync8, mkdirSync as mkdirSync3, openSync as openSync9, readSync as readSync3 } from "fs";
7
+ import { join as join7, resolve as resolve7 } from "path";
8
8
  import { ZodError } from "zod";
9
9
 
10
10
  // src/local/contacts.ts
@@ -11733,6 +11733,57 @@ function addConfirmedPerson(database, args) {
11733
11733
  }
11734
11734
  }
11735
11735
 
11736
+ // src/local/menubar.ts
11737
+ import { existsSync as existsSync3 } from "fs";
11738
+ import { dirname as dirname4, resolve as resolve3 } from "path";
11739
+ var SETTLE_MS = 400;
11740
+ function resolveMenubarBinary(environment = process.env) {
11741
+ const candidates = [
11742
+ environment.PEOPLEBLADE_DESKTOP,
11743
+ resolve3(dirname4(process.execPath), "peopleblade-menubar"),
11744
+ resolve3(import.meta.dir, "../../desktop/target/release/peopleblade-menubar"),
11745
+ resolve3(import.meta.dir, "../../desktop/target/debug/peopleblade-menubar")
11746
+ ];
11747
+ for (const candidate of candidates) {
11748
+ if (candidate !== undefined && candidate !== "" && existsSync3(candidate))
11749
+ return candidate;
11750
+ }
11751
+ return null;
11752
+ }
11753
+ async function launchMenubar(asJson) {
11754
+ const binary = resolveMenubarBinary();
11755
+ if (binary === null) {
11756
+ const message = "The PeopleBlade menu bar is not installed. Build it with `cargo build --release --manifest-path desktop/Cargo.toml` or set PEOPLEBLADE_DESKTOP.";
11757
+ if (asJson)
11758
+ console.log(JSON.stringify({ error: message }));
11759
+ else
11760
+ console.error(message);
11761
+ return 1;
11762
+ }
11763
+ let child;
11764
+ try {
11765
+ child = Bun.spawn([binary], { stdin: "ignore", stdout: "ignore", stderr: "pipe" });
11766
+ } catch {
11767
+ console.error("The PeopleBlade menu bar could not start.");
11768
+ return 1;
11769
+ }
11770
+ child.unref();
11771
+ const settled = await Promise.race([
11772
+ child.exited.then((code) => code),
11773
+ Bun.sleep(SETTLE_MS).then(() => null)
11774
+ ]);
11775
+ if (settled !== null && settled !== 0) {
11776
+ console.error("The PeopleBlade menu bar exited during startup.");
11777
+ return 1;
11778
+ }
11779
+ const alreadyRunning = settled === 0;
11780
+ if (asJson)
11781
+ console.log(JSON.stringify({ running: true, alreadyRunning }));
11782
+ else
11783
+ console.log(alreadyRunning ? "PeopleBlade menu bar is already running." : "PeopleBlade menu bar is running.");
11784
+ return 0;
11785
+ }
11786
+
11736
11787
  // src/local/providers/apple.ts
11737
11788
  import { Database as Database3 } from "bun:sqlite";
11738
11789
  import { createHash as createHash3 } from "crypto";
@@ -11742,7 +11793,7 @@ import {
11742
11793
  realpathSync as realpathSync4
11743
11794
  } from "fs";
11744
11795
  import { homedir as homedir2 } from "os";
11745
- import { join as join5, resolve as resolve3 } from "path";
11796
+ import { join as join5, resolve as resolve4 } from "path";
11746
11797
  var EMAIL_PATTERN2 = /^(?=[\x21-\x7E]+$)[^@\s]+@[^@\s]+\.[^@\s]+$/u;
11747
11798
  function clean(value) {
11748
11799
  if (typeof value !== "string")
@@ -11945,7 +11996,7 @@ function rowsByOwner(database, table, names) {
11945
11996
  return grouped;
11946
11997
  }
11947
11998
  function sourceDatabasePaths(directory) {
11948
- const base = realpathSync4(resolve3(directory));
11999
+ const base = realpathSync4(resolve4(directory));
11949
12000
  const sourceDirectory = join5(base, "Sources");
11950
12001
  const sourceIdentity = lstatSync5(sourceDirectory);
11951
12002
  if (!sourceIdentity.isDirectory() || sourceIdentity.isSymbolicLink()) {
@@ -12363,7 +12414,8 @@ var methodSchema = z10.object({
12363
12414
  var contactSchema = z10.object({
12364
12415
  person: personSchema,
12365
12416
  resource: resourceSchema,
12366
- methods: z10.array(methodSchema).max(1000)
12417
+ methods: z10.array(methodSchema).max(1000),
12418
+ existingIdentityPolicy: z10.literal("preserve").optional()
12367
12419
  }).strict().superRefine((contact, context) => {
12368
12420
  const coordinates = new Set;
12369
12421
  contact.methods.forEach((method, index) => {
@@ -12633,6 +12685,7 @@ function materializeContactSourceSnapshot(database, snapshot, sourceRealmId, sou
12633
12685
  ordinal += 1;
12634
12686
  const prior = getRow3(database, `SELECT id,person_id,source_realm_id FROM provider_resources
12635
12687
  WHERE provider=? AND account_key=? AND resource_type=? AND resource_id=?`, snapshot.provider, snapshot.accountKey, contact.resource.type, contact.resource.id);
12688
+ const preserveIdentity = prior !== null && contact.existingIdentityPolicy === "preserve";
12636
12689
  let personId;
12637
12690
  let providerResourceId;
12638
12691
  if (prior === null) {
@@ -12652,47 +12705,54 @@ function materializeContactSourceSnapshot(database, snapshot, sourceRealmId, sou
12652
12705
  }
12653
12706
  personId = prior.person_id;
12654
12707
  providerResourceId = prior.id;
12655
- const person = contact.person;
12656
- run2(database, `UPDATE people SET display_name=?,given_name=?,middle_name=?,family_name=?,name_prefix=?,
12657
- name_suffix=?,nickname=?,organization=?,department=?,title=?,birthday=?,metadata_json=? WHERE id=?`, person.displayName, person.givenName, person.middleName, person.familyName, person.namePrefix, person.nameSuffix, person.nickname, person.organization, person.department, person.title, person.birthday, canonicalJson(person.metadata), personId);
12658
- run2(database, `UPDATE provider_resources SET username=?,profile_url=?,profile_url_identity_eligible=?,name_identity_eligible=?,display_name=?,active=1,
12659
- last_seen_run_id=?,metadata_json=?,source_realm_id=coalesce(source_realm_id,?) WHERE id=?`, contact.resource.username, contact.resource.profileUrl, contact.resource.profileUrlIdentityEligible ? 1 : 0, contact.resource.nameIdentityEligible ? 1 : 0, contact.resource.displayName, sourceRunId, canonicalJson(contact.resource.metadata), sourceRealmId, providerResourceId);
12660
- }
12661
- run2(database, "UPDATE contact_methods SET active=0 WHERE provider_resource_id=?", providerResourceId);
12662
- for (const method of contact.methods) {
12663
- run2(database, `INSERT INTO contact_methods(
12664
- person_id,kind,value,normalized_value,label,is_primary,confidence,first_seen_run_id,last_seen_run_id,
12665
- metadata_json,provider_resource_id,active,identity_eligible
12666
- ) VALUES (?,?,?,?,?,?,?,?,?,?,?,1,?)
12667
- ON CONFLICT(person_id,kind,normalized_value,label) DO UPDATE SET
12668
- value=excluded.value,is_primary=excluded.is_primary,confidence=excluded.confidence,
12669
- last_seen_run_id=excluded.last_seen_run_id,metadata_json=excluded.metadata_json,
12670
- provider_resource_id=excluded.provider_resource_id,active=1,
12671
- identity_eligible=excluded.identity_eligible`, personId, method.kind, method.value, method.normalizedValue, method.label, method.primary ? 1 : 0, method.confidence, sourceRunId, sourceRunId, canonicalJson(method.metadata), providerResourceId, method.identityEligible ? 1 : 0);
12672
- }
12673
- run2(database, "UPDATE person_field_observations SET active=0 WHERE provider_resource_id=?", providerResourceId);
12674
- const observedFields = [
12675
- ["display_name", contact.person.displayName],
12676
- ["given_name", contact.person.givenName],
12677
- ["middle_name", contact.person.middleName],
12678
- ["family_name", contact.person.familyName],
12679
- ["name_prefix", contact.person.namePrefix],
12680
- ["name_suffix", contact.person.nameSuffix],
12681
- ["nickname", contact.person.nickname],
12682
- ["organization", contact.person.organization],
12683
- ["department", contact.person.department],
12684
- ["title", contact.person.title],
12685
- ["birthday", contact.person.birthday]
12686
- ];
12687
- for (const [field, fieldValue] of observedFields) {
12688
- if (fieldValue === null)
12689
- continue;
12690
- run2(database, `INSERT INTO person_field_observations(
12691
- provider_resource_id,field,value,basis,priority,active,first_seen_run_id,last_seen_run_id,metadata_json
12692
- ) VALUES (?,?,?,?,?,1,?,?,?)
12693
- ON CONFLICT(provider_resource_id,field,value,basis) DO UPDATE SET
12694
- priority=excluded.priority,active=1,last_seen_run_id=excluded.last_seen_run_id,
12695
- metadata_json=excluded.metadata_json`, providerResourceId, field, fieldValue, contact.person.observationBasis, contact.person.observationPriority, sourceRunId, sourceRunId, canonicalJson(contact.person.metadata));
12708
+ if (preserveIdentity) {
12709
+ run2(database, `UPDATE provider_resources SET active=1,last_seen_run_id=?,
12710
+ source_realm_id=coalesce(source_realm_id,?) WHERE id=?`, sourceRunId, sourceRealmId, providerResourceId);
12711
+ } else {
12712
+ const person = contact.person;
12713
+ run2(database, `UPDATE people SET display_name=?,given_name=?,middle_name=?,family_name=?,name_prefix=?,
12714
+ name_suffix=?,nickname=?,organization=?,department=?,title=?,birthday=?,metadata_json=? WHERE id=?`, person.displayName, person.givenName, person.middleName, person.familyName, person.namePrefix, person.nameSuffix, person.nickname, person.organization, person.department, person.title, person.birthday, canonicalJson(person.metadata), personId);
12715
+ run2(database, `UPDATE provider_resources SET username=?,profile_url=?,profile_url_identity_eligible=?,name_identity_eligible=?,display_name=?,active=1,
12716
+ last_seen_run_id=?,metadata_json=?,source_realm_id=coalesce(source_realm_id,?) WHERE id=?`, contact.resource.username, contact.resource.profileUrl, contact.resource.profileUrlIdentityEligible ? 1 : 0, contact.resource.nameIdentityEligible ? 1 : 0, contact.resource.displayName, sourceRunId, canonicalJson(contact.resource.metadata), sourceRealmId, providerResourceId);
12717
+ }
12718
+ }
12719
+ if (!preserveIdentity) {
12720
+ run2(database, "UPDATE contact_methods SET active=0 WHERE provider_resource_id=?", providerResourceId);
12721
+ for (const method of contact.methods) {
12722
+ run2(database, `INSERT INTO contact_methods(
12723
+ person_id,kind,value,normalized_value,label,is_primary,confidence,first_seen_run_id,last_seen_run_id,
12724
+ metadata_json,provider_resource_id,active,identity_eligible
12725
+ ) VALUES (?,?,?,?,?,?,?,?,?,?,?,1,?)
12726
+ ON CONFLICT(person_id,kind,normalized_value,label) DO UPDATE SET
12727
+ value=excluded.value,is_primary=excluded.is_primary,confidence=excluded.confidence,
12728
+ last_seen_run_id=excluded.last_seen_run_id,metadata_json=excluded.metadata_json,
12729
+ provider_resource_id=excluded.provider_resource_id,active=1,
12730
+ identity_eligible=excluded.identity_eligible`, personId, method.kind, method.value, method.normalizedValue, method.label, method.primary ? 1 : 0, method.confidence, sourceRunId, sourceRunId, canonicalJson(method.metadata), providerResourceId, method.identityEligible ? 1 : 0);
12731
+ }
12732
+ run2(database, "UPDATE person_field_observations SET active=0 WHERE provider_resource_id=?", providerResourceId);
12733
+ const observedFields = [
12734
+ ["display_name", contact.person.displayName],
12735
+ ["given_name", contact.person.givenName],
12736
+ ["middle_name", contact.person.middleName],
12737
+ ["family_name", contact.person.familyName],
12738
+ ["name_prefix", contact.person.namePrefix],
12739
+ ["name_suffix", contact.person.nameSuffix],
12740
+ ["nickname", contact.person.nickname],
12741
+ ["organization", contact.person.organization],
12742
+ ["department", contact.person.department],
12743
+ ["title", contact.person.title],
12744
+ ["birthday", contact.person.birthday]
12745
+ ];
12746
+ for (const [field, fieldValue] of observedFields) {
12747
+ if (fieldValue === null)
12748
+ continue;
12749
+ run2(database, `INSERT INTO person_field_observations(
12750
+ provider_resource_id,field,value,basis,priority,active,first_seen_run_id,last_seen_run_id,metadata_json
12751
+ ) VALUES (?,?,?,?,?,1,?,?,?)
12752
+ ON CONFLICT(provider_resource_id,field,value,basis) DO UPDATE SET
12753
+ priority=excluded.priority,active=1,last_seen_run_id=excluded.last_seen_run_id,
12754
+ metadata_json=excluded.metadata_json`, providerResourceId, field, fieldValue, contact.person.observationBasis, contact.person.observationPriority, sourceRunId, sourceRunId, canonicalJson(contact.person.metadata));
12755
+ }
12696
12756
  }
12697
12757
  if (writeSourceRecords) {
12698
12758
  run2(database, `INSERT INTO source_records(
@@ -12768,7 +12828,7 @@ function importContactSourceSnapshot(database, value) {
12768
12828
  source_rows: snapshot.contacts.length,
12769
12829
  people_created: peopleCreated,
12770
12830
  people_matched: peopleMatched,
12771
- methods_touched: snapshot.contacts.reduce((sum, contact) => sum + contact.methods.length, 0),
12831
+ methods_touched: snapshot.contacts.reduce((sum, contact) => sum + (contact.existingIdentityPolicy === "preserve" && existingCoordinates.has(`${contact.resource.type}\x00${contact.resource.id}`) ? 0 : contact.methods.length), 0),
12772
12832
  interactions_touched: snapshot.interactions.length,
12773
12833
  resources_removed: resourcesRemoved,
12774
12834
  reconciled: reconcile,
@@ -12838,7 +12898,7 @@ function sha2563(value) {
12838
12898
 
12839
12899
  // node_modules/@hraness/ghostget/dist/client.js
12840
12900
  import { spawn, spawnSync } from "child_process";
12841
- import { existsSync as existsSync3 } from "fs";
12901
+ import { existsSync as existsSync4 } from "fs";
12842
12902
  import { fileURLToPath as fileURLToPath2 } from "url";
12843
12903
  import { types as nodeTypes } from "util";
12844
12904
  var PORTABLE_OPERATION_IDENTITY_VERSION = 1;
@@ -13136,10 +13196,10 @@ function boundedMessage(value) {
13136
13196
  }
13137
13197
  function cliSourcePath() {
13138
13198
  const besideSource = fileURLToPath2(new URL("./cli.ts", import.meta.url));
13139
- if (existsSync3(besideSource))
13199
+ if (existsSync4(besideSource))
13140
13200
  return besideSource;
13141
13201
  const packagedSource = fileURLToPath2(new URL("../src/cli.ts", import.meta.url));
13142
- if (existsSync3(packagedSource))
13202
+ if (existsSync4(packagedSource))
13143
13203
  return packagedSource;
13144
13204
  throw new Error("the installed Ghostget CLI source is unavailable");
13145
13205
  }
@@ -14150,7 +14210,7 @@ var GHOSTGET_VERSION = "0.17.3";
14150
14210
 
14151
14211
  // node_modules/@hraness/ghostget/dist/beeper-client.js
14152
14212
  import { spawnSync as spawnSync2 } from "child_process";
14153
- import { existsSync as existsSync4 } from "fs";
14213
+ import { existsSync as existsSync5 } from "fs";
14154
14214
  import { fileURLToPath as fileURLToPath3 } from "url";
14155
14215
  import { types as nodeTypes3 } from "util";
14156
14216
  import { types as nodeTypes2 } from "util";
@@ -18596,10 +18656,10 @@ function fail22(message) {
18596
18656
  }
18597
18657
  function cliSourcePath2() {
18598
18658
  const besideSource = fileURLToPath3(new URL("./cli.ts", import.meta.url));
18599
- if (existsSync4(besideSource))
18659
+ if (existsSync5(besideSource))
18600
18660
  return besideSource;
18601
18661
  const packagedSource = fileURLToPath3(new URL("../src/cli.ts", import.meta.url));
18602
- if (existsSync4(packagedSource))
18662
+ if (existsSync5(packagedSource))
18603
18663
  return packagedSource;
18604
18664
  return fail22("the installed Ghostget CLI source is unavailable");
18605
18665
  }
@@ -19674,6 +19734,28 @@ function contactMethods(contact, service) {
19674
19734
  });
19675
19735
  return methods;
19676
19736
  }
19737
+ function contactsWithRelationshipParticipants(directoryContacts, interactions) {
19738
+ const contacts = [...directoryContacts];
19739
+ const known = new Set(contacts.map((contact) => contact.id));
19740
+ const relationshipOnlyIds = new Set;
19741
+ for (const interaction of [...interactions].sort((left, right) => left.contactId.localeCompare(right.contactId))) {
19742
+ if (known.has(interaction.contactId))
19743
+ continue;
19744
+ contacts.push({
19745
+ accountId: interaction.accountId,
19746
+ id: interaction.contactId,
19747
+ fullName: null,
19748
+ username: null,
19749
+ phoneNumber: null,
19750
+ email: null,
19751
+ isSelf: false,
19752
+ cannotMessage: null
19753
+ });
19754
+ known.add(interaction.contactId);
19755
+ relationshipOnlyIds.add(interaction.contactId);
19756
+ }
19757
+ return { contacts, relationshipOnlyIds };
19758
+ }
19677
19759
  function sameExecution(left, right) {
19678
19760
  return left.authId === right.authId && left.authSha256 === right.authSha256 && left.adapterSha256 === right.adapterSha256 && left.contractSha256 === right.contractSha256;
19679
19761
  }
@@ -19986,8 +20068,12 @@ function syncBeeperContacts(database, options = {}) {
19986
20068
  if (pageAccount === undefined)
19987
20069
  throw new Error("Beeper account disappeared during sequential sync.");
19988
20070
  const service = normalizeService(pageAccount);
19989
- const contacts = page.contacts.filter((contact) => contact.isSelf !== true && contact.id !== pageAccount.user.id);
19990
- const skippedSelf = page.contacts.length - contacts.length;
20071
+ const directoryContacts = page.contacts.filter((contact) => contact.isSelf !== true && contact.id !== pageAccount.user.id);
20072
+ const skippedSelf = page.contacts.length - directoryContacts.length;
20073
+ const accountInteractions = interactionExport?.output.interactions.filter((interaction) => interaction.accountId === pageAccount.accountId) ?? [];
20074
+ const relationshipContacts = contactsWithRelationshipParticipants(directoryContacts, accountInteractions);
20075
+ const contacts = relationshipContacts.contacts;
20076
+ const contactsObserved = page.contacts.length + contacts.length - directoryContacts.length;
19991
20077
  const relationshipProjection = interactionExport === null ? null : projectBeeperContactInteractions(interactionExport.output, contacts.map((contact) => ({
19992
20078
  rawAccountId: pageAccount.accountId,
19993
20079
  accountKey: realmAccountKey,
@@ -20045,7 +20131,9 @@ function syncBeeperContacts(database, options = {}) {
20045
20131
  sourceRealm: sourceRealm(pageAccount),
20046
20132
  contacts: contacts.map((contact) => {
20047
20133
  const display = contactDisplayName(contact);
20134
+ const relationshipOnly = relationshipContacts.relationshipOnlyIds.has(contact.id);
20048
20135
  return {
20136
+ ...relationshipOnly ? { existingIdentityPolicy: "preserve" } : {},
20049
20137
  person: {
20050
20138
  displayName: display.value,
20051
20139
  givenName: null,
@@ -20060,7 +20148,10 @@ function syncBeeperContacts(database, options = {}) {
20060
20148
  birthday: null,
20061
20149
  observationBasis: display.basis,
20062
20150
  observationPriority: display.priority,
20063
- metadata: { createdBy: "beeper-contact", service }
20151
+ metadata: {
20152
+ createdBy: relationshipOnly ? "beeper-direct-interaction" : "beeper-contact",
20153
+ service
20154
+ }
20064
20155
  },
20065
20156
  resource: {
20066
20157
  type: "user",
@@ -20073,6 +20164,7 @@ function syncBeeperContacts(database, options = {}) {
20073
20164
  metadata: {
20074
20165
  authority: "beeper",
20075
20166
  service,
20167
+ ...relationshipOnly ? { relationshipOnly: true } : {},
20076
20168
  cannotMessage: contact.cannotMessage,
20077
20169
  isSelf: contact.isSelf,
20078
20170
  emailVerification: "not-guaranteed",
@@ -20104,7 +20196,7 @@ function syncBeeperContacts(database, options = {}) {
20104
20196
  accountResults.push({
20105
20197
  realm_id: realm.id,
20106
20198
  service,
20107
- contacts_seen: page.contacts.length,
20199
+ contacts_seen: contactsObserved,
20108
20200
  contacts_imported: imported.people_created,
20109
20201
  contacts_matched: imported.people_matched,
20110
20202
  contacts_skipped_self: skippedSelf,
@@ -20113,7 +20205,7 @@ function syncBeeperContacts(database, options = {}) {
20113
20205
  truncated: !page.localPageComplete,
20114
20206
  cached: imported.cached
20115
20207
  });
20116
- contactsSeen += page.contacts.length;
20208
+ contactsSeen += contactsObserved;
20117
20209
  contactsImported += imported.people_created;
20118
20210
  contactsMatched += imported.people_matched;
20119
20211
  contactsSkippedSelf += skippedSelf;
@@ -20128,7 +20220,7 @@ function syncBeeperContacts(database, options = {}) {
20128
20220
  ordinal: index + 1,
20129
20221
  accounts: orderedAccounts.length,
20130
20222
  service,
20131
- contacts: page.contacts.length,
20223
+ contacts: contactsObserved,
20132
20224
  truncated: !page.localPageComplete
20133
20225
  });
20134
20226
  }
@@ -21719,7 +21811,7 @@ import { Database as Database4 } from "bun:sqlite";
21719
21811
  import { createHash as createHash6 } from "crypto";
21720
21812
  import { lstatSync as lstatSync6, realpathSync as realpathSync5 } from "fs";
21721
21813
  import { homedir as homedir3 } from "os";
21722
- import { join as join6, resolve as resolve4 } from "path";
21814
+ import { join as join6, resolve as resolve5 } from "path";
21723
21815
  var DEFAULT_IMESSAGE_DATABASE = join6(homedir3(), "Library", "Messages", "chat.db");
21724
21816
  var DEFAULT_IMESSAGE_ACCOUNT = "imessage-main";
21725
21817
  var DEFAULT_IMESSAGE_PAGE_SIZE = 1000;
@@ -22163,7 +22255,7 @@ function syncIMessageRelationships(database, options = {}) {
22163
22255
  const pageSize = options.pageSize ?? DEFAULT_IMESSAGE_PAGE_SIZE;
22164
22256
  if (!Number.isSafeInteger(pageSize) || pageSize < 1 || pageSize > MAX_PAGE_SIZE)
22165
22257
  throw new Error(`iMessage page size must be between 1 and ${MAX_PAGE_SIZE}`);
22166
- const requestedPath = resolve4(options.messagesDatabase ?? DEFAULT_IMESSAGE_DATABASE);
22258
+ const requestedPath = resolve5(options.messagesDatabase ?? DEFAULT_IMESSAGE_DATABASE);
22167
22259
  const identity = lstatSync6(requestedPath);
22168
22260
  const uid = typeof process.getuid === "function" ? process.getuid() : null;
22169
22261
  if (!identity.isFile() || identity.isSymbolicLink() || identity.size < 1 || identity.size > MAX_DATABASE_BYTES || uid !== null && identity.uid !== uid) {
@@ -23249,7 +23341,7 @@ function instagramStatsAll(database) {
23249
23341
  // src/local/providers/x-archive.ts
23250
23342
  import { createHash as createHash8 } from "crypto";
23251
23343
  import { closeSync as closeSync7, constants as constants7, fstatSync as fstatSync6, openSync as openSync7, readSync as readSync2 } from "fs";
23252
- import { resolve as resolve5 } from "path";
23344
+ import { resolve as resolve6 } from "path";
23253
23345
 
23254
23346
  // src/local/archive/x-zip-file.ts
23255
23347
  import { readSync } from "fs";
@@ -24222,7 +24314,7 @@ function sha256Descriptor(descriptor, size) {
24222
24314
  async function readArchive2(path) {
24223
24315
  if (typeof path !== "string" || path.length < 1 || path.includes("\x00"))
24224
24316
  throw new Error("X archive path is invalid");
24225
- const locator = resolve5(path);
24317
+ const locator = resolve6(path);
24226
24318
  const descriptor = openSync7(locator, constants7.O_RDONLY | (constants7.O_NOFOLLOW ?? 0));
24227
24319
  try {
24228
24320
  const before = fstatSync6(descriptor, { bigint: true });
@@ -26504,7 +26596,7 @@ function syncWhatsAppRelationships(database, options = {}) {
26504
26596
  }
26505
26597
 
26506
26598
  // src/cli/version.ts
26507
- var peoplebladeVersion = "0.3.4";
26599
+ var peoplebladeVersion = "0.3.5";
26508
26600
 
26509
26601
  // src/cli/intro.ts
26510
26602
  function terminalIntro(terminal) {
@@ -26526,6 +26618,8 @@ Usage: peopleblade [--db PATH] <command>
26526
26618
 
26527
26619
  Core:
26528
26620
  init Create or migrate the local database
26621
+ menubar Launch the detached menu-bar companion
26622
+ outputs Print the agent outputs directory
26529
26623
  backup [PATH] Create a consistent private SQLite backup
26530
26624
  migrate rolodex --from PATH Back up and migrate a legacy Rolodex database once
26531
26625
  list [--search TEXT] [--limit N] Search the local contact book
@@ -26670,7 +26764,7 @@ function yesNo(value, label) {
26670
26764
  async function readNoteMarkdown(path) {
26671
26765
  const maximum = 65536;
26672
26766
  if (path !== "-") {
26673
- const descriptor = openSync9(resolve6(path), constants9.O_RDONLY | constants9.O_NONBLOCK | (constants9.O_NOFOLLOW ?? 0));
26767
+ const descriptor = openSync9(resolve7(path), constants9.O_RDONLY | constants9.O_NONBLOCK | (constants9.O_NOFOLLOW ?? 0));
26674
26768
  try {
26675
26769
  const before = fstatSync8(descriptor);
26676
26770
  if (!before.isFile() || before.nlink !== 1 || before.size > maximum || typeof process.getuid === "function" && before.uid !== process.getuid()) {
@@ -26780,13 +26874,30 @@ async function main(argv) {
26780
26874
  print({ database: databasePath, initialized: true, ...result }, asJson);
26781
26875
  return;
26782
26876
  }
26877
+ if (command === "menubar") {
26878
+ if (args.length !== 1)
26879
+ fail3("menubar takes no arguments.");
26880
+ process.exitCode = await launchMenubar(asJson);
26881
+ return;
26882
+ }
26883
+ if (command === "outputs") {
26884
+ if (args.length !== 1)
26885
+ fail3("outputs takes no arguments.");
26886
+ const directory = join7(peoplebladeDirectory(), "outputs");
26887
+ mkdirSync3(directory, { recursive: true, mode: 448 });
26888
+ if (asJson)
26889
+ print({ outputs: directory }, true);
26890
+ else
26891
+ console.log(directory);
26892
+ return;
26893
+ }
26783
26894
  if (command === "migrate" && subcommand === "rolodex") {
26784
26895
  const options = [...rest];
26785
26896
  const source = valueAfter(options, "--from") ?? fail3("migrate rolodex requires --from PATH.");
26786
26897
  const backup = valueAfter(options, "--backup") ?? standardBackupPath("legacy-rolodex", databasePath);
26787
26898
  if (options.length > 0)
26788
26899
  fail3(`Unknown argument: ${options[0]}`);
26789
- if (!existsSync5(source))
26900
+ if (!existsSync6(source))
26790
26901
  fail3(`Legacy Rolodex does not exist: ${source}`);
26791
26902
  backupLocalDatabase(source, backup);
26792
26903
  print({ database: databasePath, legacyBackup: backup, ...migrateLegacyRolodex(source, databasePath) }, asJson);
@@ -26820,7 +26931,7 @@ ${verification}`) });
26820
26931
  print(await signOutCloud(), asJson);
26821
26932
  return;
26822
26933
  }
26823
- if (!existsSync5(databasePath))
26934
+ if (!existsSync6(databasePath))
26824
26935
  fail3("Run `peopleblade init` or `peopleblade migrate rolodex --from PATH` first.");
26825
26936
  if (command === "backup") {
26826
26937
  const destination = subcommand ?? standardBackupPath("manual", databasePath);
@@ -27258,7 +27369,7 @@ ${verification}`) });
27258
27369
  const from = valueAfter(options, "--from") ?? fail3("notes import requires --from granola.json or --from -.");
27259
27370
  if (options.length)
27260
27371
  fail3(`Unknown argument: ${options[0]}`);
27261
- const payload = from === "-" ? parseGranolaImportJson(await new Response(Bun.stdin).text()) : readGranolaImportFile(resolve6(from));
27372
+ const payload = from === "-" ? parseGranolaImportJson(await new Response(Bun.stdin).text()) : readGranolaImportFile(resolve7(from));
27262
27373
  printImport(importGranolaMeetings(database, payload), true);
27263
27374
  return;
27264
27375
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hraness/peopleblade",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "Local-first, source-aware contact intelligence for people and agents",
5
5
  "type": "module",
6
6
  "bin": {