askshepherd 0.1.38 → 0.1.40

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
@@ -75,7 +75,8 @@ above; it performs the bounded local checks.
75
75
 
76
76
  ## Set Up Coding Agent Sessions
77
77
 
78
- Use this when the user asks "Help me set up coding agent sessions":
78
+ Use this when the user asks "Help me set up coding agent sessions" or "Enable
79
+ coding agent sessions locally for Shepherd":
79
80
 
80
81
  ```sh
81
82
  npx -y askshepherd@latest agent --login
@@ -120,9 +121,10 @@ The saved MCP state includes:
120
121
  The installed MCP server is local npm first, remote brain second. For questions
121
122
  like "what do I have set up on Shepherd?", "is Shepherd syncing?", or "help me
122
123
  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.
124
+ `shepherd_status`, `shepherd_setup_coding_sessions`, and
125
+ `shepherd_enable_coding_sessions` that route agents to the local
126
+ `askshepherd status` / add-source flow. Production memory and wiki tools remain
127
+ remote Railway-backed tools for source recall and company-memory answers.
126
128
  Those local MCP tools are also the permission boundary: an MCP client should not
127
129
  use shell or file tools to inspect the user's folders or repositories for setup.
128
130
 
@@ -2,7 +2,7 @@
2
2
  import { execFile, execFileSync, spawn } from "node:child_process";
3
3
  import { createHash } from "node:crypto";
4
4
  import { constants as fsConstants, existsSync, mkdirSync, readFileSync, unlinkSync, watch, writeFileSync } from "node:fs";
5
- import { access, mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
5
+ import { access, chmod, mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
6
6
  import { createServer } from "node:http";
7
7
  import { homedir, platform } from "node:os";
8
8
  import { basename, dirname, join } from "node:path";
@@ -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.37";
15
+ const PACKAGE_VERSION = "0.1.40";
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");
@@ -133,6 +133,12 @@ async function dispatch() {
133
133
  await runMessagesChatsCommand();
134
134
  } else if (command === "messages-agent") {
135
135
  await runMessagesAgent();
136
+ } else if (command === "write-agent-state") {
137
+ await runWriteAgentState();
138
+ } else if (command === "write-messages-config") {
139
+ await runWriteMessagesConfig();
140
+ } else if (command === "install-messages-agent") {
141
+ await runInstallMessagesAgent();
136
142
  } else if (command === "coding-sessions-agent") {
137
143
  await runCodingSessionsAgent();
138
144
  } else if (command === "coding-sessions-status") {
@@ -822,13 +828,20 @@ function localMcpTools() {
822
828
  annotations: readOnlyAnnotations,
823
829
  _meta: { provider: "local_npm", command: `${agentCommand()} agent --add-sources coding-sessions` },
824
830
  },
831
+ {
832
+ name: "shepherd_enable_coding_sessions",
833
+ description: "Alias for shepherd_setup_coding_sessions. Use when the user asks to enable coding agent sessions locally for Shepherd. Return the local askshepherd npm setup commands; do not search, list, or read the user's folders or repos.",
834
+ inputSchema: emptyInputSchema,
835
+ annotations: readOnlyAnnotations,
836
+ _meta: { provider: "local_npm", command: `${agentCommand()} agent --add-sources coding-sessions` },
837
+ },
825
838
  ];
826
839
  }
827
840
 
828
841
  function localMcpInstructions(remoteInstructions, remoteConnectError) {
829
842
  return [
830
843
  "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.`,
844
+ `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 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
845
  "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
846
  `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
847
  "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.",
@@ -849,7 +862,7 @@ async function callLocalMcpTool(name) {
849
862
  ].join("\n\n"));
850
863
  }
851
864
 
852
- if (name === "shepherd_setup_coding_sessions") {
865
+ if (name === "shepherd_setup_coding_sessions" || name === "shepherd_enable_coding_sessions") {
853
866
  const status = await collectShepherdStatus();
854
867
  return localMcpTextResult(renderCodingSessionsSetupMcpResult(status));
855
868
  }
@@ -916,9 +929,18 @@ async function writeMcpState(state) {
916
929
  const path = mcpStatePath();
917
930
  await mkdir(dirname(path), { recursive: true });
918
931
  await writeFile(path, JSON.stringify(state, null, 2), { mode: 0o600 });
932
+ await chmod(path, 0o600);
919
933
  return path;
920
934
  }
921
935
 
936
+ function sanitizeUserFileId(userId) {
937
+ const safeId = String(userId ?? "").replace(/[^a-zA-Z0-9._-]/g, "-");
938
+ if (!safeId || safeId === "." || safeId === ".." || /^\.+$/.test(safeId)) {
939
+ throw new Error("Onboarding returned an invalid user ID for local Messages config.");
940
+ }
941
+ return safeId;
942
+ }
943
+
922
944
  function mcpStatePath() {
923
945
  return expandHomePath(stringArg("state") ?? DEFAULT_MCP_STATE_PATH);
924
946
  }
@@ -1338,6 +1360,96 @@ function renderLocalCodingSessionsStatus(status) {
1338
1360
  return lines;
1339
1361
  }
1340
1362
 
1363
+ async function runWriteAgentState() {
1364
+ const input = await readJsonInput();
1365
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
1366
+ throw new Error("write-agent-state expects a JSON object on stdin.");
1367
+ }
1368
+ const previous = await readOptionalAgentState();
1369
+ const statePath = await writeAgentState({ ...(previous ?? {}), ...input });
1370
+ console.log(JSON.stringify({ statePath }, null, 2));
1371
+ }
1372
+
1373
+ async function runWriteMessagesConfig() {
1374
+ const input = await readJsonInput();
1375
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
1376
+ throw new Error("write-messages-config expects a JSON object on stdin.");
1377
+ }
1378
+ const configPath = await writeMessagesConfig({
1379
+ apiUrl: trimTrailingSlash(requiredConfigString(input.apiUrl, "apiUrl")),
1380
+ userId: requiredConfigString(input.userId, "userId"),
1381
+ agentToken: requiredConfigString(input.agentToken, "agentToken"),
1382
+ backfillDays: parseBackfillDays(input.backfillDays, null),
1383
+ allowedChatIds: input.allowedChatIds,
1384
+ selectedChats: Array.isArray(input.selectedChats) ? input.selectedChats : [],
1385
+ });
1386
+ console.log(JSON.stringify({ configPath }, null, 2));
1387
+ }
1388
+
1389
+ async function runInstallMessagesAgent() {
1390
+ const configPath = stringArg("config");
1391
+ if (!configPath) throw new Error("install-messages-agent requires --config <path>");
1392
+ let config;
1393
+ try {
1394
+ config = JSON.parse(await readFile(configPath, "utf8"));
1395
+ } catch (err) {
1396
+ if (err && typeof err === "object" && "code" in err) throw err;
1397
+ throw new Error(`install-messages-agent: config file at ${configPath} does not contain valid JSON.`);
1398
+ }
1399
+ const userId = stringArg("user-id") ?? requiredConfigString(config.userId, "userId");
1400
+ const overrides = {
1401
+ programArguments: parseJsonArrayArg("program"),
1402
+ environment: parseJsonObjectArg("env"),
1403
+ };
1404
+
1405
+ if (args["dry-run"]) {
1406
+ console.log(JSON.stringify(buildMessagesAgentInstall(configPath, userId, overrides), null, 2));
1407
+ return;
1408
+ }
1409
+
1410
+ const install = await installMessagesAgent(configPath, userId, overrides);
1411
+ console.log(JSON.stringify(install, null, 2));
1412
+ }
1413
+
1414
+ async function readJsonInput() {
1415
+ const chunks = [];
1416
+ for await (const chunk of process.stdin) chunks.push(chunk);
1417
+ const text = Buffer.concat(chunks).toString("utf8").trim();
1418
+ if (!text) throw new Error("Expected JSON input on stdin.");
1419
+ return JSON.parse(text);
1420
+ }
1421
+
1422
+ function parseJsonArrayArg(name) {
1423
+ const raw = stringArg(name);
1424
+ if (!raw) return undefined;
1425
+ let parsed;
1426
+ try {
1427
+ parsed = JSON.parse(raw);
1428
+ } catch {
1429
+ throw new Error(`--${name} must be a JSON array of strings.`);
1430
+ }
1431
+ if (!Array.isArray(parsed) || parsed.some((value) => typeof value !== "string")) {
1432
+ throw new Error(`--${name} must be a JSON array of strings.`);
1433
+ }
1434
+ return parsed;
1435
+ }
1436
+
1437
+ function parseJsonObjectArg(name) {
1438
+ const raw = stringArg(name);
1439
+ if (!raw) return undefined;
1440
+ let parsed;
1441
+ try {
1442
+ parsed = JSON.parse(raw);
1443
+ } catch {
1444
+ throw new Error(`--${name} must be a JSON object of string values.`);
1445
+ }
1446
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)
1447
+ || Object.values(parsed).some((value) => typeof value !== "string")) {
1448
+ throw new Error(`--${name} must be a JSON object of string values.`);
1449
+ }
1450
+ return parsed;
1451
+ }
1452
+
1341
1453
  async function runMessagesChatsCommand() {
1342
1454
  await ensureMessagesReadPermission({ noOpen: Boolean(args["no-open"]) });
1343
1455
  const chats = await listRecentMessageChats({
@@ -1752,6 +1864,31 @@ Options:
1752
1864
  return;
1753
1865
  }
1754
1866
 
1867
+ if (which === "write-agent-state" || which === "write-messages-config" || which === "install-messages-agent") {
1868
+ console.log(`Shepherd onboarding engine commands
1869
+
1870
+ These non-interactive commands are used by GUI onboarding apps (for example the
1871
+ Shepherd macOS app) so that this CLI stays the single owner of Shepherd state
1872
+ file schemas and the launchd install flow.
1873
+
1874
+ Usage:
1875
+ shepherd-onboard write-agent-state JSON object on stdin is merged into ~/.shepherd/raw-onboarding-agent.json. Prints {statePath}.
1876
+ shepherd-onboard write-messages-config JSON object on stdin ({apiUrl, userId, agentToken, backfillDays?, allowedChatIds, selectedChats?}) is written to ~/.shepherd/raw-messages/<userId>.json. Prints {configPath}.
1877
+ shepherd-onboard install-messages-agent --config <path>
1878
+ Installs and verifies the Messages launchd agent for an existing config. Prints install metadata.
1879
+
1880
+ install-messages-agent options:
1881
+ --config <path> Messages agent config created by onboarding. Required.
1882
+ --user-id <id> Override the user ID. Defaults to the config's userId.
1883
+ --program <json_array> Replace the default npx launcher with custom ProgramArguments (e.g. a signed app binary). --config <path> is appended.
1884
+ --env <json_object> Extra EnvironmentVariables merged into the launchd plist (e.g. ELECTRON_RUN_AS_NODE).
1885
+ --dry-run Print the launchd plist and paths without writing or loading anything.
1886
+ --no-permission-prompt Fail instead of prompting when Full Disk Access is missing.
1887
+ --help Show this help.
1888
+ `);
1889
+ return;
1890
+ }
1891
+
1755
1892
  if (which === "mcp") {
1756
1893
  console.log(`Shepherd MCP stdio proxy
1757
1894
 
@@ -2142,6 +2279,9 @@ async function writeAgentState(state) {
2142
2279
  const path = agentStatePath();
2143
2280
  await mkdir(dirname(path), { recursive: true });
2144
2281
  await writeFile(path, JSON.stringify(state, null, 2), { mode: 0o600 });
2282
+ // writeFile's mode only applies on creation; an existing file keeps its
2283
+ // permissions, so enforce them on every token write.
2284
+ await chmod(path, 0o600);
2145
2285
  return path;
2146
2286
  }
2147
2287
 
@@ -2653,7 +2793,11 @@ function headers(token) {
2653
2793
  async function writeMessagesConfig(input) {
2654
2794
  const dir = join(homedir(), ".shepherd", "raw-messages");
2655
2795
  await mkdir(dir, { recursive: true });
2656
- const path = join(dir, `${input.userId}.json`);
2796
+ // The userId reaches this path from the network (server-issued session id)
2797
+ // and, via write-messages-config, from stdin; sanitize it like the launchd
2798
+ // label does so it can never traverse outside the raw-messages directory.
2799
+ const safeId = sanitizeUserFileId(input.userId);
2800
+ const path = join(dir, `${safeId}.json`);
2657
2801
  const allowedChatIds = parseAllowedChatIds(input.allowedChatIds);
2658
2802
  const allChats = selectedChatIdsIncludeAll(allowedChatIds);
2659
2803
  if (!allChats && allowedChatIds.length === 0) {
@@ -2674,25 +2818,34 @@ async function writeMessagesConfig(input) {
2674
2818
  }, null, 2),
2675
2819
  { mode: 0o600 },
2676
2820
  );
2821
+ await chmod(path, 0o600);
2677
2822
  return path;
2678
2823
  }
2679
2824
 
2680
- async function installMessagesAgent(configPath, userId) {
2681
- if (platform() !== "darwin") {
2682
- throw new Error("automatic local Messages sync is only supported on macOS");
2683
- }
2684
-
2825
+ function buildMessagesAgentInstall(configPath, userId, overrides = {}) {
2685
2826
  const safeId = userId.replace(/[^a-zA-Z0-9.-]/g, "-");
2686
2827
  const label = `ai.shepherd.raw-messages.${safeId}`;
2687
2828
  const rawDir = join(homedir(), ".shepherd", "raw-messages");
2688
2829
  const agentsDir = join(homedir(), "Library", "LaunchAgents");
2689
- await mkdir(rawDir, { recursive: true });
2690
- await mkdir(agentsDir, { recursive: true });
2691
-
2692
2830
  const plistPath = join(agentsDir, `${label}.plist`);
2693
2831
  const stdoutPath = join(rawDir, `${safeId}.out.log`);
2694
2832
  const stderrPath = join(rawDir, `${safeId}.err.log`);
2695
- const launchPath = launchAgentPath();
2833
+
2834
+ const programPrefix = Array.isArray(overrides.programArguments) && overrides.programArguments.length > 0
2835
+ ? overrides.programArguments
2836
+ : ["/usr/bin/env", "npx", "-y", PACKAGE_SPEC, "messages-agent"];
2837
+ const programArguments = [...programPrefix, "--config", configPath];
2838
+ const environment = {
2839
+ PATH: launchAgentPath(),
2840
+ ...stringRecord(overrides.environment),
2841
+ };
2842
+
2843
+ const programArgumentsXml = programArguments
2844
+ .map((value) => ` <string>${xmlEscape(value)}</string>`)
2845
+ .join("\n");
2846
+ const environmentXml = Object.entries(environment)
2847
+ .map(([key, value]) => ` <key>${xmlEscape(key)}</key>\n <string>${xmlEscape(value)}</string>`)
2848
+ .join("\n");
2696
2849
 
2697
2850
  const plist = `<?xml version="1.0" encoding="UTF-8"?>
2698
2851
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -2702,32 +2855,43 @@ async function installMessagesAgent(configPath, userId) {
2702
2855
  <string>${xmlEscape(label)}</string>
2703
2856
  <key>ProgramArguments</key>
2704
2857
  <array>
2705
- <string>/usr/bin/env</string>
2706
- <string>npx</string>
2707
- <string>-y</string>
2708
- <string>${PACKAGE_SPEC}</string>
2709
- <string>messages-agent</string>
2710
- <string>--config</string>
2711
- <string>${xmlEscape(configPath)}</string>
2858
+ ${programArgumentsXml}
2712
2859
  </array>
2713
2860
  <key>KeepAlive</key>
2714
2861
  <true/>
2715
2862
  <key>RunAtLoad</key>
2716
2863
  <true/>
2864
+ <key>ThrottleInterval</key>
2865
+ <integer>10</integer>
2866
+ <key>WorkingDirectory</key>
2867
+ <string>${xmlEscape(rawDir)}</string>
2717
2868
  <key>StandardOutPath</key>
2718
2869
  <string>${xmlEscape(stdoutPath)}</string>
2719
2870
  <key>StandardErrorPath</key>
2720
2871
  <string>${xmlEscape(stderrPath)}</string>
2721
2872
  <key>EnvironmentVariables</key>
2722
2873
  <dict>
2723
- <key>PATH</key>
2724
- <string>${xmlEscape(launchPath)}</string>
2874
+ ${environmentXml}
2725
2875
  </dict>
2726
2876
  </dict>
2727
2877
  </plist>
2728
2878
  `;
2729
2879
 
2880
+ return { label, rawDir, agentsDir, plistPath, stdoutPath, stderrPath, programArguments, environment, plist };
2881
+ }
2882
+
2883
+ async function installMessagesAgent(configPath, userId, overrides = {}) {
2884
+ if (platform() !== "darwin") {
2885
+ throw new Error("automatic local Messages sync is only supported on macOS");
2886
+ }
2887
+
2888
+ const install = buildMessagesAgentInstall(configPath, userId, overrides);
2889
+ const { label, rawDir, agentsDir, plistPath, stdoutPath, stderrPath, plist } = install;
2890
+ await mkdir(rawDir, { recursive: true });
2891
+ await mkdir(agentsDir, { recursive: true });
2892
+
2730
2893
  await writeFile(plistPath, plist, { mode: 0o600 });
2894
+ await chmod(plistPath, 0o600);
2731
2895
  while (true) {
2732
2896
  const stdoutOffset = await fileLength(stdoutPath);
2733
2897
  const stderrOffset = await fileLength(stderrPath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "askshepherd",
3
- "version": "0.1.38",
3
+ "version": "0.1.40",
4
4
  "description": "Customer-facing Shepherd production onboarding and MCP CLI",
5
5
  "type": "module",
6
6
  "bin": {