shepherd-onboard 0.1.10 → 0.1.11

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 CHANGED
@@ -10,7 +10,7 @@ Give this to a coding agent:
10
10
  npx -y shepherd-onboard@latest agent
11
11
  ```
12
12
 
13
- The command prints the exact prompt the agent should follow, then the exact follow-up commands to open Shepherd WorkOS login/signup, open source auth, open Granola's API key page, finalize, start cloud raw polling/backfills, and install local Messages sync.
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
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 --json`, shows the 20 most recent local Messages chats with contact/group names, and asks which chats 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
 
@@ -27,7 +27,7 @@ The command:
27
27
  - creates or reuses the Shepherd customer account from the WorkOS-authenticated email
28
28
  - creates or reuses the organization, including case-insensitive and close-name matches
29
29
  - only reuses an existing organization when the authenticated account is allowed to join it
30
- - opens Google Workspace authorization for Gmail, Drive, Docs, and Calendar consent
30
+ - prints Google Workspace domain-wide delegation setup for a Workspace super admin
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
@@ -38,6 +38,22 @@ The command:
38
38
 
39
39
  The command does not expose Railway, database, Redis, or internal service details to the user.
40
40
 
41
+ ## Google Workspace Domain-wide Delegation
42
+
43
+ Business customers connect Google Workspace once as an admin. Employees do not create service accounts and do not each complete Google OAuth for Gmail, Calendar, Drive, Docs, Sheets, Slides, Tasks, or Contacts.
44
+
45
+ Customer-side setup in Google Admin Console:
46
+
47
+ ```text
48
+ App name: Shepherd
49
+ Service account email: gigabrain-delegation@shepherd-gigabrain.iam.gserviceaccount.com
50
+ Domain-wide delegation OAuth Client ID: 118363960386741325727
51
+ ```
52
+
53
+ The customer super admin authorizes that Client ID with the scopes printed by the CLI. The customer does not upload service account JSON; Shepherd stores its private service account JSON server-side in `GOOGLE_WORKSPACE_SERVICE_ACCOUNT_KEY_JSON` and requests delegated tokens with `sub=<allowed_employee_email>`.
54
+
55
+ Shepherd must still enforce selected users and groups internally before impersonating or polling employee emails.
56
+
41
57
  ## Options
42
58
 
43
59
  ```sh
@@ -48,7 +64,7 @@ The command does not expose Railway, database, Redis, or internal service detail
48
64
  --messages-handle <value> Messages phone number or Apple ID email
49
65
  --messages-chat-ids <ids> Comma-separated chat IDs selected from messages-chats
50
66
  --messages-backfill-days Local Messages backfill window, default 30
51
- --no-google Skip Google Workspace (Gmail/Drive/Docs/Calendar)
67
+ --no-google Skip Google Workspace (Gmail/Drive/Docs/Calendar/Sheets/Slides/Tasks/Contacts)
52
68
  --no-slack Skip Slack
53
69
  --no-granola Skip Granola
54
70
  --no-open-granola Do not open the Granola API key screen
@@ -13,6 +13,7 @@ const DEFAULT_AGENT_STATE_PATH = join(homedir(), ".shepherd", "raw-onboarding-ag
13
13
  const MAX_BATCH_SIZE = 50;
14
14
  const MAX_QUEUE_MESSAGES = 10_000;
15
15
  const DEFAULT_RECENT_MESSAGE_CHATS = 20;
16
+ const GRANOLA_API_KEYS_PATH = "/settings/integrations/api-keys";
16
17
  const GOOGLE_WORKSPACE_DELEGATION_APP_NAME = "Shepherd";
17
18
  const GOOGLE_WORKSPACE_DELEGATION_SERVICE_ACCOUNT_EMAIL =
18
19
  "gigabrain-delegation@shepherd-gigabrain.iam.gserviceaccount.com";
@@ -283,7 +284,9 @@ async function runAgentOnboarding() {
283
284
  account: session.account,
284
285
  sources,
285
286
  authUrls: session.authUrls ?? {},
286
- googleWorkspaceDelegation: googleWorkspaceDelegationSetup(session.googleWorkspaceDelegation),
287
+ googleWorkspaceDelegation: sources.google
288
+ ? googleWorkspaceDelegationSetup(session.googleWorkspaceDelegation)
289
+ : undefined,
287
290
  workosAuth,
288
291
  createdAt: new Date().toISOString(),
289
292
  });
@@ -337,15 +340,16 @@ async function runAgentOnboarding() {
337
340
  }
338
341
  console.log(`State saved: ${statePath}`);
339
342
  console.log("\nCoding agent next steps:");
343
+ let step = 1;
340
344
  if (sources.google) {
341
- console.log("1. Ask the user's Google Workspace super admin to authorize Shepherd in Google Admin Console with the Client ID and scopes above.");
345
+ 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.`);
342
346
  }
343
- if (sources.slack) console.log("2. Ask the user to finish the opened Slack browser authorization.");
344
- if (sources.granola) console.log("3. Ask the user for their Granola API key from the Granola Mac app.");
347
+ if (sources.slack) console.log(`${step++}. Ask the user to finish the opened Slack browser authorization.`);
348
+ if (sources.granola) console.log(`${step++}. Ask the user for their Granola API key from the Granola Mac app.`);
345
349
  if (sources.messages) {
346
- console.log(`4. Run ${agentCommand()} messages-chats --json, ask the user which recent chats to sync, and keep those chat IDs.`);
350
+ console.log(`${step++}. Run ${agentCommand()} messages-chats --json, ask the user which recent chats to sync, and keep those chat IDs.`);
347
351
  }
348
- console.log("5. Run:");
352
+ console.log(`${step++}. Run:`);
349
353
  console.log(` ${agentCommand()} agent --continue --messages-handle "<phone_or_apple_id>" --messages-chat-ids "<comma_separated_chat_ids>" --granola-api-key "<granola_key>"`);
350
354
  console.log(" Omit either optional flag if that source is not being connected.");
351
355
  }
@@ -758,7 +762,7 @@ Ask with short interactive prompts, not as one pasted checklist.
758
762
 
759
763
  Start with selection questions to determine intent:
760
764
  1. Organization: Join existing org, or Create new org.
761
- 2. Sources: Google Workspace (Gmail/Drive/Docs/Calendar), Slack, Granola, Messages. Allow multi-select if your interface supports it.
765
+ 2. Sources: Google Workspace (Gmail/Drive/Docs/Calendar/Sheets/Slides/Tasks/Contacts), Slack, Granola, Messages. Allow multi-select if your interface supports it.
762
766
  3. Messages, if selected: Skip Messages, or Provide handle.
763
767
 
764
768
  When discussing existing orgs, keep it short: Shepherd verifies the join from their Shepherd login and company email domain. The org name they type is not trusted by itself.
@@ -767,6 +771,7 @@ Before source setup, always run:
767
771
  ${payload.loginCommand}
768
772
 
769
773
  That opens one WorkOS Shepherd login/signup flow and saves a local onboarding auth session. It creates or relinks the Shepherd customer account; the next setup command attaches sources to the same production cloud account rows.
774
+ Do not use WorkOS Auth, WorkOS Pipes, or per-user Google OAuth for Google Workspace. WorkOS login is only Shepherd account identity.
770
775
 
771
776
  Ask the user for:
772
777
  1. Full name
@@ -791,8 +796,21 @@ Add skip flags for sources the user did not select:
791
796
  - --no-granola
792
797
  - --no-messages
793
798
 
794
- That command creates/reuses the customer user and org, opens Google Workspace/Slack browser auth, and saves local state.
795
- If your browser automation can complete those auth screens, do it. If it cannot click through OAuth screens, leave the opened browser tabs for the user and ask them to complete Google Workspace and Slack auth.
799
+ 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.
800
+
801
+ If Google Workspace is selected, show this Admin Console setup to the user and have their Google Workspace super admin authorize it:
802
+
803
+ App name: ${payload.googleWorkspaceDelegation.appName}
804
+ Service account email: ${payload.googleWorkspaceDelegation.serviceAccountEmail}
805
+ Domain-wide delegation OAuth Client ID: ${payload.googleWorkspaceDelegation.clientId}
806
+
807
+ Scopes:
808
+ ${payload.googleWorkspaceDelegation.scopes.join("\n")}
809
+
810
+ 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.
811
+ Shepherd must still enforce selected users and groups internally before polling or impersonating any employee email.
812
+
813
+ 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.
796
814
 
797
815
  If Granola is selected, it also opens the Granola desktop app. If your local app automation can navigate it, go to:
798
816
  Settings -> Connectors -> API keys
@@ -802,7 +820,7 @@ If Granola did not come forward, run:
802
820
  ${payload.granolaApiKeyCommand}
803
821
  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.
804
822
 
805
- After Google Workspace and Slack browser auth is complete, and after the user has copied a Granola API key from the opened Granola screen if they want Granola, run:
823
+ 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:
806
824
  ${payload.continueCommand} --messages-handle "<phone_or_apple_id>" --messages-chat-ids "<comma_separated_chat_ids>" --granola-api-key "<granola_key>"
807
825
 
808
826
  Omit either optional flag if that source is not being connected.
@@ -1017,7 +1035,7 @@ async function openOrPrint(url, opts) {
1017
1035
  }
1018
1036
 
1019
1037
  async function openGranolaApiKeys(opts = {}) {
1020
- const deepLink = "granola://settings/integrations";
1038
+ const deepLink = granolaApiKeysDeepLink();
1021
1039
  if (opts.noOpen) {
1022
1040
  console.log("Granola API keys: open the Granola desktop app -> Settings -> Connectors -> API keys");
1023
1041
  return { opened: false, target: "Granola Settings -> Connectors -> API keys" };
@@ -1030,19 +1048,20 @@ async function openGranolaApiKeys(opts = {}) {
1030
1048
 
1031
1049
  console.log("\nOpening Granola API keys");
1032
1050
  const bundleResult = await execFileQuiet("open", ["-b", "com.granola.app"], { ignoreError: true, captureError: true });
1033
- await sleep(500);
1034
- await execFileQuiet("open", [deepLink], { ignoreError: true, captureError: true });
1035
- await sleep(500);
1051
+ await sleep(900);
1052
+ const deepLinkResult = await execFileQuiet("open", ["-u", deepLink], { ignoreError: true, captureError: true });
1053
+ await sleep(700);
1054
+ const deepLinkRetryResult = await execFileQuiet("open", ["-u", deepLink], { ignoreError: true, captureError: true });
1055
+ await sleep(300);
1036
1056
  const activateByBundleResult = await execFileQuiet("osascript", [
1037
1057
  "-e",
1038
1058
  'tell application id "com.granola.app" to activate',
1039
1059
  ], { ignoreError: true, captureError: true });
1040
1060
  const activateByNameResult = await execFileQuiet("open", ["-a", "Granola"], { ignoreError: true, captureError: true });
1041
1061
 
1042
- if (bundleResult.error && activateByBundleResult.error && activateByNameResult.error) {
1062
+ if (bundleResult.error && deepLinkResult.error && deepLinkRetryResult.error && activateByBundleResult.error && activateByNameResult.error) {
1043
1063
  await execFileQuiet("open", ["-a", "Granola"], { ignoreError: true });
1044
1064
  }
1045
- await navigateGranolaApiKeysWithAppleScript();
1046
1065
 
1047
1066
  return {
1048
1067
  opened: true,
@@ -1052,32 +1071,8 @@ async function openGranolaApiKeys(opts = {}) {
1052
1071
  };
1053
1072
  }
1054
1073
 
1055
- async function navigateGranolaApiKeysWithAppleScript() {
1056
- const script = `
1057
- tell application id "com.granola.app" to activate
1058
- delay 0.7
1059
- tell application "System Events"
1060
- if not (exists process "Granola") then return
1061
- tell process "Granola"
1062
- set frontmost to true
1063
- try
1064
- set {x, y} to position of window 1
1065
- set {w, h} to size of window 1
1066
- click at {x + w - 55, y + 810}
1067
- return
1068
- end try
1069
- try
1070
- keystroke "," using command down
1071
- delay 0.5
1072
- set {x, y} to position of window 1
1073
- set {w, h} to size of window 1
1074
- click at {x + w - 55, y + 810}
1075
- return
1076
- end try
1077
- end tell
1078
- end tell
1079
- `;
1080
- await execFileQuiet("osascript", ["-e", script], { ignoreError: true });
1074
+ function granolaApiKeysDeepLink() {
1075
+ return `granola://open?path=${encodeURIComponent(GRANOLA_API_KEYS_PATH)}`;
1081
1076
  }
1082
1077
 
1083
1078
  async function postJson(url, body, opts = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shepherd-onboard",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "Customer-facing Shepherd raw sync onboarding CLI",
5
5
  "type": "module",
6
6
  "bin": {