askshepherd 0.1.44 → 0.1.45

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.
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { execFile, execFileSync, spawn } from "node:child_process";
3
- import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:crypto";
3
+ import { createCipheriv, createDecipheriv, createHash, randomBytes, randomUUID } from "node:crypto";
4
4
  import { constants as fsConstants, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, unlinkSync, watch, writeFileSync } from "node:fs";
5
5
  import { access, chmod, mkdir, mkdtemp, readdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
6
6
  import { createServer } from "node:http";
@@ -37,7 +37,7 @@ const MCP_ENVIRONMENT_TARGETS = {
37
37
  const DEFAULT_API_URL = CUSTOMER_DEPLOY_API_URL;
38
38
  const PACKAGE_NAME = "askshepherd";
39
39
  const PACKAGE_SPEC = `${PACKAGE_NAME}@latest`;
40
- const PACKAGE_VERSION = "0.1.44";
40
+ const PACKAGE_VERSION = "0.1.45";
41
41
  const DEFAULT_OFFICE_AUDIO_TRANSCRIPTION_BACKEND = "pyannote_full";
42
42
  const OFFICE_AUDIO_TRANSCRIPTION_BACKENDS = new Set([
43
43
  "pyannote_full",
@@ -341,6 +341,16 @@ async function runOnboarding() {
341
341
  const finalizeBody = { sessionToken: session.sessionToken };
342
342
  let selectedMessageChats = [];
343
343
 
344
+ if (sources.github) {
345
+ console.log("\nGitHub repository sync");
346
+ const githubToken = await valueOrPrompt("github-token", "GitHub fine-grained or classic PAT with repository webhook/API access", { secret: true, optional: true });
347
+ const githubRepoFullName = await valueOrPrompt("github-repo", "GitHub repository owner/name", { optional: true });
348
+ if (githubToken || githubRepoFullName) {
349
+ finalizeBody.githubToken = githubToken;
350
+ finalizeBody.githubRepoFullName = githubRepoFullName;
351
+ }
352
+ }
353
+
344
354
  if (sources.granola) {
345
355
  if (session.authUrls?.granola) {
346
356
  console.log("\nGranola authorization");
@@ -537,7 +547,7 @@ async function runAgentOnboarding() {
537
547
  currentAction,
538
548
  statePath,
539
549
  messagesChatsCommand: sources.messages ? `${agentCommand()} messages-chats` : undefined,
540
- nextCommand: `${agentCommand()} agent --continue --messages-handle "<phone_or_apple_id[,apple_id]>" --messages-chat-ids "<comma_separated_chat_ids_or_all>" --granola-api-key "<legacy_granola_key>"`,
550
+ nextCommand: `${agentCommand()} agent --continue --github-token "<github_pat>" --github-repo "<owner/repo>" --messages-handle "<phone_or_apple_id[,apple_id]>" --messages-chat-ids "<comma_separated_chat_ids_or_all>" --granola-api-key "<legacy_granola_key>"`,
541
551
  needsUserAction: agentNeedsUserAction(sources, currentAction),
542
552
  }, null, 2));
543
553
  return;
@@ -556,7 +566,7 @@ async function runAgentOnboarding() {
556
566
  });
557
567
 
558
568
  console.log("\nAfter that modality is complete, run:");
559
- console.log(` ${agentCommand()} agent --continue --messages-handle "<phone_or_apple_id[,apple_id]>" --messages-chat-ids "<comma_separated_chat_ids_or_all>" --granola-api-key "<legacy_granola_key>"`);
569
+ console.log(` ${agentCommand()} agent --continue --github-token "<github_pat>" --github-repo "<owner/repo>" --messages-handle "<phone_or_apple_id[,apple_id]>" --messages-chat-ids "<comma_separated_chat_ids_or_all>" --granola-api-key "<legacy_granola_key>"`);
560
570
  console.log(" Omit either optional flag if that source is not being connected.");
561
571
  }
562
572
 
@@ -1196,6 +1206,20 @@ function localMcpTools(opts = {}) {
1196
1206
  };
1197
1207
 
1198
1208
  const tools = [
1209
+ {
1210
+ name: "shepherd_onboarding_guide",
1211
+ description: "LOCAL agent-facing Shepherd onboarding workflow. Use when setting up Shepherd, choosing sources, deciding usage-skill/MCP install targets, or recovering the next onboarding step.",
1212
+ inputSchema: emptyInputSchema,
1213
+ annotations: readOnlyAnnotations,
1214
+ _meta: { provider: "local_npm", command: `${agentCommand()} guide` },
1215
+ },
1216
+ {
1217
+ name: "shepherd_troubleshoot",
1218
+ description: "LOCAL state-aware Shepherd onboarding/setup help. Use when confused, blocked, auth expired, source setup is pending, local sync is unhealthy, or the wiki is still building.",
1219
+ inputSchema: emptyInputSchema,
1220
+ annotations: readOnlyAnnotations,
1221
+ _meta: { provider: "local_npm", command: `${agentCommand()} troubleshoot` },
1222
+ },
1199
1223
  {
1200
1224
  name: "shepherd_status",
1201
1225
  description: "LOCAL Shepherd setup and sync status. Use this first when the user asks what they have enabled, what is connected, whether Shepherd is syncing, or why local Messages/Coding Sessions are not running. This is backed by the local Shepherd CLI; do not use production memory/wiki tools or shell/file exploration for local setup status.",
@@ -1274,7 +1298,7 @@ function localMcpInstructions(remoteInstructions, remoteConnectError, opts = {})
1274
1298
  const environmentLabel = opts.environment ? mcpEnvironmentLabel(opts.environment, opts.mcpUrl) : "saved Shepherd MCP endpoint";
1275
1299
  return [
1276
1300
  "This MCP server is the local Shepherd CLI wrapper plus deployed Shepherd memory/wiki tools.",
1277
- `For local setup/sync questions like "what do I have set up on Shepherd", "what have I enabled", "is Shepherd syncing", "help me set up coding agent sessions", "enable coding sessions", or "enable coding agent sessions locally for Shepherd", use shepherd_status, shepherd_setup_coding_sessions, or shepherd_enable_coding_sessions first. These local tools route to the local Shepherd CLI status/setup flow. The Shepherd CLI is the only component that may perform bounded local checks of Shepherd state, LaunchAgents, and known Codex/Claude session locations.`,
1301
+ `For agent-led onboarding, source choice, usage-skill install targets, MCP install targets, or "what step is next", use shepherd_onboarding_guide first. If setup is blocked or confusing, use shepherd_troubleshoot. For local setup/sync questions like "what do I have set up on Shepherd", "what have I enabled", "is Shepherd syncing", "help me set up coding agent sessions", "enable coding sessions", or "enable coding agent sessions locally for Shepherd", use shepherd_status, shepherd_setup_coding_sessions, or shepherd_enable_coding_sessions first. These local tools route to the local Shepherd CLI status/setup flow. The Shepherd CLI is the only component that may perform bounded local checks of Shepherd state, LaunchAgents, and known Codex/Claude session locations.`,
1278
1302
  "Hard boundary: do not use shell or filesystem tools such as ls, find, rg, grep, cat, Read, Glob, or Explore to inspect the user's home directory, repositories, ~/.codex, ~/.claude, or ~/.shepherd for Shepherd setup. If local status is needed, call shepherd_status or run the exact Shepherd CLI status command.",
1279
1303
  `If the user asks for raw local status outside MCP, tell them to run ${agentCommand()} status. For setup of coding agent sessions, ask consent, then use ${agentCommand()} login if needed, ${agentCommand()} onboard --add-sources coding-sessions --name "<full_name>" --org "<organization>", ${agentCommand()} continue, then ${agentCommand()} status.`,
1280
1304
  "For memory/wiki questions when readiness is uncertain, call shepherd_wiki_status first. If it or any deployed Shepherd tool returns status: \"wiki_not_ready\", do not answer the underlying memory/wiki question yet; report the readiness progress/ETA and ask the user to retry when the initial build is ready.",
@@ -1289,6 +1313,16 @@ function localMcpInstructions(remoteInstructions, remoteConnectError, opts = {})
1289
1313
  }
1290
1314
 
1291
1315
  async function callLocalMcpTool(name, context = {}) {
1316
+ if (name === "shepherd_onboarding_guide") {
1317
+ const status = await collectShepherdStatus();
1318
+ return localMcpTextResult(renderOnboardingGuideMcpResult(status));
1319
+ }
1320
+
1321
+ if (name === "shepherd_troubleshoot") {
1322
+ const status = await collectShepherdStatus();
1323
+ return localMcpTextResult(renderTroubleshootMcpResult(status));
1324
+ }
1325
+
1292
1326
  if (name === "shepherd_status" || name === "shepherd_local_status") {
1293
1327
  const status = await collectShepherdStatus();
1294
1328
  return localMcpTextResult([
@@ -1393,6 +1427,99 @@ function renderCodingSessionsSetupMcpResult(status) {
1393
1427
  ].join("\n");
1394
1428
  }
1395
1429
 
1430
+ function renderOnboardingGuideMcpResult(status) {
1431
+ return [
1432
+ "Shepherd onboarding workflow",
1433
+ "",
1434
+ "Use the wrapper workflow as the source of truth for agent-led onboarding:",
1435
+ ` ${agentCommand()} guide`,
1436
+ "",
1437
+ "High-level flow:",
1438
+ "1. Ask where to install the Shepherd usage skill: Codex, Claude Code, both, or skip.",
1439
+ ` Command: ${agentCommand()} skill --install <codex|claude|all>`,
1440
+ "2. Ask source choices with a native multi-select UI/control when available.",
1441
+ " Offer only sources the guide says are available. Ask explicit consent before Messages or Coding Sessions.",
1442
+ `3. Authenticate the Shepherd account with WorkOS: ${agentCommand()} login`,
1443
+ `4. Start onboarding with confirmed full name, org, and selected sources: ${agentCommand()} onboard --name "<full_name>" --org "<organization>" --sources "<sources>"`,
1444
+ `5. After each browser/admin/PAT/local permission step, run: ${agentCommand()} continue`,
1445
+ `6. Verify local setup and readiness: ${agentCommand()} status, ${agentCommand()} shepherd_wiki_status, ${agentCommand()} tools --json`,
1446
+ `7. Ask where to install Shepherd MCP for querying: ${agentCommand()} mcp-login --install <codex|claude|all>`,
1447
+ "",
1448
+ "Messages guardrails: require explicit consent, Full Disk Access, and selected chats from messages-chats; never sync all chats by default.",
1449
+ "Coding Sessions guardrails: require explicit consent; Shepherd syncs bounded redacted Codex/Claude Code session metadata, not raw logs.",
1450
+ "Wiki readiness guardrail: if shepherd_wiki_status returns wiki_not_ready, say Shepherd is still learning and include progress/ETA instead of answering the memory question.",
1451
+ "",
1452
+ "Current local status:",
1453
+ renderShepherdStatus(status),
1454
+ ].join("\n");
1455
+ }
1456
+
1457
+ function renderTroubleshootMcpResult(status) {
1458
+ const diagnostics = [];
1459
+ if (!status.configured || !status.account) {
1460
+ diagnostics.push([
1461
+ "No saved Shepherd onboarding session.",
1462
+ `Run ${agentCommand()} login, then ${agentCommand()} guide.`,
1463
+ ]);
1464
+ }
1465
+ if (status.productionError) {
1466
+ diagnostics.push([
1467
+ `Backend status is unavailable: ${status.productionError}`,
1468
+ `Run ${agentCommand()} login, then ${agentCommand()} continue.`,
1469
+ ]);
1470
+ }
1471
+ for (const source of statusSourceRows(status.providers, status.savedSources).filter((row) => row.selected && !row.connected)) {
1472
+ diagnostics.push([
1473
+ `${source.label} is selected but not connected.`,
1474
+ `Run ${agentCommand()} continue and complete the current modality it prints.`,
1475
+ ]);
1476
+ }
1477
+ if (status.local.messages.configPath && status.local.messages.storage?.readable === false) {
1478
+ diagnostics.push([
1479
+ "Messages database is not readable.",
1480
+ `Grant Full Disk Access to the onboarding app and Node.js, then run ${agentCommand()} messages-chats.`,
1481
+ ]);
1482
+ }
1483
+ if (status.local.messages.configPath && status.local.messages.launch?.running === false) {
1484
+ diagnostics.push([
1485
+ "Messages LaunchAgent is installed but not running.",
1486
+ `Run ${agentCommand()} status after fixing Full Disk Access; rerun continue if status asks for it.`,
1487
+ ]);
1488
+ }
1489
+ if (status.local.codingSessions.configPath && status.local.codingSessions.launch?.running === false) {
1490
+ diagnostics.push([
1491
+ "Coding Sessions LaunchAgent is installed but not running.",
1492
+ `Run ${agentCommand()} coding-sessions-status, then ${agentCommand()} continue if status asks for setup repair.`,
1493
+ ]);
1494
+ }
1495
+ const readiness = wikiReadinessPayloadFromStatus(status);
1496
+ if (readiness.status === "wiki_not_ready") {
1497
+ diagnostics.push([
1498
+ "Wiki is still building.",
1499
+ "This is not a setup failure. Tell the user Shepherd is still learning and include progress/ETA from shepherd_wiki_status.",
1500
+ ]);
1501
+ }
1502
+ if (diagnostics.length === 0) {
1503
+ diagnostics.push([
1504
+ "No obvious local onboarding blocker found.",
1505
+ `Run ${agentCommand()} tools --json and use exact listed tool names.`,
1506
+ ]);
1507
+ }
1508
+ return [
1509
+ "Shepherd troubleshooting",
1510
+ "",
1511
+ ...diagnostics.flatMap(([symptom, fix], index) => [
1512
+ `${index + 1}. ${symptom}`,
1513
+ ` Fix: ${fix}`,
1514
+ ]),
1515
+ "",
1516
+ "Current local status:",
1517
+ renderShepherdStatus(status),
1518
+ "",
1519
+ `Full workflow: ${agentCommand()} guide`,
1520
+ ].join("\n");
1521
+ }
1522
+
1396
1523
  function localMcpTextResult(text, isError = false) {
1397
1524
  return {
1398
1525
  content: [{ type: "text", text }],
@@ -1546,10 +1673,14 @@ function printMcpInstallResults(results) {
1546
1673
  async function continueAgentOnboarding() {
1547
1674
  let state = await readAgentState();
1548
1675
  const body = { sessionToken: state.sessionToken };
1676
+ const githubToken = stringArg("github-token");
1677
+ const githubRepoFullName = stringArg("github-repo");
1549
1678
  const granolaApiKey = stringArg("granola-api-key");
1550
1679
  const messagesHandles = parseMessageHandleList(stringArg("messages-handle"));
1551
1680
  const messagesHandle = messagesHandles[0] ?? null;
1552
1681
  const selectedMessageChatIds = parseMessageChatIdsArg();
1682
+ if (githubToken) body.githubToken = githubToken;
1683
+ if (githubRepoFullName) body.githubRepoFullName = githubRepoFullName;
1553
1684
  if (granolaApiKey) body.granolaApiKey = granolaApiKey;
1554
1685
  if (messagesHandle) body.imessage = { handle: messagesHandle, handles: messagesHandles };
1555
1686
  if (state.sources.messages && messagesHandle && selectedMessageChatIds.length === 0) {
@@ -1636,7 +1767,7 @@ async function continueAgentOnboarding() {
1636
1767
  processing: finalized.processing,
1637
1768
  errors: errors ? safeErrorRecord(errors) : undefined,
1638
1769
  currentAction,
1639
- nextCommand: errors ? `${agentCommand()} agent --continue --messages-handle "<phone_or_apple_id[,apple_id]>" --messages-chat-ids "<comma_separated_chat_ids_or_all>" --granola-api-key "<legacy_granola_key>"` : undefined,
1770
+ nextCommand: errors ? `${agentCommand()} agent --continue --github-token "<github_pat>" --github-repo "<owner/repo>" --messages-handle "<phone_or_apple_id[,apple_id]>" --messages-chat-ids "<comma_separated_chat_ids_or_all>" --granola-api-key "<legacy_granola_key>"` : undefined,
1640
1771
  mcpInstall: errors ? undefined : {
1641
1772
  prompt: "Ask where to install Shepherd MCP for this customer: Codex, Claude Code, both, or none.",
1642
1773
  targets: MCP_INSTALL_TARGETS,
@@ -1657,7 +1788,7 @@ async function continueAgentOnboarding() {
1657
1788
  });
1658
1789
 
1659
1790
  console.log("\nAfter that modality is complete, rerun:");
1660
- console.log(` ${agentCommand()} agent --continue --messages-handle "<phone_or_apple_id[,apple_id]>" --messages-chat-ids "<comma_separated_chat_ids_or_all>" --granola-api-key "<legacy_granola_key>"`);
1791
+ console.log(` ${agentCommand()} agent --continue --github-token "<github_pat>" --github-repo "<owner/repo>" --messages-handle "<phone_or_apple_id[,apple_id]>" --messages-chat-ids "<comma_separated_chat_ids_or_all>" --granola-api-key "<legacy_granola_key>"`);
1661
1792
  console.log(" Omit either optional flag if that source is not being connected.");
1662
1793
  return;
1663
1794
  }
@@ -1863,7 +1994,18 @@ function renderLocalCodingSessionsStatus(status) {
1863
1994
  lines.push(` ${probe.provider}: ${probe.path} ${probe.readable ? "readable" : `not readable (${probe.reason})`}`);
1864
1995
  }
1865
1996
  if (status.lastSync) {
1866
- lines.push(` Last sync: ${status.lastSync.finishedAt ?? "unknown"} (${status.lastSync.scanned ?? 0} scanned, ${status.lastSync.changed ?? 0} changed)`);
1997
+ const uploadable = status.lastSync.uploadable ?? status.lastSync.scanned ?? 0;
1998
+ const ignored = status.lastSync.filesIgnored ?? null;
1999
+ const ignoredText = ignored == null ? "" : `, ${ignored} ignored`;
2000
+ lines.push(` Last sync: ${status.lastSync.finishedAt ?? "unknown"} (${uploadable} uploadable, ${status.lastSync.changed ?? 0} changed${ignoredText})`);
2001
+ const providerStatus = status.lastSync.providerStatus && typeof status.lastSync.providerStatus === "object" && !Array.isArray(status.lastSync.providerStatus)
2002
+ ? status.lastSync.providerStatus
2003
+ : {};
2004
+ for (const provider of ["codex", "claude"]) {
2005
+ const info = providerStatus[provider];
2006
+ if (!info || typeof info !== "object" || Array.isArray(info)) continue;
2007
+ lines.push(` ${provider}: ${info.uploadable ?? info.scanned ?? 0} uploadable${info.filesIgnored == null ? "" : `, ${info.filesIgnored} ignored`}`);
2008
+ }
1867
2009
  } else {
1868
2010
  lines.push(" Last sync: none recorded");
1869
2011
  }
@@ -2185,6 +2327,8 @@ async function runMessagesAgent() {
2185
2327
  const userId = requiredConfigString(config.userId, "userId");
2186
2328
  const agentToken = requiredConfigString(config.agentToken, "agentToken");
2187
2329
  const channel = setRuntimeLaunchAgentChannel(config.channel);
2330
+ const deviceId = loadOrCreateShepherdDeviceId(channel);
2331
+ const agentMetadata = { deviceId, agentVersion: PACKAGE_VERSION, agentChannel: channel };
2188
2332
  mergeShepherdOwnedMessageHandles(config.excludedMessageHandles);
2189
2333
  const backfillOverride = args["backfill-days"] ?? process.env.SHEPHERD_BACKFILL_DAYS;
2190
2334
  const backfillExplicit = backfillOverride !== undefined && backfillOverride !== null && backfillOverride !== "";
@@ -2199,9 +2343,9 @@ async function runMessagesAgent() {
2199
2343
 
2200
2344
  const kit = await import("@photon-ai/imessage-kit");
2201
2345
  const sdk = new kit.IMessageSDK({ debug: args.debug === true });
2202
- const sender = new MessagesBatchSender(apiUrl, agentToken, userId, channel);
2346
+ const sender = new MessagesBatchSender(apiUrl, agentToken, userId, channel, agentMetadata);
2203
2347
  const contactLookup = createMutableContactLookup(buildContactLookup({ userId, channel }));
2204
- const serializer = createMessageSerializer(kit, contactLookup);
2348
+ const serializer = createMessageSerializer(kit, contactLookup, agentMetadata);
2205
2349
  const stateCache = createMessageStateCache(userId, MESSAGES_STATE_CACHE_MAX, channel);
2206
2350
  const backfillGate = createMessagesBackfillGate();
2207
2351
  const contactSync = startMessagesContactSync(sender, contactLookup, {
@@ -2423,10 +2567,15 @@ async function runCodingSessionsAgent() {
2423
2567
  startedAt,
2424
2568
  finishedAt: new Date().toISOString(),
2425
2569
  scanned: scan.sessions.length,
2570
+ uploadable: scan.sessions.length,
2571
+ filesDiscovered: scan.filesDiscovered ?? scan.sessions.length,
2572
+ filesSelected: scan.filesSelected ?? scan.sessions.length,
2573
+ filesIgnored: Math.max(0, Number(scan.filesDiscovered ?? scan.sessions.length) - scan.sessions.length),
2426
2574
  changed: changed.length,
2427
2575
  selected: selected.length,
2428
2576
  sent: sendResult,
2429
2577
  parserVersion: CODING_SESSION_PARSER_VERSION,
2578
+ providerStatus: codingSessionsProviderStatus(scan, previous, selected),
2430
2579
  command: command ? {
2431
2580
  id: command.id,
2432
2581
  type: command.type,
@@ -6423,6 +6572,8 @@ Options:
6423
6572
  --email <email> Advanced: must match the WorkOS-authenticated email.
6424
6573
  --name <name> Full name.
6425
6574
  --org <name> Organization name.
6575
+ --github-token <token> GitHub PAT for repository API and webhook setup.
6576
+ --github-repo <owner/repo> GitHub repository to sync with webhook coverage.
6426
6577
  --granola-api-key <key> Legacy Granola API key fallback.
6427
6578
  --messages-handle <value> Messages phone number or Apple ID email; comma-separate both if needed.
6428
6579
  --messages-chat-ids <ids> Comma-separated local Messages chat IDs selected from messages-chats, or all to watch every current and future chat.
@@ -6435,7 +6586,7 @@ Options:
6435
6586
  --no-open-granola Do not open Granola OAuth/API key setup.
6436
6587
  --no-messages Skip local Messages.
6437
6588
  --coding-sessions Opt in to local Codex/Claude Code session metadata sync.
6438
- --sources <list> Exact sources to connect: google,notion,slack,granola,messages,coding-sessions,all.
6589
+ --sources <list> Exact sources to connect: google,notion,slack,github,granola,messages,coding-sessions,all.
6439
6590
  --add-sources <list> Same as --sources, named for second-time onboarding.
6440
6591
  --no-install-messages-agent
6441
6592
  Save Messages credentials without starting launchd.
@@ -6560,6 +6711,7 @@ function printAgentContract() {
6560
6711
  optionalContinueArgs: [
6561
6712
  "--messages-handle \"<phone_or_apple_id[,apple_id]>\" if local Messages is being connected",
6562
6713
  "--messages-chat-ids \"<comma_separated_chat_ids>\" if local Messages is being connected, or --messages-chat-ids all when the user explicitly wants every current and future Messages chat watched",
6714
+ "--github-token \"<github_pat>\" and --github-repo \"<owner/repo>\" if GitHub is being connected",
6563
6715
  "--granola-api-key \"<legacy_granola_key>\" only when Granola falls back to legacy API-key setup",
6564
6716
  ],
6565
6717
  statusCommand: `${command} status`,
@@ -6609,7 +6761,7 @@ Common user requests:
6609
6761
 
6610
6762
  Start with selection questions to determine intent:
6611
6763
  1. Organization: Join existing org, or Create new org.
6612
- 2. Sources: Google Workspace (Gmail/Drive/Docs/Calendar/Sheets/Slides/Tasks/Contacts), Notion, Slack, Granola, Messages, Coding Sessions (Codex/Claude Code metadata). Allow multi-select if your interface supports it.
6764
+ 2. Sources: Google Workspace (Gmail/Drive/Docs/Calendar/Sheets/Slides/Tasks/Contacts), Notion, Slack, GitHub, Granola, Messages, Coding Sessions (Codex/Claude Code metadata). Allow multi-select if your interface supports it.
6613
6765
  3. Messages, if selected: Skip Messages, or Provide handle.
6614
6766
  4. MCP install after onboarding completes: Codex, Claude Code, both, or none.
6615
6767
 
@@ -6651,7 +6803,7 @@ Add skip flags for sources the user did not select:
6651
6803
  Or pass an exact source list, especially for adding sources later:
6652
6804
  ${command} agent --add-sources coding-sessions --name "<full_name>" --org "<organization>"
6653
6805
 
6654
- 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 Notion, 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.
6806
+ 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 Notion, then Slack, then GitHub, 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.
6655
6807
 
6656
6808
  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:
6657
6809
 
@@ -6669,6 +6821,8 @@ Shepherd must still enforce selected users and groups internally before polling
6669
6821
 
6670
6822
  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.
6671
6823
 
6824
+ If GitHub is the current modality, do not use browser OAuth as the primary setup. Ask the user for a GitHub fine-grained or classic PAT and exact owner/repo, then continue with --github-token and --github-repo. Shepherd uses this path to create the repository webhook; OAuth-only GitHub is tools-only and does not provide full event sync coverage.
6825
+
6672
6826
  If Granola is the current modality, the command opens Granola browser authorization when OAuth is configured. Leave the browser tab for the user and ask them to complete authorization before continuing.
6673
6827
 
6674
6828
  If the command reports the legacy Granola API-key fallback, it opens the Granola desktop app. If your local app automation can navigate it, go to:
@@ -6680,7 +6834,7 @@ If the legacy Granola API-key fallback is needed and Granola did not come forwar
6680
6834
  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.
6681
6835
 
6682
6836
  After the current modality is complete, run:
6683
- ${payload.continueCommand} --messages-handle "<phone_or_apple_id[,apple_id]>" --messages-chat-ids "<comma_separated_chat_ids_or_all>" --granola-api-key "<legacy_granola_key>"
6837
+ ${payload.continueCommand} --github-token "<github_pat>" --github-repo "<owner/repo>" --messages-handle "<phone_or_apple_id[,apple_id]>" --messages-chat-ids "<comma_separated_chat_ids_or_all>" --granola-api-key "<legacy_granola_key>"
6684
6838
 
6685
6839
  Omit any optional flag that is not being requested. Do not pass --granola-api-key after successful Granola browser authorization.
6686
6840
 
@@ -6867,6 +7021,32 @@ function channelRecord(channel) {
6867
7021
  return channel === STABLE_CHANNEL ? {} : { channel };
6868
7022
  }
6869
7023
 
7024
+ function shepherdDeviceIdFile(channel = cliChannel()) {
7025
+ const dir = join(homedir(), ".shepherd");
7026
+ mkdirSync(dir, { recursive: true });
7027
+ return join(dir, `${channelFilePrefix(normalizeLaunchAgentChannel(channel))}device-id`);
7028
+ }
7029
+
7030
+ function loadOrCreateShepherdDeviceId(channel = cliChannel()) {
7031
+ const path = shepherdDeviceIdFile(channel);
7032
+ try {
7033
+ const existing = readFileSync(path, "utf8").trim();
7034
+ if (isUuid(existing)) return existing.toLowerCase();
7035
+ } catch {
7036
+ // Missing or unreadable device id will be repaired below.
7037
+ }
7038
+
7039
+ const next = randomUUID();
7040
+ const tmpFile = `${path}.tmp-${process.pid}-${Date.now()}`;
7041
+ writeFileSync(tmpFile, `${next}\n`, { mode: 0o600 });
7042
+ renameSync(tmpFile, path);
7043
+ return next;
7044
+ }
7045
+
7046
+ function isUuid(value) {
7047
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(String(value ?? "").trim());
7048
+ }
7049
+
6870
7050
  async function updateAgentStateFromOnboardingResponse(state, response) {
6871
7051
  const authUrls = stringRecord(response?.authUrls);
6872
7052
  const hasAuthUrls = Object.keys(authUrls).length > 0;
@@ -7012,17 +7192,13 @@ async function openNextAgentModality({ sources, authUrls = {}, noOpen = false, p
7012
7192
  }
7013
7193
 
7014
7194
  if (source === "github") {
7015
- const url = typeof authUrls.github === "string" ? authUrls.github : null;
7016
- if (!url) {
7017
- return {
7018
- source,
7019
- label: "GitHub",
7020
- opened: false,
7021
- message: "GitHub authorization URL was not returned by Shepherd.",
7022
- };
7023
- }
7024
- await openOrPrint(url, { noOpen });
7025
- return { source, label: "GitHub", opened: !noOpen, url };
7195
+ return {
7196
+ source,
7197
+ label: "GitHub",
7198
+ opened: false,
7199
+ patRequired: true,
7200
+ message: "Provide --github-token and --github-repo so Shepherd can create the repository webhook; OAuth alone is tools-only and does not provide full event sync coverage.",
7201
+ };
7026
7202
  }
7027
7203
 
7028
7204
  if (source === "granola") {
@@ -7104,14 +7280,8 @@ function printAgentCurrentAction(action, opts = {}) {
7104
7280
  }
7105
7281
 
7106
7282
  if (action.source === "github") {
7107
- if (action.opened) {
7108
- console.log("Opened GitHub authorization in the browser.");
7109
- } else if (action.url) {
7110
- console.log(`GitHub authorization URL: ${action.url}`);
7111
- } else if (action.message) {
7112
- console.log(action.message);
7113
- }
7114
- console.log("Ask the user to complete GitHub authorization before opening another source.");
7283
+ console.log(action.message ?? "Provide a GitHub PAT and owner/repo for webhook-backed event sync.");
7284
+ console.log("Do not use browser OAuth as the primary GitHub setup path for event coverage.");
7115
7285
  return;
7116
7286
  }
7117
7287
 
@@ -7148,7 +7318,7 @@ function agentNeedsUserAction(sources, action) {
7148
7318
  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."];
7149
7319
  if (action.source === "notion") return ["Complete Notion browser authorization."];
7150
7320
  if (action.source === "slack") return ["Complete Slack browser authorization."];
7151
- if (action.source === "github") return ["Complete GitHub browser authorization."];
7321
+ if (action.source === "github") return ["Provide --github-token and --github-repo so Shepherd can create the repository webhook; OAuth alone is tools-only and does not provide full event sync coverage."];
7152
7322
  if (action.source === "granola") return [action.url ? "Complete Granola browser authorization." : "Create/copy a legacy Granola API key from the Granola Mac app."];
7153
7323
  if (action.source === "messages") return ["Grant or confirm macOS Full Disk Access for the onboarding app and Node.js, run messages-chats, have the user select local Messages contacts/groups in the browser, then pass the printed chat IDs with the Messages handle."];
7154
7324
  if (action.source === "codingSessions") return ["Run the continue command to install local Codex and Claude Code session metadata sync."];
@@ -9428,6 +9598,9 @@ function startMessagesCommandPoller(opts) {
9428
9598
  allChats: opts.allChats,
9429
9599
  allowedChatIds: opts.allowedChatIds,
9430
9600
  queueDepth,
9601
+ deviceId: opts.sender.deviceId,
9602
+ agentVersion: opts.sender.agentVersion,
9603
+ agentChannel: opts.sender.agentChannel,
9431
9604
  }),
9432
9605
  });
9433
9606
  clearMessagesAgentCommandState(opts.userId);
@@ -9534,6 +9707,9 @@ function messagesSourceSyncReceipt({
9534
9707
  allChats,
9535
9708
  allowedChatIds,
9536
9709
  queueDepth,
9710
+ deviceId,
9711
+ agentVersion = PACKAGE_VERSION,
9712
+ agentChannel = cliChannel(),
9537
9713
  }) {
9538
9714
  const stored = countValue(result.totalStored);
9539
9715
  const updated = countValue(result.totalUpdated);
@@ -9548,7 +9724,9 @@ function messagesSourceSyncReceipt({
9548
9724
  sourceSyncRunId,
9549
9725
  commandId: command.id,
9550
9726
  provider: "imessage",
9551
- agentVersion: PACKAGE_VERSION,
9727
+ deviceId,
9728
+ agentVersion,
9729
+ agentChannel,
9552
9730
  coverageFrom: coverageFrom ? coverageFrom.toISOString() : null,
9553
9731
  coverageTo: coverageTo.toISOString(),
9554
9732
  highWatermark: receiptWatermark(`${sourceSyncRunId}:imessage-rowid:${countValue(result.watermark)}`),
@@ -10246,11 +10424,20 @@ function snapshotContactMappings(mappings) {
10246
10424
  return new Map(mappings.map((mapping) => [mapping.handle, mapping.name]));
10247
10425
  }
10248
10426
 
10249
- function createMessageSerializer(kit, contactLookup = emptyContactLookup()) {
10427
+ function createMessageSerializer(kit, contactLookup = emptyContactLookup(), agentMetadata = {}) {
10250
10428
  const chatNames = new Map();
10251
10429
  const isImageAttachment = kit.isImageAttachment ?? (() => false);
10252
10430
  const isVideoAttachment = kit.isVideoAttachment ?? (() => false);
10253
10431
  const isAudioAttachment = kit.isAudioAttachment ?? (() => false);
10432
+ const deviceId = typeof agentMetadata.deviceId === "string" && agentMetadata.deviceId.trim()
10433
+ ? agentMetadata.deviceId.trim()
10434
+ : null;
10435
+ const agentVersion = typeof agentMetadata.agentVersion === "string" && agentMetadata.agentVersion.trim()
10436
+ ? agentMetadata.agentVersion.trim()
10437
+ : PACKAGE_VERSION;
10438
+ const agentChannel = typeof agentMetadata.agentChannel === "string" && agentMetadata.agentChannel.trim()
10439
+ ? normalizeLaunchAgentChannel(agentMetadata.agentChannel)
10440
+ : cliChannel();
10254
10441
 
10255
10442
  return {
10256
10443
  setChatName(chatId, name) {
@@ -10266,6 +10453,9 @@ function createMessageSerializer(kit, contactLookup = emptyContactLookup()) {
10266
10453
  messageId: String(providerMessageGuid?.value ?? msg.messageId ?? msg.rowId),
10267
10454
  providerMessageGuid: providerMessageGuid == null ? null : String(providerMessageGuid.value),
10268
10455
  _provider_guid_explicit: providerMessageGuid == null ? null : providerMessageGuid.explicit,
10456
+ deviceId,
10457
+ agentVersion,
10458
+ agentChannel,
10269
10459
  localRowId: Number(msg.rowId ?? 0),
10270
10460
  rowId: Number(msg.rowId ?? 0),
10271
10461
  text: msg.text ?? null,
@@ -10734,7 +10924,8 @@ function codingSessionsCommandScanOptions(command) {
10734
10924
  if (type === "source_sync") {
10735
10925
  return {
10736
10926
  exhaustive: true,
10737
- fullInventory: true,
10927
+ maxFiles: maxFiles ?? DEFAULT_CODING_SESSION_FILES_PER_PROVIDER,
10928
+ ...(providers.length ? { providers } : {}),
10738
10929
  };
10739
10930
  }
10740
10931
  return {
@@ -10748,7 +10939,7 @@ function codingSessionsCommandPlan(command, scan, previousState) {
10748
10939
  if (!command) return { sessions: [], force: false, exactSelection: false, result: base };
10749
10940
  const payload = objectValue(command.payload) ?? {};
10750
10941
  const type = stringValue(command.type);
10751
- const maxFiles = positiveCommandInteger(payload.maxFiles) ?? 100;
10942
+ const maxFiles = positiveCommandInteger(payload.maxFiles) ?? DEFAULT_CODING_SESSION_FILES_PER_PROVIDER;
10752
10943
  const force = type === "force_rescan" ? true : payload.force === false ? false : true;
10753
10944
 
10754
10945
  if (type === "source_sync") {
@@ -10866,7 +11057,8 @@ function codingSessionsSourceSyncReceipt({
10866
11057
  finishedAt,
10867
11058
  coverageFrom: Object.prototype.hasOwnProperty.call(payload, "coverageFrom") ? stringValue(payload.coverageFrom) : null,
10868
11059
  coverageTo: stringValue(payload.coverageTo) ?? finishedAt,
10869
- fullInventory: true,
11060
+ fullInventory: scan.capped !== true,
11061
+ maxFilesPerProvider: positiveCommandInteger(payload.maxFiles) ?? DEFAULT_CODING_SESSION_FILES_PER_PROVIDER,
10870
11062
  exhausted: failedCount === 0 && pendingUploads === 0,
10871
11063
  capHit: scan.capped === true,
10872
11064
  highWatermark,
@@ -10906,6 +11098,7 @@ function selectCodingSessionsPerProvider(sessions, maxFilesPerProvider) {
10906
11098
 
10907
11099
  function codingSessionsCommandDiagnostics(scan, previousState) {
10908
11100
  const providerCounts = {};
11101
+ const providerStatus = codingSessionsProviderStatus(scan, previousState, []);
10909
11102
  let userMessages = 0;
10910
11103
  let agentResponses = 0;
10911
11104
  let sessionsWithVisibleTranscript = 0;
@@ -10922,7 +11115,10 @@ function codingSessionsCommandDiagnostics(scan, previousState) {
10922
11115
  return {
10923
11116
  parserVersion: CODING_SESSION_PARSER_VERSION,
10924
11117
  scanned: scan.sessions.length,
11118
+ uploadable: scan.sessions.length,
10925
11119
  filesDiscovered: scan.filesDiscovered ?? scan.sessions.length,
11120
+ filesSelected: scan.filesSelected ?? scan.sessions.length,
11121
+ filesIgnored: Math.max(0, Number(scan.filesDiscovered ?? scan.sessions.length) - scan.sessions.length),
10926
11122
  filesParsed: scan.filesParsed ?? scan.sessions.length,
10927
11123
  capped: scan.capped === true,
10928
11124
  unchanged,
@@ -10931,11 +11127,42 @@ function codingSessionsCommandDiagnostics(scan, previousState) {
10931
11127
  userMessages,
10932
11128
  agentResponses,
10933
11129
  providerCounts,
11130
+ providerStatus,
10934
11131
  probes: sanitizeCodingSessionProbes(scan.probes),
10935
11132
  errors: sanitizeCodingSessionErrors(scan.errors),
10936
11133
  };
10937
11134
  }
10938
11135
 
11136
+ function codingSessionsProviderStatus(scan, previousState, selectedSessions = []) {
11137
+ const status = {};
11138
+ const inventory = scan.providerStatus && typeof scan.providerStatus === "object" && !Array.isArray(scan.providerStatus)
11139
+ ? scan.providerStatus
11140
+ : {};
11141
+ for (const provider of ["codex", "claude"]) {
11142
+ const providerSessions = scan.sessions.filter((session) => session.provider === provider);
11143
+ const selected = selectedSessions.filter((session) => session.provider === provider);
11144
+ const unchanged = providerSessions.filter((session) => previousState.hashes?.[session.sourcePathHash] === session.contentHash).length;
11145
+ const raw = inventory[provider] && typeof inventory[provider] === "object" && !Array.isArray(inventory[provider])
11146
+ ? inventory[provider]
11147
+ : {};
11148
+ const filesDiscovered = countValue(raw.filesDiscovered);
11149
+ const filesSelected = countValue(raw.filesSelected);
11150
+ const uploadable = providerSessions.length;
11151
+ status[provider] = {
11152
+ filesDiscovered,
11153
+ filesSelected,
11154
+ filesIgnored: Math.max(0, filesDiscovered - uploadable),
11155
+ uploadable,
11156
+ scanned: uploadable,
11157
+ unchanged,
11158
+ changed: Math.max(0, uploadable - unchanged),
11159
+ selected: selected.length,
11160
+ capped: raw.capped === true,
11161
+ };
11162
+ }
11163
+ return status;
11164
+ }
11165
+
10939
11166
  function sanitizeCodingSessionProbes(probes) {
10940
11167
  if (!Array.isArray(probes)) return [];
10941
11168
  return probes.map((probe) => ({
@@ -11008,7 +11235,9 @@ async function scanCodingSessions(config, previousState = { hashes: {} }, option
11008
11235
  probes,
11009
11236
  errors,
11010
11237
  filesDiscovered: inventory.filesDiscovered,
11238
+ filesSelected: files.length,
11011
11239
  filesParsed: sessions.length,
11240
+ providerStatus: inventory.providerStatus,
11012
11241
  capped: inventory.capped,
11013
11242
  sessions,
11014
11243
  previousState,
@@ -11093,9 +11322,23 @@ async function codingSessionJsonlFiles({ codexDirs, claudeDir, errors, maxFilesP
11093
11322
  ...newestCodingSessionFiles(claudeFiles, maxFilesPerProvider),
11094
11323
  ].sort((a, b) => b.mtimeMs - a.mtimeMs || a.provider.localeCompare(b.provider) || a.path.localeCompare(b.path));
11095
11324
  const filesDiscovered = codexFiles.length + claudeFiles.length;
11325
+ const providerStatus = {
11326
+ codex: {
11327
+ filesDiscovered: codexFiles.length,
11328
+ filesSelected: Math.min(codexFiles.length, maxFilesPerProvider),
11329
+ capped: codexFiles.length > maxFilesPerProvider,
11330
+ },
11331
+ claude: {
11332
+ filesDiscovered: claudeFiles.length,
11333
+ filesSelected: Math.min(claudeFiles.length, maxFilesPerProvider),
11334
+ capped: claudeFiles.length > maxFilesPerProvider,
11335
+ },
11336
+ };
11096
11337
  return {
11097
11338
  files,
11098
11339
  filesDiscovered,
11340
+ filesSelected: files.length,
11341
+ providerStatus,
11099
11342
  capped: files.length < filesDiscovered,
11100
11343
  };
11101
11344
  }
@@ -11749,6 +11992,11 @@ class MessagesBatchSender {
11749
11992
  this.agentToken = agentToken;
11750
11993
  this.userId = userId;
11751
11994
  this.channel = normalizeLaunchAgentChannel(channel);
11995
+ this.deviceId = typeof options.deviceId === "string" && options.deviceId.trim() ? options.deviceId.trim() : null;
11996
+ this.agentVersion = typeof options.agentVersion === "string" && options.agentVersion.trim() ? options.agentVersion.trim() : PACKAGE_VERSION;
11997
+ this.agentChannel = typeof options.agentChannel === "string" && options.agentChannel.trim()
11998
+ ? normalizeLaunchAgentChannel(options.agentChannel)
11999
+ : this.channel;
11752
12000
  this.queueFile = join(homedir(), ".shepherd", "raw-messages", `${channelFilePrefix(this.channel)}${safeFileId(userId)}-queue.json`);
11753
12001
  this.sendChain = Promise.resolve();
11754
12002
  this.batchSize = clampInt(Number(options.batchSize ?? MESSAGES_UPLOAD_BATCH_SIZE), 1, 10_000);
@@ -11825,13 +12073,14 @@ class MessagesBatchSender {
11825
12073
  }
11826
12074
 
11827
12075
  async postBatch(messages) {
12076
+ const messagesWithMetadata = messages.map((message) => this.withAgentMetadata(message));
11828
12077
  const res = await messagesAgentFetch(`${this.apiUrl}/api/imessage/ingest`, {
11829
12078
  method: "POST",
11830
12079
  headers: {
11831
12080
  "Content-Type": "application/json",
11832
12081
  "x-api-key": this.agentToken,
11833
12082
  },
11834
- body: JSON.stringify({ userId: this.userId, messages }),
12083
+ body: JSON.stringify({ userId: this.userId, messages: messagesWithMetadata }),
11835
12084
  });
11836
12085
 
11837
12086
  const json = await res.json().catch(() => ({}));
@@ -11882,13 +12131,14 @@ class MessagesBatchSender {
11882
12131
  }
11883
12132
 
11884
12133
  async postLocalStatus(status) {
12134
+ const statusWithMetadata = this.withAgentMetadata(publicMessagesBackfillStatus(status) ?? status);
11885
12135
  const res = await messagesAgentFetch(`${this.apiUrl}/api/imessage/ingest/local-status`, {
11886
12136
  method: "POST",
11887
12137
  headers: {
11888
12138
  "Content-Type": "application/json",
11889
12139
  "x-api-key": this.agentToken,
11890
12140
  },
11891
- body: JSON.stringify({ status: publicMessagesBackfillStatus(status) ?? status }),
12141
+ body: JSON.stringify({ status: statusWithMetadata }),
11892
12142
  });
11893
12143
 
11894
12144
  const json = await res.json().catch(() => ({}));
@@ -11896,6 +12146,16 @@ class MessagesBatchSender {
11896
12146
  return json;
11897
12147
  }
11898
12148
 
12149
+ withAgentMetadata(value) {
12150
+ if (!value || typeof value !== "object" || Array.isArray(value)) return value;
12151
+ return {
12152
+ ...value,
12153
+ ...(this.deviceId ? { deviceId: this.deviceId } : {}),
12154
+ agentVersion: this.agentVersion,
12155
+ agentChannel: this.agentChannel,
12156
+ };
12157
+ }
12158
+
11899
12159
  async fetchCommand() {
11900
12160
  const res = await messagesAgentFetch(`${this.apiUrl}/api/imessage/ingest/command`, {
11901
12161
  headers: {