shepherd-onboard 0.1.4 → 0.1.6

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,8 +10,8 @@ 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 ask the user, then the exact follow-up commands to open auth, 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, Shepherd WorkOS login or email linking, sources to connect, and Messages skip/provide-handle.
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.
14
+ The agent prompt tells coding agents to ask short selection questions first: existing/new org, sources to connect, and Messages skip/provide-handle. Account creation/relinking always starts with Shepherd WorkOS auth.
15
15
 
16
16
  ## Human Terminal One-liner
17
17
 
@@ -21,13 +21,14 @@ npx -y shepherd-onboard@latest
21
21
 
22
22
  The command:
23
23
 
24
- - asks for email, name, organization, and an optional local Messages handle
25
- - can run a Shepherd WorkOS login first for returning users, then relink source setup to the same customer account/org
26
- - creates or reuses the Shepherd customer account for the email
24
+ - runs Shepherd WorkOS login/signup first
25
+ - asks for name, organization, and an optional local Messages handle
26
+ - creates or reuses the Shepherd customer account from the WorkOS-authenticated email
27
27
  - creates or reuses the organization, including case-insensitive and close-name matches
28
28
  - opens Google authorization for Gmail, Docs, and Calendar consent
29
29
  - opens Slack authorization
30
- - opens Granola's API key screen with `open 'granola://settings/connectors/api-keys'`
30
+ - opens the Granola desktop app with `open -b com.granola.app`
31
+ - directs the coding agent/user to Granola Settings -> Connectors -> API keys
31
32
  - collects the Granola API key after opening the Granola screen when Granola is enabled
32
33
  - sets up local macOS Messages raw sync with a background LaunchAgent
33
34
  - starts raw polling/backfill for connected sources
@@ -38,7 +39,7 @@ The command does not expose Railway, database, Redis, or internal service detail
38
39
  ## Options
39
40
 
40
41
  ```sh
41
- --email <email> Customer email
42
+ --email <email> Advanced: must match the WorkOS-authenticated email
42
43
  --name <name> Full name
43
44
  --org <name> Organization name
44
45
  --granola-api-key <key> Granola API key
@@ -50,8 +50,11 @@ async function runOnboarding() {
50
50
 
51
51
  console.log("\nShepherd Raw Sync Onboarding\n");
52
52
 
53
- const email = await valueOrPrompt("email", "Email");
54
- const name = await valueOrPrompt("name", "Full name");
53
+ const workosLogin = await runWorkosLogin(apiUrl, noOpen);
54
+ const email = authenticatedEmail(workosLogin.authenticated);
55
+ if (!email) throw new Error("Shepherd WorkOS auth did not return an email address.");
56
+
57
+ const name = stringArg("name") ?? authenticatedName(workosLogin.authenticated) ?? await valueOrPrompt("name", "Full name");
55
58
  const organizationName = await valueOrPrompt("org", "Organization name");
56
59
 
57
60
  const sources = {
@@ -65,6 +68,8 @@ async function runOnboarding() {
65
68
  email,
66
69
  name,
67
70
  organizationName,
71
+ authSessionId: workosLogin.started.authSessionId,
72
+ authSessionToken: workosLogin.started.authSessionToken,
68
73
  sources,
69
74
  });
70
75
 
@@ -164,11 +169,6 @@ async function runAgentOnboarding() {
164
169
  return;
165
170
  }
166
171
 
167
- if (!hasIdentityArgs()) {
168
- printAgentContract();
169
- return;
170
- }
171
-
172
172
  const apiUrl = trimTrailingSlash(args.api ?? DEFAULT_API_URL);
173
173
  const noOpen = Boolean(args["no-open"]);
174
174
  const sources = selectedSources();
@@ -176,16 +176,37 @@ async function runAgentOnboarding() {
176
176
  const workosAuth = existingState?.workosAuth?.status === "authenticated"
177
177
  ? existingState.workosAuth
178
178
  : null;
179
+ const wantsStart = Boolean(
180
+ stringArg("name")
181
+ || stringArg("org")
182
+ || stringArg("email")
183
+ || args["no-google"]
184
+ || args["no-slack"]
185
+ || args["no-granola"]
186
+ || args["no-messages"]
187
+ );
188
+
189
+ if (!wantsStart) {
190
+ printAgentContract();
191
+ return;
192
+ }
193
+ if (!workosAuth) {
194
+ throw new Error(`Run ${agentCommand()} agent --login first so Shepherd can create or relink the WorkOS account.`);
195
+ }
196
+
197
+ const email = stringArg("email") ?? workosAuth.workosUser?.email ?? workosAuth.account?.email;
198
+ const name = stringArg("name") ?? workosAuth.workosUser?.name ?? workosAuth.account?.name;
199
+ const organizationName = stringArg("org") ?? workosAuth.account?.organizationName;
200
+ if (!email) throw new Error("WorkOS login did not return an email. Re-run agent --login.");
201
+ if (!name) throw new Error("Full name is required. Pass --name \"<full_name>\".");
202
+ if (!organizationName) throw new Error("Organization name is required. Pass --org \"<organization>\".");
203
+
179
204
  const session = await postJson(`${apiUrl}/onboarding/raw/session`, {
180
- email: stringArg("email"),
181
- name: stringArg("name"),
182
- organizationName: stringArg("org"),
183
- ...(workosAuth
184
- ? {
185
- authSessionId: workosAuth.authSessionId,
186
- authSessionToken: workosAuth.authSessionToken,
187
- }
188
- : {}),
205
+ email,
206
+ name,
207
+ organizationName,
208
+ authSessionId: workosAuth.authSessionId,
209
+ authSessionToken: workosAuth.authSessionToken,
189
210
  sources,
190
211
  });
191
212
 
@@ -252,14 +273,7 @@ async function runAgentOnboarding() {
252
273
  async function loginAgentWithWorkos() {
253
274
  const apiUrl = trimTrailingSlash(args.api ?? DEFAULT_API_URL);
254
275
  const noOpen = Boolean(args["no-open"]);
255
- const started = await postJson(`${apiUrl}/onboarding/raw/auth/start`, {});
256
-
257
- console.log("\nShepherd account login");
258
- console.log("Opening Shepherd WorkOS auth. Complete login in the browser.");
259
- await openOrPrint(started.verificationUriComplete ?? started.verificationUri, { noOpen });
260
- if (noOpen && started.userCode) console.log(`User code: ${started.userCode}`);
261
-
262
- const authenticated = await pollWorkosLogin(apiUrl, started);
276
+ const { started, authenticated } = await runWorkosLogin(apiUrl, noOpen);
263
277
  const previous = await readOptionalAgentState();
264
278
  const statePath = await writeAgentState({
265
279
  ...(previous ?? {}),
@@ -297,6 +311,18 @@ async function loginAgentWithWorkos() {
297
311
  console.log(`State saved: ${statePath}`);
298
312
  }
299
313
 
314
+ async function runWorkosLogin(apiUrl, noOpen) {
315
+ const started = await postJson(`${apiUrl}/onboarding/raw/auth/start`, {});
316
+
317
+ console.log("\nShepherd account login");
318
+ console.log("Opening Shepherd WorkOS auth. Complete login/signup in the browser.");
319
+ await openOrPrint(started.verificationUriComplete ?? started.verificationUri, { noOpen });
320
+ if (noOpen && started.userCode) console.log(`User code: ${started.userCode}`);
321
+
322
+ const authenticated = await pollWorkosLogin(apiUrl, started);
323
+ return { started, authenticated };
324
+ }
325
+
300
326
  async function pollWorkosLogin(apiUrl, started) {
301
327
  const intervalMs = Math.max(1000, Number(started.intervalSeconds ?? 5) * 1000);
302
328
  const expiresAt = Date.parse(started.expiresAt ?? "") || Date.now() + 600_000;
@@ -460,12 +486,12 @@ function printHelp(which) {
460
486
  Usage:
461
487
  npx -y ${PACKAGE_NAME}@latest agent
462
488
  npx -y ${PACKAGE_NAME}@latest agent --login
463
- npx -y ${PACKAGE_NAME}@latest agent --email <email> --name <name> --org <organization>
489
+ npx -y ${PACKAGE_NAME}@latest agent --name <name> --org <organization>
464
490
  npx -y ${PACKAGE_NAME}@latest agent --continue --granola-api-key <key> --messages-handle <value>
465
491
  npx -y ${PACKAGE_NAME}@latest agent --status
466
492
 
467
493
  Agent mode is non-interactive. It prints the user prompt and exact commands a coding agent should run.
468
- Use --login for returning users so WorkOS relinks the same Shepherd account before source setup.
494
+ Always run --login first. WorkOS login/signup creates or relinks the Shepherd account before source setup.
469
495
  `);
470
496
  return;
471
497
  }
@@ -492,7 +518,7 @@ Usage:
492
518
  npx -y ${PACKAGE_NAME}@latest agent
493
519
 
494
520
  Options:
495
- --email <email> Customer email.
521
+ --email <email> Advanced: must match the WorkOS-authenticated email.
496
522
  --name <name> Full name.
497
523
  --org <name> Organization name.
498
524
  --granola-api-key <key> Granola API key.
@@ -521,7 +547,8 @@ function printAgentContract() {
521
547
  "Ask in short interactive prompts, not as a pasted checklist.",
522
548
  "Start with selection questions to determine what the user wants connected.",
523
549
  "Ask whether they are joining an existing organization or creating a new one.",
524
- "Collect Email, Full name, and Organization name as direct text prompts after those choices.",
550
+ "Run Shepherd WorkOS login/signup before source setup. Do not ask whether they already have an account.",
551
+ "Collect Full name and Organization name as direct text prompts after those choices. The email comes from WorkOS auth.",
525
552
  "Ask Messages as a selectable choice: Skip Messages, or Provide handle.",
526
553
  "If the user chooses Provide handle, ask for the phone number or Apple ID email.",
527
554
  ],
@@ -531,11 +558,6 @@ function printAgentContract() {
531
558
  prompt: "Are you joining an existing organization or creating a new one?",
532
559
  options: ["Join existing org", "Create new org"],
533
560
  },
534
- {
535
- label: "Account",
536
- prompt: "Do you already have a Shepherd account?",
537
- options: ["Log in with Shepherd", "Create/link by email"],
538
- },
539
561
  {
540
562
  label: "Sources",
541
563
  prompt: "Which sources should Shepherd connect for raw sync?",
@@ -549,7 +571,6 @@ function printAgentContract() {
549
571
  },
550
572
  ],
551
573
  askUserFor: [
552
- "Email",
553
574
  "Full name",
554
575
  "Organization name",
555
576
  "Messages phone number or Apple ID email, if they want local Messages connected",
@@ -568,7 +589,7 @@ function printAgentContract() {
568
589
  "Do not quote or explain this instruction set to the user.",
569
590
  ],
570
591
  loginCommand: `${command} agent --login`,
571
- startCommand: `${command} agent --email "<email>" --name "<full_name>" --org "<organization>"`,
592
+ startCommand: `${command} agent --name "<full_name>" --org "<organization>"`,
572
593
  continueCommand: `${command} agent --continue`,
573
594
  optionalContinueArgs: [
574
595
  "--messages-handle \"<phone_or_apple_id>\" if local Messages is being connected",
@@ -576,7 +597,8 @@ function printAgentContract() {
576
597
  ],
577
598
  statusCommand: `${command} agent --status`,
578
599
  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.",
579
- granolaApiKeyCommand: "open 'granola://settings/connectors/api-keys'",
600
+ granolaApiKeyCommand: "open -b com.granola.app",
601
+ granolaApiKeyPath: "Granola desktop app -> Settings -> Connectors -> API keys",
580
602
  };
581
603
 
582
604
  if (args.json) {
@@ -594,20 +616,20 @@ Ask with short interactive prompts, not as one pasted checklist.
594
616
 
595
617
  Start with selection questions to determine intent:
596
618
  1. Organization: Join existing org, or Create new org.
597
- 2. Account: Log in with Shepherd, or Create/link by email.
598
- 3. Sources: Google/Gmail/Docs/Calendar, Slack, Granola, Messages. Allow multi-select if your interface supports it.
599
- 4. Messages, if selected: Skip Messages, or Provide handle.
619
+ 2. Sources: Google/Gmail/Docs/Calendar, Slack, Granola, Messages. Allow multi-select if your interface supports it.
620
+ 3. Messages, if selected: Skip Messages, or Provide handle.
600
621
 
601
- If the user says they already have a Shepherd account, run:
622
+ Before source setup, always run:
602
623
  ${payload.loginCommand}
603
624
 
604
- That opens one WorkOS Shepherd auth flow and saves a local onboarding auth session. The next setup command will relink to the same Shepherd customer user/org and therefore the same production cloud database rows.
625
+ 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.
605
626
 
606
627
  Ask the user for:
607
- 1. Email
608
- 2. Full name
609
- 3. Organization name
610
- 4. Messages phone number or Apple ID email, only if they selected Messages and chose Provide handle
628
+ 1. Full name
629
+ 2. Organization name
630
+ 3. Messages phone number or Apple ID email, only if they selected Messages and chose Provide handle
631
+
632
+ Do not ask for their email separately. Use the email returned by WorkOS auth.
611
633
 
612
634
  If they are joining an existing org, ask for the org name they believe they belong to. Shepherd will auto-match similar/case-different org names.
613
635
 
@@ -621,9 +643,13 @@ Add skip flags for sources the user did not select:
621
643
  - --no-messages
622
644
 
623
645
  That command creates/reuses the customer user and org, opens Google/Slack browser auth, and saves local state.
624
- It also runs:
625
- open 'granola://settings/connectors/api-keys'
626
- and activates Granola so the user can create/copy the API key.
646
+ If Granola is selected, it also opens the Granola desktop app. Use local computer control to navigate Granola to:
647
+ Settings -> Connectors -> API keys
648
+ Then have the user create/copy the API key.
649
+
650
+ If Granola did not come forward, run:
651
+ ${payload.granolaApiKeyCommand}
652
+ Then navigate the visible Granola app to Settings -> Connectors -> API keys.
627
653
 
628
654
  After Google/Gmail/Docs/Calendar 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:
629
655
  ${payload.continueCommand} --messages-handle "<phone_or_apple_id>" --granola-api-key "<granola_key>"
@@ -692,6 +718,14 @@ function publicAgentAccount(account) {
692
718
  };
693
719
  }
694
720
 
721
+ function authenticatedEmail(authenticated) {
722
+ return authenticated?.workosUser?.email ?? authenticated?.account?.email ?? null;
723
+ }
724
+
725
+ function authenticatedName(authenticated) {
726
+ return authenticated?.workosUser?.name ?? authenticated?.account?.name ?? null;
727
+ }
728
+
695
729
  function agentNeedsUserAction(sources, opened) {
696
730
  const actions = [];
697
731
  if (sources.google && opened.includes("google")) actions.push("Complete Google browser authorization for Gmail, Docs, and Calendar consent.");
@@ -811,8 +845,8 @@ async function openOrPrint(url, opts) {
811
845
  async function openGranolaApiKeys(opts = {}) {
812
846
  const deepLink = "granola://settings/connectors/api-keys";
813
847
  if (opts.noOpen) {
814
- console.log(`Granola API keys: ${deepLink}`);
815
- return { opened: false, target: deepLink };
848
+ console.log("Granola API keys: open the Granola desktop app -> Settings -> Connectors -> API keys");
849
+ return { opened: false, target: "Granola Settings -> Connectors -> API keys" };
816
850
  }
817
851
 
818
852
  if (platform() !== "darwin") {
@@ -821,21 +855,25 @@ async function openGranolaApiKeys(opts = {}) {
821
855
  }
822
856
 
823
857
  console.log("\nOpening Granola API keys");
824
- const deepLinkResult = await execFileQuiet("open", [deepLink], { ignoreError: true, captureError: true });
825
- await sleep(700);
826
- const activateResult = await execFileQuiet("osascript", [
858
+ const bundleResult = await execFileQuiet("open", ["-b", "com.granola.app"], { ignoreError: true, captureError: true });
859
+ await sleep(500);
860
+ await execFileQuiet("open", [deepLink], { ignoreError: true, captureError: true });
861
+ await sleep(500);
862
+ const activateByBundleResult = await execFileQuiet("osascript", [
827
863
  "-e",
828
864
  'tell application id "com.granola.app" to activate',
829
865
  ], { ignoreError: true, captureError: true });
866
+ const activateByNameResult = await execFileQuiet("open", ["-a", "Granola"], { ignoreError: true, captureError: true });
830
867
 
831
- if (deepLinkResult.error || activateResult.error) {
868
+ if (bundleResult.error && activateByBundleResult.error && activateByNameResult.error) {
832
869
  await execFileQuiet("open", ["-a", "Granola"], { ignoreError: true });
833
870
  }
834
871
 
835
872
  return {
836
873
  opened: true,
837
- target: deepLink,
838
- fallback: "If Granola does not land on the API key screen, open Settings -> Connectors -> API keys in Granola.",
874
+ target: "Granola Settings -> Connectors -> API keys",
875
+ attemptedDeepLink: deepLink,
876
+ fallback: "Use local computer control to navigate Granola to Settings -> Connectors -> API keys.",
839
877
  };
840
878
  }
841
879
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shepherd-onboard",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Customer-facing Shepherd raw sync onboarding CLI",
5
5
  "type": "module",
6
6
  "bin": {