@hraness/peopleblade 0.3.3 → 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 +5 -3
- package/dist/peopleblade.js +450 -75
- package/package.json +1 -1
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.
|
|
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.
|
|
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.
|
|
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
|
package/dist/peopleblade.js
CHANGED
|
@@ -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
|
|
7
|
-
import { resolve as
|
|
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
|
|
@@ -6424,7 +6424,7 @@ function parseGranolaImportJson(raw) {
|
|
|
6424
6424
|
var paper_theme_default = `/* @hraness/design-kit paper-theme.css, portable contract 1.
|
|
6425
6425
|
* Generated by scripts/generate-paper-theme.ts from src/palettes.ts.
|
|
6426
6426
|
* Opt in with data-hraness-theme="paper". Import after existing theme rules.
|
|
6427
|
-
* No imports, asset requests, resets,
|
|
6427
|
+
* No imports, asset requests, resets, layout recipes, or runtime dependencies.
|
|
6428
6428
|
* Appearance uses inherited color-scheme; set data-theme on the boundary for
|
|
6429
6429
|
* an explicit island. A root without a mode follows the operating system.
|
|
6430
6430
|
* A non-paper data-palette on the boundary retains its selected colors.
|
|
@@ -6487,6 +6487,41 @@ var paper_theme_default = `/* @hraness/design-kit paper-theme.css, portable cont
|
|
|
6487
6487
|
[data-hraness-theme="paper"]:where([data-theme="dark"], .dark:not([data-theme="light"])) { color-scheme: dark; }
|
|
6488
6488
|
[data-hraness-theme="paper"][data-theme="light"] { color-scheme: light; }
|
|
6489
6489
|
|
|
6490
|
+
/* Opted-in shared headers retain their layout and receive the shared paint
|
|
6491
|
+
* contract, including snapshots used with older component releases. */
|
|
6492
|
+
@layer components.hraness-design-kit.legacy {
|
|
6493
|
+
/* Keep the browser aliases in separate feature queries: CSS optimizers can
|
|
6494
|
+
* collapse adjacent aliases to the prefixed property and lose Chromium blur. */
|
|
6495
|
+
:where([data-hraness-theme="paper"]) .hraness-marketing-header, .hraness-marketing-header:where([data-hraness-theme="paper"]) {
|
|
6496
|
+
--hraness-marketing-header-background: var(--hraness-marketing-background);
|
|
6497
|
+
--hraness-marketing-header-backdrop: none;
|
|
6498
|
+
background: var(--hraness-marketing-header-background);
|
|
6499
|
+
}
|
|
6500
|
+
|
|
6501
|
+
@supports (-webkit-backdrop-filter: blur(1px)) {
|
|
6502
|
+
:where([data-hraness-theme="paper"]) .hraness-marketing-header, .hraness-marketing-header:where([data-hraness-theme="paper"]) {
|
|
6503
|
+
--hraness-marketing-header-background: color-mix(in oklch, var(--hraness-marketing-background) 82%, transparent);
|
|
6504
|
+
--hraness-marketing-header-backdrop: blur(14px) saturate(1.4);
|
|
6505
|
+
-webkit-backdrop-filter: var(--hraness-marketing-header-backdrop);
|
|
6506
|
+
}
|
|
6507
|
+
}
|
|
6508
|
+
|
|
6509
|
+
@supports (backdrop-filter: blur(1px)) {
|
|
6510
|
+
:where([data-hraness-theme="paper"]) .hraness-marketing-header, .hraness-marketing-header:where([data-hraness-theme="paper"]) {
|
|
6511
|
+
--hraness-marketing-header-background: color-mix(in oklch, var(--hraness-marketing-background) 82%, transparent);
|
|
6512
|
+
--hraness-marketing-header-backdrop: blur(14px) saturate(1.4);
|
|
6513
|
+
backdrop-filter: var(--hraness-marketing-header-backdrop);
|
|
6514
|
+
}
|
|
6515
|
+
}
|
|
6516
|
+
|
|
6517
|
+
@media (prefers-reduced-transparency: reduce), (forced-colors: active) {
|
|
6518
|
+
:where([data-hraness-theme="paper"]) .hraness-marketing-header, .hraness-marketing-header:where([data-hraness-theme="paper"]) {
|
|
6519
|
+
--hraness-marketing-header-background: var(--hraness-marketing-background);
|
|
6520
|
+
--hraness-marketing-header-backdrop: none;
|
|
6521
|
+
}
|
|
6522
|
+
}
|
|
6523
|
+
}
|
|
6524
|
+
|
|
6490
6525
|
:root[data-hraness-theme="paper"]:where(:not([data-palette]), [data-palette="paper"]),
|
|
6491
6526
|
[data-hraness-theme="paper"]:where(:not([data-palette]), [data-palette="paper"]) {
|
|
6492
6527
|
--background: light-dark(#f8f7f4, #12100f);
|
|
@@ -6605,6 +6640,7 @@ var paper_theme_default = `/* @hraness/design-kit paper-theme.css, portable cont
|
|
|
6605
6640
|
--control-border: ButtonText;
|
|
6606
6641
|
--primary: Highlight;
|
|
6607
6642
|
--primary-foreground: HighlightText;
|
|
6643
|
+
--plain-link: LinkText;
|
|
6608
6644
|
--accent: Highlight;
|
|
6609
6645
|
--accent-foreground: HighlightText;
|
|
6610
6646
|
--focus: Highlight;
|
|
@@ -6619,19 +6655,228 @@ var paper_theme_default = `/* @hraness/design-kit paper-theme.css, portable cont
|
|
|
6619
6655
|
}
|
|
6620
6656
|
`;
|
|
6621
6657
|
|
|
6658
|
+
// styles/vendor/hraness-lantern/lantern-material.css
|
|
6659
|
+
var lantern_material_default = `/* Lantern is an opt-in material, independent of palette and typography.
|
|
6660
|
+
* All paint is deterministic CSS. No texture is placed over meaningful content. */
|
|
6661
|
+
@layer base {
|
|
6662
|
+
[data-hraness-material="lantern"] {
|
|
6663
|
+
--hraness-material-module: 6rem;
|
|
6664
|
+
--hraness-material-warm: #d49a54;
|
|
6665
|
+
--hraness-material-cool: #8d9fc5;
|
|
6666
|
+
--hraness-material-plane: var(--surface, var(--ui-background, Canvas));
|
|
6667
|
+
--hraness-material-ink: var(--foreground, var(--ui-foreground, CanvasText));
|
|
6668
|
+
--hraness-material-muted: var(--muted, var(--ui-muted-foreground, CanvasText));
|
|
6669
|
+
--hraness-material-seam: color-mix(in oklch, var(--hraness-material-ink) 7%, var(--hraness-material-plane));
|
|
6670
|
+
--hraness-material-edge: color-mix(in oklch, white 18%, transparent);
|
|
6671
|
+
--hraness-material-shade: color-mix(in oklch, black 14%, transparent);
|
|
6672
|
+
--hraness-material-warm-plane: color-mix(in oklch, var(--hraness-material-warm) 12%, var(--hraness-material-plane));
|
|
6673
|
+
--hraness-material-inset: inset 2px 3px 6px color-mix(in oklch, black 9%, transparent), inset -1px -1px 2px color-mix(in oklch, white 12%, transparent);
|
|
6674
|
+
--hraness-material-lift: inset 0 1px 0 var(--hraness-material-edge), -3px -3px 10px color-mix(in oklch, white 4%, transparent), 2px 4px 8px color-mix(in oklch, black 7%, transparent), 8px 18px 40px color-mix(in oklch, black 9%, transparent);
|
|
6675
|
+
--hraness-material-focus: var(--focus, var(--ui-ring, Highlight));
|
|
6676
|
+
--hraness-material-duration: 160ms;
|
|
6677
|
+
}
|
|
6678
|
+
}
|
|
6679
|
+
|
|
6680
|
+
@layer components.hraness-design-kit.legacy {
|
|
6681
|
+
/* A quiet, opaque reading plane. The seam belongs to the surface perimeter. */
|
|
6682
|
+
:where([data-hraness-material="lantern"], [data-hraness-material="lantern"] *):is(.hraness-material-pane) {
|
|
6683
|
+
background-color: var(--hraness-material-plane);
|
|
6684
|
+
border: 1px solid var(--hraness-material-seam);
|
|
6685
|
+
border-radius: var(--radius-lg, 0.75rem);
|
|
6686
|
+
color: var(--hraness-material-ink);
|
|
6687
|
+
min-inline-size: 0;
|
|
6688
|
+
}
|
|
6689
|
+
:where([data-hraness-material="lantern"], [data-hraness-material="lantern"] *):is(.hraness-material-pane[data-depth="raised"]) {
|
|
6690
|
+
box-shadow: var(--hraness-material-lift);
|
|
6691
|
+
}
|
|
6692
|
+
:where([data-hraness-material="lantern"], [data-hraness-material="lantern"] *):is(.hraness-material-pane[data-depth="inset"]) {
|
|
6693
|
+
background-color: color-mix(in oklch, var(--background, Canvas) 70%, var(--hraness-material-plane));
|
|
6694
|
+
box-shadow: var(--hraness-material-inset);
|
|
6695
|
+
}
|
|
6696
|
+
|
|
6697
|
+
/* A translucent enclosure, only where content actually passes behind it.
|
|
6698
|
+
* Resolve these tokens on the chrome itself for nested palette islands. */
|
|
6699
|
+
:where([data-hraness-material="lantern"], [data-hraness-material="lantern"] *):is(.hraness-material-chrome) {
|
|
6700
|
+
--hraness-material-chrome-paint: var(--surface, var(--ui-background, Canvas));
|
|
6701
|
+
--hraness-material-chrome-blur: none;
|
|
6702
|
+
background-color: var(--hraness-material-chrome-paint);
|
|
6703
|
+
border-block-end: 1px solid var(--hraness-material-seam);
|
|
6704
|
+
box-shadow: inset 0 1px 0 var(--hraness-material-edge), 0 4px 12px color-mix(in oklch, black 4%, transparent);
|
|
6705
|
+
}
|
|
6706
|
+
/* Bind the compiled TopBar's public paint seam at its own surface boundary. */
|
|
6707
|
+
:where([data-hraness-material="lantern"], [data-hraness-material="lantern"] *):is(.hraness-material-chrome.hraness-design-top-bar[data-surface="glass"]) {
|
|
6708
|
+
--hraness-design-top-bar-background: var(--hraness-material-chrome-paint);
|
|
6709
|
+
--hraness-design-top-bar-backdrop: var(--hraness-material-chrome-blur);
|
|
6710
|
+
}
|
|
6711
|
+
@supports (-webkit-backdrop-filter: blur(1px)) {
|
|
6712
|
+
:where([data-hraness-material="lantern"], [data-hraness-material="lantern"] *):is(.hraness-material-chrome) {
|
|
6713
|
+
--hraness-material-chrome-paint: color-mix(in oklch, var(--surface, var(--ui-background, Canvas)) 90%, transparent);
|
|
6714
|
+
--hraness-material-chrome-blur: blur(20px) saturate(1.1);
|
|
6715
|
+
-webkit-backdrop-filter: var(--hraness-material-chrome-blur);
|
|
6716
|
+
}
|
|
6717
|
+
}
|
|
6718
|
+
@supports (backdrop-filter: blur(1px)) {
|
|
6719
|
+
:where([data-hraness-material="lantern"], [data-hraness-material="lantern"] *):is(.hraness-material-chrome) {
|
|
6720
|
+
--hraness-material-chrome-paint: color-mix(in oklch, var(--surface, var(--ui-background, Canvas)) 90%, transparent);
|
|
6721
|
+
--hraness-material-chrome-blur: blur(20px) saturate(1.1);
|
|
6722
|
+
backdrop-filter: var(--hraness-material-chrome-blur);
|
|
6723
|
+
}
|
|
6724
|
+
}
|
|
6725
|
+
|
|
6726
|
+
/* Glazing uses shaded faces rather than drawn seams, with a warm band
|
|
6727
|
+
* of transmitted light. Its edges suggest a rounded enclosure without
|
|
6728
|
+
* distorting the document, blurring text, or masking brand artwork. */
|
|
6729
|
+
:where([data-hraness-material="lantern"], [data-hraness-material="lantern"] *):is(.hraness-material-wall) {
|
|
6730
|
+
background-color: var(--background, var(--ui-background, Canvas));
|
|
6731
|
+
background-image:
|
|
6732
|
+
var(--hraness-material-wall-grain, none),
|
|
6733
|
+
var(--hraness-material-wall-cells, linear-gradient(145deg, color-mix(in oklch, white 5%, transparent), transparent 48%, color-mix(in oklch, black 7%, transparent))),
|
|
6734
|
+
radial-gradient(ellipse at 60% 110%, color-mix(in oklch, var(--hraness-material-warm) 22%, transparent), transparent 68%),
|
|
6735
|
+
linear-gradient(110deg, color-mix(in oklch, var(--hraness-material-cool) 14%, transparent), transparent 38%, color-mix(in oklch, white 4%, transparent) 65%, transparent);
|
|
6736
|
+
background-size: 64px 64px, var(--hraness-material-wall-cell-size, var(--hraness-material-module)) var(--hraness-material-wall-cell-size, var(--hraness-material-module)), 100% 100%, 100% 100%;
|
|
6737
|
+
background-position: 0 0, center top, center top, center top;
|
|
6738
|
+
color: var(--hraness-material-ink);
|
|
6739
|
+
}
|
|
6740
|
+
|
|
6741
|
+
/* A terminal remains a readable document. Source lines scroll intact;
|
|
6742
|
+
* only its enclosure catches light. Syntax spans inherit semantic colors. */
|
|
6743
|
+
:where([data-hraness-material="lantern"], [data-hraness-material="lantern"] *):is(.hraness-material-terminal) {
|
|
6744
|
+
min-inline-size: 0;
|
|
6745
|
+
overflow: hidden;
|
|
6746
|
+
border: 1px solid var(--hraness-material-seam);
|
|
6747
|
+
border-radius: .875rem;
|
|
6748
|
+
background: var(--hraness-material-plane);
|
|
6749
|
+
color: var(--hraness-material-ink);
|
|
6750
|
+
box-shadow: var(--hraness-material-lift);
|
|
6751
|
+
}
|
|
6752
|
+
:where([data-hraness-material="lantern"], [data-hraness-material="lantern"] *):is(.hraness-material-terminal__bar) {
|
|
6753
|
+
padding: .8rem 1.25rem;
|
|
6754
|
+
background: linear-gradient(to bottom, color-mix(in oklch, white 5%, var(--hraness-material-plane)), var(--hraness-material-plane));
|
|
6755
|
+
box-shadow: inset 0 1px 0 var(--hraness-material-edge), 0 1px 3px color-mix(in oklch, black 8%, transparent);
|
|
6756
|
+
color: var(--hraness-material-muted);
|
|
6757
|
+
font: .8125rem/1.5 var(--font-mono, ui-monospace, monospace);
|
|
6758
|
+
}
|
|
6759
|
+
:where([data-hraness-material="lantern"], [data-hraness-material="lantern"] *):is(.hraness-material-code) {
|
|
6760
|
+
min-inline-size: 0;
|
|
6761
|
+
max-inline-size: 100%;
|
|
6762
|
+
margin: 0;
|
|
6763
|
+
padding: 1.25rem;
|
|
6764
|
+
overflow: auto;
|
|
6765
|
+
background: var(--hraness-material-plane);
|
|
6766
|
+
color: var(--hraness-material-ink);
|
|
6767
|
+
font: .875rem/1.75 var(--font-mono, ui-monospace, monospace);
|
|
6768
|
+
white-space: pre;
|
|
6769
|
+
overflow-wrap: normal;
|
|
6770
|
+
word-break: normal;
|
|
6771
|
+
tab-size: 2;
|
|
6772
|
+
}
|
|
6773
|
+
:where([data-hraness-material="lantern"], [data-hraness-material="lantern"] *):is(.hraness-material-code) code {
|
|
6774
|
+
background: none;
|
|
6775
|
+
padding: 0;
|
|
6776
|
+
font: inherit;
|
|
6777
|
+
white-space: inherit;
|
|
6778
|
+
overflow-wrap: inherit;
|
|
6779
|
+
word-break: inherit;
|
|
6780
|
+
}
|
|
6781
|
+
|
|
6782
|
+
/* Plain HTML control adapters. Compiled UI primitives use the public
|
|
6783
|
+
* lanternControlStyles recipes through their documented controlXstyle seam.
|
|
6784
|
+
* Edge paint uses background images, leaving focus shadows and outlines alone. */
|
|
6785
|
+
:where([data-hraness-material="lantern"], [data-hraness-material="lantern"] *):is(.hraness-material-control) {
|
|
6786
|
+
background-image: linear-gradient(to bottom, var(--hraness-material-edge), transparent 2px);
|
|
6787
|
+
}
|
|
6788
|
+
:where([data-hraness-material="lantern"], [data-hraness-material="lantern"] *):is(.hraness-material-input) {
|
|
6789
|
+
background-image: linear-gradient(to bottom, var(--hraness-material-shade), transparent 4px);
|
|
6790
|
+
caret-color: var(--hraness-material-focus);
|
|
6791
|
+
}
|
|
6792
|
+
:where([data-hraness-material="lantern"], [data-hraness-material="lantern"] *):is(.hraness-material-choice[aria-pressed="true"], .hraness-material-choice[aria-selected="true"], .hraness-material-choice[data-selected]) {
|
|
6793
|
+
background-color: var(--hraness-material-warm-plane);
|
|
6794
|
+
color: var(--hraness-material-ink);
|
|
6795
|
+
background-image: linear-gradient(to top, var(--hraness-material-warm), var(--hraness-material-warm) 2px, transparent 2px);
|
|
6796
|
+
}
|
|
6797
|
+
:where([data-hraness-material="lantern"], [data-hraness-material="lantern"] *):is(.hraness-material-control:disabled, .hraness-material-control[aria-disabled="true"], .hraness-material-control[data-disabled], .hraness-material-input:disabled) {
|
|
6798
|
+
background-image: none;
|
|
6799
|
+
}
|
|
6800
|
+
|
|
6801
|
+
/* A collection is a readable sequence, never a requirement to fill a grid.
|
|
6802
|
+
* Products own grouping, columns and the meaning of each row. */
|
|
6803
|
+
:where([data-hraness-material="lantern"], [data-hraness-material="lantern"] *):is(.hraness-material-rows) {
|
|
6804
|
+
margin: 0;
|
|
6805
|
+
padding: 0;
|
|
6806
|
+
list-style: none;
|
|
6807
|
+
}
|
|
6808
|
+
:where([data-hraness-material="lantern"], [data-hraness-material="lantern"] *):is(.hraness-material-rows > * + *) {
|
|
6809
|
+
border-block-start: 1px solid var(--hraness-material-seam);
|
|
6810
|
+
}
|
|
6811
|
+
:where([data-hraness-material="lantern"], [data-hraness-material="lantern"] *):is(.hraness-material-disclosure > summary) {
|
|
6812
|
+
cursor: pointer;
|
|
6813
|
+
min-block-size: 2.75rem;
|
|
6814
|
+
padding-block: 0.75rem;
|
|
6815
|
+
}
|
|
6816
|
+
:where([data-hraness-material="lantern"], [data-hraness-material="lantern"] *):is(.hraness-material-disclosure > summary:focus-visible) {
|
|
6817
|
+
outline: 2px solid var(--hraness-material-focus);
|
|
6818
|
+
outline-offset: 3px;
|
|
6819
|
+
}
|
|
6820
|
+
[data-hraness-material="lantern"] ::selection {
|
|
6821
|
+
background-color: var(--hraness-material-warm-plane);
|
|
6822
|
+
color: var(--hraness-material-ink);
|
|
6823
|
+
}
|
|
6824
|
+
|
|
6825
|
+
@media (prefers-reduced-motion: reduce) {
|
|
6826
|
+
[data-hraness-material="lantern"] { --hraness-material-duration: 0ms; }
|
|
6827
|
+
}
|
|
6828
|
+
@media (prefers-reduced-transparency: reduce) {
|
|
6829
|
+
:where([data-hraness-material="lantern"], [data-hraness-material="lantern"] *):is(.hraness-material-chrome) {
|
|
6830
|
+
--hraness-material-chrome-paint: var(--surface, var(--ui-background, Canvas));
|
|
6831
|
+
--hraness-material-chrome-blur: none;
|
|
6832
|
+
}
|
|
6833
|
+
:where([data-hraness-material="lantern"], [data-hraness-material="lantern"] *):is(.hraness-material-wall) { background-image: none; }
|
|
6834
|
+
}
|
|
6835
|
+
@media (forced-colors: active) {
|
|
6836
|
+
[data-hraness-material="lantern"] {
|
|
6837
|
+
--hraness-material-plane: Canvas;
|
|
6838
|
+
--hraness-material-ink: CanvasText;
|
|
6839
|
+
--hraness-material-muted: CanvasText;
|
|
6840
|
+
--hraness-material-seam: CanvasText;
|
|
6841
|
+
--hraness-material-focus: Highlight;
|
|
6842
|
+
--hraness-material-warm-plane: Canvas;
|
|
6843
|
+
--hraness-material-warm: Highlight;
|
|
6844
|
+
--hraness-material-edge: transparent;
|
|
6845
|
+
--hraness-material-inset: none;
|
|
6846
|
+
--hraness-material-lift: none;
|
|
6847
|
+
}
|
|
6848
|
+
:where([data-hraness-material="lantern"], [data-hraness-material="lantern"] *):is(.hraness-material-wall, .hraness-material-chrome, .hraness-material-pane, .hraness-material-terminal, .hraness-material-terminal__bar, .hraness-material-code) {
|
|
6849
|
+
--hraness-material-chrome-paint: Canvas;
|
|
6850
|
+
--hraness-material-chrome-blur: none;
|
|
6851
|
+
background-color: Canvas;
|
|
6852
|
+
background-image: none;
|
|
6853
|
+
box-shadow: none;
|
|
6854
|
+
}
|
|
6855
|
+
:where([data-hraness-material="lantern"], [data-hraness-material="lantern"] *):is(.hraness-material-control, .hraness-material-input, .hraness-material-choice) { background-image: none; }
|
|
6856
|
+
:where([data-hraness-material="lantern"], [data-hraness-material="lantern"] *):is(.hraness-material-choice[aria-pressed="true"], .hraness-material-choice[aria-selected="true"], .hraness-material-choice[data-selected]) {
|
|
6857
|
+
background-color: Highlight;
|
|
6858
|
+
color: HighlightText;
|
|
6859
|
+
background-image: none;
|
|
6860
|
+
}
|
|
6861
|
+
}
|
|
6862
|
+
}
|
|
6863
|
+
`;
|
|
6864
|
+
|
|
6622
6865
|
// src/local/workspace-assets.ts
|
|
6623
6866
|
var workspaceHtml = `<!doctype html>
|
|
6624
|
-
<html lang="en" data-hraness-theme="paper"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6867
|
+
<html lang="en" data-hraness-theme="paper" data-hraness-material="lantern"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6625
6868
|
<title>PeopleBlade \xB7 Local workspace</title><link rel="stylesheet" href="/workspace.css"><script src="/workspace.js" defer></script></head>
|
|
6626
|
-
<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>
|
|
6627
|
-
<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>
|
|
6869
|
+
<body><a class="skip" href="#workspace">Skip to contacts</a><header class="workspace-header hraness-material-chrome"><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>
|
|
6870
|
+
<main id="workspace"><section id="unlock" class="unlock hraness-material-pane"><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>
|
|
6628
6871
|
<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>
|
|
6629
6872
|
<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>
|
|
6630
|
-
<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>
|
|
6631
|
-
<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>
|
|
6873
|
+
<div class="workspace-grid"><section class="records hraness-material-pane" 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>
|
|
6874
|
+
<aside id="detail" class="detail hraness-material-pane" 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>
|
|
6632
6875
|
<p id="status" role="status" aria-live="polite"></p></main><footer>PeopleBlade \xB7 Local authority, reviewable identity, private notes.</footer></body></html>`;
|
|
6633
6876
|
var workspaceCss = paper_theme_default + `
|
|
6634
|
-
|
|
6877
|
+
` + lantern_material_default + `
|
|
6878
|
+
:root{--bg:var(--background);--panel:var(--surface);--text:var(--foreground);--soft:var(--secondary)}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:14px/1.55 var(--font-text)}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(--primary);background:var(--soft)}button:disabled{opacity:.45;cursor:default}a{color:var(--primary)}:focus-visible{outline:3px solid var(--primary);outline-offset:3px}header{display:flex;align-items:center;gap:30px;padding:22px 32px;border-bottom:1px solid var(--hraness-material-seam);background-color:var(--hraness-material-chrome-paint)}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(--hraness-material-plane);border:1px solid var(--hraness-material-seam);border-radius:12px;color:var(--hraness-material-ink)}.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(--hraness-material-seam);background:var(--hraness-material-plane);border-radius:10px;color:var(--hraness-material-ink);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:hover{background:var(--soft)}tr.selected,tr.selected:hover{background:var(--hraness-material-warm-plane);color:var(--hraness-material-ink)}tr.selected td small,tr.selected td button{color:var(--hraness-material-ink)}.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(--primary);color:var(--primary-foreground)}.primary:hover{background:var(--primary);color:var(--primary-foreground)}.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}
|
|
6879
|
+
@media(forced-colors:active){tr.selected,tr.selected:hover{background:Highlight;color:HighlightText}tr.selected td small,tr.selected td button{color:HighlightText}}
|
|
6635
6880
|
@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}}
|
|
6636
6881
|
`;
|
|
6637
6882
|
function workspaceClient() {
|
|
@@ -6701,6 +6946,7 @@ function workspaceClient() {
|
|
|
6701
6946
|
el("rows").replaceChildren();
|
|
6702
6947
|
for (const person of result.items) {
|
|
6703
6948
|
const row = node("tr", "", selectedPerson === person.id ? "selected" : "");
|
|
6949
|
+
row.dataset.personId = String(person.id);
|
|
6704
6950
|
const name = node("td");
|
|
6705
6951
|
const open = node("button", person.displayName);
|
|
6706
6952
|
open.type = "button";
|
|
@@ -6945,6 +7191,9 @@ function workspaceClient() {
|
|
|
6945
7191
|
}
|
|
6946
7192
|
activeEditor = null;
|
|
6947
7193
|
selectedPerson = id;
|
|
7194
|
+
for (const row of el("rows").querySelectorAll("tr[data-person-id]")) {
|
|
7195
|
+
row.classList.toggle("selected", row.dataset.personId === String(id));
|
|
7196
|
+
}
|
|
6948
7197
|
target.replaceChildren();
|
|
6949
7198
|
target.append(node("span", `PERSON ${id} \xB7 ${result.memberCount} SOURCE RECORD${result.memberCount === 1 ? "" : "S"}`, "eyebrow"), node("h2", result.contact.displayName));
|
|
6950
7199
|
const pills = node("div");
|
|
@@ -11484,6 +11733,57 @@ function addConfirmedPerson(database, args) {
|
|
|
11484
11733
|
}
|
|
11485
11734
|
}
|
|
11486
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
|
+
|
|
11487
11787
|
// src/local/providers/apple.ts
|
|
11488
11788
|
import { Database as Database3 } from "bun:sqlite";
|
|
11489
11789
|
import { createHash as createHash3 } from "crypto";
|
|
@@ -11493,7 +11793,7 @@ import {
|
|
|
11493
11793
|
realpathSync as realpathSync4
|
|
11494
11794
|
} from "fs";
|
|
11495
11795
|
import { homedir as homedir2 } from "os";
|
|
11496
|
-
import { join as join5, resolve as
|
|
11796
|
+
import { join as join5, resolve as resolve4 } from "path";
|
|
11497
11797
|
var EMAIL_PATTERN2 = /^(?=[\x21-\x7E]+$)[^@\s]+@[^@\s]+\.[^@\s]+$/u;
|
|
11498
11798
|
function clean(value) {
|
|
11499
11799
|
if (typeof value !== "string")
|
|
@@ -11696,7 +11996,7 @@ function rowsByOwner(database, table, names) {
|
|
|
11696
11996
|
return grouped;
|
|
11697
11997
|
}
|
|
11698
11998
|
function sourceDatabasePaths(directory) {
|
|
11699
|
-
const base = realpathSync4(
|
|
11999
|
+
const base = realpathSync4(resolve4(directory));
|
|
11700
12000
|
const sourceDirectory = join5(base, "Sources");
|
|
11701
12001
|
const sourceIdentity = lstatSync5(sourceDirectory);
|
|
11702
12002
|
if (!sourceIdentity.isDirectory() || sourceIdentity.isSymbolicLink()) {
|
|
@@ -12114,7 +12414,8 @@ var methodSchema = z10.object({
|
|
|
12114
12414
|
var contactSchema = z10.object({
|
|
12115
12415
|
person: personSchema,
|
|
12116
12416
|
resource: resourceSchema,
|
|
12117
|
-
methods: z10.array(methodSchema).max(1000)
|
|
12417
|
+
methods: z10.array(methodSchema).max(1000),
|
|
12418
|
+
existingIdentityPolicy: z10.literal("preserve").optional()
|
|
12118
12419
|
}).strict().superRefine((contact, context) => {
|
|
12119
12420
|
const coordinates = new Set;
|
|
12120
12421
|
contact.methods.forEach((method, index) => {
|
|
@@ -12384,6 +12685,7 @@ function materializeContactSourceSnapshot(database, snapshot, sourceRealmId, sou
|
|
|
12384
12685
|
ordinal += 1;
|
|
12385
12686
|
const prior = getRow3(database, `SELECT id,person_id,source_realm_id FROM provider_resources
|
|
12386
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";
|
|
12387
12689
|
let personId;
|
|
12388
12690
|
let providerResourceId;
|
|
12389
12691
|
if (prior === null) {
|
|
@@ -12403,47 +12705,54 @@ function materializeContactSourceSnapshot(database, snapshot, sourceRealmId, sou
|
|
|
12403
12705
|
}
|
|
12404
12706
|
personId = prior.person_id;
|
|
12405
12707
|
providerResourceId = prior.id;
|
|
12406
|
-
|
|
12407
|
-
|
|
12408
|
-
|
|
12409
|
-
|
|
12410
|
-
|
|
12411
|
-
|
|
12412
|
-
|
|
12413
|
-
|
|
12414
|
-
|
|
12415
|
-
|
|
12416
|
-
|
|
12417
|
-
|
|
12418
|
-
|
|
12419
|
-
|
|
12420
|
-
|
|
12421
|
-
|
|
12422
|
-
|
|
12423
|
-
|
|
12424
|
-
|
|
12425
|
-
|
|
12426
|
-
|
|
12427
|
-
|
|
12428
|
-
|
|
12429
|
-
|
|
12430
|
-
|
|
12431
|
-
[
|
|
12432
|
-
|
|
12433
|
-
|
|
12434
|
-
|
|
12435
|
-
|
|
12436
|
-
|
|
12437
|
-
|
|
12438
|
-
|
|
12439
|
-
|
|
12440
|
-
|
|
12441
|
-
|
|
12442
|
-
|
|
12443
|
-
|
|
12444
|
-
|
|
12445
|
-
|
|
12446
|
-
|
|
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
|
+
}
|
|
12447
12756
|
}
|
|
12448
12757
|
if (writeSourceRecords) {
|
|
12449
12758
|
run2(database, `INSERT INTO source_records(
|
|
@@ -12519,7 +12828,7 @@ function importContactSourceSnapshot(database, value) {
|
|
|
12519
12828
|
source_rows: snapshot.contacts.length,
|
|
12520
12829
|
people_created: peopleCreated,
|
|
12521
12830
|
people_matched: peopleMatched,
|
|
12522
|
-
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),
|
|
12523
12832
|
interactions_touched: snapshot.interactions.length,
|
|
12524
12833
|
resources_removed: resourcesRemoved,
|
|
12525
12834
|
reconciled: reconcile,
|
|
@@ -12589,7 +12898,7 @@ function sha2563(value) {
|
|
|
12589
12898
|
|
|
12590
12899
|
// node_modules/@hraness/ghostget/dist/client.js
|
|
12591
12900
|
import { spawn, spawnSync } from "child_process";
|
|
12592
|
-
import { existsSync as
|
|
12901
|
+
import { existsSync as existsSync4 } from "fs";
|
|
12593
12902
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
12594
12903
|
import { types as nodeTypes } from "util";
|
|
12595
12904
|
var PORTABLE_OPERATION_IDENTITY_VERSION = 1;
|
|
@@ -12887,10 +13196,10 @@ function boundedMessage(value) {
|
|
|
12887
13196
|
}
|
|
12888
13197
|
function cliSourcePath() {
|
|
12889
13198
|
const besideSource = fileURLToPath2(new URL("./cli.ts", import.meta.url));
|
|
12890
|
-
if (
|
|
13199
|
+
if (existsSync4(besideSource))
|
|
12891
13200
|
return besideSource;
|
|
12892
13201
|
const packagedSource = fileURLToPath2(new URL("../src/cli.ts", import.meta.url));
|
|
12893
|
-
if (
|
|
13202
|
+
if (existsSync4(packagedSource))
|
|
12894
13203
|
return packagedSource;
|
|
12895
13204
|
throw new Error("the installed Ghostget CLI source is unavailable");
|
|
12896
13205
|
}
|
|
@@ -13901,7 +14210,7 @@ var GHOSTGET_VERSION = "0.17.3";
|
|
|
13901
14210
|
|
|
13902
14211
|
// node_modules/@hraness/ghostget/dist/beeper-client.js
|
|
13903
14212
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
13904
|
-
import { existsSync as
|
|
14213
|
+
import { existsSync as existsSync5 } from "fs";
|
|
13905
14214
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
13906
14215
|
import { types as nodeTypes3 } from "util";
|
|
13907
14216
|
import { types as nodeTypes2 } from "util";
|
|
@@ -18347,10 +18656,10 @@ function fail22(message) {
|
|
|
18347
18656
|
}
|
|
18348
18657
|
function cliSourcePath2() {
|
|
18349
18658
|
const besideSource = fileURLToPath3(new URL("./cli.ts", import.meta.url));
|
|
18350
|
-
if (
|
|
18659
|
+
if (existsSync5(besideSource))
|
|
18351
18660
|
return besideSource;
|
|
18352
18661
|
const packagedSource = fileURLToPath3(new URL("../src/cli.ts", import.meta.url));
|
|
18353
|
-
if (
|
|
18662
|
+
if (existsSync5(packagedSource))
|
|
18354
18663
|
return packagedSource;
|
|
18355
18664
|
return fail22("the installed Ghostget CLI source is unavailable");
|
|
18356
18665
|
}
|
|
@@ -19425,6 +19734,28 @@ function contactMethods(contact, service) {
|
|
|
19425
19734
|
});
|
|
19426
19735
|
return methods;
|
|
19427
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
|
+
}
|
|
19428
19759
|
function sameExecution(left, right) {
|
|
19429
19760
|
return left.authId === right.authId && left.authSha256 === right.authSha256 && left.adapterSha256 === right.adapterSha256 && left.contractSha256 === right.contractSha256;
|
|
19430
19761
|
}
|
|
@@ -19737,8 +20068,12 @@ function syncBeeperContacts(database, options = {}) {
|
|
|
19737
20068
|
if (pageAccount === undefined)
|
|
19738
20069
|
throw new Error("Beeper account disappeared during sequential sync.");
|
|
19739
20070
|
const service = normalizeService(pageAccount);
|
|
19740
|
-
const
|
|
19741
|
-
const skippedSelf = page.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;
|
|
19742
20077
|
const relationshipProjection = interactionExport === null ? null : projectBeeperContactInteractions(interactionExport.output, contacts.map((contact) => ({
|
|
19743
20078
|
rawAccountId: pageAccount.accountId,
|
|
19744
20079
|
accountKey: realmAccountKey,
|
|
@@ -19796,7 +20131,9 @@ function syncBeeperContacts(database, options = {}) {
|
|
|
19796
20131
|
sourceRealm: sourceRealm(pageAccount),
|
|
19797
20132
|
contacts: contacts.map((contact) => {
|
|
19798
20133
|
const display = contactDisplayName(contact);
|
|
20134
|
+
const relationshipOnly = relationshipContacts.relationshipOnlyIds.has(contact.id);
|
|
19799
20135
|
return {
|
|
20136
|
+
...relationshipOnly ? { existingIdentityPolicy: "preserve" } : {},
|
|
19800
20137
|
person: {
|
|
19801
20138
|
displayName: display.value,
|
|
19802
20139
|
givenName: null,
|
|
@@ -19811,7 +20148,10 @@ function syncBeeperContacts(database, options = {}) {
|
|
|
19811
20148
|
birthday: null,
|
|
19812
20149
|
observationBasis: display.basis,
|
|
19813
20150
|
observationPriority: display.priority,
|
|
19814
|
-
metadata: {
|
|
20151
|
+
metadata: {
|
|
20152
|
+
createdBy: relationshipOnly ? "beeper-direct-interaction" : "beeper-contact",
|
|
20153
|
+
service
|
|
20154
|
+
}
|
|
19815
20155
|
},
|
|
19816
20156
|
resource: {
|
|
19817
20157
|
type: "user",
|
|
@@ -19824,6 +20164,7 @@ function syncBeeperContacts(database, options = {}) {
|
|
|
19824
20164
|
metadata: {
|
|
19825
20165
|
authority: "beeper",
|
|
19826
20166
|
service,
|
|
20167
|
+
...relationshipOnly ? { relationshipOnly: true } : {},
|
|
19827
20168
|
cannotMessage: contact.cannotMessage,
|
|
19828
20169
|
isSelf: contact.isSelf,
|
|
19829
20170
|
emailVerification: "not-guaranteed",
|
|
@@ -19855,7 +20196,7 @@ function syncBeeperContacts(database, options = {}) {
|
|
|
19855
20196
|
accountResults.push({
|
|
19856
20197
|
realm_id: realm.id,
|
|
19857
20198
|
service,
|
|
19858
|
-
contacts_seen:
|
|
20199
|
+
contacts_seen: contactsObserved,
|
|
19859
20200
|
contacts_imported: imported.people_created,
|
|
19860
20201
|
contacts_matched: imported.people_matched,
|
|
19861
20202
|
contacts_skipped_self: skippedSelf,
|
|
@@ -19864,7 +20205,7 @@ function syncBeeperContacts(database, options = {}) {
|
|
|
19864
20205
|
truncated: !page.localPageComplete,
|
|
19865
20206
|
cached: imported.cached
|
|
19866
20207
|
});
|
|
19867
|
-
contactsSeen +=
|
|
20208
|
+
contactsSeen += contactsObserved;
|
|
19868
20209
|
contactsImported += imported.people_created;
|
|
19869
20210
|
contactsMatched += imported.people_matched;
|
|
19870
20211
|
contactsSkippedSelf += skippedSelf;
|
|
@@ -19879,7 +20220,7 @@ function syncBeeperContacts(database, options = {}) {
|
|
|
19879
20220
|
ordinal: index + 1,
|
|
19880
20221
|
accounts: orderedAccounts.length,
|
|
19881
20222
|
service,
|
|
19882
|
-
contacts:
|
|
20223
|
+
contacts: contactsObserved,
|
|
19883
20224
|
truncated: !page.localPageComplete
|
|
19884
20225
|
});
|
|
19885
20226
|
}
|
|
@@ -21470,7 +21811,7 @@ import { Database as Database4 } from "bun:sqlite";
|
|
|
21470
21811
|
import { createHash as createHash6 } from "crypto";
|
|
21471
21812
|
import { lstatSync as lstatSync6, realpathSync as realpathSync5 } from "fs";
|
|
21472
21813
|
import { homedir as homedir3 } from "os";
|
|
21473
|
-
import { join as join6, resolve as
|
|
21814
|
+
import { join as join6, resolve as resolve5 } from "path";
|
|
21474
21815
|
var DEFAULT_IMESSAGE_DATABASE = join6(homedir3(), "Library", "Messages", "chat.db");
|
|
21475
21816
|
var DEFAULT_IMESSAGE_ACCOUNT = "imessage-main";
|
|
21476
21817
|
var DEFAULT_IMESSAGE_PAGE_SIZE = 1000;
|
|
@@ -21914,7 +22255,7 @@ function syncIMessageRelationships(database, options = {}) {
|
|
|
21914
22255
|
const pageSize = options.pageSize ?? DEFAULT_IMESSAGE_PAGE_SIZE;
|
|
21915
22256
|
if (!Number.isSafeInteger(pageSize) || pageSize < 1 || pageSize > MAX_PAGE_SIZE)
|
|
21916
22257
|
throw new Error(`iMessage page size must be between 1 and ${MAX_PAGE_SIZE}`);
|
|
21917
|
-
const requestedPath =
|
|
22258
|
+
const requestedPath = resolve5(options.messagesDatabase ?? DEFAULT_IMESSAGE_DATABASE);
|
|
21918
22259
|
const identity = lstatSync6(requestedPath);
|
|
21919
22260
|
const uid = typeof process.getuid === "function" ? process.getuid() : null;
|
|
21920
22261
|
if (!identity.isFile() || identity.isSymbolicLink() || identity.size < 1 || identity.size > MAX_DATABASE_BYTES || uid !== null && identity.uid !== uid) {
|
|
@@ -23000,7 +23341,7 @@ function instagramStatsAll(database) {
|
|
|
23000
23341
|
// src/local/providers/x-archive.ts
|
|
23001
23342
|
import { createHash as createHash8 } from "crypto";
|
|
23002
23343
|
import { closeSync as closeSync7, constants as constants7, fstatSync as fstatSync6, openSync as openSync7, readSync as readSync2 } from "fs";
|
|
23003
|
-
import { resolve as
|
|
23344
|
+
import { resolve as resolve6 } from "path";
|
|
23004
23345
|
|
|
23005
23346
|
// src/local/archive/x-zip-file.ts
|
|
23006
23347
|
import { readSync } from "fs";
|
|
@@ -23973,7 +24314,7 @@ function sha256Descriptor(descriptor, size) {
|
|
|
23973
24314
|
async function readArchive2(path) {
|
|
23974
24315
|
if (typeof path !== "string" || path.length < 1 || path.includes("\x00"))
|
|
23975
24316
|
throw new Error("X archive path is invalid");
|
|
23976
|
-
const locator =
|
|
24317
|
+
const locator = resolve6(path);
|
|
23977
24318
|
const descriptor = openSync7(locator, constants7.O_RDONLY | (constants7.O_NOFOLLOW ?? 0));
|
|
23978
24319
|
try {
|
|
23979
24320
|
const before = fstatSync6(descriptor, { bigint: true });
|
|
@@ -26255,7 +26596,20 @@ function syncWhatsAppRelationships(database, options = {}) {
|
|
|
26255
26596
|
}
|
|
26256
26597
|
|
|
26257
26598
|
// src/cli/version.ts
|
|
26258
|
-
var peoplebladeVersion = "0.3.
|
|
26599
|
+
var peoplebladeVersion = "0.3.5";
|
|
26600
|
+
|
|
26601
|
+
// src/cli/intro.ts
|
|
26602
|
+
function terminalIntro(terminal) {
|
|
26603
|
+
if (terminal.isTTY !== true || terminal.term === "dumb" || (terminal.columns ?? 80) < 48)
|
|
26604
|
+
return "";
|
|
26605
|
+
return ` .--.--.
|
|
26606
|
+
/______/_ peopleblade
|
|
26607
|
+
| o --- | Your contacts, notes, and context.
|
|
26608
|
+
|___ --- |
|
|
26609
|
+
|____|
|
|
26610
|
+
|
|
26611
|
+
`;
|
|
26612
|
+
}
|
|
26259
26613
|
|
|
26260
26614
|
// src/cli/main.ts
|
|
26261
26615
|
var usage = `PeopleBlade \u2014 local-first contact intelligence
|
|
@@ -26264,6 +26618,8 @@ Usage: peopleblade [--db PATH] <command>
|
|
|
26264
26618
|
|
|
26265
26619
|
Core:
|
|
26266
26620
|
init Create or migrate the local database
|
|
26621
|
+
menubar Launch the detached menu-bar companion
|
|
26622
|
+
outputs Print the agent outputs directory
|
|
26267
26623
|
backup [PATH] Create a consistent private SQLite backup
|
|
26268
26624
|
migrate rolodex --from PATH Back up and migrate a legacy Rolodex database once
|
|
26269
26625
|
list [--search TEXT] [--limit N] Search the local contact book
|
|
@@ -26408,7 +26764,7 @@ function yesNo(value, label) {
|
|
|
26408
26764
|
async function readNoteMarkdown(path) {
|
|
26409
26765
|
const maximum = 65536;
|
|
26410
26766
|
if (path !== "-") {
|
|
26411
|
-
const descriptor = openSync9(
|
|
26767
|
+
const descriptor = openSync9(resolve7(path), constants9.O_RDONLY | constants9.O_NONBLOCK | (constants9.O_NOFOLLOW ?? 0));
|
|
26412
26768
|
try {
|
|
26413
26769
|
const before = fstatSync8(descriptor);
|
|
26414
26770
|
if (!before.isFile() || before.nlink !== 1 || before.size > maximum || typeof process.getuid === "function" && before.uid !== process.getuid()) {
|
|
@@ -26490,6 +26846,8 @@ async function main(argv) {
|
|
|
26490
26846
|
const asJson = flag(args, "--json");
|
|
26491
26847
|
const autoAccept = flag(args, "--auto-accept");
|
|
26492
26848
|
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
26849
|
+
if (!asJson)
|
|
26850
|
+
process.stdout.write(terminalIntro({ isTTY: process.stdout.isTTY, columns: process.stdout.columns, term: process.env.TERM }));
|
|
26493
26851
|
console.log(usage);
|
|
26494
26852
|
return;
|
|
26495
26853
|
}
|
|
@@ -26516,13 +26874,30 @@ async function main(argv) {
|
|
|
26516
26874
|
print({ database: databasePath, initialized: true, ...result }, asJson);
|
|
26517
26875
|
return;
|
|
26518
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
|
+
}
|
|
26519
26894
|
if (command === "migrate" && subcommand === "rolodex") {
|
|
26520
26895
|
const options = [...rest];
|
|
26521
26896
|
const source = valueAfter(options, "--from") ?? fail3("migrate rolodex requires --from PATH.");
|
|
26522
26897
|
const backup = valueAfter(options, "--backup") ?? standardBackupPath("legacy-rolodex", databasePath);
|
|
26523
26898
|
if (options.length > 0)
|
|
26524
26899
|
fail3(`Unknown argument: ${options[0]}`);
|
|
26525
|
-
if (!
|
|
26900
|
+
if (!existsSync6(source))
|
|
26526
26901
|
fail3(`Legacy Rolodex does not exist: ${source}`);
|
|
26527
26902
|
backupLocalDatabase(source, backup);
|
|
26528
26903
|
print({ database: databasePath, legacyBackup: backup, ...migrateLegacyRolodex(source, databasePath) }, asJson);
|
|
@@ -26556,7 +26931,7 @@ ${verification}`) });
|
|
|
26556
26931
|
print(await signOutCloud(), asJson);
|
|
26557
26932
|
return;
|
|
26558
26933
|
}
|
|
26559
|
-
if (!
|
|
26934
|
+
if (!existsSync6(databasePath))
|
|
26560
26935
|
fail3("Run `peopleblade init` or `peopleblade migrate rolodex --from PATH` first.");
|
|
26561
26936
|
if (command === "backup") {
|
|
26562
26937
|
const destination = subcommand ?? standardBackupPath("manual", databasePath);
|
|
@@ -26994,7 +27369,7 @@ ${verification}`) });
|
|
|
26994
27369
|
const from = valueAfter(options, "--from") ?? fail3("notes import requires --from granola.json or --from -.");
|
|
26995
27370
|
if (options.length)
|
|
26996
27371
|
fail3(`Unknown argument: ${options[0]}`);
|
|
26997
|
-
const payload = from === "-" ? parseGranolaImportJson(await new Response(Bun.stdin).text()) : readGranolaImportFile(
|
|
27372
|
+
const payload = from === "-" ? parseGranolaImportJson(await new Response(Bun.stdin).text()) : readGranolaImportFile(resolve7(from));
|
|
26998
27373
|
printImport(importGranolaMeetings(database, payload), true);
|
|
26999
27374
|
return;
|
|
27000
27375
|
}
|