shepherd-onboard 0.1.16 → 0.1.18

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.
@@ -824,6 +824,8 @@ Domain-wide delegation OAuth Client ID: ${payload.googleWorkspaceDelegation.clie
824
824
  Scopes:
825
825
  ${payload.googleWorkspaceDelegation.scopes.join("\n")}
826
826
 
827
+ The setup command copies those scopes to the clipboard as one comma-separated string on macOS. Tell the user they can paste directly into the OAuth scopes field. If clipboard copy is unavailable, use the scopes printed above.
828
+
827
829
  The customer does not create a service account and does not upload service account JSON in the default Shepherd-managed flow. Their super admin only authorizes the Client ID and scopes in Google Admin Console. Shepherd's backend stores and uses its private service account JSON server-side.
828
830
  Shepherd must still enforce selected users and groups internally before polling or impersonating any employee email.
829
831
 
@@ -918,15 +920,34 @@ function googleWorkspaceDelegationSetup(setup) {
918
920
 
919
921
  function printGoogleWorkspaceDelegationSetup(setup) {
920
922
  const resolved = googleWorkspaceDelegationSetup(setup);
923
+ const copiedScopes = copyTextToClipboard(resolved.scopes.join(","));
921
924
  console.log(`App name: ${resolved.appName}`);
922
925
  console.log(`Service account email: ${resolved.serviceAccountEmail}`);
923
926
  console.log(`Domain-wide delegation OAuth Client ID: ${resolved.clientId}`);
924
927
  console.log("Customer action: in Google Admin Console, add the Client ID above and paste these scopes.");
925
928
  console.log("Customers do not create a service account or upload service account JSON; Shepherd stores its private service account JSON server-side.");
929
+ if (copiedScopes) {
930
+ console.log("Copied comma-separated scopes to the clipboard.");
931
+ } else if (platform() === "darwin") {
932
+ console.log("Could not copy scopes to the clipboard; copy the scopes below instead.");
933
+ }
926
934
  console.log("\nScopes:");
927
935
  for (const scope of resolved.scopes) console.log(scope);
928
936
  }
929
937
 
938
+ function copyTextToClipboard(text) {
939
+ if (platform() !== "darwin") return false;
940
+ try {
941
+ execFileSync("pbcopy", [], {
942
+ input: text,
943
+ stdio: ["pipe", "ignore", "ignore"],
944
+ });
945
+ return true;
946
+ } catch {
947
+ return false;
948
+ }
949
+ }
950
+
930
951
  function authenticatedEmail(authenticated) {
931
952
  return authenticated?.workosUser?.email ?? authenticated?.account?.email ?? null;
932
953
  }
@@ -1356,15 +1377,15 @@ function renderMessagesSelectorPage(chats, token, error = "") {
1356
1377
  <title>Select Messages Chats</title>
1357
1378
  <style>
1358
1379
  :root {
1359
- --bg: #fbfbfa;
1380
+ --bg: #ffffff;
1360
1381
  --fg: #141614;
1361
- --muted: #767d78;
1362
- --faint: #9aa09b;
1363
- --line: #dedfdd;
1382
+ --muted: #615d59;
1383
+ --faint: #a39e98;
1384
+ --line: #e6e6e6;
1364
1385
  --green: #1f5c2e;
1365
1386
  --green-hover: #246836;
1366
1387
  --radius: 8px;
1367
- --grid: 20px minmax(0, 1fr) 78px 104px;
1388
+ --grid: 20px minmax(0, 1fr) 76px 108px;
1368
1389
  }
1369
1390
  * { box-sizing: border-box; }
1370
1391
  body {
@@ -1377,9 +1398,9 @@ function renderMessagesSelectorPage(chats, token, error = "") {
1377
1398
  -webkit-font-smoothing: antialiased;
1378
1399
  }
1379
1400
  main {
1380
- width: min(680px, calc(100vw - 32px));
1401
+ width: min(640px, calc(100vw - 32px));
1381
1402
  margin: 0 auto;
1382
- padding: 48px 0 28px;
1403
+ padding: 56px 0 28px;
1383
1404
  }
1384
1405
  .header {
1385
1406
  display: grid;
@@ -1390,7 +1411,7 @@ function renderMessagesSelectorPage(chats, token, error = "") {
1390
1411
  .brand {
1391
1412
  display: grid;
1392
1413
  justify-items: center;
1393
- gap: 14px;
1414
+ gap: 16px;
1394
1415
  min-width: 0;
1395
1416
  }
1396
1417
  .logo {
@@ -1412,14 +1433,14 @@ function renderMessagesSelectorPage(chats, token, error = "") {
1412
1433
  }
1413
1434
  h1 {
1414
1435
  margin: 0;
1415
- font-size: 28px;
1436
+ font-size: 30px;
1416
1437
  line-height: 1.1;
1417
1438
  font-weight: 700;
1418
1439
  letter-spacing: 0;
1419
1440
  }
1420
1441
  .subtitle {
1421
1442
  margin: 8px 0 0;
1422
- font-size: 15px;
1443
+ font-size: 14px;
1423
1444
  line-height: 1.4;
1424
1445
  color: var(--muted);
1425
1446
  }
@@ -1431,11 +1452,11 @@ function renderMessagesSelectorPage(chats, token, error = "") {
1431
1452
  }
1432
1453
  .search {
1433
1454
  width: 100%;
1434
- border: 1px solid #d7d9d6;
1455
+ border: 1px solid var(--line);
1435
1456
  border-radius: 8px;
1436
1457
  background: #ffffff;
1437
1458
  color: var(--fg);
1438
- padding: 11px 12px;
1459
+ padding: 12px 13px;
1439
1460
  font: inherit;
1440
1461
  font-size: 15px;
1441
1462
  outline: none;
@@ -1463,6 +1484,7 @@ function renderMessagesSelectorPage(chats, token, error = "") {
1463
1484
  font-size: 11px;
1464
1485
  font-weight: 600;
1465
1486
  text-transform: uppercase;
1487
+ letter-spacing: 0.02em;
1466
1488
  }
1467
1489
  .list-head .right { text-align: left; }
1468
1490
  .chat-list { display: block; }
@@ -1476,7 +1498,7 @@ function renderMessagesSelectorPage(chats, token, error = "") {
1476
1498
  cursor: pointer;
1477
1499
  transition: background 120ms ease;
1478
1500
  }
1479
- .chat-row:hover { background: #f6f7f5; }
1501
+ .chat-row:hover { background: #f6f5f4; }
1480
1502
  input[type="checkbox"] {
1481
1503
  position: absolute;
1482
1504
  opacity: 0;
@@ -1514,7 +1536,7 @@ function renderMessagesSelectorPage(chats, token, error = "") {
1514
1536
  text-overflow: ellipsis;
1515
1537
  white-space: nowrap;
1516
1538
  font-size: 15px;
1517
- font-weight: 550;
1539
+ font-weight: 500;
1518
1540
  }
1519
1541
  .chat-people {
1520
1542
  color: var(--muted);
@@ -1528,9 +1550,9 @@ function renderMessagesSelectorPage(chats, token, error = "") {
1528
1550
  justify-self: start;
1529
1551
  color: var(--muted);
1530
1552
  font-size: 12.5px;
1531
- font-weight: 500;
1553
+ font-weight: 400;
1532
1554
  }
1533
- .chat-kind--group { color: var(--green); }
1555
+ .chat-kind--group { color: var(--fg); }
1534
1556
  .chat-meta {
1535
1557
  color: var(--muted);
1536
1558
  font-size: 12.5px;
@@ -1563,7 +1585,7 @@ function renderMessagesSelectorPage(chats, token, error = "") {
1563
1585
  button {
1564
1586
  appearance: none;
1565
1587
  border: 0;
1566
- border-radius: 6px;
1588
+ border-radius: 8px;
1567
1589
  background: var(--green);
1568
1590
  color: #FFFFFF;
1569
1591
  padding: 9px 13px;
@@ -1600,7 +1622,6 @@ function renderMessagesSelectorPage(chats, token, error = "") {
1600
1622
  </div>
1601
1623
  ${error ? `<p class="error">${html(error)}</p>` : ""}
1602
1624
  <div class="list-head">
1603
- <span></span>
1604
1625
  <span></span>
1605
1626
  <span>Name</span>
1606
1627
  <span>Type</span>
@@ -1685,7 +1706,9 @@ function renderChatPeople(chat) {
1685
1706
 
1686
1707
  function chatPeopleLine(chat) {
1687
1708
  if (chat.kind !== "group") return "";
1688
- const names = chat.participants?.map((participant) => participant.name ?? participant.handle).filter(Boolean) ?? [];
1709
+ const names = chat.participants
1710
+ ?.map((participant) => participant.name)
1711
+ .filter((name) => name && !looksLikeHandleList(name)) ?? [];
1689
1712
  const line = names.slice(0, 6).join(", ");
1690
1713
  if (!line || normalizeDisplayText(line) === normalizeDisplayText(chat.label)) return "";
1691
1714
  return line;
@@ -2022,8 +2045,8 @@ function createMessageSerializer(kit, contactLookup = emptyContactLookup()) {
2022
2045
  };
2023
2046
  }
2024
2047
 
2025
- function buildContactLookup() {
2026
- const contacts = loadContacts();
2048
+ function buildContactLookup(opts = {}) {
2049
+ const contacts = opts.loadAll === false ? [] : loadContacts();
2027
2050
  const myCard = loadMyCard();
2028
2051
  const handleToName = new Map();
2029
2052
  const selfHandles = new Set();
@@ -2070,6 +2093,9 @@ function emptyContactLookup() {
2070
2093
 
2071
2094
  function loadContacts() {
2072
2095
  if (platform() !== "darwin") return [];
2096
+ const sqliteContacts = loadContactsFromAddressBookDb();
2097
+ if (sqliteContacts.length > 0) return sqliteContacts;
2098
+
2073
2099
  const script = `
2074
2100
  set output to ""
2075
2101
  tell application "Contacts"
@@ -2093,14 +2119,71 @@ return output`;
2093
2119
  try {
2094
2120
  const raw = execFileSync("osascript", ["-e", script], {
2095
2121
  encoding: "utf8",
2096
- timeout: 30_000,
2122
+ timeout: 120_000,
2097
2123
  });
2098
2124
  return parseContacts(raw);
2099
- } catch {
2125
+ } catch (err) {
2126
+ if (args.debug === true) console.error("Could not load Contacts:", safeError(err));
2100
2127
  return [];
2101
2128
  }
2102
2129
  }
2103
2130
 
2131
+ function loadContactsFromAddressBookDb() {
2132
+ const contacts = new Map();
2133
+ for (const dbPath of addressBookDatabasePaths()) {
2134
+ const query = `
2135
+ select r.Z_PK,
2136
+ coalesce(nullif(r.ZNAME, ''), nullif(trim(coalesce(r.ZFIRSTNAME, '') || ' ' || coalesce(r.ZLASTNAME, '')), ''), nullif(r.ZORGANIZATION, ''), '') as display_name,
2137
+ coalesce(p.ZFULLNUMBER, '') as phone,
2138
+ '' as email
2139
+ from ZABCDRECORD r
2140
+ join ZABCDPHONENUMBER p on p.ZOWNER = r.Z_PK
2141
+ where p.ZFULLNUMBER is not null and p.ZFULLNUMBER != ''
2142
+ union all
2143
+ select r.Z_PK,
2144
+ coalesce(nullif(r.ZNAME, ''), nullif(trim(coalesce(r.ZFIRSTNAME, '') || ' ' || coalesce(r.ZLASTNAME, '')), ''), nullif(r.ZORGANIZATION, ''), '') as display_name,
2145
+ '' as phone,
2146
+ coalesce(e.ZADDRESS, '') as email
2147
+ from ZABCDRECORD r
2148
+ join ZABCDEMAILADDRESS e on e.ZOWNER = r.Z_PK
2149
+ where e.ZADDRESS is not null and e.ZADDRESS != '';`;
2150
+
2151
+ try {
2152
+ const raw = execFileSync("sqlite3", ["-separator", "\t", dbPath, query], {
2153
+ encoding: "utf8",
2154
+ timeout: 10_000,
2155
+ });
2156
+ for (const line of raw.split("\n").filter(Boolean)) {
2157
+ const [id, rawName, phone, email] = line.split("\t");
2158
+ const name = rawName?.trim();
2159
+ if (!id || !name) continue;
2160
+ const key = `${dbPath}:${id}`;
2161
+ const current = contacts.get(key) ?? { name, phones: [], emails: [] };
2162
+ if (phone) current.phones.push(phone.trim());
2163
+ if (email) current.emails.push(email.trim());
2164
+ contacts.set(key, current);
2165
+ }
2166
+ } catch (err) {
2167
+ if (args.debug === true) console.error(`Could not read Contacts DB ${dbPath}:`, safeError(err));
2168
+ }
2169
+ }
2170
+
2171
+ return [...contacts.values()].filter((contact) => contact.name);
2172
+ }
2173
+
2174
+ function addressBookDatabasePaths() {
2175
+ const addressBookDir = join(homedir(), "Library", "Application Support", "AddressBook");
2176
+ try {
2177
+ const raw = execFileSync("find", [addressBookDir, "-maxdepth", "4", "-name", "AddressBook-v22.abcddb"], {
2178
+ encoding: "utf8",
2179
+ timeout: 5_000,
2180
+ });
2181
+ return [...new Set(raw.split("\n").map((path) => path.trim()).filter(Boolean))];
2182
+ } catch {
2183
+ return [join(addressBookDir, "AddressBook-v22.abcddb")];
2184
+ }
2185
+ }
2186
+
2104
2187
  function loadMyCard() {
2105
2188
  if (platform() !== "darwin") return null;
2106
2189
  const script = `
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shepherd-onboard",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "Customer-facing Shepherd raw sync onboarding CLI",
5
5
  "type": "module",
6
6
  "bin": {