askshepherd 0.1.36 → 0.1.37

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
@@ -54,6 +54,33 @@ The command:
54
54
 
55
55
  The command does not expose Railway, database, Redis, or internal service details to the user.
56
56
 
57
+ ## Check Sync Status
58
+
59
+ Use this when the user asks "Check what I've enabled for Shepherd?":
60
+
61
+ ```sh
62
+ npx -y askshepherd@latest status
63
+ npx -y askshepherd@latest status --json
64
+ ```
65
+
66
+ It reports the saved Shepherd account, connected cloud sources, downstream
67
+ processing state, and local background sync health for Messages and Coding
68
+ Sessions.
69
+
70
+ ## Set Up Coding Agent Sessions
71
+
72
+ Use this when the user asks "Help me set up coding agent sessions":
73
+
74
+ ```sh
75
+ npx -y askshepherd@latest agent --login
76
+ npx -y askshepherd@latest agent --add-sources coding-sessions --name "<name>" --org "<organization>"
77
+ npx -y askshepherd@latest agent --continue
78
+ npx -y askshepherd@latest status
79
+ ```
80
+
81
+ The coding agent should ask for consent before enabling this source. The local
82
+ collector syncs redacted Codex and Claude Code summaries, not full transcripts.
83
+
57
84
  ## Customer MCP Login
58
85
 
59
86
  After raw onboarding creates the Shepherd customer account, customers can
@@ -82,6 +109,13 @@ The saved MCP state includes:
82
109
  - `authSource`: `local_onboarding` or `workos`
83
110
  - `localAuth`: raw onboarding state reference when local auth was used
84
111
 
112
+ The installed MCP server is local npm first, remote brain second. For questions
113
+ like "what do I have set up on Shepherd?", "is Shepherd syncing?", or "help me
114
+ set up coding agent sessions", the MCP exposes local tools such as
115
+ `shepherd_status` and `shepherd_setup_coding_sessions` that route agents to the
116
+ local `askshepherd status` / add-source flow. Production memory and wiki tools
117
+ remain remote Railway-backed tools for source recall and company-memory answers.
118
+
85
119
  Use `--json` when an agent or setup script needs machine-readable endpoint and
86
120
  header details.
87
121
 
@@ -12,7 +12,7 @@ import { fileURLToPath } from "node:url";
12
12
  const DEFAULT_API_URL = "https://brain-api-customer-facing.up.railway.app";
13
13
  const PACKAGE_NAME = "askshepherd";
14
14
  const PACKAGE_SPEC = `${PACKAGE_NAME}@latest`;
15
- const PACKAGE_VERSION = "0.1.36";
15
+ const PACKAGE_VERSION = "0.1.37";
16
16
  const MCP_SERVER_NAME = "shepherd";
17
17
  const PACKAGE_DIR = dirname(dirname(fileURLToPath(import.meta.url)));
18
18
  const DEFAULT_AGENT_STATE_PATH = join(homedir(), ".shepherd", "raw-onboarding-agent.json");
@@ -127,6 +127,8 @@ async function dispatch() {
127
127
  await runMcpInstall();
128
128
  } else if (command === "mcp") {
129
129
  await runMcpProxy();
130
+ } else if (command === "status" || command === "sync-status" || command === "check") {
131
+ await runStatusCommand();
130
132
  } else if (command === "messages-chats") {
131
133
  await runMessagesChatsCommand();
132
134
  } else if (command === "messages-agent") {
@@ -720,39 +722,169 @@ async function runMcpProxy() {
720
722
  { name: "askshepherd-mcp-proxy", version: PACKAGE_VERSION },
721
723
  { capabilities: {} },
722
724
  );
723
- await remote.connect(new StreamableHTTPClientTransport(new URL(mcpUrl), {
724
- requestInit: {
725
- headers: {
726
- Authorization: `Bearer ${token}`,
725
+ let remoteConnected = false;
726
+ let remoteConnectError = null;
727
+ try {
728
+ await remote.connect(new StreamableHTTPClientTransport(new URL(mcpUrl), {
729
+ requestInit: {
730
+ headers: {
731
+ Authorization: `Bearer ${token}`,
732
+ },
727
733
  },
728
- },
729
- }));
734
+ }));
735
+ remoteConnected = true;
736
+ } catch (err) {
737
+ remoteConnectError = safeError(err);
738
+ }
730
739
 
731
740
  const passthroughResultSchema = typeof ResultSchema.passthrough === "function"
732
741
  ? ResultSchema.passthrough()
733
742
  : ResultSchema;
743
+ const remoteCapabilities = remoteConnected ? remote.getServerCapabilities() ?? {} : {};
744
+ const remoteInstructions = remoteConnected ? remote.getInstructions() ?? "" : "";
745
+ const localTools = localMcpTools();
746
+ const localToolNames = new Set(localTools.map((tool) => tool.name));
734
747
  const local = new Server(
735
748
  { name: "askshepherd", version: PACKAGE_VERSION },
736
749
  {
737
- capabilities: remote.getServerCapabilities() ?? {},
738
- instructions: remote.getInstructions(),
739
- fallbackRequestHandler: async (request, extra) => remote.request(
740
- request,
741
- passthroughResultSchema,
742
- { ...proxyRequestOptions, signal: extra.signal },
743
- ),
744
- fallbackNotificationHandler: async (notification) => {
745
- await remote.notification(notification);
746
- },
750
+ capabilities: { ...remoteCapabilities, tools: { listChanged: false } },
751
+ instructions: localMcpInstructions(remoteInstructions, remoteConnectError),
752
+ ...(remoteConnected
753
+ ? {
754
+ fallbackRequestHandler: async (request, extra) => remote.request(
755
+ request,
756
+ passthroughResultSchema,
757
+ { ...proxyRequestOptions, signal: extra.signal },
758
+ ),
759
+ fallbackNotificationHandler: async (notification) => {
760
+ await remote.notification(notification);
761
+ },
762
+ }
763
+ : {}),
747
764
  },
748
765
  );
749
- local.setRequestHandler(ListToolsRequestSchema, async (request, extra) =>
750
- remote.listTools(request.params, { ...proxyRequestOptions, signal: extra.signal }));
751
- local.setRequestHandler(CallToolRequestSchema, async (request, extra) =>
752
- remote.callTool(request.params, passthroughResultSchema, { ...proxyRequestOptions, signal: extra.signal }));
766
+ local.setRequestHandler(ListToolsRequestSchema, async (request, extra) => {
767
+ if (!remoteConnected) return { tools: localTools };
768
+ const remoteTools = await remote
769
+ .listTools(request.params, { ...proxyRequestOptions, signal: extra.signal })
770
+ .catch(() => ({ tools: [] }));
771
+ return {
772
+ ...remoteTools,
773
+ tools: [
774
+ ...localTools,
775
+ ...(remoteTools.tools ?? []).filter((tool) => !localToolNames.has(tool.name)),
776
+ ],
777
+ };
778
+ });
779
+ local.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
780
+ if (localToolNames.has(request.params.name)) {
781
+ return callLocalMcpTool(request.params.name);
782
+ }
783
+ if (!remoteConnected) {
784
+ return localMcpTextResult(`Production Shepherd MCP is unavailable (${remoteConnectError ?? "not connected"}). For local setup/sync status, use shepherd_status or run ${agentCommand()} status.`, true);
785
+ }
786
+ return remote.callTool(request.params, passthroughResultSchema, { ...proxyRequestOptions, signal: extra.signal });
787
+ });
753
788
  await local.connect(new StdioServerTransport());
754
789
  }
755
790
 
791
+ function localMcpTools() {
792
+ const emptyInputSchema = {
793
+ type: "object",
794
+ properties: {},
795
+ additionalProperties: false,
796
+ };
797
+ const readOnlyAnnotations = {
798
+ readOnlyHint: true,
799
+ destructiveHint: false,
800
+ openWorldHint: false,
801
+ };
802
+
803
+ return [
804
+ {
805
+ name: "shepherd_status",
806
+ 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 askshepherd npm CLI; do not use production memory/wiki tools for local setup status.",
807
+ inputSchema: emptyInputSchema,
808
+ annotations: readOnlyAnnotations,
809
+ _meta: { provider: "local_npm", command: `${agentCommand()} status` },
810
+ },
811
+ {
812
+ name: "shepherd_local_status",
813
+ description: "Explicit local alias for shepherd_status. Returns the authoritative local askshepherd npm status path and current local setup/sync state.",
814
+ inputSchema: emptyInputSchema,
815
+ annotations: readOnlyAnnotations,
816
+ _meta: { provider: "local_npm", command: `${agentCommand()} status` },
817
+ },
818
+ {
819
+ name: "shepherd_setup_coding_sessions",
820
+ description: "LOCAL setup guide for Codex and Claude Code coding-session sync. Use when the user asks to set up coding agent sessions. Ask for consent, then guide the local askshepherd npm login/add-sources/continue/status commands.",
821
+ inputSchema: emptyInputSchema,
822
+ annotations: readOnlyAnnotations,
823
+ _meta: { provider: "local_npm", command: `${agentCommand()} agent --add-sources coding-sessions` },
824
+ },
825
+ ];
826
+ }
827
+
828
+ function localMcpInstructions(remoteInstructions, remoteConnectError) {
829
+ return [
830
+ "This MCP server is the local askshepherd npm wrapper plus production Shepherd memory/wiki tools.",
831
+ `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", or "enable coding sessions", use shepherd_status or shepherd_setup_coding_sessions first. These local tools route to the local askshepherd npm status/setup flow and can inspect ~/.shepherd, LaunchAgents, and local Codex/Claude paths.`,
832
+ `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()} agent --login if needed, ${agentCommand()} agent --add-sources coding-sessions --name "<full_name>" --org "<organization>", ${agentCommand()} agent --continue, then ${agentCommand()} status.`,
833
+ "Use production memory/wiki tools only for company-memory questions, source recall, wiki lookup, messages/meetings retrieval, or coding-session work history that has already synced to Shepherd.",
834
+ "Important override: any production instruction saying not to use local shell commands applies only to production memory/wiki answers. It does not apply to local Shepherd setup, source enablement, or sync health.",
835
+ remoteConnectError ? `Production Shepherd MCP connection failed at startup: ${remoteConnectError}. Local setup/status tools are still available.` : "",
836
+ remoteInstructions ? `Production memory/wiki instructions: ${remoteInstructions}` : "",
837
+ ].filter(Boolean).join(" ");
838
+ }
839
+
840
+ async function callLocalMcpTool(name) {
841
+ if (name === "shepherd_status" || name === "shepherd_local_status") {
842
+ const status = await collectShepherdStatus();
843
+ return localMcpTextResult([
844
+ `Authoritative local status path: ${agentCommand()} status`,
845
+ "Use this result for setup/source/sync-health questions. Do not use production memory/wiki tools to answer what is enabled locally.",
846
+ renderShepherdStatus(status),
847
+ ].join("\n\n"));
848
+ }
849
+
850
+ if (name === "shepherd_setup_coding_sessions") {
851
+ const status = await collectShepherdStatus();
852
+ return localMcpTextResult(renderCodingSessionsSetupMcpResult(status));
853
+ }
854
+
855
+ return localMcpTextResult(`Unknown local Shepherd MCP tool: ${name}`, true);
856
+ }
857
+
858
+ function renderCodingSessionsSetupMcpResult(status) {
859
+ const command = status.commands.addCodingSessions;
860
+ const alreadyConfigured = Boolean(status.local.codingSessions.configPath);
861
+ return [
862
+ "Local Shepherd coding-session setup",
863
+ "",
864
+ "Use this when the user asks to set up coding agent sessions. Ask for explicit consent before enabling this source: Shepherd will read local Codex and Claude Code session logs, redact sensitive strings locally, and sync bounded summaries plus repo/command/file metadata, not full raw transcripts.",
865
+ "",
866
+ alreadyConfigured
867
+ ? "Current state: Coding Sessions already has a local config. Check whether the LaunchAgent is running and whether the last sync is healthy below."
868
+ : "Current state: Coding Sessions is not configured locally yet.",
869
+ "",
870
+ "Commands to run locally:",
871
+ `1. If there is no saved Shepherd login, run: ${status.commands.login}`,
872
+ `2. Add only this source: ${command}`,
873
+ `3. Finish/install the local agent: ${status.commands.continueSetup}`,
874
+ `4. Verify: ${status.commands.checkStatus}`,
875
+ "",
876
+ "Current local status:",
877
+ renderLocalCodingSessionsStatus(status.local.codingSessions).join("\n"),
878
+ ].join("\n");
879
+ }
880
+
881
+ function localMcpTextResult(text, isError = false) {
882
+ return {
883
+ content: [{ type: "text", text }],
884
+ isError,
885
+ };
886
+ }
887
+
756
888
  async function pollMcpLogin(apiUrl, started) {
757
889
  const intervalMs = Math.max(1000, Number(started.intervalSeconds ?? 5) * 1000);
758
890
  const expiresAt = Date.parse(started.expiresAt ?? "") || Date.now() + 600_000;
@@ -1021,20 +1153,186 @@ async function continueAgentOnboarding() {
1021
1153
  }
1022
1154
 
1023
1155
  async function printAgentStatus() {
1024
- const state = await readAgentState();
1025
- const status = await getJson(
1026
- `${state.apiUrl}/onboarding/raw/session/${encodeURIComponent(state.sessionId)}/status`,
1027
- { token: state.sessionToken },
1028
- );
1029
- await updateAgentStateFromOnboardingResponse(state, status);
1030
- console.log(JSON.stringify({
1031
- status: status.status,
1032
- account: status.account,
1033
- providers: status.providers,
1034
- rawOnly: status.rawOnly,
1035
- processingEnabled: status.processingEnabled === true,
1036
- processing: status.processing,
1037
- }, null, 2));
1156
+ await runStatusCommand();
1157
+ }
1158
+
1159
+ async function runStatusCommand() {
1160
+ const status = await collectShepherdStatus();
1161
+ if (args.json) {
1162
+ console.log(JSON.stringify(status, null, 2));
1163
+ return;
1164
+ }
1165
+ printShepherdStatus(status);
1166
+ }
1167
+
1168
+ async function collectShepherdStatus() {
1169
+ const statePath = agentStatePath();
1170
+ const state = await readOptionalAgentState();
1171
+ let production = null;
1172
+ let productionError = null;
1173
+
1174
+ if (state?.apiUrl && state?.sessionId && state?.sessionToken) {
1175
+ try {
1176
+ production = await getJson(
1177
+ `${trimTrailingSlash(state.apiUrl)}/onboarding/raw/session/${encodeURIComponent(state.sessionId)}/status`,
1178
+ { token: state.sessionToken },
1179
+ );
1180
+ await updateAgentStateFromOnboardingResponse(state, production);
1181
+ } catch (err) {
1182
+ productionError = safeError(err);
1183
+ }
1184
+ }
1185
+
1186
+ const userId = production?.sessionId ?? state?.sessionId ?? null;
1187
+ const providers = production?.providers ?? state?.providers ?? {};
1188
+ const messagesLocal = await collectMessagesLocalStatus(userId);
1189
+ const codingSessionsLocal = await collectCodingSessionsLocalStatus(userId);
1190
+
1191
+ return {
1192
+ statePath,
1193
+ configured: Boolean(state),
1194
+ account: production?.account ?? state?.account ?? null,
1195
+ savedSources: state?.sources ?? {},
1196
+ providers,
1197
+ production: production
1198
+ ? {
1199
+ status: production.status,
1200
+ providers,
1201
+ rawOnly: production.rawOnly === true,
1202
+ processingEnabled: production.processingEnabled === true,
1203
+ processing: production.processing,
1204
+ }
1205
+ : null,
1206
+ productionError,
1207
+ local: {
1208
+ messages: messagesLocal,
1209
+ codingSessions: codingSessionsLocal,
1210
+ },
1211
+ commands: {
1212
+ login: `${agentCommand()} agent --login`,
1213
+ checkStatus: `${agentCommand()} status`,
1214
+ addCodingSessions: `${agentCommand()} agent --add-sources coding-sessions --name "<full_name>" --org "<organization>"`,
1215
+ continueSetup: `${agentCommand()} agent --continue`,
1216
+ codingSessionsStatus: `${agentCommand()} coding-sessions-status`,
1217
+ messagesChats: `${agentCommand()} messages-chats`,
1218
+ },
1219
+ };
1220
+ }
1221
+
1222
+ function printShepherdStatus(status) {
1223
+ console.log(renderShepherdStatus(status));
1224
+ }
1225
+
1226
+ function renderShepherdStatus(status) {
1227
+ const lines = ["", "Shepherd sync status", ""];
1228
+
1229
+ if (status.account) {
1230
+ const email = status.account.email ? ` <${status.account.email}>` : "";
1231
+ const org = status.account.organizationName ? ` / ${status.account.organizationName}` : "";
1232
+ lines.push(`Account: ${status.account.name ?? "unknown"}${email}${org}`);
1233
+ } else {
1234
+ lines.push("Account: no saved Shepherd onboarding session found");
1235
+ }
1236
+
1237
+ if (status.productionError) {
1238
+ lines.push(`Production status: unavailable (${status.productionError})`);
1239
+ } else if (status.production) {
1240
+ lines.push(`Production status: ${status.production.status ?? "unknown"}`);
1241
+ lines.push(`Downstream processing: ${status.production.processingEnabled ? "enabled" : "not enabled"}`);
1242
+ } else {
1243
+ lines.push("Production status: not checked");
1244
+ }
1245
+
1246
+ lines.push("", "Sources:");
1247
+ for (const source of statusSourceRows(status.providers, status.savedSources)) {
1248
+ const label = source.connected
1249
+ ? "connected"
1250
+ : source.seen
1251
+ ? "not connected"
1252
+ : source.selected
1253
+ ? "selected in saved setup; connection unknown"
1254
+ : "not enabled in saved setup";
1255
+ lines.push(`- ${source.label}: ${label}`);
1256
+ }
1257
+
1258
+ lines.push("", "Local sync:");
1259
+ lines.push(...renderLocalMessagesStatus(status.local.messages));
1260
+ lines.push(...renderLocalCodingSessionsStatus(status.local.codingSessions));
1261
+
1262
+ lines.push("", "Useful commands:");
1263
+ if (!status.configured) lines.push(`- Sign in: ${status.commands.login}`);
1264
+ lines.push(`- Check again: ${status.commands.checkStatus}`);
1265
+ lines.push(`- Add coding sessions: ${status.commands.addCodingSessions}`);
1266
+ lines.push(`- Continue pending setup: ${status.commands.continueSetup}`);
1267
+ return lines.join("\n");
1268
+ }
1269
+
1270
+ function statusSourceRows(providers, savedSources = {}) {
1271
+ const definitions = [
1272
+ ["google", "Google Workspace", "google"],
1273
+ ["slack", "Slack", "slack"],
1274
+ ["granola", "Granola", "granola"],
1275
+ ["messages", "Messages", "messages"],
1276
+ ["codingSessions", "Coding Sessions", "codingSessions"],
1277
+ ];
1278
+ return definitions.map(([key, label, sourceKey]) => ({
1279
+ key,
1280
+ label,
1281
+ seen: Boolean(providers?.[key]),
1282
+ selected: savedSources?.[sourceKey] === true,
1283
+ connected: providers?.[key]?.connected === true,
1284
+ }));
1285
+ }
1286
+
1287
+ function printLocalMessagesStatus(status) {
1288
+ console.log(renderLocalMessagesStatus(status).join("\n"));
1289
+ }
1290
+
1291
+ function renderLocalMessagesStatus(status) {
1292
+ const lines = [];
1293
+ const prefix = "- Messages local agent";
1294
+ if (!status.configPath) {
1295
+ lines.push(`${prefix}: not configured`);
1296
+ } else {
1297
+ lines.push(`${prefix}: configured at ${status.configPath}`);
1298
+ }
1299
+ if (status.launch) {
1300
+ lines.push(` LaunchAgent: ${status.launch.label} ${status.launch.running ? "running" : "not running or unknown"}`);
1301
+ } else {
1302
+ lines.push(" LaunchAgent: not installed or unavailable");
1303
+ }
1304
+ lines.push(` Messages database: ${status.storage.readable ? "readable" : `not readable (${status.storage.reason})`}`);
1305
+ lines.push(` Queued unsent messages: ${status.queueDepth}`);
1306
+ return lines;
1307
+ }
1308
+
1309
+ function printLocalCodingSessionsStatus(status) {
1310
+ console.log(renderLocalCodingSessionsStatus(status).join("\n"));
1311
+ }
1312
+
1313
+ function renderLocalCodingSessionsStatus(status) {
1314
+ const lines = [];
1315
+ const prefix = "- Coding Sessions local agent";
1316
+ if (!status.configPath) {
1317
+ lines.push(`${prefix}: not configured`);
1318
+ } else {
1319
+ lines.push(`${prefix}: configured at ${status.configPath}`);
1320
+ }
1321
+ if (status.launch) {
1322
+ lines.push(` LaunchAgent: ${status.launch.label} ${status.launch.running ? "running" : "not running or unknown"}`);
1323
+ } else {
1324
+ lines.push(" LaunchAgent: not installed or unavailable");
1325
+ }
1326
+ for (const probe of status.localFolders) {
1327
+ lines.push(` ${probe.provider}: ${probe.path} ${probe.readable ? "readable" : `not readable (${probe.reason})`}`);
1328
+ }
1329
+ if (status.lastSync) {
1330
+ lines.push(` Last sync: ${status.lastSync.finishedAt ?? "unknown"} (${status.lastSync.scanned ?? 0} scanned, ${status.lastSync.changed ?? 0} changed)`);
1331
+ } else {
1332
+ lines.push(" Last sync: none recorded");
1333
+ }
1334
+ lines.push(` Queued unsent sessions: ${status.queueDepth}`);
1335
+ return lines;
1038
1336
  }
1039
1337
 
1040
1338
  async function runMessagesChatsCommand() {
@@ -1173,55 +1471,85 @@ async function runCodingSessionsAgent() {
1173
1471
  }
1174
1472
  }
1175
1473
 
1176
- async function runCodingSessionsStatus() {
1177
- const configPath = stringArg("config") ?? await latestCodingSessionsConfigPath();
1178
- const config = configPath ? JSON.parse(await readFile(configPath, "utf8")) : null;
1179
- const userId = config?.userId ?? null;
1474
+ async function collectMessagesLocalStatus(preferredUserId = null) {
1475
+ const configPath = await messagesConfigPathForUser(preferredUserId) ?? await latestMessagesConfigPath();
1476
+ const config = configPath ? readJsonOptional(configPath) : null;
1477
+ const userId = config?.userId ?? preferredUserId ?? null;
1478
+ const safeId = userId ? safeFileId(userId) : null;
1479
+ const label = safeId ? `ai.shepherd.raw-messages.${safeId}` : null;
1480
+ const queue = safeId ? readJsonOptional(join(homedir(), ".shepherd", "raw-messages", `${safeId}-queue.json`)) : null;
1481
+
1482
+ return {
1483
+ configPath: configPath ?? null,
1484
+ userId,
1485
+ allChats: config?.allChats === true,
1486
+ selectedChatCount: Array.isArray(config?.allowedChatIds) ? config.allowedChatIds.length : 0,
1487
+ storage: await probePath("messages", MESSAGES_CHAT_DB_PATH),
1488
+ launch: localLaunchStatus(label),
1489
+ queueDepth: Array.isArray(queue) ? queue.length : 0,
1490
+ };
1491
+ }
1492
+
1493
+ async function collectCodingSessionsLocalStatus(preferredUserId = null, explicitConfigPath = null) {
1494
+ const configPath = explicitConfigPath ?? await codingSessionsConfigPathForUser(preferredUserId) ?? await latestCodingSessionsConfigPath();
1495
+ const config = configPath ? readJsonOptional(configPath) : null;
1496
+ const userId = config?.userId ?? preferredUserId ?? null;
1180
1497
  const safeId = userId ? safeFileId(userId) : null;
1181
1498
  const label = safeId ? `ai.shepherd.coding-sessions.${safeId}` : null;
1182
- const probes = await probeCodingSessionPaths(config ?? {});
1183
- const lastSync = userId ? readJsonOptional(codingSessionsStatusFile(userId)) : null;
1184
- const queue = userId ? readJsonOptional(join(homedir(), ".shepherd", "coding-sessions", `${safeFileId(userId)}-queue.json`)) : null;
1185
- const launch = label && platform() === "darwin"
1186
- ? {
1187
- label,
1188
- state: readLaunchctlPrint(`gui/${process.getuid?.() ?? 501}/${label}`),
1189
- }
1190
- : null;
1191
- const production = await productionOnboardingStatusForCodingSessions(config).catch((err) => ({ error: safeError(err) }));
1192
- const status = {
1499
+ const queue = safeId ? readJsonOptional(join(homedir(), ".shepherd", "coding-sessions", `${safeId}-queue.json`)) : null;
1500
+
1501
+ return {
1193
1502
  configPath: configPath ?? null,
1194
1503
  userId,
1195
- localFolders: probes,
1196
- launch,
1197
- lastSync,
1504
+ localFolders: await probeCodingSessionPaths(config ?? {}),
1505
+ launch: localLaunchStatus(label),
1506
+ lastSync: userId ? readJsonOptional(codingSessionsStatusFile(userId)) : null,
1198
1507
  queueDepth: Array.isArray(queue) ? queue.length : 0,
1508
+ };
1509
+ }
1510
+
1511
+ function localLaunchStatus(label) {
1512
+ if (!label || platform() !== "darwin") return null;
1513
+ const state = readLaunchctlPrint(`gui/${process.getuid?.() ?? 501}/${label}`);
1514
+ return {
1515
+ label,
1516
+ running: /state = running|job state = running/i.test(state),
1517
+ state,
1518
+ };
1519
+ }
1520
+
1521
+ async function runCodingSessionsStatus() {
1522
+ const configPath = stringArg("config") ?? await latestCodingSessionsConfigPath();
1523
+ const status = await collectCodingSessionsLocalStatus(null, configPath);
1524
+ const config = configPath ? readJsonOptional(configPath) : null;
1525
+ const production = await productionOnboardingStatusForCodingSessions(config).catch((err) => ({ error: safeError(err) }));
1526
+ const detailedStatus = {
1527
+ ...status,
1199
1528
  production,
1200
1529
  };
1201
1530
 
1202
1531
  if (args.json) {
1203
- console.log(JSON.stringify(status, null, 2));
1532
+ console.log(JSON.stringify(detailedStatus, null, 2));
1204
1533
  return;
1205
1534
  }
1206
1535
 
1207
1536
  console.log("\nShepherd coding-session sync status\n");
1208
- console.log(`Config: ${configPath ?? "not found"}`);
1209
- if (userId) console.log(`User: ${userId}`);
1210
- for (const probe of probes) {
1537
+ console.log(`Config: ${detailedStatus.configPath ?? "not found"}`);
1538
+ if (detailedStatus.userId) console.log(`User: ${detailedStatus.userId}`);
1539
+ for (const probe of detailedStatus.localFolders) {
1211
1540
  console.log(`- ${probe.provider}: ${probe.path} ${probe.readable ? "readable" : `not readable (${probe.reason})`}`);
1212
1541
  }
1213
- if (launch) {
1214
- const running = /state = running|job state = running/i.test(launch.state);
1215
- console.log(`LaunchAgent: ${launch.label} ${running ? "running" : "not running or unknown"}`);
1542
+ if (detailedStatus.launch) {
1543
+ console.log(`LaunchAgent: ${detailedStatus.launch.label} ${detailedStatus.launch.running ? "running" : "not running or unknown"}`);
1216
1544
  } else {
1217
1545
  console.log("LaunchAgent: not installed or unavailable");
1218
1546
  }
1219
- if (lastSync) {
1220
- console.log(`Last sync: ${lastSync.finishedAt ?? "unknown"} (${lastSync.scanned ?? 0} scanned, ${lastSync.changed ?? 0} changed)`);
1547
+ if (detailedStatus.lastSync) {
1548
+ console.log(`Last sync: ${detailedStatus.lastSync.finishedAt ?? "unknown"} (${detailedStatus.lastSync.scanned ?? 0} scanned, ${detailedStatus.lastSync.changed ?? 0} changed)`);
1221
1549
  } else {
1222
1550
  console.log("Last sync: none recorded");
1223
1551
  }
1224
- console.log(`Queued unsent sessions: ${Array.isArray(queue) ? queue.length : 0}`);
1552
+ console.log(`Queued unsent sessions: ${detailedStatus.queueDepth}`);
1225
1553
  if (production?.providers?.codingSessions) {
1226
1554
  console.log(`Production provider: ${production.providers.codingSessions.connected ? "connected" : "not connected"}`);
1227
1555
  } else if (production?.error) {
@@ -1264,6 +1592,7 @@ Usage:
1264
1592
  npx -y ${PACKAGE_NAME}@latest agent --continue --granola-api-key <key> --messages-handle <value> --messages-chat-ids <ids|all>
1265
1593
  npx -y ${PACKAGE_NAME}@latest agent --add-sources coding-sessions --name <name> --org <organization>
1266
1594
  npx -y ${PACKAGE_NAME}@latest agent --status
1595
+ npx -y ${PACKAGE_NAME}@latest status
1267
1596
  npx -y ${PACKAGE_NAME}@latest coding-sessions-status
1268
1597
  npx -y ${PACKAGE_NAME}@latest messages-chats
1269
1598
  npx -y ${PACKAGE_NAME}@latest granola-api-keys
@@ -1274,6 +1603,28 @@ The bare agent command is intended for coding-agent shells. For direct terminal
1274
1603
  return;
1275
1604
  }
1276
1605
 
1606
+ if (which === "status" || which === "sync-status" || which === "check") {
1607
+ console.log(`Shepherd sync status
1608
+
1609
+ Usage:
1610
+ npx -y ${PACKAGE_NAME}@latest status
1611
+ npx -y ${PACKAGE_NAME}@latest status --json
1612
+
1613
+ Shows the saved Shepherd account, connected cloud sources, production processing
1614
+ state, and local background sync health for Messages and Coding Sessions.
1615
+
1616
+ Aliases:
1617
+ sync-status
1618
+ check
1619
+
1620
+ Options:
1621
+ --json Print machine-readable status.
1622
+ --state <path> Local onboarding state file. Defaults to ~/.shepherd/raw-onboarding-agent.json.
1623
+ --help Show this help.
1624
+ `);
1625
+ return;
1626
+ }
1627
+
1277
1628
  if (which === "coding-sessions-agent") {
1278
1629
  console.log(`Shepherd coding-session sync agent
1279
1630
 
@@ -1406,6 +1757,9 @@ Usage:
1406
1757
 
1407
1758
  This command is installed into MCP clients. It reads ~/.shepherd/mcp.json and
1408
1759
  proxies stdio MCP traffic to the authenticated production Shepherd MCP endpoint.
1760
+ It also exposes local npm-backed setup/status tools such as shepherd_status and
1761
+ shepherd_setup_coding_sessions so agents can answer what is enabled locally and
1762
+ guide coding-session setup.
1409
1763
 
1410
1764
  Options:
1411
1765
  --state <path> Token state file. Defaults to ~/.shepherd/mcp.json.
@@ -1420,6 +1774,7 @@ Options:
1420
1774
  Usage:
1421
1775
  npx -y ${PACKAGE_NAME}@latest
1422
1776
  npx -y ${PACKAGE_NAME}@latest agent
1777
+ npx -y ${PACKAGE_NAME}@latest status
1423
1778
  npx -y ${PACKAGE_NAME}@latest mcp-login
1424
1779
  npx -y ${PACKAGE_NAME}@latest mcp-install
1425
1780
  npx -y ${PACKAGE_NAME}@latest messages-chats
@@ -1479,6 +1834,16 @@ function printAgentContract() {
1479
1834
  const payload = {
1480
1835
  task: "Complete Shepherd customer-facing production raw-source onboarding for this user with the public customer setup flow.",
1481
1836
  instruction: "Use this as a public onboarding checklist. Confirm required onboarding choices and fields directly with the user instead of assuming them from local repo context, shell identity, machine paths, or earlier conversations.",
1837
+ commonUserRequests: [
1838
+ {
1839
+ userSays: "Check what I've enabled for Shepherd.",
1840
+ agentShould: `Run ${command} status and summarize connected sources, local background agents, unreadable folders, queued items, and any next command it prints.`,
1841
+ },
1842
+ {
1843
+ userSays: "Help me set up coding agent sessions.",
1844
+ agentShould: `Ask for consent to sync Codex and Claude Code summaries, then run ${command} agent --login if there is no saved Shepherd login, ${command} agent --add-sources coding-sessions --name "<full_name>" --org "<organization>", ${command} agent --continue, and finally ${command} status.`,
1845
+ },
1846
+ ],
1482
1847
  askStyle: [
1483
1848
  "Ask in short interactive prompts, not as a pasted checklist.",
1484
1849
  "Start with selection questions to determine what the user wants connected.",
@@ -1547,13 +1912,15 @@ function printAgentContract() {
1547
1912
  startCommand: `${command} agent --name "<full_name>" --org "<organization>"`,
1548
1913
  addSourcesCommand: `${command} agent --add-sources coding-sessions --name "<full_name>" --org "<organization>"`,
1549
1914
  continueCommand: `${command} agent --continue`,
1915
+ checkCommand: `${command} status`,
1550
1916
  mcpLoginCommand: `${command} mcp-login`,
1551
1917
  optionalContinueArgs: [
1552
1918
  "--messages-handle \"<phone_or_apple_id>\" if local Messages is being connected",
1553
1919
  "--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",
1554
1920
  "--granola-api-key \"<granola_key>\" if Granola is being connected",
1555
1921
  ],
1556
- statusCommand: `${command} agent --status`,
1922
+ statusCommand: `${command} status`,
1923
+ agentStatusCommand: `${command} agent --status`,
1557
1924
  messagesChatsCommand: `${command} messages-chats`,
1558
1925
  messagesPermissions: {
1559
1926
  macOS: "Local Messages raw sync needs Full Disk Access for the app running onboarding and for Node.js used by the background LaunchAgent. The Messages selector command validates local chat.db access, opens Full Disk Access settings if needed, and keeps checking until access works in interactive onboarding. Background sync install also checks that launchd can start the Messages agent. Contacts permission may also appear when resolving local contact names. The background Messages agent reloads Contacts on startup, watches AddressBook changes when available, and runs fallback contact sync so renamed contacts can hydrate prior ingested Messages rows for the token-bound customer account.",
@@ -1585,6 +1952,16 @@ Confirm onboarding choices and fields directly with the user instead of assuming
1585
1952
 
1586
1953
  Ask with short interactive prompts, not as one pasted checklist. Do not paste this whole checklist into the chat unless the user explicitly asks to see it.
1587
1954
 
1955
+ Common user requests:
1956
+ - If the user asks "Check what I've enabled for Shepherd?", run:
1957
+ ${payload.checkCommand}
1958
+ Then summarize connected sources, local background agents, unreadable folders, queued items, and the next command to fix anything missing.
1959
+ - If the user asks "Help me set up coding agent sessions", ask for consent to sync Codex and Claude Code summaries, then run login if needed, add only the coding-sessions source, continue setup, and finish by checking status:
1960
+ ${payload.loginCommand}
1961
+ ${payload.addSourcesCommand}
1962
+ ${payload.continueCommand}
1963
+ ${payload.checkCommand}
1964
+
1588
1965
  Start with selection questions to determine intent:
1589
1966
  1. Organization: Join existing org, or Create new org.
1590
1967
  2. Sources: Google Workspace (Gmail/Drive/Docs/Calendar/Sheets/Slides/Tasks/Contacts), Slack, Granola, Messages, Coding Sessions (Codex/Claude Code summaries). Allow multi-select if your interface supports it.
@@ -2509,6 +2886,7 @@ function readLaunchctlPrint(domainLabel) {
2509
2886
  try {
2510
2887
  return execFileSync("launchctl", ["print", domainLabel], {
2511
2888
  encoding: "utf8",
2889
+ stdio: ["ignore", "pipe", "pipe"],
2512
2890
  timeout: 5_000,
2513
2891
  });
2514
2892
  } catch (err) {
@@ -4317,15 +4695,36 @@ function extractCodexCommands(payloads) {
4317
4695
  function extractClaudeCommands(lines) {
4318
4696
  const commands = [];
4319
4697
  for (const line of lines) {
4320
- const text = JSON.stringify(line.toolUseResult ?? line.message?.content ?? "");
4321
- const bashMatch = text.match(/(?:"command"|"input")\s*:\s*"([^"]{2,500})"/);
4322
- if (bashMatch && /\b(?:git|npm|pnpm|yarn|bun|pytest|vitest|cargo|go|python|node|tsc|ruff|eslint|make)\b/.test(bashMatch[1])) {
4323
- commands.push({ command: redactText(unescapeJsonString(bashMatch[1]), 600), exitCode: null, summary: null });
4698
+ for (const command of extractCommandStrings(line.toolUseResult ?? line.message?.content ?? line)) {
4699
+ if (isLikelyShellCommand(command)) {
4700
+ commands.push({ command: redactText(command, 600), exitCode: null, summary: null });
4701
+ }
4324
4702
  }
4325
4703
  }
4326
4704
  return commands.slice(-100);
4327
4705
  }
4328
4706
 
4707
+ function extractCommandStrings(value, depth = 0) {
4708
+ if (depth > 6 || value == null) return [];
4709
+ if (typeof value === "string") return [];
4710
+ if (Array.isArray(value)) return value.flatMap((item) => extractCommandStrings(item, depth + 1));
4711
+ if (typeof value !== "object") return [];
4712
+
4713
+ const commands = [];
4714
+ for (const [key, nested] of Object.entries(value)) {
4715
+ if ((key === "command" || key === "input") && typeof nested === "string" && nested.trim()) {
4716
+ commands.push(nested);
4717
+ continue;
4718
+ }
4719
+ commands.push(...extractCommandStrings(nested, depth + 1));
4720
+ }
4721
+ return commands;
4722
+ }
4723
+
4724
+ function isLikelyShellCommand(value) {
4725
+ return /\b(?:git|npm|pnpm|yarn|bun|pytest|vitest|cargo|go|python|node|tsc|ruff|eslint|make)\b/.test(value);
4726
+ }
4727
+
4329
4728
  async function repoMetadata(cwd) {
4330
4729
  const base = { fullName: null, remote: null, branch: null, commit: null };
4331
4730
  if (!cwd) return base;
@@ -4474,20 +4873,12 @@ function redactText(value, maxLength = 1000) {
4474
4873
  .replace(/-----BEGIN [^-]+PRIVATE KEY-----[\s\S]*?-----END [^-]+PRIVATE KEY-----/g, "[redacted-private-key]")
4475
4874
  .replace(/\b(?:sk|pk|rk|ghp|github_pat|xox[baprs])_[A-Za-z0-9_=-]{12,}\b/g, "[redacted-token]")
4476
4875
  .replace(/\b[A-Za-z0-9._%+-]+:[A-Za-z0-9._%+-]+@/g, "[redacted-credentials]@")
4477
- .replace(/\b(?:authorization|x-api-key|api[_-]?key|token|secret|password)\s*[:=]\s*['\"]?[^'\"\s]+/gi, "$1=[redacted]")
4876
+ .replace(/\b(authorization|x-api-key|api[_-]?key|token|secret|password)(\s*[:=]\s*)['"]?[^'"\s]+/gi, "$1$2[redacted]")
4478
4877
  .replace(/(OPENAI_API_KEY|FIREWORKS_API_KEY|ANTHROPIC_API_KEY|DATABASE_URL|REDIS_URL)=\S+/g, "$1=[redacted]")
4479
4878
  .replace(new RegExp(homedir().replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"), "~")
4480
4879
  .slice(0, maxLength);
4481
4880
  }
4482
4881
 
4483
- function unescapeJsonString(value) {
4484
- try {
4485
- return JSON.parse(`"${value.replace(/"/g, '\\"')}"`);
4486
- } catch {
4487
- return value;
4488
- }
4489
- }
4490
-
4491
4882
  function hashString(value) {
4492
4883
  return createHash("sha256").update(String(value ?? "")).digest("hex");
4493
4884
  }
@@ -4515,6 +4906,35 @@ function codingSessionsStatusFile(userId) {
4515
4906
  return path;
4516
4907
  }
4517
4908
 
4909
+ async function messagesConfigPathForUser(userId) {
4910
+ if (!userId) return null;
4911
+ const path = join(homedir(), ".shepherd", "raw-messages", `${userId}.json`);
4912
+ return existsSync(path) ? path : null;
4913
+ }
4914
+
4915
+ async function codingSessionsConfigPathForUser(userId) {
4916
+ if (!userId) return null;
4917
+ const path = join(homedir(), ".shepherd", "coding-sessions", `${userId}.json`);
4918
+ return existsSync(path) ? path : null;
4919
+ }
4920
+
4921
+ async function latestMessagesConfigPath() {
4922
+ const dir = join(homedir(), ".shepherd", "raw-messages");
4923
+ try {
4924
+ const entries = await readdir(dir, { withFileTypes: true });
4925
+ const files = [];
4926
+ for (const entry of entries) {
4927
+ if (!entry.isFile() || !entry.name.endsWith(".json") || entry.name.includes("-queue")) continue;
4928
+ const path = join(dir, entry.name);
4929
+ const info = await stat(path).catch(() => null);
4930
+ if (info) files.push({ path, mtimeMs: info.mtimeMs });
4931
+ }
4932
+ return files.sort((a, b) => b.mtimeMs - a.mtimeMs)[0]?.path ?? null;
4933
+ } catch {
4934
+ return null;
4935
+ }
4936
+ }
4937
+
4518
4938
  async function latestCodingSessionsConfigPath() {
4519
4939
  const dir = join(homedir(), ".shepherd", "coding-sessions");
4520
4940
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "askshepherd",
3
- "version": "0.1.36",
3
+ "version": "0.1.37",
4
4
  "description": "Customer-facing Shepherd production onboarding and MCP CLI",
5
5
  "type": "module",
6
6
  "bin": {