shepherd-onboard 0.1.12 → 0.1.14
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 +4 -2
- package/assets/shepherd_G_vector_136033.png +0 -0
- package/bin/shepherd-onboard.js +352 -118
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ npx -y shepherd-onboard@latest agent
|
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
The command prints the exact prompt the agent should follow, then the exact follow-up commands to open Shepherd WorkOS login/signup, guide Google Workspace Admin Console delegation, open source auth for non-Google sources, open Granola's API key page, finalize, start cloud raw polling/backfills, and install local Messages sync.
|
|
14
|
-
The agent prompt tells coding agents to ask short selection questions first: existing/new org, sources to connect, and Messages skip/provide-handle. If Messages is selected, the agent runs `messages-chats`, opens a minimal local webpage
|
|
14
|
+
The agent prompt tells coding agents to ask short selection questions first: existing/new org, sources to connect, and Messages skip/provide-handle. If Messages is selected, the agent runs `messages-chats`, opens a minimal searchable local webpage, and has the user select which contacts/groups to sync. Account creation/relinking always starts with Shepherd WorkOS auth.
|
|
15
15
|
Existing-organization joins are verified from Shepherd login and company email domain; the typed org name is not trusted by itself.
|
|
16
16
|
|
|
17
17
|
## Human Terminal One-liner
|
|
@@ -31,7 +31,7 @@ The command:
|
|
|
31
31
|
- opens Slack authorization
|
|
32
32
|
- opens the Granola desktop app to Settings -> Connectors -> API keys
|
|
33
33
|
- collects the Granola API key after opening the Granola screen when Granola is enabled
|
|
34
|
-
- opens a minimal local webpage
|
|
34
|
+
- opens a minimal searchable local webpage with contact/group names and only syncs the chats selected by the user
|
|
35
35
|
- sets up local macOS Messages raw sync with a background LaunchAgent scoped to the selected chats
|
|
36
36
|
- starts raw polling/backfill for connected sources
|
|
37
37
|
- does not start wiki generation, memory compilation, or doc summaries
|
|
@@ -82,6 +82,8 @@ npx -y shepherd-onboard@latest messages-chats
|
|
|
82
82
|
|
|
83
83
|
Use `--json` for machine-readable chat metadata, or `--text` for a terminal list.
|
|
84
84
|
|
|
85
|
+
The browser selector displays the first page of recent chats and lets the user search contacts or groups loaded from local Messages.
|
|
86
|
+
|
|
85
87
|
Pass the selected chat IDs when finishing onboarding:
|
|
86
88
|
|
|
87
89
|
```sh
|
|
Binary file
|
package/bin/shepherd-onboard.js
CHANGED
|
@@ -6,14 +6,18 @@ import { createServer } from "node:http";
|
|
|
6
6
|
import { homedir, platform } from "node:os";
|
|
7
7
|
import { dirname, join } from "node:path";
|
|
8
8
|
import readline from "node:readline";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
9
10
|
|
|
10
11
|
const DEFAULT_API_URL = "https://brain-api-customer-facing.up.railway.app";
|
|
11
12
|
const PACKAGE_NAME = "shepherd-onboard";
|
|
12
13
|
const PACKAGE_SPEC = `${PACKAGE_NAME}@latest`;
|
|
14
|
+
const PACKAGE_DIR = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
13
15
|
const DEFAULT_AGENT_STATE_PATH = join(homedir(), ".shepherd", "raw-onboarding-agent.json");
|
|
14
16
|
const MAX_BATCH_SIZE = 50;
|
|
15
17
|
const MAX_QUEUE_MESSAGES = 10_000;
|
|
16
|
-
const
|
|
18
|
+
const DEFAULT_MESSAGE_CHAT_SEARCH_LIMIT = 200;
|
|
19
|
+
const INITIAL_MESSAGE_CHAT_ROWS = 20;
|
|
20
|
+
const SHEPHERD_LOGO_PATH = join(PACKAGE_DIR, "assets", "shepherd_G_vector_136033.png");
|
|
17
21
|
const GRANOLA_API_KEYS_PATH = "/settings/integrations/api-keys";
|
|
18
22
|
const GOOGLE_WORKSPACE_DELEGATION_APP_NAME = "Shepherd";
|
|
19
23
|
const GOOGLE_WORKSPACE_DELEGATION_SERVICE_ACCOUNT_EMAIL =
|
|
@@ -515,7 +519,7 @@ async function printAgentStatus() {
|
|
|
515
519
|
|
|
516
520
|
async function runMessagesChatsCommand() {
|
|
517
521
|
const chats = await listRecentMessageChats({
|
|
518
|
-
limit: clampInt(Number(args.limit ??
|
|
522
|
+
limit: clampInt(Number(args.limit ?? DEFAULT_MESSAGE_CHAT_SEARCH_LIMIT), 1, 500),
|
|
519
523
|
});
|
|
520
524
|
|
|
521
525
|
if (args.json) {
|
|
@@ -647,7 +651,7 @@ Usage:
|
|
|
647
651
|
npx -y ${PACKAGE_NAME}@latest messages-chats --json
|
|
648
652
|
|
|
649
653
|
Options:
|
|
650
|
-
--limit <n> Number of recent chats to
|
|
654
|
+
--limit <n> Number of recent chats to load for search. Defaults to ${DEFAULT_MESSAGE_CHAT_SEARCH_LIMIT}.
|
|
651
655
|
--text Print a terminal list instead of opening the selector page.
|
|
652
656
|
--no-open Print the local selector URL instead of opening it.
|
|
653
657
|
--json Print machine-readable chat IDs and labels.
|
|
@@ -702,7 +706,7 @@ function printAgentContract() {
|
|
|
702
706
|
"If Google Workspace is selected, guide the customer's Google Workspace super admin to authorize Shepherd's Client ID and scopes in Google Admin Console.",
|
|
703
707
|
"Ask Messages as a selectable choice: Skip Messages, or Provide handle.",
|
|
704
708
|
"If the user chooses Provide handle, ask for the phone number or Apple ID email.",
|
|
705
|
-
"If Messages is selected, run the recent-chat command
|
|
709
|
+
"If Messages is selected, run the recent-chat command. It opens a browser selector with recent chats and search. Never sync all Messages chats by default.",
|
|
706
710
|
],
|
|
707
711
|
selectionQuestions: [
|
|
708
712
|
{
|
|
@@ -726,7 +730,7 @@ function printAgentContract() {
|
|
|
726
730
|
"Full name",
|
|
727
731
|
"Organization name",
|
|
728
732
|
"Messages phone number or Apple ID email, if they want local Messages connected",
|
|
729
|
-
"Selected local Messages chats from the
|
|
733
|
+
"Selected local Messages chats from the browser selector, if they want local Messages connected",
|
|
730
734
|
],
|
|
731
735
|
afterStartCommand: [
|
|
732
736
|
"For Google Workspace, have a super admin authorize the Shepherd Client ID and scopes in Google Admin Console.",
|
|
@@ -798,7 +802,7 @@ If they are joining an existing org, ask for the org name they believe they belo
|
|
|
798
802
|
If Messages is selected, run:
|
|
799
803
|
${payload.messagesChatsCommand}
|
|
800
804
|
|
|
801
|
-
This opens a minimal local webpage with
|
|
805
|
+
This opens a minimal local webpage with recent local Messages chats and search. Have the user select which contacts/groups Shepherd should sync. Do not select all chats by default. When the command returns, keep the printed comma-separated chat IDs.
|
|
802
806
|
|
|
803
807
|
Then run:
|
|
804
808
|
${payload.startCommand}
|
|
@@ -1207,7 +1211,7 @@ async function selectRecentMessageChats() {
|
|
|
1207
1211
|
throw new Error(`Messages sync requires selected chat IDs. Run ${agentCommand()} messages-chats and pass --messages-chat-ids "<id1>,<id2>".`);
|
|
1208
1212
|
}
|
|
1209
1213
|
|
|
1210
|
-
const chats = await listRecentMessageChats({ limit:
|
|
1214
|
+
const chats = await listRecentMessageChats({ limit: clampInt(Number(args.limit ?? DEFAULT_MESSAGE_CHAT_SEARCH_LIMIT), 1, 500) });
|
|
1211
1215
|
if (chats.length === 0) {
|
|
1212
1216
|
throw new Error("No recent local Messages chats were found on this Mac.");
|
|
1213
1217
|
}
|
|
@@ -1314,19 +1318,36 @@ async function selectChatsInBrowser(chats, opts = {}) {
|
|
|
1314
1318
|
}
|
|
1315
1319
|
|
|
1316
1320
|
function renderMessagesSelectorPage(chats, token, error = "") {
|
|
1317
|
-
const
|
|
1318
|
-
|
|
1321
|
+
const logo = shepherdLogoDataUri();
|
|
1322
|
+
const rows = chats.map((chat, index) => {
|
|
1323
|
+
const people = chatPeopleLine(chat);
|
|
1324
|
+
const when = formatMessageChatMeta(chat);
|
|
1325
|
+
const searchText = [
|
|
1326
|
+
chat.label,
|
|
1327
|
+
chat.kind,
|
|
1328
|
+
people,
|
|
1329
|
+
when,
|
|
1330
|
+
...(chat.participants ?? []).flatMap((participant) => [participant.name, participant.handle]),
|
|
1331
|
+
]
|
|
1332
|
+
.filter(Boolean)
|
|
1333
|
+
.join(" ")
|
|
1334
|
+
.toLowerCase();
|
|
1335
|
+
|
|
1336
|
+
const kindKey = chat.kind === "group" ? "group" : chat.kind === "dm" ? "dm" : "other";
|
|
1337
|
+
const kindLabel = kindKey === "group" ? "Group" : kindKey === "dm" ? "Contact" : "Chat";
|
|
1338
|
+
return `
|
|
1339
|
+
<label class="chat-row" data-index="${index}" data-search="${htmlAttr(searchText)}">
|
|
1319
1340
|
<input type="checkbox" name="chatId" value="${htmlAttr(chat.chatId)}">
|
|
1320
1341
|
<span class="box" aria-hidden="true"></span>
|
|
1321
|
-
<span class="
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
</span>
|
|
1326
|
-
${renderChatPeople(chat)}
|
|
1327
|
-
<span class="chat-meta">${html(formatMessageChatMeta(chat))}</span>
|
|
1342
|
+
<span class="avatar" aria-hidden="true">${html(chatInitials(chat.label))}</span>
|
|
1343
|
+
<span class="chat-name-block">
|
|
1344
|
+
<span class="chat-name">${html(chat.label)}</span>
|
|
1345
|
+
${people ? `<span class="chat-people">${html(people)}</span>` : ""}
|
|
1328
1346
|
</span>
|
|
1329
|
-
|
|
1347
|
+
<span class="chat-kind chat-kind--${kindKey}">${html(kindLabel)}</span>
|
|
1348
|
+
<span class="chat-meta">${when ? html(when) : "—"}</span>
|
|
1349
|
+
</label>`;
|
|
1350
|
+
}).join("");
|
|
1330
1351
|
|
|
1331
1352
|
return `<!doctype html>
|
|
1332
1353
|
<html lang="en">
|
|
@@ -1336,87 +1357,131 @@ function renderMessagesSelectorPage(chats, token, error = "") {
|
|
|
1336
1357
|
<title>Select Messages Chats</title>
|
|
1337
1358
|
<style>
|
|
1338
1359
|
:root {
|
|
1339
|
-
|
|
1340
|
-
--
|
|
1341
|
-
--fg: #111111;
|
|
1342
|
-
--muted: #6D726D;
|
|
1343
|
-
--line: #E8ECE8;
|
|
1360
|
+
--hero-top: #136033;
|
|
1361
|
+
--hero-bottom: #0c3f22;
|
|
1344
1362
|
--panel: #FFFFFF;
|
|
1345
|
-
--
|
|
1346
|
-
--
|
|
1347
|
-
--
|
|
1348
|
-
--
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
:
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
--button: #FFFFFF;
|
|
1358
|
-
--button-text: #000000;
|
|
1359
|
-
--link: #136033;
|
|
1360
|
-
}
|
|
1363
|
+
--fg: #15201a;
|
|
1364
|
+
--muted: #6B736D;
|
|
1365
|
+
--faint: #8A938C;
|
|
1366
|
+
--line: #EEF1EE;
|
|
1367
|
+
--green: #136033;
|
|
1368
|
+
--green-soft: #E7F3EC;
|
|
1369
|
+
--green-text: #1F7A43;
|
|
1370
|
+
--neutral-soft: #F0F1EF;
|
|
1371
|
+
--button: #1F6E3C;
|
|
1372
|
+
--button-hover: #237b44;
|
|
1373
|
+
--radius: 12px;
|
|
1374
|
+
--grid: 20px 36px minmax(0, 1fr) 92px 124px;
|
|
1361
1375
|
}
|
|
1362
1376
|
* { box-sizing: border-box; }
|
|
1363
1377
|
body {
|
|
1364
1378
|
margin: 0;
|
|
1365
|
-
|
|
1379
|
+
min-height: 100vh;
|
|
1380
|
+
background: linear-gradient(180deg, var(--hero-top) 0%, var(--hero-top) 150px, var(--hero-bottom) 520px, var(--hero-bottom) 100%);
|
|
1366
1381
|
color: var(--fg);
|
|
1367
1382
|
font-family: Geist, "Geist Sans", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
1368
1383
|
letter-spacing: 0;
|
|
1384
|
+
-webkit-font-smoothing: antialiased;
|
|
1369
1385
|
}
|
|
1370
1386
|
main {
|
|
1371
1387
|
width: min(760px, calc(100vw - 32px));
|
|
1372
|
-
margin:
|
|
1388
|
+
margin: 0 auto;
|
|
1389
|
+
padding: 48px 0 40px;
|
|
1373
1390
|
}
|
|
1374
|
-
|
|
1391
|
+
.hero {
|
|
1375
1392
|
display: flex;
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1393
|
+
gap: 22px;
|
|
1394
|
+
align-items: center;
|
|
1395
|
+
padding: 0 6px 34px;
|
|
1396
|
+
color: #FFFFFF;
|
|
1397
|
+
}
|
|
1398
|
+
.hero .logo {
|
|
1399
|
+
width: 96px;
|
|
1400
|
+
height: 96px;
|
|
1401
|
+
object-fit: contain;
|
|
1402
|
+
flex: none;
|
|
1403
|
+
}
|
|
1404
|
+
.hero .logo-fallback {
|
|
1405
|
+
width: 88px;
|
|
1406
|
+
height: 88px;
|
|
1407
|
+
border-radius: 20px;
|
|
1408
|
+
display: grid;
|
|
1409
|
+
place-items: center;
|
|
1410
|
+
color: #FFFFFF;
|
|
1411
|
+
border: 2px solid rgba(255, 255, 255, 0.35);
|
|
1412
|
+
font-size: 34px;
|
|
1413
|
+
font-weight: 700;
|
|
1414
|
+
flex: none;
|
|
1381
1415
|
}
|
|
1382
1416
|
h1 {
|
|
1383
1417
|
margin: 0;
|
|
1384
|
-
font-size:
|
|
1385
|
-
line-height: 1.
|
|
1386
|
-
font-weight:
|
|
1418
|
+
font-size: clamp(30px, 4.6vw, 44px);
|
|
1419
|
+
line-height: 1.05;
|
|
1420
|
+
font-weight: 700;
|
|
1421
|
+
letter-spacing: -0.02em;
|
|
1387
1422
|
}
|
|
1388
|
-
.
|
|
1389
|
-
|
|
1390
|
-
font-size:
|
|
1391
|
-
|
|
1423
|
+
.subtitle {
|
|
1424
|
+
margin: 12px 0 0;
|
|
1425
|
+
font-size: 16px;
|
|
1426
|
+
line-height: 1.45;
|
|
1427
|
+
color: rgba(231, 243, 235, 0.72);
|
|
1428
|
+
}
|
|
1429
|
+
.panel {
|
|
1430
|
+
background: var(--panel);
|
|
1431
|
+
border-radius: 22px;
|
|
1432
|
+
box-shadow: 0 18px 50px rgba(0, 0, 0, 0.22);
|
|
1433
|
+
padding: 8px 0 0;
|
|
1434
|
+
overflow: hidden;
|
|
1435
|
+
}
|
|
1436
|
+
.panel-head {
|
|
1437
|
+
padding: 18px 24px 14px;
|
|
1438
|
+
}
|
|
1439
|
+
.search {
|
|
1440
|
+
width: 100%;
|
|
1441
|
+
border: 1px solid #E6EAE6;
|
|
1442
|
+
border-radius: 10px;
|
|
1443
|
+
background: #F6F8F6;
|
|
1444
|
+
color: var(--fg);
|
|
1445
|
+
padding: 11px 13px;
|
|
1446
|
+
font: inherit;
|
|
1447
|
+
font-size: 14px;
|
|
1448
|
+
outline: none;
|
|
1392
1449
|
}
|
|
1393
|
-
|
|
1394
|
-
|
|
1450
|
+
.search:focus {
|
|
1451
|
+
border-color: var(--green);
|
|
1452
|
+
background: #FFFFFF;
|
|
1453
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--green) 16%, transparent);
|
|
1395
1454
|
}
|
|
1455
|
+
.search::placeholder { color: var(--faint); }
|
|
1396
1456
|
.error {
|
|
1397
|
-
margin:
|
|
1457
|
+
margin: 12px 24px 0;
|
|
1398
1458
|
color: #9B1C1C;
|
|
1399
1459
|
font-size: 14px;
|
|
1400
1460
|
}
|
|
1401
|
-
.
|
|
1461
|
+
.list-head {
|
|
1402
1462
|
display: grid;
|
|
1403
|
-
|
|
1404
|
-
|
|
1463
|
+
grid-template-columns: var(--grid);
|
|
1464
|
+
gap: 14px;
|
|
1465
|
+
align-items: center;
|
|
1466
|
+
padding: 6px 24px 12px;
|
|
1467
|
+
border-bottom: 1px solid var(--line);
|
|
1468
|
+
color: var(--faint);
|
|
1469
|
+
font-size: 13px;
|
|
1470
|
+
font-weight: 500;
|
|
1405
1471
|
}
|
|
1472
|
+
.list-head .right { text-align: left; }
|
|
1473
|
+
.chat-list { display: block; }
|
|
1406
1474
|
.chat-row {
|
|
1407
1475
|
display: grid;
|
|
1408
|
-
grid-template-columns:
|
|
1409
|
-
gap:
|
|
1410
|
-
align-items:
|
|
1411
|
-
padding: 13px
|
|
1412
|
-
|
|
1413
|
-
border: 1px solid var(--line);
|
|
1414
|
-
border-radius: var(--radius);
|
|
1476
|
+
grid-template-columns: var(--grid);
|
|
1477
|
+
gap: 14px;
|
|
1478
|
+
align-items: center;
|
|
1479
|
+
padding: 13px 24px;
|
|
1480
|
+
border-bottom: 1px solid var(--line);
|
|
1415
1481
|
cursor: pointer;
|
|
1482
|
+
transition: background 120ms ease;
|
|
1416
1483
|
}
|
|
1417
|
-
.chat-row:hover {
|
|
1418
|
-
border-color: color-mix(in srgb, var(--link) 45%, var(--line));
|
|
1419
|
-
}
|
|
1484
|
+
.chat-row:hover { background: #F7FAF8; }
|
|
1420
1485
|
input[type="checkbox"] {
|
|
1421
1486
|
position: absolute;
|
|
1422
1487
|
opacity: 0;
|
|
@@ -1425,34 +1490,41 @@ function renderMessagesSelectorPage(chats, token, error = "") {
|
|
|
1425
1490
|
.box {
|
|
1426
1491
|
width: 18px;
|
|
1427
1492
|
height: 18px;
|
|
1428
|
-
|
|
1429
|
-
border: 1.5px solid var(--muted);
|
|
1493
|
+
border: 1.5px solid #C7CDC8;
|
|
1430
1494
|
border-radius: 5px;
|
|
1431
1495
|
display: inline-grid;
|
|
1432
1496
|
place-items: center;
|
|
1497
|
+
transition: background 120ms ease, border-color 120ms ease;
|
|
1433
1498
|
}
|
|
1499
|
+
.chat-row:hover .box { border-color: var(--green); }
|
|
1434
1500
|
input[type="checkbox"]:checked + .box {
|
|
1435
|
-
background: var(--
|
|
1436
|
-
border-color: var(--
|
|
1501
|
+
background: var(--green);
|
|
1502
|
+
border-color: var(--green);
|
|
1437
1503
|
}
|
|
1438
1504
|
input[type="checkbox"]:checked + .box::after {
|
|
1439
1505
|
content: "";
|
|
1440
1506
|
width: 7px;
|
|
1441
1507
|
height: 4px;
|
|
1442
|
-
border-left: 2px solid
|
|
1443
|
-
border-bottom: 2px solid
|
|
1508
|
+
border-left: 2px solid #FFFFFF;
|
|
1509
|
+
border-bottom: 2px solid #FFFFFF;
|
|
1444
1510
|
transform: rotate(-45deg) translateY(-1px);
|
|
1445
1511
|
}
|
|
1446
|
-
.
|
|
1447
|
-
|
|
1512
|
+
.avatar {
|
|
1513
|
+
width: 36px;
|
|
1514
|
+
height: 36px;
|
|
1515
|
+
border-radius: 50%;
|
|
1516
|
+
background: var(--green);
|
|
1517
|
+
color: #FFFFFF;
|
|
1448
1518
|
display: grid;
|
|
1449
|
-
|
|
1519
|
+
place-items: center;
|
|
1520
|
+
font-size: 13px;
|
|
1521
|
+
font-weight: 650;
|
|
1522
|
+
letter-spacing: 0.02em;
|
|
1450
1523
|
}
|
|
1451
|
-
.chat-
|
|
1452
|
-
display: flex;
|
|
1453
|
-
gap: 10px;
|
|
1454
|
-
align-items: center;
|
|
1524
|
+
.chat-name-block {
|
|
1455
1525
|
min-width: 0;
|
|
1526
|
+
display: grid;
|
|
1527
|
+
gap: 2px;
|
|
1456
1528
|
}
|
|
1457
1529
|
.chat-name {
|
|
1458
1530
|
overflow: hidden;
|
|
@@ -1461,55 +1533,145 @@ function renderMessagesSelectorPage(chats, token, error = "") {
|
|
|
1461
1533
|
font-size: 15px;
|
|
1462
1534
|
font-weight: 600;
|
|
1463
1535
|
}
|
|
1536
|
+
.chat-people {
|
|
1537
|
+
color: var(--muted);
|
|
1538
|
+
font-size: 13px;
|
|
1539
|
+
line-height: 1.3;
|
|
1540
|
+
overflow: hidden;
|
|
1541
|
+
text-overflow: ellipsis;
|
|
1542
|
+
white-space: nowrap;
|
|
1543
|
+
}
|
|
1464
1544
|
.chat-kind {
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1545
|
+
justify-self: start;
|
|
1546
|
+
padding: 3px 11px;
|
|
1547
|
+
border-radius: 999px;
|
|
1548
|
+
font-size: 12.5px;
|
|
1549
|
+
font-weight: 500;
|
|
1468
1550
|
}
|
|
1469
|
-
.chat-
|
|
1551
|
+
.chat-kind--group { background: var(--green-soft); color: var(--green-text); }
|
|
1552
|
+
.chat-kind--dm,
|
|
1553
|
+
.chat-kind--other { background: var(--neutral-soft); color: var(--muted); }
|
|
1470
1554
|
.chat-meta {
|
|
1471
1555
|
color: var(--muted);
|
|
1472
|
-
font-size:
|
|
1473
|
-
line-height: 1.35;
|
|
1556
|
+
font-size: 13.5px;
|
|
1474
1557
|
overflow-wrap: anywhere;
|
|
1475
1558
|
}
|
|
1559
|
+
[hidden] { display: none !important; }
|
|
1560
|
+
.empty {
|
|
1561
|
+
margin: 0;
|
|
1562
|
+
padding: 28px 24px 32px;
|
|
1563
|
+
color: var(--muted);
|
|
1564
|
+
font-size: 14px;
|
|
1565
|
+
text-align: center;
|
|
1566
|
+
}
|
|
1476
1567
|
.actions {
|
|
1477
1568
|
display: flex;
|
|
1478
|
-
justify-content:
|
|
1569
|
+
justify-content: space-between;
|
|
1570
|
+
align-items: center;
|
|
1571
|
+
gap: 12px;
|
|
1479
1572
|
position: sticky;
|
|
1480
1573
|
bottom: 0;
|
|
1481
|
-
|
|
1482
|
-
|
|
1574
|
+
margin-top: 22px;
|
|
1575
|
+
padding: 16px 6px 4px;
|
|
1576
|
+
}
|
|
1577
|
+
.selection-count {
|
|
1578
|
+
color: rgba(231, 243, 235, 0.82);
|
|
1579
|
+
font-size: 14.5px;
|
|
1580
|
+
font-weight: 500;
|
|
1483
1581
|
}
|
|
1484
1582
|
button {
|
|
1485
1583
|
appearance: none;
|
|
1486
1584
|
border: 0;
|
|
1487
|
-
border-radius:
|
|
1585
|
+
border-radius: 14px;
|
|
1488
1586
|
background: var(--button);
|
|
1489
|
-
color:
|
|
1490
|
-
padding:
|
|
1587
|
+
color: #FFFFFF;
|
|
1588
|
+
padding: 14px 24px;
|
|
1491
1589
|
font: inherit;
|
|
1492
|
-
font-
|
|
1590
|
+
font-size: 15px;
|
|
1591
|
+
font-weight: 600;
|
|
1493
1592
|
cursor: pointer;
|
|
1593
|
+
display: inline-flex;
|
|
1594
|
+
align-items: center;
|
|
1595
|
+
gap: 10px;
|
|
1596
|
+
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.28);
|
|
1597
|
+
transition: background 120ms ease, transform 120ms ease;
|
|
1598
|
+
}
|
|
1599
|
+
button:hover { background: var(--button-hover); }
|
|
1600
|
+
button:active { transform: scale(0.985); }
|
|
1601
|
+
button svg { width: 18px; height: 18px; }
|
|
1602
|
+
@media (max-width: 560px) {
|
|
1603
|
+
:root { --grid: 18px 32px minmax(0, 1fr) auto; }
|
|
1604
|
+
.list-head span:last-child,
|
|
1605
|
+
.chat-meta { display: none; }
|
|
1494
1606
|
}
|
|
1495
|
-
a { color: var(--link); }
|
|
1496
1607
|
</style>
|
|
1497
1608
|
</head>
|
|
1498
1609
|
<body>
|
|
1499
1610
|
<main>
|
|
1500
|
-
<header>
|
|
1501
|
-
|
|
1502
|
-
<div
|
|
1611
|
+
<header class="hero">
|
|
1612
|
+
${logo ? `<img class="logo" src="${htmlAttr(logo)}" alt="">` : `<span class="logo-fallback" aria-hidden="true">S</span>`}
|
|
1613
|
+
<div>
|
|
1614
|
+
<h1>Select Your Recent Chats</h1>
|
|
1615
|
+
<p class="subtitle">Choose the chats Shepherd should sync. Search if you do not see one.</p>
|
|
1616
|
+
</div>
|
|
1503
1617
|
</header>
|
|
1504
1618
|
<form method="post" action="/select">
|
|
1505
1619
|
<input type="hidden" name="token" value="${htmlAttr(token)}">
|
|
1506
|
-
|
|
1507
|
-
|
|
1620
|
+
<div class="panel">
|
|
1621
|
+
<div class="panel-head">
|
|
1622
|
+
<input class="search" id="search" type="search" placeholder="Search contacts or groups" autocomplete="off">
|
|
1623
|
+
</div>
|
|
1624
|
+
${error ? `<p class="error">${html(error)}</p>` : ""}
|
|
1625
|
+
<div class="list-head">
|
|
1626
|
+
<span></span>
|
|
1627
|
+
<span></span>
|
|
1628
|
+
<span>Name</span>
|
|
1629
|
+
<span>Type</span>
|
|
1630
|
+
<span class="right">Last message</span>
|
|
1631
|
+
</div>
|
|
1632
|
+
<div class="chat-list">${rows}</div>
|
|
1633
|
+
<p class="empty" id="empty" hidden>No chats found.</p>
|
|
1634
|
+
</div>
|
|
1508
1635
|
<div class="actions">
|
|
1509
|
-
<
|
|
1636
|
+
<span class="selection-count" id="selection-count">0 selected</span>
|
|
1637
|
+
<button type="submit">
|
|
1638
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M19 12H5"/><path d="m12 19-7-7 7-7"/></svg>
|
|
1639
|
+
Return
|
|
1640
|
+
</button>
|
|
1510
1641
|
</div>
|
|
1511
1642
|
</form>
|
|
1512
1643
|
</main>
|
|
1644
|
+
<script>
|
|
1645
|
+
const initialRows = ${INITIAL_MESSAGE_CHAT_ROWS};
|
|
1646
|
+
const rows = Array.from(document.querySelectorAll(".chat-row"));
|
|
1647
|
+
const search = document.getElementById("search");
|
|
1648
|
+
const empty = document.getElementById("empty");
|
|
1649
|
+
const selected = document.getElementById("selection-count");
|
|
1650
|
+
const checks = Array.from(document.querySelectorAll('input[name="chatId"]'));
|
|
1651
|
+
|
|
1652
|
+
function updateRows() {
|
|
1653
|
+
const query = search.value.trim().toLowerCase();
|
|
1654
|
+
let visible = 0;
|
|
1655
|
+
for (const row of rows) {
|
|
1656
|
+
const matches = query
|
|
1657
|
+
? row.dataset.search.includes(query)
|
|
1658
|
+
: Number(row.dataset.index) < initialRows;
|
|
1659
|
+
row.hidden = !matches;
|
|
1660
|
+
if (matches) visible += 1;
|
|
1661
|
+
}
|
|
1662
|
+
empty.hidden = visible !== 0;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
function updateSelected() {
|
|
1666
|
+
const count = checks.filter((check) => check.checked).length;
|
|
1667
|
+
selected.textContent = count + " selected";
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
search.addEventListener("input", updateRows);
|
|
1671
|
+
for (const check of checks) check.addEventListener("change", updateSelected);
|
|
1672
|
+
updateRows();
|
|
1673
|
+
updateSelected();
|
|
1674
|
+
</script>
|
|
1513
1675
|
</body>
|
|
1514
1676
|
</html>`;
|
|
1515
1677
|
}
|
|
@@ -1542,16 +1704,66 @@ function renderMessagesDonePage(message, isError = false) {
|
|
|
1542
1704
|
}
|
|
1543
1705
|
|
|
1544
1706
|
function renderChatPeople(chat) {
|
|
1707
|
+
const people = chatPeopleLine(chat);
|
|
1708
|
+
if (!people) return "";
|
|
1709
|
+
return `<span class="chat-people">${html(people)}</span>`;
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
function chatInitials(label) {
|
|
1713
|
+
const words = String(label ?? "").trim().split(/\s+/).filter(Boolean);
|
|
1714
|
+
if (words.length === 0) return "?";
|
|
1715
|
+
if (words.length === 1) return words[0].slice(0, 2).toUpperCase();
|
|
1716
|
+
return (words[0][0] + words[words.length - 1][0]).toUpperCase();
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
function chatPeopleLine(chat) {
|
|
1720
|
+
if (chat.kind !== "group") return "";
|
|
1545
1721
|
const names = chat.participants?.map((participant) => participant.name ?? participant.handle).filter(Boolean) ?? [];
|
|
1546
|
-
|
|
1547
|
-
|
|
1722
|
+
const line = names.slice(0, 6).join(", ");
|
|
1723
|
+
if (!line || normalizeDisplayText(line) === normalizeDisplayText(chat.label)) return "";
|
|
1724
|
+
return line;
|
|
1548
1725
|
}
|
|
1549
1726
|
|
|
1550
1727
|
function formatMessageChatMeta(chat) {
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1728
|
+
return formatShortChatTime(chat.lastMessageAt);
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
function formatShortChatTime(value) {
|
|
1732
|
+
if (!value) return "";
|
|
1733
|
+
const date = new Date(value);
|
|
1734
|
+
if (Number.isNaN(date.getTime())) return "";
|
|
1735
|
+
|
|
1736
|
+
const now = new Date();
|
|
1737
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
1738
|
+
const day = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
1739
|
+
const diffDays = Math.round((today.getTime() - day.getTime()) / (24 * 60 * 60 * 1000));
|
|
1740
|
+
const time = new Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "2-digit" })
|
|
1741
|
+
.format(date)
|
|
1742
|
+
.replace(":00", "")
|
|
1743
|
+
.replace(/\s/g, "")
|
|
1744
|
+
.toLowerCase();
|
|
1745
|
+
|
|
1746
|
+
if (diffDays === 0) return `Today ${time}`;
|
|
1747
|
+
if (diffDays === 1) return `Yesterday ${time}`;
|
|
1748
|
+
if (diffDays >= 0 && diffDays < 7) {
|
|
1749
|
+
const weekday = new Intl.DateTimeFormat(undefined, { weekday: "long" }).format(date);
|
|
1750
|
+
return `${weekday} ${time}`;
|
|
1751
|
+
}
|
|
1752
|
+
const monthDay = new Intl.DateTimeFormat(undefined, { month: "short", day: "numeric" }).format(date);
|
|
1753
|
+
return `${monthDay} ${time}`;
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
function normalizeDisplayText(value) {
|
|
1757
|
+
return String(value ?? "").trim().replace(/\s+/g, " ").toLowerCase();
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
function shepherdLogoDataUri() {
|
|
1761
|
+
try {
|
|
1762
|
+
const bytes = readFileSync(SHEPHERD_LOGO_PATH);
|
|
1763
|
+
return `data:image/png;base64,${bytes.toString("base64")}`;
|
|
1764
|
+
} catch {
|
|
1765
|
+
return null;
|
|
1766
|
+
}
|
|
1555
1767
|
}
|
|
1556
1768
|
|
|
1557
1769
|
function sendHtml(res, body, status = 200) {
|
|
@@ -1589,7 +1801,7 @@ async function listRecentMessageChats({ limit }) {
|
|
|
1589
1801
|
try {
|
|
1590
1802
|
const chats = await sdk.listChats({
|
|
1591
1803
|
sortBy: "recent",
|
|
1592
|
-
limit: Math.max(limit,
|
|
1804
|
+
limit: Math.max(limit, INITIAL_MESSAGE_CHAT_ROWS),
|
|
1593
1805
|
});
|
|
1594
1806
|
const visible = chats
|
|
1595
1807
|
.filter((chat) => typeof chat.chatId === "string" && chat.chatId.trim())
|
|
@@ -1614,10 +1826,12 @@ async function enrichMessageChat(sdk, chat, contactLookup) {
|
|
|
1614
1826
|
: null;
|
|
1615
1827
|
const dmName = dmHandle ? contactLookup.resolveName(dmHandle) : null;
|
|
1616
1828
|
const groupNames = participants.map((participant) => participant.name ?? participant.handle).filter(Boolean);
|
|
1617
|
-
const
|
|
1618
|
-
|
|
1619
|
-
?? (
|
|
1620
|
-
|
|
1829
|
+
const chatName = cleanChatName(chat.name);
|
|
1830
|
+
const label = chat.kind === "dm"
|
|
1831
|
+
? dmName ?? nonHandleChatName(chatName) ?? dmHandle ?? "Contact"
|
|
1832
|
+
: nonHandleChatName(chatName)
|
|
1833
|
+
?? (groupNames.length ? groupNames.slice(0, 4).join(", ") : null)
|
|
1834
|
+
?? "Group";
|
|
1621
1835
|
|
|
1622
1836
|
return {
|
|
1623
1837
|
chatId: chat.chatId,
|
|
@@ -1647,11 +1861,10 @@ function uniqueParticipants(messages, contactLookup) {
|
|
|
1647
1861
|
}
|
|
1648
1862
|
|
|
1649
1863
|
function formatMessageChatOption(chat) {
|
|
1650
|
-
const kind = chat.kind === "group" ? "
|
|
1651
|
-
const
|
|
1652
|
-
const
|
|
1653
|
-
|
|
1654
|
-
return `${chat.label} (${kind})${people}${when}`;
|
|
1864
|
+
const kind = chat.kind === "group" ? "Group" : chat.kind === "dm" ? "Contact" : "Chat";
|
|
1865
|
+
const people = chatPeopleLine(chat);
|
|
1866
|
+
const when = formatMessageChatMeta(chat);
|
|
1867
|
+
return [chat.label, kind, people, when].filter(Boolean).join(" · ");
|
|
1655
1868
|
}
|
|
1656
1869
|
|
|
1657
1870
|
function publicMessageChat(chat) {
|
|
@@ -2008,6 +2221,27 @@ function cleanChatName(name) {
|
|
|
2008
2221
|
return trimmed || null;
|
|
2009
2222
|
}
|
|
2010
2223
|
|
|
2224
|
+
function nonHandleChatName(name) {
|
|
2225
|
+
if (!name) return null;
|
|
2226
|
+
return looksLikeHandleList(name) ? null : name;
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
function looksLikeHandleList(value) {
|
|
2230
|
+
const text = String(value ?? "").trim();
|
|
2231
|
+
if (!text) return false;
|
|
2232
|
+
const tokens = text
|
|
2233
|
+
.split(/[,;/&\s]+/)
|
|
2234
|
+
.map((token) => token.trim())
|
|
2235
|
+
.filter(Boolean);
|
|
2236
|
+
if (!tokens.length) return false;
|
|
2237
|
+
return tokens.every((token) => {
|
|
2238
|
+
if (token.includes("@")) return true;
|
|
2239
|
+
const digits = token.replace(/\D/g, "");
|
|
2240
|
+
if (digits.length >= 4 && /^[+()\-\d.\s]+$/.test(token)) return true;
|
|
2241
|
+
return /^\d{4,}$/.test(token);
|
|
2242
|
+
});
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2011
2245
|
function parseDmHandleFromChatId(chatId) {
|
|
2012
2246
|
const parts = String(chatId ?? "").split(";");
|
|
2013
2247
|
if (parts.length >= 3 && parts[1] === "-") return parts.slice(2).join(";") || null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shepherd-onboard",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"description": "Customer-facing Shepherd raw sync onboarding CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"@photon-ai/imessage-kit": "^3.0.0"
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
|
+
"assets",
|
|
13
14
|
"bin",
|
|
14
15
|
"README.md"
|
|
15
16
|
],
|