shepherd-onboard 0.1.11 → 0.1.13
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 +8 -4
- package/assets/shepherd_G_vector_136033.png +0 -0
- package/bin/shepherd-onboard.js +576 -24
- 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
|
|
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
|
-
-
|
|
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
|
|
@@ -74,12 +74,16 @@ 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
|
+
|
|
85
|
+
The browser selector displays the first page of recent chats and lets the user search contacts or groups loaded from local Messages.
|
|
86
|
+
|
|
83
87
|
Pass the selected chat IDs when finishing onboarding:
|
|
84
88
|
|
|
85
89
|
```sh
|
|
Binary file
|
package/bin/shepherd-onboard.js
CHANGED
|
@@ -2,17 +2,22 @@
|
|
|
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";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
8
10
|
|
|
9
11
|
const DEFAULT_API_URL = "https://brain-api-customer-facing.up.railway.app";
|
|
10
12
|
const PACKAGE_NAME = "shepherd-onboard";
|
|
11
13
|
const PACKAGE_SPEC = `${PACKAGE_NAME}@latest`;
|
|
14
|
+
const PACKAGE_DIR = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
12
15
|
const DEFAULT_AGENT_STATE_PATH = join(homedir(), ".shepherd", "raw-onboarding-agent.json");
|
|
13
16
|
const MAX_BATCH_SIZE = 50;
|
|
14
17
|
const MAX_QUEUE_MESSAGES = 10_000;
|
|
15
|
-
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");
|
|
16
21
|
const GRANOLA_API_KEYS_PATH = "/settings/integrations/api-keys";
|
|
17
22
|
const GOOGLE_WORKSPACE_DELEGATION_APP_NAME = "Shepherd";
|
|
18
23
|
const GOOGLE_WORKSPACE_DELEGATION_SERVICE_ACCOUNT_EMAIL =
|
|
@@ -312,7 +317,7 @@ async function runAgentOnboarding() {
|
|
|
312
317
|
googleWorkspaceDelegation: sources.google ? googleWorkspaceDelegationSetup(session.googleWorkspaceDelegation) : undefined,
|
|
313
318
|
granolaApiKeyPage,
|
|
314
319
|
statePath,
|
|
315
|
-
messagesChatsCommand: sources.messages ? `${agentCommand()} messages-chats
|
|
320
|
+
messagesChatsCommand: sources.messages ? `${agentCommand()} messages-chats` : undefined,
|
|
316
321
|
nextCommand: `${agentCommand()} agent --continue --messages-handle "<phone_or_apple_id>" --messages-chat-ids "<comma_separated_chat_ids>" --granola-api-key "<granola_key>"`,
|
|
317
322
|
needsUserAction: agentNeedsUserAction(sources, opened),
|
|
318
323
|
}, null, 2));
|
|
@@ -347,7 +352,7 @@ async function runAgentOnboarding() {
|
|
|
347
352
|
if (sources.slack) console.log(`${step++}. Ask the user to finish the opened Slack browser authorization.`);
|
|
348
353
|
if (sources.granola) console.log(`${step++}. Ask the user for their Granola API key from the Granola Mac app.`);
|
|
349
354
|
if (sources.messages) {
|
|
350
|
-
console.log(`${step++}. Run ${agentCommand()} messages-chats
|
|
355
|
+
console.log(`${step++}. Run ${agentCommand()} messages-chats, have the user select chats in the browser page, and keep the printed chat IDs.`);
|
|
351
356
|
}
|
|
352
357
|
console.log(`${step++}. Run:`);
|
|
353
358
|
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 +445,7 @@ async function continueAgentOnboarding() {
|
|
|
440
445
|
if (granolaApiKey) body.granolaApiKey = granolaApiKey;
|
|
441
446
|
if (messagesHandle) body.imessage = { handle: messagesHandle };
|
|
442
447
|
if (state.sources.messages && messagesHandle && selectedMessageChatIds.length === 0) {
|
|
443
|
-
throw new Error(`Messages sync requires selected chat IDs. Run ${agentCommand()} messages-chats
|
|
448
|
+
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
449
|
}
|
|
445
450
|
|
|
446
451
|
const finalized = await postJson(
|
|
@@ -514,7 +519,7 @@ async function printAgentStatus() {
|
|
|
514
519
|
|
|
515
520
|
async function runMessagesChatsCommand() {
|
|
516
521
|
const chats = await listRecentMessageChats({
|
|
517
|
-
limit: clampInt(Number(args.limit ??
|
|
522
|
+
limit: clampInt(Number(args.limit ?? DEFAULT_MESSAGE_CHAT_SEARCH_LIMIT), 1, 500),
|
|
518
523
|
});
|
|
519
524
|
|
|
520
525
|
if (args.json) {
|
|
@@ -522,6 +527,16 @@ async function runMessagesChatsCommand() {
|
|
|
522
527
|
return;
|
|
523
528
|
}
|
|
524
529
|
|
|
530
|
+
if (!args.text && !args.list) {
|
|
531
|
+
const selected = await selectChatsInBrowser(chats, { noOpen: Boolean(args["no-open"]) });
|
|
532
|
+
const selectedIds = selected.map((chat) => chat.chatId).join(",");
|
|
533
|
+
console.log(`\nSelected ${selected.length} Messages chat(s).`);
|
|
534
|
+
console.log(`messages-chat-ids=${selectedIds}`);
|
|
535
|
+
console.log("\nContinue with:");
|
|
536
|
+
console.log(` ${agentCommand()} agent --continue --messages-handle "<phone_or_apple_id>" --messages-chat-ids "${selectedIds}"`);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
|
|
525
540
|
console.log(`\nRecent local Messages chats (${chats.length})\n`);
|
|
526
541
|
for (let i = 0; i < chats.length; i++) {
|
|
527
542
|
console.log(`${String(i + 1).padStart(2, " ")}. ${formatMessageChatOption(chats[i])}`);
|
|
@@ -602,7 +617,7 @@ Usage:
|
|
|
602
617
|
npx -y ${PACKAGE_NAME}@latest agent
|
|
603
618
|
npx -y ${PACKAGE_NAME}@latest agent --login
|
|
604
619
|
npx -y ${PACKAGE_NAME}@latest agent --name <name> --org <organization>
|
|
605
|
-
npx -y ${PACKAGE_NAME}@latest messages-chats
|
|
620
|
+
npx -y ${PACKAGE_NAME}@latest messages-chats
|
|
606
621
|
npx -y ${PACKAGE_NAME}@latest agent --continue --granola-api-key <key> --messages-handle <value> --messages-chat-ids <ids>
|
|
607
622
|
npx -y ${PACKAGE_NAME}@latest agent --status
|
|
608
623
|
npx -y ${PACKAGE_NAME}@latest granola-api-keys
|
|
@@ -636,7 +651,9 @@ Usage:
|
|
|
636
651
|
npx -y ${PACKAGE_NAME}@latest messages-chats --json
|
|
637
652
|
|
|
638
653
|
Options:
|
|
639
|
-
--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}.
|
|
655
|
+
--text Print a terminal list instead of opening the selector page.
|
|
656
|
+
--no-open Print the local selector URL instead of opening it.
|
|
640
657
|
--json Print machine-readable chat IDs and labels.
|
|
641
658
|
--help Show this help.
|
|
642
659
|
`);
|
|
@@ -689,7 +706,7 @@ function printAgentContract() {
|
|
|
689
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.",
|
|
690
707
|
"Ask Messages as a selectable choice: Skip Messages, or Provide handle.",
|
|
691
708
|
"If the user chooses Provide handle, ask for the phone number or Apple ID email.",
|
|
692
|
-
"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.",
|
|
693
710
|
],
|
|
694
711
|
selectionQuestions: [
|
|
695
712
|
{
|
|
@@ -713,7 +730,7 @@ function printAgentContract() {
|
|
|
713
730
|
"Full name",
|
|
714
731
|
"Organization name",
|
|
715
732
|
"Messages phone number or Apple ID email, if they want local Messages connected",
|
|
716
|
-
"Selected local Messages chats from the
|
|
733
|
+
"Selected local Messages chats from the browser selector, if they want local Messages connected",
|
|
717
734
|
],
|
|
718
735
|
afterStartCommand: [
|
|
719
736
|
"For Google Workspace, have a super admin authorize the Shepherd Client ID and scopes in Google Admin Console.",
|
|
@@ -739,7 +756,7 @@ function printAgentContract() {
|
|
|
739
756
|
"--granola-api-key \"<granola_key>\" if Granola is being connected",
|
|
740
757
|
],
|
|
741
758
|
statusCommand: `${command} agent --status`,
|
|
742
|
-
messagesChatsCommand: `${command} messages-chats
|
|
759
|
+
messagesChatsCommand: `${command} messages-chats`,
|
|
743
760
|
googleWorkspaceDelegation: googleWorkspaceDelegationSetup(),
|
|
744
761
|
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
762
|
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 +802,7 @@ If they are joining an existing org, ask for the org name they believe they belo
|
|
|
785
802
|
If Messages is selected, run:
|
|
786
803
|
${payload.messagesChatsCommand}
|
|
787
804
|
|
|
788
|
-
|
|
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.
|
|
789
806
|
|
|
790
807
|
Then run:
|
|
791
808
|
${payload.startCommand}
|
|
@@ -923,7 +940,7 @@ function agentNeedsUserAction(sources, opened) {
|
|
|
923
940
|
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
941
|
if (sources.slack && opened.includes("slack")) actions.push("Complete Slack browser authorization.");
|
|
925
942
|
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,
|
|
943
|
+
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
944
|
return actions;
|
|
928
945
|
}
|
|
929
946
|
|
|
@@ -1191,14 +1208,18 @@ async function selectRecentMessageChats() {
|
|
|
1191
1208
|
}
|
|
1192
1209
|
|
|
1193
1210
|
if (!process.stdin.isTTY) {
|
|
1194
|
-
throw new Error(`Messages sync requires selected chat IDs. Run ${agentCommand()} messages-chats
|
|
1211
|
+
throw new Error(`Messages sync requires selected chat IDs. Run ${agentCommand()} messages-chats and pass --messages-chat-ids "<id1>,<id2>".`);
|
|
1195
1212
|
}
|
|
1196
1213
|
|
|
1197
|
-
const chats = await listRecentMessageChats({ limit:
|
|
1214
|
+
const chats = await listRecentMessageChats({ limit: clampInt(Number(args.limit ?? DEFAULT_MESSAGE_CHAT_SEARCH_LIMIT), 1, 500) });
|
|
1198
1215
|
if (chats.length === 0) {
|
|
1199
1216
|
throw new Error("No recent local Messages chats were found on this Mac.");
|
|
1200
1217
|
}
|
|
1201
1218
|
|
|
1219
|
+
if (!args.text && !args.list) {
|
|
1220
|
+
return selectChatsInBrowser(chats, { noOpen: Boolean(args["no-open"]) });
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1202
1223
|
console.log(`\nSelect local Messages chats to sync\n`);
|
|
1203
1224
|
console.log("Shepherd will only pull from the chats you select.");
|
|
1204
1225
|
for (let i = 0; i < chats.length; i++) {
|
|
@@ -1211,6 +1232,504 @@ async function selectRecentMessageChats() {
|
|
|
1211
1232
|
return indexes.map((idx) => chats[idx]);
|
|
1212
1233
|
}
|
|
1213
1234
|
|
|
1235
|
+
async function selectChatsInBrowser(chats, opts = {}) {
|
|
1236
|
+
if (!chats.length) throw new Error("No recent local Messages chats were found on this Mac.");
|
|
1237
|
+
|
|
1238
|
+
const token = Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
|
|
1239
|
+
let settled = false;
|
|
1240
|
+
let server;
|
|
1241
|
+
|
|
1242
|
+
return new Promise((resolve, reject) => {
|
|
1243
|
+
const timeout = setTimeout(() => {
|
|
1244
|
+
if (settled) return;
|
|
1245
|
+
settled = true;
|
|
1246
|
+
server?.close();
|
|
1247
|
+
reject(new Error("Messages chat selection timed out."));
|
|
1248
|
+
}, 20 * 60 * 1000);
|
|
1249
|
+
|
|
1250
|
+
server = createServer(async (req, res) => {
|
|
1251
|
+
try {
|
|
1252
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "127.0.0.1"}`);
|
|
1253
|
+
|
|
1254
|
+
if (req.method === "GET" && url.pathname === "/") {
|
|
1255
|
+
sendHtml(res, renderMessagesSelectorPage(chats, token));
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
if (req.method === "POST" && url.pathname === "/select") {
|
|
1260
|
+
const body = await readRequestBody(req);
|
|
1261
|
+
const form = new URLSearchParams(body);
|
|
1262
|
+
if (form.get("token") !== token) {
|
|
1263
|
+
sendHtml(res, renderMessagesDonePage("Invalid selection session.", true), 403);
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
const selectedIds = form.getAll("chatId").filter(Boolean);
|
|
1267
|
+
const selectedSet = new Set(selectedIds);
|
|
1268
|
+
const selected = chats.filter((chat) => selectedSet.has(chat.chatId));
|
|
1269
|
+
if (selected.length === 0) {
|
|
1270
|
+
sendHtml(res, renderMessagesSelectorPage(chats, token, "Select at least one chat."));
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
sendHtml(res, renderMessagesDonePage(`${selected.length} chat${selected.length === 1 ? "" : "s"} selected.`));
|
|
1275
|
+
if (!settled) {
|
|
1276
|
+
settled = true;
|
|
1277
|
+
clearTimeout(timeout);
|
|
1278
|
+
setTimeout(() => server.close(), 100);
|
|
1279
|
+
resolve(selected);
|
|
1280
|
+
}
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
sendHtml(res, renderMessagesDonePage("Not found.", true), 404);
|
|
1285
|
+
} catch (err) {
|
|
1286
|
+
if (!settled) {
|
|
1287
|
+
settled = true;
|
|
1288
|
+
clearTimeout(timeout);
|
|
1289
|
+
server?.close();
|
|
1290
|
+
reject(err);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
});
|
|
1294
|
+
|
|
1295
|
+
server.on("error", (err) => {
|
|
1296
|
+
if (settled) return;
|
|
1297
|
+
settled = true;
|
|
1298
|
+
clearTimeout(timeout);
|
|
1299
|
+
reject(err);
|
|
1300
|
+
});
|
|
1301
|
+
|
|
1302
|
+
server.listen(0, "127.0.0.1", async () => {
|
|
1303
|
+
const address = server.address();
|
|
1304
|
+
const port = typeof address === "object" && address ? address.port : null;
|
|
1305
|
+
if (!port) {
|
|
1306
|
+
settled = true;
|
|
1307
|
+
clearTimeout(timeout);
|
|
1308
|
+
server.close();
|
|
1309
|
+
reject(new Error("Could not start local Messages selector."));
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
const url = `http://127.0.0.1:${port}/`;
|
|
1313
|
+
console.log(`\nOpening Messages chat selector: ${url}`);
|
|
1314
|
+
await openOrPrint(url, { noOpen: Boolean(opts.noOpen) });
|
|
1315
|
+
console.log("Select the Messages chats to sync in the browser.");
|
|
1316
|
+
});
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
function renderMessagesSelectorPage(chats, token, error = "") {
|
|
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
|
+
return `
|
|
1337
|
+
<label class="chat-row" data-index="${index}" data-search="${htmlAttr(searchText)}">
|
|
1338
|
+
<input type="checkbox" name="chatId" value="${htmlAttr(chat.chatId)}">
|
|
1339
|
+
<span class="box" aria-hidden="true"></span>
|
|
1340
|
+
<span class="chat-main">
|
|
1341
|
+
<span class="chat-top">
|
|
1342
|
+
<span class="chat-name">${html(chat.label)}</span>
|
|
1343
|
+
<span class="chat-kind">${html(chat.kind === "group" ? "Group" : chat.kind === "dm" ? "Contact" : "Chat")}</span>
|
|
1344
|
+
</span>
|
|
1345
|
+
${people ? `<span class="chat-people">${html(people)}</span>` : ""}
|
|
1346
|
+
${when ? `<span class="chat-meta">${html(when)}</span>` : ""}
|
|
1347
|
+
</span>
|
|
1348
|
+
</label>`;
|
|
1349
|
+
}).join("");
|
|
1350
|
+
|
|
1351
|
+
return `<!doctype html>
|
|
1352
|
+
<html lang="en">
|
|
1353
|
+
<head>
|
|
1354
|
+
<meta charset="utf-8">
|
|
1355
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1356
|
+
<title>Select Messages Chats</title>
|
|
1357
|
+
<style>
|
|
1358
|
+
:root {
|
|
1359
|
+
color-scheme: light dark;
|
|
1360
|
+
--bg: #FCFCFC;
|
|
1361
|
+
--fg: #111111;
|
|
1362
|
+
--muted: #6D726D;
|
|
1363
|
+
--line: #E8ECE8;
|
|
1364
|
+
--panel: #FFFFFF;
|
|
1365
|
+
--button: #136033;
|
|
1366
|
+
--button-text: #FFFFFF;
|
|
1367
|
+
--link: #136033;
|
|
1368
|
+
--radius: 10px;
|
|
1369
|
+
}
|
|
1370
|
+
@media (prefers-color-scheme: dark) {
|
|
1371
|
+
:root {
|
|
1372
|
+
--bg: #000000;
|
|
1373
|
+
--fg: #F8F8F8;
|
|
1374
|
+
--muted: #A2A8A2;
|
|
1375
|
+
--line: #202520;
|
|
1376
|
+
--panel: #070907;
|
|
1377
|
+
--button: #FFFFFF;
|
|
1378
|
+
--button-text: #000000;
|
|
1379
|
+
--link: #136033;
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
* { box-sizing: border-box; }
|
|
1383
|
+
body {
|
|
1384
|
+
margin: 0;
|
|
1385
|
+
background: var(--bg);
|
|
1386
|
+
color: var(--fg);
|
|
1387
|
+
font-family: Geist, "Geist Sans", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
1388
|
+
letter-spacing: 0;
|
|
1389
|
+
}
|
|
1390
|
+
main {
|
|
1391
|
+
width: min(700px, calc(100vw - 32px));
|
|
1392
|
+
margin: 36px auto;
|
|
1393
|
+
}
|
|
1394
|
+
header {
|
|
1395
|
+
display: flex;
|
|
1396
|
+
justify-content: space-between;
|
|
1397
|
+
gap: 16px;
|
|
1398
|
+
align-items: center;
|
|
1399
|
+
padding-bottom: 18px;
|
|
1400
|
+
border-bottom: 1px solid var(--line);
|
|
1401
|
+
}
|
|
1402
|
+
.brand {
|
|
1403
|
+
display: flex;
|
|
1404
|
+
gap: 10px;
|
|
1405
|
+
align-items: center;
|
|
1406
|
+
min-width: 0;
|
|
1407
|
+
}
|
|
1408
|
+
.logo {
|
|
1409
|
+
width: 30px;
|
|
1410
|
+
height: 30px;
|
|
1411
|
+
border-radius: 8px;
|
|
1412
|
+
object-fit: contain;
|
|
1413
|
+
flex: none;
|
|
1414
|
+
}
|
|
1415
|
+
.logo-fallback {
|
|
1416
|
+
width: 30px;
|
|
1417
|
+
height: 30px;
|
|
1418
|
+
border-radius: 8px;
|
|
1419
|
+
display: grid;
|
|
1420
|
+
place-items: center;
|
|
1421
|
+
color: #FFFFFF;
|
|
1422
|
+
background: #136033;
|
|
1423
|
+
font-weight: 700;
|
|
1424
|
+
flex: none;
|
|
1425
|
+
}
|
|
1426
|
+
h1 {
|
|
1427
|
+
margin: 0;
|
|
1428
|
+
font-size: 22px;
|
|
1429
|
+
line-height: 1.1;
|
|
1430
|
+
font-weight: 650;
|
|
1431
|
+
}
|
|
1432
|
+
form {
|
|
1433
|
+
margin-top: 18px;
|
|
1434
|
+
}
|
|
1435
|
+
.search {
|
|
1436
|
+
width: 100%;
|
|
1437
|
+
margin: 0 0 12px;
|
|
1438
|
+
border: 1px solid var(--line);
|
|
1439
|
+
border-radius: var(--radius);
|
|
1440
|
+
background: var(--panel);
|
|
1441
|
+
color: var(--fg);
|
|
1442
|
+
padding: 11px 12px;
|
|
1443
|
+
font: inherit;
|
|
1444
|
+
font-size: 14px;
|
|
1445
|
+
outline: none;
|
|
1446
|
+
}
|
|
1447
|
+
.search:focus {
|
|
1448
|
+
border-color: #136033;
|
|
1449
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, #136033 16%, transparent);
|
|
1450
|
+
}
|
|
1451
|
+
.search::placeholder {
|
|
1452
|
+
color: var(--muted);
|
|
1453
|
+
}
|
|
1454
|
+
.error {
|
|
1455
|
+
margin: 0 0 12px;
|
|
1456
|
+
color: #9B1C1C;
|
|
1457
|
+
font-size: 14px;
|
|
1458
|
+
}
|
|
1459
|
+
.chat-list {
|
|
1460
|
+
display: grid;
|
|
1461
|
+
gap: 8px;
|
|
1462
|
+
margin: 0 0 18px;
|
|
1463
|
+
}
|
|
1464
|
+
.chat-row {
|
|
1465
|
+
display: grid;
|
|
1466
|
+
grid-template-columns: 24px 1fr;
|
|
1467
|
+
gap: 12px;
|
|
1468
|
+
align-items: start;
|
|
1469
|
+
padding: 13px 14px;
|
|
1470
|
+
background: var(--panel);
|
|
1471
|
+
border: 1px solid var(--line);
|
|
1472
|
+
border-radius: var(--radius);
|
|
1473
|
+
cursor: pointer;
|
|
1474
|
+
}
|
|
1475
|
+
.chat-row:hover {
|
|
1476
|
+
border-color: color-mix(in srgb, var(--link) 45%, var(--line));
|
|
1477
|
+
}
|
|
1478
|
+
input[type="checkbox"] {
|
|
1479
|
+
position: absolute;
|
|
1480
|
+
opacity: 0;
|
|
1481
|
+
pointer-events: none;
|
|
1482
|
+
}
|
|
1483
|
+
.box {
|
|
1484
|
+
width: 18px;
|
|
1485
|
+
height: 18px;
|
|
1486
|
+
margin-top: 2px;
|
|
1487
|
+
border: 1.5px solid var(--muted);
|
|
1488
|
+
border-radius: 5px;
|
|
1489
|
+
display: inline-grid;
|
|
1490
|
+
place-items: center;
|
|
1491
|
+
}
|
|
1492
|
+
input[type="checkbox"]:checked + .box {
|
|
1493
|
+
background: var(--button);
|
|
1494
|
+
border-color: var(--button);
|
|
1495
|
+
}
|
|
1496
|
+
input[type="checkbox"]:checked + .box::after {
|
|
1497
|
+
content: "";
|
|
1498
|
+
width: 7px;
|
|
1499
|
+
height: 4px;
|
|
1500
|
+
border-left: 2px solid var(--button-text);
|
|
1501
|
+
border-bottom: 2px solid var(--button-text);
|
|
1502
|
+
transform: rotate(-45deg) translateY(-1px);
|
|
1503
|
+
}
|
|
1504
|
+
.chat-main {
|
|
1505
|
+
min-width: 0;
|
|
1506
|
+
display: grid;
|
|
1507
|
+
gap: 4px;
|
|
1508
|
+
}
|
|
1509
|
+
.chat-top {
|
|
1510
|
+
display: flex;
|
|
1511
|
+
gap: 10px;
|
|
1512
|
+
align-items: center;
|
|
1513
|
+
min-width: 0;
|
|
1514
|
+
}
|
|
1515
|
+
.chat-name {
|
|
1516
|
+
overflow: hidden;
|
|
1517
|
+
text-overflow: ellipsis;
|
|
1518
|
+
white-space: nowrap;
|
|
1519
|
+
font-size: 15px;
|
|
1520
|
+
font-weight: 600;
|
|
1521
|
+
}
|
|
1522
|
+
.chat-kind {
|
|
1523
|
+
color: var(--link);
|
|
1524
|
+
font-size: 12px;
|
|
1525
|
+
flex: none;
|
|
1526
|
+
}
|
|
1527
|
+
.chat-people,
|
|
1528
|
+
.chat-meta {
|
|
1529
|
+
color: var(--muted);
|
|
1530
|
+
font-size: 13px;
|
|
1531
|
+
line-height: 1.35;
|
|
1532
|
+
overflow-wrap: anywhere;
|
|
1533
|
+
}
|
|
1534
|
+
[hidden] {
|
|
1535
|
+
display: none !important;
|
|
1536
|
+
}
|
|
1537
|
+
.empty {
|
|
1538
|
+
margin: 18px 0 28px;
|
|
1539
|
+
color: var(--muted);
|
|
1540
|
+
font-size: 14px;
|
|
1541
|
+
text-align: center;
|
|
1542
|
+
}
|
|
1543
|
+
.actions {
|
|
1544
|
+
display: flex;
|
|
1545
|
+
justify-content: space-between;
|
|
1546
|
+
align-items: center;
|
|
1547
|
+
gap: 12px;
|
|
1548
|
+
position: sticky;
|
|
1549
|
+
bottom: 0;
|
|
1550
|
+
padding: 14px 0 0;
|
|
1551
|
+
background: linear-gradient(to top, var(--bg) 70%, transparent);
|
|
1552
|
+
}
|
|
1553
|
+
.selection-count {
|
|
1554
|
+
color: var(--muted);
|
|
1555
|
+
font-size: 13px;
|
|
1556
|
+
}
|
|
1557
|
+
button {
|
|
1558
|
+
appearance: none;
|
|
1559
|
+
border: 0;
|
|
1560
|
+
border-radius: var(--radius);
|
|
1561
|
+
background: var(--button);
|
|
1562
|
+
color: var(--button-text);
|
|
1563
|
+
padding: 10px 14px;
|
|
1564
|
+
font: inherit;
|
|
1565
|
+
font-weight: 620;
|
|
1566
|
+
cursor: pointer;
|
|
1567
|
+
}
|
|
1568
|
+
a { color: var(--link); }
|
|
1569
|
+
</style>
|
|
1570
|
+
</head>
|
|
1571
|
+
<body>
|
|
1572
|
+
<main>
|
|
1573
|
+
<header>
|
|
1574
|
+
<div class="brand">
|
|
1575
|
+
${logo ? `<img class="logo" src="${htmlAttr(logo)}" alt="">` : `<span class="logo-fallback" aria-hidden="true">G</span>`}
|
|
1576
|
+
<h1>Select chats</h1>
|
|
1577
|
+
</div>
|
|
1578
|
+
</header>
|
|
1579
|
+
<form method="post" action="/select">
|
|
1580
|
+
<input type="hidden" name="token" value="${htmlAttr(token)}">
|
|
1581
|
+
<input class="search" id="search" type="search" placeholder="Search contacts or groups" autocomplete="off">
|
|
1582
|
+
${error ? `<p class="error">${html(error)}</p>` : ""}
|
|
1583
|
+
<div class="chat-list">${rows}</div>
|
|
1584
|
+
<p class="empty" id="empty" hidden>No chats found.</p>
|
|
1585
|
+
<div class="actions">
|
|
1586
|
+
<span class="selection-count" id="selection-count">Select one or more</span>
|
|
1587
|
+
<button type="submit">return</button>
|
|
1588
|
+
</div>
|
|
1589
|
+
</form>
|
|
1590
|
+
</main>
|
|
1591
|
+
<script>
|
|
1592
|
+
const initialRows = ${INITIAL_MESSAGE_CHAT_ROWS};
|
|
1593
|
+
const rows = Array.from(document.querySelectorAll(".chat-row"));
|
|
1594
|
+
const search = document.getElementById("search");
|
|
1595
|
+
const empty = document.getElementById("empty");
|
|
1596
|
+
const selected = document.getElementById("selection-count");
|
|
1597
|
+
const checks = Array.from(document.querySelectorAll('input[name="chatId"]'));
|
|
1598
|
+
|
|
1599
|
+
function updateRows() {
|
|
1600
|
+
const query = search.value.trim().toLowerCase();
|
|
1601
|
+
let visible = 0;
|
|
1602
|
+
for (const row of rows) {
|
|
1603
|
+
const matches = query
|
|
1604
|
+
? row.dataset.search.includes(query)
|
|
1605
|
+
: Number(row.dataset.index) < initialRows;
|
|
1606
|
+
row.hidden = !matches;
|
|
1607
|
+
if (matches) visible += 1;
|
|
1608
|
+
}
|
|
1609
|
+
empty.hidden = visible !== 0;
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
function updateSelected() {
|
|
1613
|
+
const count = checks.filter((check) => check.checked).length;
|
|
1614
|
+
selected.textContent = count ? count + " selected" : "Select one or more";
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
search.addEventListener("input", updateRows);
|
|
1618
|
+
for (const check of checks) check.addEventListener("change", updateSelected);
|
|
1619
|
+
updateRows();
|
|
1620
|
+
updateSelected();
|
|
1621
|
+
</script>
|
|
1622
|
+
</body>
|
|
1623
|
+
</html>`;
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
function renderMessagesDonePage(message, isError = false) {
|
|
1627
|
+
return `<!doctype html>
|
|
1628
|
+
<html lang="en">
|
|
1629
|
+
<head>
|
|
1630
|
+
<meta charset="utf-8">
|
|
1631
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1632
|
+
<title>Messages Selection</title>
|
|
1633
|
+
<style>
|
|
1634
|
+
:root { color-scheme: light dark; --bg: #FCFCFC; --fg: #111; --muted: #6D726D; --button: #136033; --button-text: #FFFFFF; --radius: 10px; }
|
|
1635
|
+
@media (prefers-color-scheme: dark) { :root { --bg: #000; --fg: #F8F8F8; --muted: #A2A8A2; --button: #FFFFFF; --button-text: #000; } }
|
|
1636
|
+
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; }
|
|
1637
|
+
main { width: min(420px, calc(100vw - 32px)); }
|
|
1638
|
+
h1 { margin: 0 0 8px; font-size: 24px; line-height: 1.1; font-weight: 650; }
|
|
1639
|
+
p { margin: 0; color: var(--muted); font-size: 14px; line-height: 1.45; }
|
|
1640
|
+
.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; }
|
|
1641
|
+
</style>
|
|
1642
|
+
</head>
|
|
1643
|
+
<body>
|
|
1644
|
+
<main>
|
|
1645
|
+
<div class="mark">${isError ? "!" : "OK"}</div>
|
|
1646
|
+
<h1>${html(message)}</h1>
|
|
1647
|
+
<p>${isError ? "Return to the terminal and retry." : "You can close this tab and return to the terminal."}</p>
|
|
1648
|
+
</main>
|
|
1649
|
+
</body>
|
|
1650
|
+
</html>`;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
function renderChatPeople(chat) {
|
|
1654
|
+
const people = chatPeopleLine(chat);
|
|
1655
|
+
if (!people) return "";
|
|
1656
|
+
return `<span class="chat-people">${html(people)}</span>`;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
function chatPeopleLine(chat) {
|
|
1660
|
+
if (chat.kind !== "group") return "";
|
|
1661
|
+
const names = chat.participants?.map((participant) => participant.name ?? participant.handle).filter(Boolean) ?? [];
|
|
1662
|
+
const line = names.slice(0, 6).join(", ");
|
|
1663
|
+
if (!line || normalizeDisplayText(line) === normalizeDisplayText(chat.label)) return "";
|
|
1664
|
+
return line;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
function formatMessageChatMeta(chat) {
|
|
1668
|
+
return formatShortChatTime(chat.lastMessageAt);
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
function formatShortChatTime(value) {
|
|
1672
|
+
if (!value) return "";
|
|
1673
|
+
const date = new Date(value);
|
|
1674
|
+
if (Number.isNaN(date.getTime())) return "";
|
|
1675
|
+
|
|
1676
|
+
const now = new Date();
|
|
1677
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
1678
|
+
const day = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
1679
|
+
const diffDays = Math.round((today.getTime() - day.getTime()) / (24 * 60 * 60 * 1000));
|
|
1680
|
+
const time = new Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "2-digit" })
|
|
1681
|
+
.format(date)
|
|
1682
|
+
.replace(":00", "")
|
|
1683
|
+
.replace(/\s/g, "")
|
|
1684
|
+
.toLowerCase();
|
|
1685
|
+
|
|
1686
|
+
if (diffDays === 0) return `Today ${time}`;
|
|
1687
|
+
if (diffDays === 1) return `Yesterday ${time}`;
|
|
1688
|
+
if (diffDays >= 0 && diffDays < 7) {
|
|
1689
|
+
const weekday = new Intl.DateTimeFormat(undefined, { weekday: "long" }).format(date);
|
|
1690
|
+
return `${weekday} ${time}`;
|
|
1691
|
+
}
|
|
1692
|
+
const monthDay = new Intl.DateTimeFormat(undefined, { month: "short", day: "numeric" }).format(date);
|
|
1693
|
+
return `${monthDay} ${time}`;
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
function normalizeDisplayText(value) {
|
|
1697
|
+
return String(value ?? "").trim().replace(/\s+/g, " ").toLowerCase();
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
function shepherdLogoDataUri() {
|
|
1701
|
+
try {
|
|
1702
|
+
const bytes = readFileSync(SHEPHERD_LOGO_PATH);
|
|
1703
|
+
return `data:image/png;base64,${bytes.toString("base64")}`;
|
|
1704
|
+
} catch {
|
|
1705
|
+
return null;
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
function sendHtml(res, body, status = 200) {
|
|
1710
|
+
res.writeHead(status, {
|
|
1711
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
1712
|
+
"Cache-Control": "no-store",
|
|
1713
|
+
});
|
|
1714
|
+
res.end(body);
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
function readRequestBody(req) {
|
|
1718
|
+
return new Promise((resolve, reject) => {
|
|
1719
|
+
let body = "";
|
|
1720
|
+
req.setEncoding("utf8");
|
|
1721
|
+
req.on("data", (chunk) => {
|
|
1722
|
+
body += chunk;
|
|
1723
|
+
if (body.length > 64_000) {
|
|
1724
|
+
reject(new Error("Request body too large."));
|
|
1725
|
+
req.destroy();
|
|
1726
|
+
}
|
|
1727
|
+
});
|
|
1728
|
+
req.on("end", () => resolve(body));
|
|
1729
|
+
req.on("error", reject);
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1214
1733
|
async function listRecentMessageChats({ limit }) {
|
|
1215
1734
|
if (platform() !== "darwin") {
|
|
1216
1735
|
throw new Error("local Messages chat discovery is only supported on macOS");
|
|
@@ -1222,7 +1741,7 @@ async function listRecentMessageChats({ limit }) {
|
|
|
1222
1741
|
try {
|
|
1223
1742
|
const chats = await sdk.listChats({
|
|
1224
1743
|
sortBy: "recent",
|
|
1225
|
-
limit: Math.max(limit,
|
|
1744
|
+
limit: Math.max(limit, INITIAL_MESSAGE_CHAT_ROWS),
|
|
1226
1745
|
});
|
|
1227
1746
|
const visible = chats
|
|
1228
1747
|
.filter((chat) => typeof chat.chatId === "string" && chat.chatId.trim())
|
|
@@ -1247,10 +1766,12 @@ async function enrichMessageChat(sdk, chat, contactLookup) {
|
|
|
1247
1766
|
: null;
|
|
1248
1767
|
const dmName = dmHandle ? contactLookup.resolveName(dmHandle) : null;
|
|
1249
1768
|
const groupNames = participants.map((participant) => participant.name ?? participant.handle).filter(Boolean);
|
|
1250
|
-
const
|
|
1251
|
-
|
|
1252
|
-
?? (
|
|
1253
|
-
|
|
1769
|
+
const chatName = cleanChatName(chat.name);
|
|
1770
|
+
const label = chat.kind === "dm"
|
|
1771
|
+
? dmName ?? nonHandleChatName(chatName) ?? dmHandle ?? "Contact"
|
|
1772
|
+
: nonHandleChatName(chatName)
|
|
1773
|
+
?? (groupNames.length ? groupNames.slice(0, 4).join(", ") : null)
|
|
1774
|
+
?? "Group";
|
|
1254
1775
|
|
|
1255
1776
|
return {
|
|
1256
1777
|
chatId: chat.chatId,
|
|
@@ -1280,11 +1801,10 @@ function uniqueParticipants(messages, contactLookup) {
|
|
|
1280
1801
|
}
|
|
1281
1802
|
|
|
1282
1803
|
function formatMessageChatOption(chat) {
|
|
1283
|
-
const kind = chat.kind === "group" ? "
|
|
1284
|
-
const
|
|
1285
|
-
const
|
|
1286
|
-
|
|
1287
|
-
return `${chat.label} (${kind})${people}${when}`;
|
|
1804
|
+
const kind = chat.kind === "group" ? "Group" : chat.kind === "dm" ? "Contact" : "Chat";
|
|
1805
|
+
const people = chatPeopleLine(chat);
|
|
1806
|
+
const when = formatMessageChatMeta(chat);
|
|
1807
|
+
return [chat.label, kind, people, when].filter(Boolean).join(" · ");
|
|
1288
1808
|
}
|
|
1289
1809
|
|
|
1290
1810
|
function publicMessageChat(chat) {
|
|
@@ -1641,6 +2161,27 @@ function cleanChatName(name) {
|
|
|
1641
2161
|
return trimmed || null;
|
|
1642
2162
|
}
|
|
1643
2163
|
|
|
2164
|
+
function nonHandleChatName(name) {
|
|
2165
|
+
if (!name) return null;
|
|
2166
|
+
return looksLikeHandleList(name) ? null : name;
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
function looksLikeHandleList(value) {
|
|
2170
|
+
const text = String(value ?? "").trim();
|
|
2171
|
+
if (!text) return false;
|
|
2172
|
+
const tokens = text
|
|
2173
|
+
.split(/[,;/&\s]+/)
|
|
2174
|
+
.map((token) => token.trim())
|
|
2175
|
+
.filter(Boolean);
|
|
2176
|
+
if (!tokens.length) return false;
|
|
2177
|
+
return tokens.every((token) => {
|
|
2178
|
+
if (token.includes("@")) return true;
|
|
2179
|
+
const digits = token.replace(/\D/g, "");
|
|
2180
|
+
if (digits.length >= 4 && /^[+()\-\d.\s]+$/.test(token)) return true;
|
|
2181
|
+
return /^\d{4,}$/.test(token);
|
|
2182
|
+
});
|
|
2183
|
+
}
|
|
2184
|
+
|
|
1644
2185
|
function parseDmHandleFromChatId(chatId) {
|
|
1645
2186
|
const parts = String(chatId ?? "").split(";");
|
|
1646
2187
|
if (parts.length >= 3 && parts[1] === "-") return parts.slice(2).join(";") || null;
|
|
@@ -1675,6 +2216,17 @@ function parseAllowedChatIds(value) {
|
|
|
1675
2216
|
return [...new Set(raw.map((chatId) => String(chatId).trim()).filter(Boolean))];
|
|
1676
2217
|
}
|
|
1677
2218
|
|
|
2219
|
+
function html(value) {
|
|
2220
|
+
return String(value ?? "")
|
|
2221
|
+
.replace(/&/g, "&")
|
|
2222
|
+
.replace(/</g, "<")
|
|
2223
|
+
.replace(/>/g, ">");
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
function htmlAttr(value) {
|
|
2227
|
+
return html(value).replace(/"/g, """);
|
|
2228
|
+
}
|
|
2229
|
+
|
|
1678
2230
|
class MessagesBatchSender {
|
|
1679
2231
|
constructor(apiUrl, agentToken, userId) {
|
|
1680
2232
|
this.apiUrl = trimTrailingSlash(apiUrl);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shepherd-onboard",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
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
|
],
|