shepherd-onboard 0.1.11 → 0.1.12
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 +6 -4
- package/bin/shepherd-onboard.js +386 -8
- package/package.json +1 -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
|
|
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 with the 20 most recent local Messages chats, 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
|
-
-
|
|
34
|
+
- opens a minimal local webpage showing the 20 most recent local Messages chats 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
|
|
@@ -74,12 +74,14 @@ Shepherd must still enforce selected users and groups internally before imperson
|
|
|
74
74
|
|
|
75
75
|
## Messages Chat Selection
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
Open the local Messages chat selector:
|
|
78
78
|
|
|
79
79
|
```sh
|
|
80
|
-
npx -y shepherd-onboard@latest messages-chats
|
|
80
|
+
npx -y shepherd-onboard@latest messages-chats
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
+
Use `--json` for machine-readable chat metadata, or `--text` for a terminal list.
|
|
84
|
+
|
|
83
85
|
Pass the selected chat IDs when finishing onboarding:
|
|
84
86
|
|
|
85
87
|
```sh
|
package/bin/shepherd-onboard.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { execFile, execFileSync, spawn } from "node:child_process";
|
|
3
3
|
import { mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
5
|
+
import { createServer } from "node:http";
|
|
5
6
|
import { homedir, platform } from "node:os";
|
|
6
7
|
import { dirname, join } from "node:path";
|
|
7
8
|
import readline from "node:readline";
|
|
@@ -312,7 +313,7 @@ async function runAgentOnboarding() {
|
|
|
312
313
|
googleWorkspaceDelegation: sources.google ? googleWorkspaceDelegationSetup(session.googleWorkspaceDelegation) : undefined,
|
|
313
314
|
granolaApiKeyPage,
|
|
314
315
|
statePath,
|
|
315
|
-
messagesChatsCommand: sources.messages ? `${agentCommand()} messages-chats
|
|
316
|
+
messagesChatsCommand: sources.messages ? `${agentCommand()} messages-chats` : undefined,
|
|
316
317
|
nextCommand: `${agentCommand()} agent --continue --messages-handle "<phone_or_apple_id>" --messages-chat-ids "<comma_separated_chat_ids>" --granola-api-key "<granola_key>"`,
|
|
317
318
|
needsUserAction: agentNeedsUserAction(sources, opened),
|
|
318
319
|
}, null, 2));
|
|
@@ -347,7 +348,7 @@ async function runAgentOnboarding() {
|
|
|
347
348
|
if (sources.slack) console.log(`${step++}. Ask the user to finish the opened Slack browser authorization.`);
|
|
348
349
|
if (sources.granola) console.log(`${step++}. Ask the user for their Granola API key from the Granola Mac app.`);
|
|
349
350
|
if (sources.messages) {
|
|
350
|
-
console.log(`${step++}. Run ${agentCommand()} messages-chats
|
|
351
|
+
console.log(`${step++}. Run ${agentCommand()} messages-chats, have the user select chats in the browser page, and keep the printed chat IDs.`);
|
|
351
352
|
}
|
|
352
353
|
console.log(`${step++}. Run:`);
|
|
353
354
|
console.log(` ${agentCommand()} agent --continue --messages-handle "<phone_or_apple_id>" --messages-chat-ids "<comma_separated_chat_ids>" --granola-api-key "<granola_key>"`);
|
|
@@ -440,7 +441,7 @@ async function continueAgentOnboarding() {
|
|
|
440
441
|
if (granolaApiKey) body.granolaApiKey = granolaApiKey;
|
|
441
442
|
if (messagesHandle) body.imessage = { handle: messagesHandle };
|
|
442
443
|
if (state.sources.messages && messagesHandle && selectedMessageChatIds.length === 0) {
|
|
443
|
-
throw new Error(`Messages sync requires selected chat IDs. Run ${agentCommand()} messages-chats
|
|
444
|
+
throw new Error(`Messages sync requires selected chat IDs. Run ${agentCommand()} messages-chats, have the user select chats in the browser page, then rerun --continue with --messages-chat-ids "<id1>,<id2>".`);
|
|
444
445
|
}
|
|
445
446
|
|
|
446
447
|
const finalized = await postJson(
|
|
@@ -522,6 +523,16 @@ async function runMessagesChatsCommand() {
|
|
|
522
523
|
return;
|
|
523
524
|
}
|
|
524
525
|
|
|
526
|
+
if (!args.text && !args.list) {
|
|
527
|
+
const selected = await selectChatsInBrowser(chats, { noOpen: Boolean(args["no-open"]) });
|
|
528
|
+
const selectedIds = selected.map((chat) => chat.chatId).join(",");
|
|
529
|
+
console.log(`\nSelected ${selected.length} Messages chat(s).`);
|
|
530
|
+
console.log(`messages-chat-ids=${selectedIds}`);
|
|
531
|
+
console.log("\nContinue with:");
|
|
532
|
+
console.log(` ${agentCommand()} agent --continue --messages-handle "<phone_or_apple_id>" --messages-chat-ids "${selectedIds}"`);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
|
|
525
536
|
console.log(`\nRecent local Messages chats (${chats.length})\n`);
|
|
526
537
|
for (let i = 0; i < chats.length; i++) {
|
|
527
538
|
console.log(`${String(i + 1).padStart(2, " ")}. ${formatMessageChatOption(chats[i])}`);
|
|
@@ -602,7 +613,7 @@ Usage:
|
|
|
602
613
|
npx -y ${PACKAGE_NAME}@latest agent
|
|
603
614
|
npx -y ${PACKAGE_NAME}@latest agent --login
|
|
604
615
|
npx -y ${PACKAGE_NAME}@latest agent --name <name> --org <organization>
|
|
605
|
-
npx -y ${PACKAGE_NAME}@latest messages-chats
|
|
616
|
+
npx -y ${PACKAGE_NAME}@latest messages-chats
|
|
606
617
|
npx -y ${PACKAGE_NAME}@latest agent --continue --granola-api-key <key> --messages-handle <value> --messages-chat-ids <ids>
|
|
607
618
|
npx -y ${PACKAGE_NAME}@latest agent --status
|
|
608
619
|
npx -y ${PACKAGE_NAME}@latest granola-api-keys
|
|
@@ -637,6 +648,8 @@ Usage:
|
|
|
637
648
|
|
|
638
649
|
Options:
|
|
639
650
|
--limit <n> Number of recent chats to show. Defaults to ${DEFAULT_RECENT_MESSAGE_CHATS}.
|
|
651
|
+
--text Print a terminal list instead of opening the selector page.
|
|
652
|
+
--no-open Print the local selector URL instead of opening it.
|
|
640
653
|
--json Print machine-readable chat IDs and labels.
|
|
641
654
|
--help Show this help.
|
|
642
655
|
`);
|
|
@@ -739,7 +752,7 @@ function printAgentContract() {
|
|
|
739
752
|
"--granola-api-key \"<granola_key>\" if Granola is being connected",
|
|
740
753
|
],
|
|
741
754
|
statusCommand: `${command} agent --status`,
|
|
742
|
-
messagesChatsCommand: `${command} messages-chats
|
|
755
|
+
messagesChatsCommand: `${command} messages-chats`,
|
|
743
756
|
googleWorkspaceDelegation: googleWorkspaceDelegationSetup(),
|
|
744
757
|
orgSecurity: "Existing organizations are only reused when Shepherd can verify the authenticated user belongs there, for example by an existing Shepherd account/membership or matching non-personal company email domain. Similar spelling helps match verified orgs, but cannot attach an unverified user to someone else's org.",
|
|
745
758
|
expectedResult: "Cloud sources start raw polling/backfill in the customer-facing Shepherd environment. Local Messages starts via a macOS LaunchAgent when run on macOS. Downstream wiki, memory, and summary compilers remain outside this onboarding flow.",
|
|
@@ -785,7 +798,7 @@ If they are joining an existing org, ask for the org name they believe they belo
|
|
|
785
798
|
If Messages is selected, run:
|
|
786
799
|
${payload.messagesChatsCommand}
|
|
787
800
|
|
|
788
|
-
|
|
801
|
+
This opens a minimal local webpage with the 20 recent local Messages chats. 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.
|
|
789
802
|
|
|
790
803
|
Then run:
|
|
791
804
|
${payload.startCommand}
|
|
@@ -923,7 +936,7 @@ function agentNeedsUserAction(sources, opened) {
|
|
|
923
936
|
if (sources.google) actions.push("Have the customer's Google Workspace super admin authorize Shepherd's domain-wide delegation Client ID and scopes in Google Admin Console.");
|
|
924
937
|
if (sources.slack && opened.includes("slack")) actions.push("Complete Slack browser authorization.");
|
|
925
938
|
if (sources.granola) actions.push("Create/copy a Granola API key from the Granola Mac app.");
|
|
926
|
-
if (sources.messages) actions.push("Run messages-chats,
|
|
939
|
+
if (sources.messages) actions.push("Run messages-chats, have the user select local Messages contacts/groups in the browser, then pass the printed chat IDs with the Messages handle.");
|
|
927
940
|
return actions;
|
|
928
941
|
}
|
|
929
942
|
|
|
@@ -1191,7 +1204,7 @@ async function selectRecentMessageChats() {
|
|
|
1191
1204
|
}
|
|
1192
1205
|
|
|
1193
1206
|
if (!process.stdin.isTTY) {
|
|
1194
|
-
throw new Error(`Messages sync requires selected chat IDs. Run ${agentCommand()} messages-chats
|
|
1207
|
+
throw new Error(`Messages sync requires selected chat IDs. Run ${agentCommand()} messages-chats and pass --messages-chat-ids "<id1>,<id2>".`);
|
|
1195
1208
|
}
|
|
1196
1209
|
|
|
1197
1210
|
const chats = await listRecentMessageChats({ limit: DEFAULT_RECENT_MESSAGE_CHATS });
|
|
@@ -1199,6 +1212,10 @@ async function selectRecentMessageChats() {
|
|
|
1199
1212
|
throw new Error("No recent local Messages chats were found on this Mac.");
|
|
1200
1213
|
}
|
|
1201
1214
|
|
|
1215
|
+
if (!args.text && !args.list) {
|
|
1216
|
+
return selectChatsInBrowser(chats, { noOpen: Boolean(args["no-open"]) });
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1202
1219
|
console.log(`\nSelect local Messages chats to sync\n`);
|
|
1203
1220
|
console.log("Shepherd will only pull from the chats you select.");
|
|
1204
1221
|
for (let i = 0; i < chats.length; i++) {
|
|
@@ -1211,6 +1228,356 @@ async function selectRecentMessageChats() {
|
|
|
1211
1228
|
return indexes.map((idx) => chats[idx]);
|
|
1212
1229
|
}
|
|
1213
1230
|
|
|
1231
|
+
async function selectChatsInBrowser(chats, opts = {}) {
|
|
1232
|
+
if (!chats.length) throw new Error("No recent local Messages chats were found on this Mac.");
|
|
1233
|
+
|
|
1234
|
+
const token = Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
|
|
1235
|
+
let settled = false;
|
|
1236
|
+
let server;
|
|
1237
|
+
|
|
1238
|
+
return new Promise((resolve, reject) => {
|
|
1239
|
+
const timeout = setTimeout(() => {
|
|
1240
|
+
if (settled) return;
|
|
1241
|
+
settled = true;
|
|
1242
|
+
server?.close();
|
|
1243
|
+
reject(new Error("Messages chat selection timed out."));
|
|
1244
|
+
}, 20 * 60 * 1000);
|
|
1245
|
+
|
|
1246
|
+
server = createServer(async (req, res) => {
|
|
1247
|
+
try {
|
|
1248
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "127.0.0.1"}`);
|
|
1249
|
+
|
|
1250
|
+
if (req.method === "GET" && url.pathname === "/") {
|
|
1251
|
+
sendHtml(res, renderMessagesSelectorPage(chats, token));
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
if (req.method === "POST" && url.pathname === "/select") {
|
|
1256
|
+
const body = await readRequestBody(req);
|
|
1257
|
+
const form = new URLSearchParams(body);
|
|
1258
|
+
if (form.get("token") !== token) {
|
|
1259
|
+
sendHtml(res, renderMessagesDonePage("Invalid selection session.", true), 403);
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
const selectedIds = form.getAll("chatId").filter(Boolean);
|
|
1263
|
+
const selectedSet = new Set(selectedIds);
|
|
1264
|
+
const selected = chats.filter((chat) => selectedSet.has(chat.chatId));
|
|
1265
|
+
if (selected.length === 0) {
|
|
1266
|
+
sendHtml(res, renderMessagesSelectorPage(chats, token, "Select at least one chat."));
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
sendHtml(res, renderMessagesDonePage(`${selected.length} chat${selected.length === 1 ? "" : "s"} selected.`));
|
|
1271
|
+
if (!settled) {
|
|
1272
|
+
settled = true;
|
|
1273
|
+
clearTimeout(timeout);
|
|
1274
|
+
setTimeout(() => server.close(), 100);
|
|
1275
|
+
resolve(selected);
|
|
1276
|
+
}
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
sendHtml(res, renderMessagesDonePage("Not found.", true), 404);
|
|
1281
|
+
} catch (err) {
|
|
1282
|
+
if (!settled) {
|
|
1283
|
+
settled = true;
|
|
1284
|
+
clearTimeout(timeout);
|
|
1285
|
+
server?.close();
|
|
1286
|
+
reject(err);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1291
|
+
server.on("error", (err) => {
|
|
1292
|
+
if (settled) return;
|
|
1293
|
+
settled = true;
|
|
1294
|
+
clearTimeout(timeout);
|
|
1295
|
+
reject(err);
|
|
1296
|
+
});
|
|
1297
|
+
|
|
1298
|
+
server.listen(0, "127.0.0.1", async () => {
|
|
1299
|
+
const address = server.address();
|
|
1300
|
+
const port = typeof address === "object" && address ? address.port : null;
|
|
1301
|
+
if (!port) {
|
|
1302
|
+
settled = true;
|
|
1303
|
+
clearTimeout(timeout);
|
|
1304
|
+
server.close();
|
|
1305
|
+
reject(new Error("Could not start local Messages selector."));
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
const url = `http://127.0.0.1:${port}/`;
|
|
1309
|
+
console.log(`\nOpening Messages chat selector: ${url}`);
|
|
1310
|
+
await openOrPrint(url, { noOpen: Boolean(opts.noOpen) });
|
|
1311
|
+
console.log("Select the Messages chats to sync in the browser.");
|
|
1312
|
+
});
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
function renderMessagesSelectorPage(chats, token, error = "") {
|
|
1317
|
+
const rows = chats.map((chat) => `
|
|
1318
|
+
<label class="chat-row">
|
|
1319
|
+
<input type="checkbox" name="chatId" value="${htmlAttr(chat.chatId)}">
|
|
1320
|
+
<span class="box" aria-hidden="true"></span>
|
|
1321
|
+
<span class="chat-main">
|
|
1322
|
+
<span class="chat-top">
|
|
1323
|
+
<span class="chat-name">${html(chat.label)}</span>
|
|
1324
|
+
<span class="chat-kind">${html(chat.kind === "group" ? "Group" : chat.kind === "dm" ? "Contact" : "Chat")}</span>
|
|
1325
|
+
</span>
|
|
1326
|
+
${renderChatPeople(chat)}
|
|
1327
|
+
<span class="chat-meta">${html(formatMessageChatMeta(chat))}</span>
|
|
1328
|
+
</span>
|
|
1329
|
+
</label>`).join("");
|
|
1330
|
+
|
|
1331
|
+
return `<!doctype html>
|
|
1332
|
+
<html lang="en">
|
|
1333
|
+
<head>
|
|
1334
|
+
<meta charset="utf-8">
|
|
1335
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1336
|
+
<title>Select Messages Chats</title>
|
|
1337
|
+
<style>
|
|
1338
|
+
:root {
|
|
1339
|
+
color-scheme: light dark;
|
|
1340
|
+
--bg: #FCFCFC;
|
|
1341
|
+
--fg: #111111;
|
|
1342
|
+
--muted: #6D726D;
|
|
1343
|
+
--line: #E8ECE8;
|
|
1344
|
+
--panel: #FFFFFF;
|
|
1345
|
+
--button: #136033;
|
|
1346
|
+
--button-text: #FFFFFF;
|
|
1347
|
+
--link: #136033;
|
|
1348
|
+
--radius: 10px;
|
|
1349
|
+
}
|
|
1350
|
+
@media (prefers-color-scheme: dark) {
|
|
1351
|
+
:root {
|
|
1352
|
+
--bg: #000000;
|
|
1353
|
+
--fg: #F8F8F8;
|
|
1354
|
+
--muted: #A2A8A2;
|
|
1355
|
+
--line: #202520;
|
|
1356
|
+
--panel: #070907;
|
|
1357
|
+
--button: #FFFFFF;
|
|
1358
|
+
--button-text: #000000;
|
|
1359
|
+
--link: #136033;
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
* { box-sizing: border-box; }
|
|
1363
|
+
body {
|
|
1364
|
+
margin: 0;
|
|
1365
|
+
background: var(--bg);
|
|
1366
|
+
color: var(--fg);
|
|
1367
|
+
font-family: Geist, "Geist Sans", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
1368
|
+
letter-spacing: 0;
|
|
1369
|
+
}
|
|
1370
|
+
main {
|
|
1371
|
+
width: min(760px, calc(100vw - 32px));
|
|
1372
|
+
margin: 40px auto;
|
|
1373
|
+
}
|
|
1374
|
+
header {
|
|
1375
|
+
display: flex;
|
|
1376
|
+
justify-content: space-between;
|
|
1377
|
+
gap: 16px;
|
|
1378
|
+
align-items: end;
|
|
1379
|
+
padding-bottom: 18px;
|
|
1380
|
+
border-bottom: 1px solid var(--line);
|
|
1381
|
+
}
|
|
1382
|
+
h1 {
|
|
1383
|
+
margin: 0;
|
|
1384
|
+
font-size: 24px;
|
|
1385
|
+
line-height: 1.1;
|
|
1386
|
+
font-weight: 650;
|
|
1387
|
+
}
|
|
1388
|
+
.count {
|
|
1389
|
+
color: var(--muted);
|
|
1390
|
+
font-size: 13px;
|
|
1391
|
+
white-space: nowrap;
|
|
1392
|
+
}
|
|
1393
|
+
form {
|
|
1394
|
+
margin-top: 18px;
|
|
1395
|
+
}
|
|
1396
|
+
.error {
|
|
1397
|
+
margin: 0 0 12px;
|
|
1398
|
+
color: #9B1C1C;
|
|
1399
|
+
font-size: 14px;
|
|
1400
|
+
}
|
|
1401
|
+
.chat-list {
|
|
1402
|
+
display: grid;
|
|
1403
|
+
gap: 8px;
|
|
1404
|
+
margin: 0 0 18px;
|
|
1405
|
+
}
|
|
1406
|
+
.chat-row {
|
|
1407
|
+
display: grid;
|
|
1408
|
+
grid-template-columns: 24px 1fr;
|
|
1409
|
+
gap: 12px;
|
|
1410
|
+
align-items: start;
|
|
1411
|
+
padding: 13px 14px;
|
|
1412
|
+
background: var(--panel);
|
|
1413
|
+
border: 1px solid var(--line);
|
|
1414
|
+
border-radius: var(--radius);
|
|
1415
|
+
cursor: pointer;
|
|
1416
|
+
}
|
|
1417
|
+
.chat-row:hover {
|
|
1418
|
+
border-color: color-mix(in srgb, var(--link) 45%, var(--line));
|
|
1419
|
+
}
|
|
1420
|
+
input[type="checkbox"] {
|
|
1421
|
+
position: absolute;
|
|
1422
|
+
opacity: 0;
|
|
1423
|
+
pointer-events: none;
|
|
1424
|
+
}
|
|
1425
|
+
.box {
|
|
1426
|
+
width: 18px;
|
|
1427
|
+
height: 18px;
|
|
1428
|
+
margin-top: 2px;
|
|
1429
|
+
border: 1.5px solid var(--muted);
|
|
1430
|
+
border-radius: 5px;
|
|
1431
|
+
display: inline-grid;
|
|
1432
|
+
place-items: center;
|
|
1433
|
+
}
|
|
1434
|
+
input[type="checkbox"]:checked + .box {
|
|
1435
|
+
background: var(--button);
|
|
1436
|
+
border-color: var(--button);
|
|
1437
|
+
}
|
|
1438
|
+
input[type="checkbox"]:checked + .box::after {
|
|
1439
|
+
content: "";
|
|
1440
|
+
width: 7px;
|
|
1441
|
+
height: 4px;
|
|
1442
|
+
border-left: 2px solid var(--button-text);
|
|
1443
|
+
border-bottom: 2px solid var(--button-text);
|
|
1444
|
+
transform: rotate(-45deg) translateY(-1px);
|
|
1445
|
+
}
|
|
1446
|
+
.chat-main {
|
|
1447
|
+
min-width: 0;
|
|
1448
|
+
display: grid;
|
|
1449
|
+
gap: 4px;
|
|
1450
|
+
}
|
|
1451
|
+
.chat-top {
|
|
1452
|
+
display: flex;
|
|
1453
|
+
gap: 10px;
|
|
1454
|
+
align-items: center;
|
|
1455
|
+
min-width: 0;
|
|
1456
|
+
}
|
|
1457
|
+
.chat-name {
|
|
1458
|
+
overflow: hidden;
|
|
1459
|
+
text-overflow: ellipsis;
|
|
1460
|
+
white-space: nowrap;
|
|
1461
|
+
font-size: 15px;
|
|
1462
|
+
font-weight: 600;
|
|
1463
|
+
}
|
|
1464
|
+
.chat-kind {
|
|
1465
|
+
color: var(--link);
|
|
1466
|
+
font-size: 12px;
|
|
1467
|
+
flex: none;
|
|
1468
|
+
}
|
|
1469
|
+
.chat-people,
|
|
1470
|
+
.chat-meta {
|
|
1471
|
+
color: var(--muted);
|
|
1472
|
+
font-size: 13px;
|
|
1473
|
+
line-height: 1.35;
|
|
1474
|
+
overflow-wrap: anywhere;
|
|
1475
|
+
}
|
|
1476
|
+
.actions {
|
|
1477
|
+
display: flex;
|
|
1478
|
+
justify-content: flex-end;
|
|
1479
|
+
position: sticky;
|
|
1480
|
+
bottom: 0;
|
|
1481
|
+
padding: 14px 0 0;
|
|
1482
|
+
background: linear-gradient(to top, var(--bg) 70%, transparent);
|
|
1483
|
+
}
|
|
1484
|
+
button {
|
|
1485
|
+
appearance: none;
|
|
1486
|
+
border: 0;
|
|
1487
|
+
border-radius: var(--radius);
|
|
1488
|
+
background: var(--button);
|
|
1489
|
+
color: var(--button-text);
|
|
1490
|
+
padding: 10px 14px;
|
|
1491
|
+
font: inherit;
|
|
1492
|
+
font-weight: 620;
|
|
1493
|
+
cursor: pointer;
|
|
1494
|
+
}
|
|
1495
|
+
a { color: var(--link); }
|
|
1496
|
+
</style>
|
|
1497
|
+
</head>
|
|
1498
|
+
<body>
|
|
1499
|
+
<main>
|
|
1500
|
+
<header>
|
|
1501
|
+
<h1>Select Messages chats</h1>
|
|
1502
|
+
<div class="count">${chats.length} recent chats</div>
|
|
1503
|
+
</header>
|
|
1504
|
+
<form method="post" action="/select">
|
|
1505
|
+
<input type="hidden" name="token" value="${htmlAttr(token)}">
|
|
1506
|
+
${error ? `<p class="error">${html(error)}</p>` : ""}
|
|
1507
|
+
<div class="chat-list">${rows}</div>
|
|
1508
|
+
<div class="actions">
|
|
1509
|
+
<button type="submit">Use selected chats</button>
|
|
1510
|
+
</div>
|
|
1511
|
+
</form>
|
|
1512
|
+
</main>
|
|
1513
|
+
</body>
|
|
1514
|
+
</html>`;
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
function renderMessagesDonePage(message, isError = false) {
|
|
1518
|
+
return `<!doctype html>
|
|
1519
|
+
<html lang="en">
|
|
1520
|
+
<head>
|
|
1521
|
+
<meta charset="utf-8">
|
|
1522
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1523
|
+
<title>Messages Selection</title>
|
|
1524
|
+
<style>
|
|
1525
|
+
:root { color-scheme: light dark; --bg: #FCFCFC; --fg: #111; --muted: #6D726D; --button: #136033; --button-text: #FFFFFF; --radius: 10px; }
|
|
1526
|
+
@media (prefers-color-scheme: dark) { :root { --bg: #000; --fg: #F8F8F8; --muted: #A2A8A2; --button: #FFFFFF; --button-text: #000; } }
|
|
1527
|
+
body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: var(--bg); color: var(--fg); font-family: Geist, "Geist Sans", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; letter-spacing: 0; }
|
|
1528
|
+
main { width: min(420px, calc(100vw - 32px)); }
|
|
1529
|
+
h1 { margin: 0 0 8px; font-size: 24px; line-height: 1.1; font-weight: 650; }
|
|
1530
|
+
p { margin: 0; color: var(--muted); font-size: 14px; line-height: 1.45; }
|
|
1531
|
+
.mark { width: 34px; height: 34px; border-radius: var(--radius); display: grid; place-items: center; margin-bottom: 14px; background: var(--button); color: var(--button-text); font-weight: 700; }
|
|
1532
|
+
</style>
|
|
1533
|
+
</head>
|
|
1534
|
+
<body>
|
|
1535
|
+
<main>
|
|
1536
|
+
<div class="mark">${isError ? "!" : "OK"}</div>
|
|
1537
|
+
<h1>${html(message)}</h1>
|
|
1538
|
+
<p>${isError ? "Return to the terminal and retry." : "You can close this tab and return to the terminal."}</p>
|
|
1539
|
+
</main>
|
|
1540
|
+
</body>
|
|
1541
|
+
</html>`;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
function renderChatPeople(chat) {
|
|
1545
|
+
const names = chat.participants?.map((participant) => participant.name ?? participant.handle).filter(Boolean) ?? [];
|
|
1546
|
+
if (!names.length) return "";
|
|
1547
|
+
return `<span class="chat-people">${html(names.slice(0, 6).join(", "))}</span>`;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
function formatMessageChatMeta(chat) {
|
|
1551
|
+
const parts = [];
|
|
1552
|
+
if (chat.service) parts.push(chat.service);
|
|
1553
|
+
if (chat.lastMessageAt) parts.push(new Date(chat.lastMessageAt).toLocaleString());
|
|
1554
|
+
return parts.join(" · ");
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
function sendHtml(res, body, status = 200) {
|
|
1558
|
+
res.writeHead(status, {
|
|
1559
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
1560
|
+
"Cache-Control": "no-store",
|
|
1561
|
+
});
|
|
1562
|
+
res.end(body);
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
function readRequestBody(req) {
|
|
1566
|
+
return new Promise((resolve, reject) => {
|
|
1567
|
+
let body = "";
|
|
1568
|
+
req.setEncoding("utf8");
|
|
1569
|
+
req.on("data", (chunk) => {
|
|
1570
|
+
body += chunk;
|
|
1571
|
+
if (body.length > 64_000) {
|
|
1572
|
+
reject(new Error("Request body too large."));
|
|
1573
|
+
req.destroy();
|
|
1574
|
+
}
|
|
1575
|
+
});
|
|
1576
|
+
req.on("end", () => resolve(body));
|
|
1577
|
+
req.on("error", reject);
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1214
1581
|
async function listRecentMessageChats({ limit }) {
|
|
1215
1582
|
if (platform() !== "darwin") {
|
|
1216
1583
|
throw new Error("local Messages chat discovery is only supported on macOS");
|
|
@@ -1675,6 +2042,17 @@ function parseAllowedChatIds(value) {
|
|
|
1675
2042
|
return [...new Set(raw.map((chatId) => String(chatId).trim()).filter(Boolean))];
|
|
1676
2043
|
}
|
|
1677
2044
|
|
|
2045
|
+
function html(value) {
|
|
2046
|
+
return String(value ?? "")
|
|
2047
|
+
.replace(/&/g, "&")
|
|
2048
|
+
.replace(/</g, "<")
|
|
2049
|
+
.replace(/>/g, ">");
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
function htmlAttr(value) {
|
|
2053
|
+
return html(value).replace(/"/g, """);
|
|
2054
|
+
}
|
|
2055
|
+
|
|
1678
2056
|
class MessagesBatchSender {
|
|
1679
2057
|
constructor(apiUrl, agentToken, userId) {
|
|
1680
2058
|
this.apiUrl = trimTrailingSlash(apiUrl);
|