askshepherd 0.1.36 → 0.1.38

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,41 @@ 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
+ Agents must not inspect the user's folders or repositories to answer setup
71
+ status. Do not run `ls`, `find`, `rg`, `grep`, `cat`, `Read`, `Glob`, or
72
+ `Explore` against the user's home directory, repositories, `~/.codex`,
73
+ `~/.claude`, or `~/.shepherd` for Shepherd setup. Use the npm status command
74
+ above; it performs the bounded local checks.
75
+
76
+ ## Set Up Coding Agent Sessions
77
+
78
+ Use this when the user asks "Help me set up coding agent sessions":
79
+
80
+ ```sh
81
+ npx -y askshepherd@latest agent --login
82
+ npx -y askshepherd@latest agent --add-sources coding-sessions --name "<name>" --org "<organization>"
83
+ npx -y askshepherd@latest agent --continue
84
+ npx -y askshepherd@latest status
85
+ ```
86
+
87
+ The coding agent should ask for consent before enabling this source. The local
88
+ collector syncs redacted Codex and Claude Code summaries, not full transcripts.
89
+ The agent should run only the commands above and should not search the local
90
+ filesystem for a Shepherd agent implementation.
91
+
57
92
  ## Customer MCP Login
58
93
 
59
94
  After raw onboarding creates the Shepherd customer account, customers can
@@ -82,6 +117,15 @@ The saved MCP state includes:
82
117
  - `authSource`: `local_onboarding` or `workos`
83
118
  - `localAuth`: raw onboarding state reference when local auth was used
84
119
 
120
+ The installed MCP server is local npm first, remote brain second. For questions
121
+ like "what do I have set up on Shepherd?", "is Shepherd syncing?", or "help me
122
+ set up coding agent sessions", the MCP exposes local tools such as
123
+ `shepherd_status` and `shepherd_setup_coding_sessions` that route agents to the
124
+ local `askshepherd status` / add-source flow. Production memory and wiki tools
125
+ remain remote Railway-backed tools for source recall and company-memory answers.
126
+ Those local MCP tools are also the permission boundary: an MCP client should not
127
+ use shell or file tools to inspect the user's folders or repositories for setup.
128
+
85
129
  Use `--json` when an agent or setup script needs machine-readable endpoint and
86
130
  header details.
87
131
 
@@ -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,172 @@ 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 or shell/file exploration 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. Do not inspect user folders or repositories yourself.",
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. Do not search, list, or read the user's folders or repos to discover setup.",
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. The askshepherd CLI is the only component that may perform bounded local checks of Shepherd state, LaunchAgents, and known Codex/Claude session locations.`,
832
+ "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 askshepherd status command.",
833
+ `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.`,
834
+ "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.",
835
+ "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.",
836
+ remoteConnectError ? `Production Shepherd MCP connection failed at startup: ${remoteConnectError}. Local setup/status tools are still available.` : "",
837
+ remoteInstructions ? `Production memory/wiki instructions: ${remoteInstructions}` : "",
838
+ ].filter(Boolean).join(" ");
839
+ }
840
+
841
+ async function callLocalMcpTool(name) {
842
+ if (name === "shepherd_status" || name === "shepherd_local_status") {
843
+ const status = await collectShepherdStatus();
844
+ return localMcpTextResult([
845
+ `Authoritative local status path: ${agentCommand()} status`,
846
+ "Use this result for setup/source/sync-health questions. Do not use production memory/wiki tools to answer what is enabled locally.",
847
+ "Do not inspect the user's folders or repositories yourself. Do not run ls/find/rg/grep/cat/Read/Glob/Explore against the user's home directory, repos, ~/.codex, ~/.claude, or ~/.shepherd for Shepherd setup.",
848
+ renderShepherdStatus(status),
849
+ ].join("\n\n"));
850
+ }
851
+
852
+ if (name === "shepherd_setup_coding_sessions") {
853
+ const status = await collectShepherdStatus();
854
+ return localMcpTextResult(renderCodingSessionsSetupMcpResult(status));
855
+ }
856
+
857
+ return localMcpTextResult(`Unknown local Shepherd MCP tool: ${name}`, true);
858
+ }
859
+
860
+ function renderCodingSessionsSetupMcpResult(status) {
861
+ const command = status.commands.addCodingSessions;
862
+ const alreadyConfigured = Boolean(status.local.codingSessions.configPath);
863
+ return [
864
+ "Local Shepherd coding-session setup",
865
+ "",
866
+ "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.",
867
+ "Do not inspect the user's folders or repositories to set this up. Do not run ls/find/rg/grep/cat/Read/Glob/Explore against the user's home directory, repos, ~/.codex, ~/.claude, or ~/.shepherd. Use only the Shepherd npm commands below and the status result they print.",
868
+ "",
869
+ alreadyConfigured
870
+ ? "Current state: Coding Sessions already has a local config. Check whether the LaunchAgent is running and whether the last sync is healthy below."
871
+ : "Current state: Coding Sessions is not configured locally yet.",
872
+ "",
873
+ "Commands to run locally:",
874
+ `1. If there is no saved Shepherd login, run: ${status.commands.login}`,
875
+ `2. Add only this source: ${command}`,
876
+ `3. Finish/install the local agent: ${status.commands.continueSetup}`,
877
+ `4. Verify: ${status.commands.checkStatus}`,
878
+ "",
879
+ "Current local status:",
880
+ renderLocalCodingSessionsStatus(status.local.codingSessions).join("\n"),
881
+ ].join("\n");
882
+ }
883
+
884
+ function localMcpTextResult(text, isError = false) {
885
+ return {
886
+ content: [{ type: "text", text }],
887
+ isError,
888
+ };
889
+ }
890
+
756
891
  async function pollMcpLogin(apiUrl, started) {
757
892
  const intervalMs = Math.max(1000, Number(started.intervalSeconds ?? 5) * 1000);
758
893
  const expiresAt = Date.parse(started.expiresAt ?? "") || Date.now() + 600_000;
@@ -1021,20 +1156,186 @@ async function continueAgentOnboarding() {
1021
1156
  }
1022
1157
 
1023
1158
  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));
1159
+ await runStatusCommand();
1160
+ }
1161
+
1162
+ async function runStatusCommand() {
1163
+ const status = await collectShepherdStatus();
1164
+ if (args.json) {
1165
+ console.log(JSON.stringify(status, null, 2));
1166
+ return;
1167
+ }
1168
+ printShepherdStatus(status);
1169
+ }
1170
+
1171
+ async function collectShepherdStatus() {
1172
+ const statePath = agentStatePath();
1173
+ const state = await readOptionalAgentState();
1174
+ let production = null;
1175
+ let productionError = null;
1176
+
1177
+ if (state?.apiUrl && state?.sessionId && state?.sessionToken) {
1178
+ try {
1179
+ production = await getJson(
1180
+ `${trimTrailingSlash(state.apiUrl)}/onboarding/raw/session/${encodeURIComponent(state.sessionId)}/status`,
1181
+ { token: state.sessionToken },
1182
+ );
1183
+ await updateAgentStateFromOnboardingResponse(state, production);
1184
+ } catch (err) {
1185
+ productionError = safeError(err);
1186
+ }
1187
+ }
1188
+
1189
+ const userId = production?.sessionId ?? state?.sessionId ?? null;
1190
+ const providers = production?.providers ?? state?.providers ?? {};
1191
+ const messagesLocal = await collectMessagesLocalStatus(userId);
1192
+ const codingSessionsLocal = await collectCodingSessionsLocalStatus(userId);
1193
+
1194
+ return {
1195
+ statePath,
1196
+ configured: Boolean(state),
1197
+ account: production?.account ?? state?.account ?? null,
1198
+ savedSources: state?.sources ?? {},
1199
+ providers,
1200
+ production: production
1201
+ ? {
1202
+ status: production.status,
1203
+ providers,
1204
+ rawOnly: production.rawOnly === true,
1205
+ processingEnabled: production.processingEnabled === true,
1206
+ processing: production.processing,
1207
+ }
1208
+ : null,
1209
+ productionError,
1210
+ local: {
1211
+ messages: messagesLocal,
1212
+ codingSessions: codingSessionsLocal,
1213
+ },
1214
+ commands: {
1215
+ login: `${agentCommand()} agent --login`,
1216
+ checkStatus: `${agentCommand()} status`,
1217
+ addCodingSessions: `${agentCommand()} agent --add-sources coding-sessions --name "<full_name>" --org "<organization>"`,
1218
+ continueSetup: `${agentCommand()} agent --continue`,
1219
+ codingSessionsStatus: `${agentCommand()} coding-sessions-status`,
1220
+ messagesChats: `${agentCommand()} messages-chats`,
1221
+ },
1222
+ };
1223
+ }
1224
+
1225
+ function printShepherdStatus(status) {
1226
+ console.log(renderShepherdStatus(status));
1227
+ }
1228
+
1229
+ function renderShepherdStatus(status) {
1230
+ const lines = ["", "Shepherd sync status", ""];
1231
+
1232
+ if (status.account) {
1233
+ const email = status.account.email ? ` <${status.account.email}>` : "";
1234
+ const org = status.account.organizationName ? ` / ${status.account.organizationName}` : "";
1235
+ lines.push(`Account: ${status.account.name ?? "unknown"}${email}${org}`);
1236
+ } else {
1237
+ lines.push("Account: no saved Shepherd onboarding session found");
1238
+ }
1239
+
1240
+ if (status.productionError) {
1241
+ lines.push(`Production status: unavailable (${status.productionError})`);
1242
+ } else if (status.production) {
1243
+ lines.push(`Production status: ${status.production.status ?? "unknown"}`);
1244
+ lines.push(`Downstream processing: ${status.production.processingEnabled ? "enabled" : "not enabled"}`);
1245
+ } else {
1246
+ lines.push("Production status: not checked");
1247
+ }
1248
+
1249
+ lines.push("", "Sources:");
1250
+ for (const source of statusSourceRows(status.providers, status.savedSources)) {
1251
+ const label = source.connected
1252
+ ? "connected"
1253
+ : source.seen
1254
+ ? "not connected"
1255
+ : source.selected
1256
+ ? "selected in saved setup; connection unknown"
1257
+ : "not enabled in saved setup";
1258
+ lines.push(`- ${source.label}: ${label}`);
1259
+ }
1260
+
1261
+ lines.push("", "Local sync:");
1262
+ lines.push(...renderLocalMessagesStatus(status.local.messages));
1263
+ lines.push(...renderLocalCodingSessionsStatus(status.local.codingSessions));
1264
+
1265
+ lines.push("", "Useful commands:");
1266
+ if (!status.configured) lines.push(`- Sign in: ${status.commands.login}`);
1267
+ lines.push(`- Check again: ${status.commands.checkStatus}`);
1268
+ lines.push(`- Add coding sessions: ${status.commands.addCodingSessions}`);
1269
+ lines.push(`- Continue pending setup: ${status.commands.continueSetup}`);
1270
+ return lines.join("\n");
1271
+ }
1272
+
1273
+ function statusSourceRows(providers, savedSources = {}) {
1274
+ const definitions = [
1275
+ ["google", "Google Workspace", "google"],
1276
+ ["slack", "Slack", "slack"],
1277
+ ["granola", "Granola", "granola"],
1278
+ ["messages", "Messages", "messages"],
1279
+ ["codingSessions", "Coding Sessions", "codingSessions"],
1280
+ ];
1281
+ return definitions.map(([key, label, sourceKey]) => ({
1282
+ key,
1283
+ label,
1284
+ seen: Boolean(providers?.[key]),
1285
+ selected: savedSources?.[sourceKey] === true,
1286
+ connected: providers?.[key]?.connected === true,
1287
+ }));
1288
+ }
1289
+
1290
+ function printLocalMessagesStatus(status) {
1291
+ console.log(renderLocalMessagesStatus(status).join("\n"));
1292
+ }
1293
+
1294
+ function renderLocalMessagesStatus(status) {
1295
+ const lines = [];
1296
+ const prefix = "- Messages local agent";
1297
+ if (!status.configPath) {
1298
+ lines.push(`${prefix}: not configured`);
1299
+ } else {
1300
+ lines.push(`${prefix}: configured at ${status.configPath}`);
1301
+ }
1302
+ if (status.launch) {
1303
+ lines.push(` LaunchAgent: ${status.launch.label} ${status.launch.running ? "running" : "not running or unknown"}`);
1304
+ } else {
1305
+ lines.push(" LaunchAgent: not installed or unavailable");
1306
+ }
1307
+ lines.push(` Messages database: ${status.storage.readable ? "readable" : `not readable (${status.storage.reason})`}`);
1308
+ lines.push(` Queued unsent messages: ${status.queueDepth}`);
1309
+ return lines;
1310
+ }
1311
+
1312
+ function printLocalCodingSessionsStatus(status) {
1313
+ console.log(renderLocalCodingSessionsStatus(status).join("\n"));
1314
+ }
1315
+
1316
+ function renderLocalCodingSessionsStatus(status) {
1317
+ const lines = [];
1318
+ const prefix = "- Coding Sessions local agent";
1319
+ if (!status.configPath) {
1320
+ lines.push(`${prefix}: not configured`);
1321
+ } else {
1322
+ lines.push(`${prefix}: configured at ${status.configPath}`);
1323
+ }
1324
+ if (status.launch) {
1325
+ lines.push(` LaunchAgent: ${status.launch.label} ${status.launch.running ? "running" : "not running or unknown"}`);
1326
+ } else {
1327
+ lines.push(" LaunchAgent: not installed or unavailable");
1328
+ }
1329
+ for (const probe of status.localFolders) {
1330
+ lines.push(` ${probe.provider}: ${probe.path} ${probe.readable ? "readable" : `not readable (${probe.reason})`}`);
1331
+ }
1332
+ if (status.lastSync) {
1333
+ lines.push(` Last sync: ${status.lastSync.finishedAt ?? "unknown"} (${status.lastSync.scanned ?? 0} scanned, ${status.lastSync.changed ?? 0} changed)`);
1334
+ } else {
1335
+ lines.push(" Last sync: none recorded");
1336
+ }
1337
+ lines.push(` Queued unsent sessions: ${status.queueDepth}`);
1338
+ return lines;
1038
1339
  }
1039
1340
 
1040
1341
  async function runMessagesChatsCommand() {
@@ -1173,55 +1474,85 @@ async function runCodingSessionsAgent() {
1173
1474
  }
1174
1475
  }
1175
1476
 
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;
1477
+ async function collectMessagesLocalStatus(preferredUserId = null) {
1478
+ const configPath = await messagesConfigPathForUser(preferredUserId) ?? await latestMessagesConfigPath();
1479
+ const config = configPath ? readJsonOptional(configPath) : null;
1480
+ const userId = config?.userId ?? preferredUserId ?? null;
1481
+ const safeId = userId ? safeFileId(userId) : null;
1482
+ const label = safeId ? `ai.shepherd.raw-messages.${safeId}` : null;
1483
+ const queue = safeId ? readJsonOptional(join(homedir(), ".shepherd", "raw-messages", `${safeId}-queue.json`)) : null;
1484
+
1485
+ return {
1486
+ configPath: configPath ?? null,
1487
+ userId,
1488
+ allChats: config?.allChats === true,
1489
+ selectedChatCount: Array.isArray(config?.allowedChatIds) ? config.allowedChatIds.length : 0,
1490
+ storage: await probePath("messages", MESSAGES_CHAT_DB_PATH),
1491
+ launch: localLaunchStatus(label),
1492
+ queueDepth: Array.isArray(queue) ? queue.length : 0,
1493
+ };
1494
+ }
1495
+
1496
+ async function collectCodingSessionsLocalStatus(preferredUserId = null, explicitConfigPath = null) {
1497
+ const configPath = explicitConfigPath ?? await codingSessionsConfigPathForUser(preferredUserId) ?? await latestCodingSessionsConfigPath();
1498
+ const config = configPath ? readJsonOptional(configPath) : null;
1499
+ const userId = config?.userId ?? preferredUserId ?? null;
1180
1500
  const safeId = userId ? safeFileId(userId) : null;
1181
1501
  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 = {
1502
+ const queue = safeId ? readJsonOptional(join(homedir(), ".shepherd", "coding-sessions", `${safeId}-queue.json`)) : null;
1503
+
1504
+ return {
1193
1505
  configPath: configPath ?? null,
1194
1506
  userId,
1195
- localFolders: probes,
1196
- launch,
1197
- lastSync,
1507
+ localFolders: await probeCodingSessionPaths(config ?? {}),
1508
+ launch: localLaunchStatus(label),
1509
+ lastSync: userId ? readJsonOptional(codingSessionsStatusFile(userId)) : null,
1198
1510
  queueDepth: Array.isArray(queue) ? queue.length : 0,
1511
+ };
1512
+ }
1513
+
1514
+ function localLaunchStatus(label) {
1515
+ if (!label || platform() !== "darwin") return null;
1516
+ const state = readLaunchctlPrint(`gui/${process.getuid?.() ?? 501}/${label}`);
1517
+ return {
1518
+ label,
1519
+ running: /state = running|job state = running/i.test(state),
1520
+ state,
1521
+ };
1522
+ }
1523
+
1524
+ async function runCodingSessionsStatus() {
1525
+ const configPath = stringArg("config") ?? await latestCodingSessionsConfigPath();
1526
+ const status = await collectCodingSessionsLocalStatus(null, configPath);
1527
+ const config = configPath ? readJsonOptional(configPath) : null;
1528
+ const production = await productionOnboardingStatusForCodingSessions(config).catch((err) => ({ error: safeError(err) }));
1529
+ const detailedStatus = {
1530
+ ...status,
1199
1531
  production,
1200
1532
  };
1201
1533
 
1202
1534
  if (args.json) {
1203
- console.log(JSON.stringify(status, null, 2));
1535
+ console.log(JSON.stringify(detailedStatus, null, 2));
1204
1536
  return;
1205
1537
  }
1206
1538
 
1207
1539
  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) {
1540
+ console.log(`Config: ${detailedStatus.configPath ?? "not found"}`);
1541
+ if (detailedStatus.userId) console.log(`User: ${detailedStatus.userId}`);
1542
+ for (const probe of detailedStatus.localFolders) {
1211
1543
  console.log(`- ${probe.provider}: ${probe.path} ${probe.readable ? "readable" : `not readable (${probe.reason})`}`);
1212
1544
  }
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"}`);
1545
+ if (detailedStatus.launch) {
1546
+ console.log(`LaunchAgent: ${detailedStatus.launch.label} ${detailedStatus.launch.running ? "running" : "not running or unknown"}`);
1216
1547
  } else {
1217
1548
  console.log("LaunchAgent: not installed or unavailable");
1218
1549
  }
1219
- if (lastSync) {
1220
- console.log(`Last sync: ${lastSync.finishedAt ?? "unknown"} (${lastSync.scanned ?? 0} scanned, ${lastSync.changed ?? 0} changed)`);
1550
+ if (detailedStatus.lastSync) {
1551
+ console.log(`Last sync: ${detailedStatus.lastSync.finishedAt ?? "unknown"} (${detailedStatus.lastSync.scanned ?? 0} scanned, ${detailedStatus.lastSync.changed ?? 0} changed)`);
1221
1552
  } else {
1222
1553
  console.log("Last sync: none recorded");
1223
1554
  }
1224
- console.log(`Queued unsent sessions: ${Array.isArray(queue) ? queue.length : 0}`);
1555
+ console.log(`Queued unsent sessions: ${detailedStatus.queueDepth}`);
1225
1556
  if (production?.providers?.codingSessions) {
1226
1557
  console.log(`Production provider: ${production.providers.codingSessions.connected ? "connected" : "not connected"}`);
1227
1558
  } else if (production?.error) {
@@ -1264,6 +1595,7 @@ Usage:
1264
1595
  npx -y ${PACKAGE_NAME}@latest agent --continue --granola-api-key <key> --messages-handle <value> --messages-chat-ids <ids|all>
1265
1596
  npx -y ${PACKAGE_NAME}@latest agent --add-sources coding-sessions --name <name> --org <organization>
1266
1597
  npx -y ${PACKAGE_NAME}@latest agent --status
1598
+ npx -y ${PACKAGE_NAME}@latest status
1267
1599
  npx -y ${PACKAGE_NAME}@latest coding-sessions-status
1268
1600
  npx -y ${PACKAGE_NAME}@latest messages-chats
1269
1601
  npx -y ${PACKAGE_NAME}@latest granola-api-keys
@@ -1274,6 +1606,28 @@ The bare agent command is intended for coding-agent shells. For direct terminal
1274
1606
  return;
1275
1607
  }
1276
1608
 
1609
+ if (which === "status" || which === "sync-status" || which === "check") {
1610
+ console.log(`Shepherd sync status
1611
+
1612
+ Usage:
1613
+ npx -y ${PACKAGE_NAME}@latest status
1614
+ npx -y ${PACKAGE_NAME}@latest status --json
1615
+
1616
+ Shows the saved Shepherd account, connected cloud sources, production processing
1617
+ state, and local background sync health for Messages and Coding Sessions.
1618
+
1619
+ Aliases:
1620
+ sync-status
1621
+ check
1622
+
1623
+ Options:
1624
+ --json Print machine-readable status.
1625
+ --state <path> Local onboarding state file. Defaults to ~/.shepherd/raw-onboarding-agent.json.
1626
+ --help Show this help.
1627
+ `);
1628
+ return;
1629
+ }
1630
+
1277
1631
  if (which === "coding-sessions-agent") {
1278
1632
  console.log(`Shepherd coding-session sync agent
1279
1633
 
@@ -1406,6 +1760,9 @@ Usage:
1406
1760
 
1407
1761
  This command is installed into MCP clients. It reads ~/.shepherd/mcp.json and
1408
1762
  proxies stdio MCP traffic to the authenticated production Shepherd MCP endpoint.
1763
+ It also exposes local npm-backed setup/status tools such as shepherd_status and
1764
+ shepherd_setup_coding_sessions so agents can answer what is enabled locally and
1765
+ guide coding-session setup.
1409
1766
 
1410
1767
  Options:
1411
1768
  --state <path> Token state file. Defaults to ~/.shepherd/mcp.json.
@@ -1420,6 +1777,7 @@ Options:
1420
1777
  Usage:
1421
1778
  npx -y ${PACKAGE_NAME}@latest
1422
1779
  npx -y ${PACKAGE_NAME}@latest agent
1780
+ npx -y ${PACKAGE_NAME}@latest status
1423
1781
  npx -y ${PACKAGE_NAME}@latest mcp-login
1424
1782
  npx -y ${PACKAGE_NAME}@latest mcp-install
1425
1783
  npx -y ${PACKAGE_NAME}@latest messages-chats
@@ -1479,6 +1837,16 @@ function printAgentContract() {
1479
1837
  const payload = {
1480
1838
  task: "Complete Shepherd customer-facing production raw-source onboarding for this user with the public customer setup flow.",
1481
1839
  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.",
1840
+ commonUserRequests: [
1841
+ {
1842
+ userSays: "Check what I've enabled for Shepherd.",
1843
+ agentShould: `Run ${command} status and summarize connected sources, local background agents, unreadable folders, queued items, and any next command it prints. Do not inspect local folders or repositories yourself.`,
1844
+ },
1845
+ {
1846
+ userSays: "Help me set up coding agent sessions.",
1847
+ 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. Do not search the user's filesystem or repos for an agent implementation.`,
1848
+ },
1849
+ ],
1482
1850
  askStyle: [
1483
1851
  "Ask in short interactive prompts, not as a pasted checklist.",
1484
1852
  "Start with selection questions to determine what the user wants connected.",
@@ -1542,18 +1910,21 @@ function printAgentContract() {
1542
1910
  "Do not ask the customer to create a Google service account or upload service account JSON for the default Shepherd-managed flow.",
1543
1911
  "Do not use WorkOS Auth, WorkOS Pipes, or per-user Google OAuth for Google Workspace delegation.",
1544
1912
  "Do not fill onboarding fields from local repository context, shell identity, machine paths, or earlier conversations; confirm them directly with the user.",
1913
+ "Do not use shell or file tools to list, search, or read the user's home directory, repositories, ~/.codex, ~/.claude, or ~/.shepherd for Shepherd setup. Only the askshepherd npm commands should perform local setup/status checks.",
1545
1914
  ],
1546
1915
  loginCommand: `${command} agent --login`,
1547
1916
  startCommand: `${command} agent --name "<full_name>" --org "<organization>"`,
1548
1917
  addSourcesCommand: `${command} agent --add-sources coding-sessions --name "<full_name>" --org "<organization>"`,
1549
1918
  continueCommand: `${command} agent --continue`,
1919
+ checkCommand: `${command} status`,
1550
1920
  mcpLoginCommand: `${command} mcp-login`,
1551
1921
  optionalContinueArgs: [
1552
1922
  "--messages-handle \"<phone_or_apple_id>\" if local Messages is being connected",
1553
1923
  "--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
1924
  "--granola-api-key \"<granola_key>\" if Granola is being connected",
1555
1925
  ],
1556
- statusCommand: `${command} agent --status`,
1926
+ statusCommand: `${command} status`,
1927
+ agentStatusCommand: `${command} agent --status`,
1557
1928
  messagesChatsCommand: `${command} messages-chats`,
1558
1929
  messagesPermissions: {
1559
1930
  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.",
@@ -1583,8 +1954,20 @@ Use this as a public checklist for coding-agent onboarding to Shepherd raw sync.
1583
1954
 
1584
1955
  Confirm onboarding choices and fields directly with the user instead of assuming them from the local repo, shell username, machine paths, or earlier conversations.
1585
1956
 
1957
+ Do not inspect the user's folders or repositories for Shepherd setup. Do not run ls, find, rg, grep, cat, Read, Glob, or Explore against the user's home directory, repos, ~/.codex, ~/.claude, or ~/.shepherd. Use the askshepherd npm commands below; those commands perform the bounded local checks.
1958
+
1586
1959
  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
1960
 
1961
+ Common user requests:
1962
+ - If the user asks "Check what I've enabled for Shepherd?", run:
1963
+ ${payload.checkCommand}
1964
+ Then summarize connected sources, local background agents, unreadable folders, queued items, and the next command to fix anything missing.
1965
+ - 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:
1966
+ ${payload.loginCommand}
1967
+ ${payload.addSourcesCommand}
1968
+ ${payload.continueCommand}
1969
+ ${payload.checkCommand}
1970
+
1588
1971
  Start with selection questions to determine intent:
1589
1972
  1. Organization: Join existing org, or Create new org.
1590
1973
  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.
@@ -1661,6 +2044,7 @@ After the current modality is complete, run:
1661
2044
  Omit either optional flag if that source is not being connected.
1662
2045
 
1663
2046
  If Coding Sessions was selected, the continue step installs local Codex/Claude Code session summary sync. It probes ~/.codex and ~/.claude, redacts sensitive strings, and uploads bounded summaries and work metadata rather than full transcripts. It usually does not need Full Disk Access unless macOS denies access to those folders.
2047
+ The coding agent must not probe those folders directly; only the askshepherd CLI may perform that local check.
1664
2048
 
1665
2049
  Check progress with:
1666
2050
  ${payload.statusCommand}
@@ -2509,6 +2893,7 @@ function readLaunchctlPrint(domainLabel) {
2509
2893
  try {
2510
2894
  return execFileSync("launchctl", ["print", domainLabel], {
2511
2895
  encoding: "utf8",
2896
+ stdio: ["ignore", "pipe", "pipe"],
2512
2897
  timeout: 5_000,
2513
2898
  });
2514
2899
  } catch (err) {
@@ -4317,15 +4702,36 @@ function extractCodexCommands(payloads) {
4317
4702
  function extractClaudeCommands(lines) {
4318
4703
  const commands = [];
4319
4704
  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 });
4705
+ for (const command of extractCommandStrings(line.toolUseResult ?? line.message?.content ?? line)) {
4706
+ if (isLikelyShellCommand(command)) {
4707
+ commands.push({ command: redactText(command, 600), exitCode: null, summary: null });
4708
+ }
4324
4709
  }
4325
4710
  }
4326
4711
  return commands.slice(-100);
4327
4712
  }
4328
4713
 
4714
+ function extractCommandStrings(value, depth = 0) {
4715
+ if (depth > 6 || value == null) return [];
4716
+ if (typeof value === "string") return [];
4717
+ if (Array.isArray(value)) return value.flatMap((item) => extractCommandStrings(item, depth + 1));
4718
+ if (typeof value !== "object") return [];
4719
+
4720
+ const commands = [];
4721
+ for (const [key, nested] of Object.entries(value)) {
4722
+ if ((key === "command" || key === "input") && typeof nested === "string" && nested.trim()) {
4723
+ commands.push(nested);
4724
+ continue;
4725
+ }
4726
+ commands.push(...extractCommandStrings(nested, depth + 1));
4727
+ }
4728
+ return commands;
4729
+ }
4730
+
4731
+ function isLikelyShellCommand(value) {
4732
+ return /\b(?:git|npm|pnpm|yarn|bun|pytest|vitest|cargo|go|python|node|tsc|ruff|eslint|make)\b/.test(value);
4733
+ }
4734
+
4329
4735
  async function repoMetadata(cwd) {
4330
4736
  const base = { fullName: null, remote: null, branch: null, commit: null };
4331
4737
  if (!cwd) return base;
@@ -4474,20 +4880,12 @@ function redactText(value, maxLength = 1000) {
4474
4880
  .replace(/-----BEGIN [^-]+PRIVATE KEY-----[\s\S]*?-----END [^-]+PRIVATE KEY-----/g, "[redacted-private-key]")
4475
4881
  .replace(/\b(?:sk|pk|rk|ghp|github_pat|xox[baprs])_[A-Za-z0-9_=-]{12,}\b/g, "[redacted-token]")
4476
4882
  .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]")
4883
+ .replace(/\b(authorization|x-api-key|api[_-]?key|token|secret|password)(\s*[:=]\s*)['"]?[^'"\s]+/gi, "$1$2[redacted]")
4478
4884
  .replace(/(OPENAI_API_KEY|FIREWORKS_API_KEY|ANTHROPIC_API_KEY|DATABASE_URL|REDIS_URL)=\S+/g, "$1=[redacted]")
4479
4885
  .replace(new RegExp(homedir().replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"), "~")
4480
4886
  .slice(0, maxLength);
4481
4887
  }
4482
4888
 
4483
- function unescapeJsonString(value) {
4484
- try {
4485
- return JSON.parse(`"${value.replace(/"/g, '\\"')}"`);
4486
- } catch {
4487
- return value;
4488
- }
4489
- }
4490
-
4491
4889
  function hashString(value) {
4492
4890
  return createHash("sha256").update(String(value ?? "")).digest("hex");
4493
4891
  }
@@ -4515,6 +4913,35 @@ function codingSessionsStatusFile(userId) {
4515
4913
  return path;
4516
4914
  }
4517
4915
 
4916
+ async function messagesConfigPathForUser(userId) {
4917
+ if (!userId) return null;
4918
+ const path = join(homedir(), ".shepherd", "raw-messages", `${userId}.json`);
4919
+ return existsSync(path) ? path : null;
4920
+ }
4921
+
4922
+ async function codingSessionsConfigPathForUser(userId) {
4923
+ if (!userId) return null;
4924
+ const path = join(homedir(), ".shepherd", "coding-sessions", `${userId}.json`);
4925
+ return existsSync(path) ? path : null;
4926
+ }
4927
+
4928
+ async function latestMessagesConfigPath() {
4929
+ const dir = join(homedir(), ".shepherd", "raw-messages");
4930
+ try {
4931
+ const entries = await readdir(dir, { withFileTypes: true });
4932
+ const files = [];
4933
+ for (const entry of entries) {
4934
+ if (!entry.isFile() || !entry.name.endsWith(".json") || entry.name.includes("-queue")) continue;
4935
+ const path = join(dir, entry.name);
4936
+ const info = await stat(path).catch(() => null);
4937
+ if (info) files.push({ path, mtimeMs: info.mtimeMs });
4938
+ }
4939
+ return files.sort((a, b) => b.mtimeMs - a.mtimeMs)[0]?.path ?? null;
4940
+ } catch {
4941
+ return null;
4942
+ }
4943
+ }
4944
+
4518
4945
  async function latestCodingSessionsConfigPath() {
4519
4946
  const dir = join(homedir(), ".shepherd", "coding-sessions");
4520
4947
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "askshepherd",
3
- "version": "0.1.36",
3
+ "version": "0.1.38",
4
4
  "description": "Customer-facing Shepherd production onboarding and MCP CLI",
5
5
  "type": "module",
6
6
  "bin": {