shepherd-onboard 0.1.19 → 0.1.21

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.
@@ -17,8 +17,10 @@ const MAX_BATCH_SIZE = 50;
17
17
  const MAX_QUEUE_MESSAGES = 10_000;
18
18
  const DEFAULT_MESSAGE_CHAT_SEARCH_LIMIT = 200;
19
19
  const INITIAL_MESSAGE_CHAT_ROWS = 20;
20
+ const AGENT_MODALITY_ORDER = ["google", "slack", "granola", "messages"];
20
21
  const SHEPHERD_LOGO_PATH = join(PACKAGE_DIR, "assets", "shepherd_G_vector_136033.png");
21
22
  const GRANOLA_API_KEYS_PATH = "/settings/integrations/api-keys";
23
+ const GOOGLE_WORKSPACE_DELEGATION_ADMIN_URL = "https://admin.google.com/ac/owl/domainwidedelegation";
22
24
  const GOOGLE_WORKSPACE_DELEGATION_APP_NAME = "Shepherd";
23
25
  const GOOGLE_WORKSPACE_DELEGATION_SERVICE_ACCOUNT_EMAIL =
24
26
  "gigabrain-delegation@shepherd-gigabrain.iam.gserviceaccount.com";
@@ -148,6 +150,7 @@ async function runOnboarding() {
148
150
  if (sources.google) {
149
151
  console.log("\nGoogle Workspace domain-wide delegation");
150
152
  printGoogleWorkspaceDelegationSetup(session.googleWorkspaceDelegation);
153
+ await openGoogleWorkspaceDelegationAdmin({ noOpen });
151
154
  await waitForEnter("After the Google Workspace super admin authorizes Shepherd in Admin Console, press Enter.");
152
155
  }
153
156
 
@@ -296,30 +299,23 @@ async function runAgentOnboarding() {
296
299
  createdAt: new Date().toISOString(),
297
300
  });
298
301
 
299
- const opened = [];
300
- for (const [provider, url] of Object.entries(session.authUrls ?? {})) {
301
- if (typeof url !== "string") continue;
302
- if (provider === "google") continue;
303
- if (!noOpen) await openOrPrint(url, { noOpen: false });
304
- opened.push(provider);
305
- }
306
-
307
- let granolaApiKeyPage = null;
308
- if (sources.granola && !args["no-open-granola"]) {
309
- granolaApiKeyPage = await openGranolaApiKeys({ noOpen });
310
- }
302
+ const currentAction = await openNextAgentModality({
303
+ sources,
304
+ authUrls: session.authUrls ?? {},
305
+ noOpen,
306
+ });
311
307
 
312
308
  if (args.json) {
313
309
  console.log(JSON.stringify({
314
310
  status: "auth_required",
315
311
  account: publicAgentAccount(session.account),
316
- opened,
312
+ opened: currentAction?.opened ? [currentAction.source] : [],
317
313
  googleWorkspaceDelegation: sources.google ? googleWorkspaceDelegationSetup(session.googleWorkspaceDelegation) : undefined,
318
- granolaApiKeyPage,
314
+ currentAction,
319
315
  statePath,
320
316
  messagesChatsCommand: sources.messages ? `${agentCommand()} messages-chats` : undefined,
321
317
  nextCommand: `${agentCommand()} agent --continue --messages-handle "<phone_or_apple_id>" --messages-chat-ids "<comma_separated_chat_ids>" --granola-api-key "<granola_key>"`,
322
- needsUserAction: agentNeedsUserAction(sources, opened),
318
+ needsUserAction: agentNeedsUserAction(sources, currentAction),
323
319
  }, null, 2));
324
320
  return;
325
321
  }
@@ -330,31 +326,13 @@ async function runAgentOnboarding() {
330
326
  if (session.account.organizationMatch?.type && session.account.organizationMatch.type !== "created") {
331
327
  console.log(`Matched existing organization by ${session.account.organizationMatch.type}.`);
332
328
  }
333
- if (opened.length) {
334
- console.log(`Opened browser authorization: ${opened.join(", ")}`);
335
- }
336
- if (sources.google) {
337
- console.log("\nGoogle Workspace domain-wide delegation setup:");
338
- printGoogleWorkspaceDelegationSetup(session.googleWorkspaceDelegation);
339
- }
340
- if (noOpen) {
341
- for (const [provider, url] of Object.entries(session.authUrls ?? {})) {
342
- if (provider === "google") continue;
343
- console.log(`${provider} auth URL: ${url}`);
344
- }
345
- }
346
329
  console.log(`State saved: ${statePath}`);
347
- console.log("\nCoding agent next steps:");
348
- let step = 1;
349
- if (sources.google) {
350
- console.log(`${step++}. Ask the user's Google Workspace super admin to authorize Shepherd in Google Admin Console with the Client ID and scopes above.`);
351
- }
352
- if (sources.slack) console.log(`${step++}. Ask the user to finish the opened Slack browser authorization.`);
353
- if (sources.granola) console.log(`${step++}. Ask the user for their Granola API key from the Granola Mac app.`);
354
- if (sources.messages) {
355
- console.log(`${step++}. Run ${agentCommand()} messages-chats, have the user select chats in the browser page, and keep the printed chat IDs.`);
356
- }
357
- console.log(`${step++}. Run:`);
330
+
331
+ printAgentCurrentAction(currentAction, {
332
+ googleWorkspaceDelegation: session.googleWorkspaceDelegation,
333
+ });
334
+
335
+ console.log("\nAfter that modality is complete, run:");
358
336
  console.log(` ${agentCommand()} agent --continue --messages-handle "<phone_or_apple_id>" --messages-chat-ids "<comma_separated_chat_ids>" --granola-api-key "<granola_key>"`);
359
337
  console.log(" Omit either optional flag if that source is not being connected.");
360
338
  }
@@ -477,11 +455,20 @@ async function continueAgentOnboarding() {
477
455
  }
478
456
 
479
457
  const errors = finalized.errors && Object.keys(finalized.errors).length ? finalized.errors : null;
458
+ const currentAction = errors
459
+ ? await openNextAgentModality({
460
+ sources: state.sources,
461
+ authUrls: state.authUrls ?? {},
462
+ noOpen: Boolean(args["no-open"]),
463
+ pendingSources: pendingAgentSources(state.sources, errors),
464
+ })
465
+ : null;
480
466
  if (args.json) {
481
467
  console.log(JSON.stringify({
482
468
  status: errors ? "waiting" : "completed",
483
469
  connected: Object.keys(finalized.connected ?? {}),
484
470
  errors: errors ? safeErrorRecord(errors) : undefined,
471
+ currentAction,
485
472
  nextCommand: errors ? `${agentCommand()} agent --continue --messages-handle "<phone_or_apple_id>" --messages-chat-ids "<comma_separated_chat_ids>" --granola-api-key "<granola_key>"` : undefined,
486
473
  }, null, 2));
487
474
  return;
@@ -492,7 +479,12 @@ async function continueAgentOnboarding() {
492
479
  for (const [source, message] of Object.entries(errors)) {
493
480
  console.log(`- ${source}: ${safeError(message)}`);
494
481
  }
495
- console.log("\nAfter the user completes missing auth/details, rerun:");
482
+
483
+ printAgentCurrentAction(currentAction, {
484
+ googleWorkspaceDelegation: state.googleWorkspaceDelegation,
485
+ });
486
+
487
+ console.log("\nAfter that modality is complete, rerun:");
496
488
  console.log(` ${agentCommand()} agent --continue --messages-handle "<phone_or_apple_id>" --messages-chat-ids "<comma_separated_chat_ids>" --granola-api-key "<granola_key>"`);
497
489
  console.log(" Omit either optional flag if that source is not being connected.");
498
490
  return;
@@ -733,9 +725,9 @@ function printAgentContract() {
733
725
  "Selected local Messages chats from the browser selector, if they want local Messages connected",
734
726
  ],
735
727
  afterStartCommand: [
736
- "For Google Workspace, have a super admin authorize the Shepherd Client ID and scopes in Google Admin Console.",
737
- "Complete the opened Slack authorization.",
738
- "Create/copy the Granola API key from the opened Granola Mac app API key screen, if they want Granola connected.",
728
+ "Handle only the current modality opened or printed by the command.",
729
+ "After the current modality is complete, run the continue command to advance to the next unresolved modality.",
730
+ "Do not open Slack, Granola, or Messages while Google Workspace is still waiting; do not open Granola or Messages while Slack is still waiting.",
739
731
  ],
740
732
  doNotDo: [
741
733
  "Do not run wiki generation.",
@@ -813,9 +805,9 @@ Add skip flags for sources the user did not select:
813
805
  - --no-granola
814
806
  - --no-messages
815
807
 
816
- That command creates/reuses the customer user and org, prints Google Workspace domain-wide delegation setup values, opens Slack browser auth if selected, and saves local state.
808
+ That command creates/reuses the customer user and org, saves local state, and opens at most one source setup surface. It works one modality at a time after account setup: Google Workspace, then Slack, then Granola. If Messages details are still missing, it prints the Messages selector command instead of opening another auth surface. Do not manually open later source setup surfaces until the command tells you that source is the current modality.
817
809
 
818
- If Google Workspace is selected, show this Admin Console setup to the user and have their Google Workspace super admin authorize it:
810
+ If Google Workspace is the current modality, the setup command opens the Admin Console domain-wide delegation page. Show this setup to the user and have their Google Workspace super admin authorize it:
819
811
 
820
812
  App name: ${payload.googleWorkspaceDelegation.appName}
821
813
  Service account email: ${payload.googleWorkspaceDelegation.serviceAccountEmail}
@@ -829,17 +821,17 @@ The setup command copies those scopes to the clipboard as one comma-separated st
829
821
  The customer does not create a service account and does not upload service account JSON in the default Shepherd-managed flow. Their super admin only authorizes the Client ID and scopes in Google Admin Console. Shepherd's backend stores and uses its private service account JSON server-side.
830
822
  Shepherd must still enforce selected users and groups internally before polling or impersonating any employee email.
831
823
 
832
- If Slack is selected and your browser automation can complete that auth screen, do it. If it cannot click through OAuth screens, leave the opened browser tab for the user and ask them to complete Slack auth.
824
+ If Slack is the current modality and your browser automation can complete that auth screen, do it. If it cannot click through OAuth screens, leave the opened browser tab for the user and ask them to complete Slack auth. Do not open Granola or Messages until Slack is complete and the continue command advances.
833
825
 
834
- If Granola is selected, it also opens the Granola desktop app. If your local app automation can navigate it, go to:
826
+ If Granola is the current modality, the command opens the Granola desktop app. If your local app automation can navigate it, go to:
835
827
  Settings -> Connectors -> API keys
836
828
  Then have the user create/copy the API key.
837
829
 
838
- If Granola did not come forward, run:
830
+ If Granola is the current modality and did not come forward, run:
839
831
  ${payload.granolaApiKeyCommand}
840
832
  That command opens Granola and tries to navigate to Settings -> Connectors -> API keys. If your tool cannot click inside Granola, leave Granola open and ask the user to go to that screen.
841
833
 
842
- After Google Workspace Admin Console delegation and Slack browser auth are complete, and after the user has copied a Granola API key from the opened Granola screen if they want Granola, run:
834
+ After the current modality is complete, run:
843
835
  ${payload.continueCommand} --messages-handle "<phone_or_apple_id>" --messages-chat-ids "<comma_separated_chat_ids>" --granola-api-key "<granola_key>"
844
836
 
845
837
  Omit either optional flag if that source is not being connected.
@@ -956,13 +948,108 @@ function authenticatedName(authenticated) {
956
948
  return authenticated?.workosUser?.name ?? authenticated?.account?.name ?? null;
957
949
  }
958
950
 
959
- function agentNeedsUserAction(sources, opened) {
960
- const actions = [];
961
- 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.");
962
- if (sources.slack && opened.includes("slack")) actions.push("Complete Slack browser authorization.");
963
- if (sources.granola) actions.push("Create/copy a Granola API key from the Granola Mac app.");
964
- 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.");
965
- return actions;
951
+ function pendingAgentSources(sources, errors) {
952
+ const pending = new Set(Object.keys(errors ?? {}));
953
+ return AGENT_MODALITY_ORDER.filter((source) => sources?.[source] && pending.has(source));
954
+ }
955
+
956
+ async function openNextAgentModality({ sources, authUrls = {}, noOpen = false, pendingSources = null }) {
957
+ const pending = pendingSources ?? AGENT_MODALITY_ORDER.filter((source) => sources?.[source]);
958
+ for (const source of AGENT_MODALITY_ORDER) {
959
+ if (!sources?.[source] || !pending.includes(source)) continue;
960
+
961
+ if (source === "google") {
962
+ return {
963
+ source,
964
+ label: "Google Workspace",
965
+ ...await openGoogleWorkspaceDelegationAdmin({ noOpen }),
966
+ };
967
+ }
968
+
969
+ if (source === "slack") {
970
+ const url = typeof authUrls.slack === "string" ? authUrls.slack : null;
971
+ if (!url) {
972
+ return {
973
+ source,
974
+ label: "Slack",
975
+ opened: false,
976
+ message: "Slack authorization URL was not returned by Shepherd.",
977
+ };
978
+ }
979
+ await openOrPrint(url, { noOpen });
980
+ return { source, label: "Slack", opened: !noOpen, url };
981
+ }
982
+
983
+ if (source === "granola") {
984
+ const result = await openGranolaApiKeys({ noOpen: noOpen || Boolean(args["no-open-granola"]) });
985
+ return { source, label: "Granola", ...result };
986
+ }
987
+
988
+ if (source === "messages") {
989
+ return {
990
+ source,
991
+ label: "Messages",
992
+ opened: false,
993
+ command: `${agentCommand()} messages-chats`,
994
+ message: "Run the local Messages chat selector and keep the printed chat IDs.",
995
+ };
996
+ }
997
+ }
998
+
999
+ return null;
1000
+ }
1001
+
1002
+ function printAgentCurrentAction(action, opts = {}) {
1003
+ if (!action) {
1004
+ console.log("\nNo source setup page was opened.");
1005
+ return;
1006
+ }
1007
+
1008
+ console.log(`\nCurrent modality: ${action.label}`);
1009
+
1010
+ if (action.source === "google") {
1011
+ if (action.opened) {
1012
+ console.log(`Opened Google Admin Console: ${action.url}`);
1013
+ } else {
1014
+ console.log(`Google Admin Console domain-wide delegation URL: ${action.url}`);
1015
+ }
1016
+ console.log("\nGoogle Workspace domain-wide delegation setup:");
1017
+ printGoogleWorkspaceDelegationSetup(opts.googleWorkspaceDelegation);
1018
+ console.log("\nAsk the user's Google Workspace super admin to authorize Shepherd before opening another source.");
1019
+ return;
1020
+ }
1021
+
1022
+ if (action.source === "slack") {
1023
+ if (action.opened) {
1024
+ console.log("Opened Slack authorization in the browser.");
1025
+ } else if (action.url) {
1026
+ console.log(`Slack authorization URL: ${action.url}`);
1027
+ } else if (action.message) {
1028
+ console.log(action.message);
1029
+ }
1030
+ console.log("Ask the user to complete Slack authorization before opening another source.");
1031
+ return;
1032
+ }
1033
+
1034
+ if (action.source === "granola") {
1035
+ if (action.target) console.log(`Granola target: ${action.target}`);
1036
+ console.log("Ask the user to create/copy the Granola API key before opening another source.");
1037
+ return;
1038
+ }
1039
+
1040
+ if (action.source === "messages") {
1041
+ console.log(`Run: ${action.command}`);
1042
+ console.log("Have the user select specific local Messages chats; do not select all by default.");
1043
+ }
1044
+ }
1045
+
1046
+ function agentNeedsUserAction(sources, action) {
1047
+ if (!action) return [];
1048
+ if (action.source === "google") return ["Have the customer's Google Workspace super admin authorize Shepherd's domain-wide delegation Client ID and scopes in Google Admin Console."];
1049
+ if (action.source === "slack") return ["Complete Slack browser authorization."];
1050
+ if (action.source === "granola") return ["Create/copy a Granola API key from the Granola Mac app."];
1051
+ if (action.source === "messages") return ["Run messages-chats, have the user select local Messages contacts/groups in the browser, then pass the printed chat IDs with the Messages handle."];
1052
+ return [];
966
1053
  }
967
1054
 
968
1055
  function agentCommand() {
@@ -1072,6 +1159,15 @@ async function openOrPrint(url, opts) {
1072
1159
  });
1073
1160
  }
1074
1161
 
1162
+ async function openGoogleWorkspaceDelegationAdmin(opts = {}) {
1163
+ if (opts.noOpen) {
1164
+ console.log(`Google Admin Console domain-wide delegation URL: ${GOOGLE_WORKSPACE_DELEGATION_ADMIN_URL}`);
1165
+ return { opened: false, url: GOOGLE_WORKSPACE_DELEGATION_ADMIN_URL };
1166
+ }
1167
+ await openOrPrint(GOOGLE_WORKSPACE_DELEGATION_ADMIN_URL, { noOpen: false });
1168
+ return { opened: true, url: GOOGLE_WORKSPACE_DELEGATION_ADMIN_URL };
1169
+ }
1170
+
1075
1171
  async function openGranolaApiKeys(opts = {}) {
1076
1172
  const deepLink = granolaApiKeysDeepLink();
1077
1173
  if (opts.noOpen) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shepherd-onboard",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "description": "Customer-facing Shepherd raw sync onboarding CLI",
5
5
  "type": "module",
6
6
  "bin": {