askshepherd 0.1.41 → 0.1.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,18 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
  import { execFile, execFileSync, spawn } from "node:child_process";
3
3
  import { createHash } from "node:crypto";
4
- import { constants as fsConstants, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, unlinkSync, watch, writeFileSync } from "node:fs";
4
+ import { constants as fsConstants, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, unlinkSync, watch, writeFileSync } from "node:fs";
5
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";
9
9
  import readline from "node:readline";
10
10
  import { fileURLToPath } from "node:url";
11
+ import sourceSelectionHelpers from "../source-selection.cjs";
12
+
13
+ const { sourceSelectionFromSession } = sourceSelectionHelpers;
11
14
 
12
15
  const DEFAULT_API_URL = "https://brain-api-customer-facing.up.railway.app";
13
16
  const PACKAGE_NAME = "askshepherd";
14
17
  const PACKAGE_SPEC = `${PACKAGE_NAME}@latest`;
15
- const PACKAGE_VERSION = "0.1.40";
18
+ const PACKAGE_VERSION = "0.1.42";
16
19
  const MCP_SERVER_NAME = "shepherd";
17
20
  const PACKAGE_DIR = dirname(dirname(fileURLToPath(import.meta.url)));
18
21
  const DEFAULT_AGENT_STATE_PATH = join(homedir(), ".shepherd", "raw-onboarding-agent.json");
@@ -115,18 +118,30 @@ const command = rawArgv[0] && !rawArgv[0].startsWith("--") ? rawArgv[0] : "onboa
115
118
  const args = parseArgs(command === "onboard" ? rawArgv : rawArgv.slice(1));
116
119
  let messagesPermissionNoticePrinted = false;
117
120
 
118
- if (command === "help" || args.help) {
119
- printHelp(command === "help" ? "onboard" : command);
120
- process.exit(0);
121
+ if (isCliEntrypoint()) {
122
+ if (command === "help" || args.help) {
123
+ printHelp(command === "help" ? "onboard" : command);
124
+ process.exit(0);
125
+ }
126
+
127
+ void dispatch().catch((err) => {
128
+ console.error(`\nShepherd onboarding failed: ${safeError(err)}`);
129
+ if (args.debug === true) {
130
+ console.error(rawErrorDetails(err));
131
+ }
132
+ process.exit(1);
133
+ });
121
134
  }
122
135
 
123
- void dispatch().catch((err) => {
124
- console.error(`\nShepherd onboarding failed: ${safeError(err)}`);
125
- if (args.debug === true) {
126
- console.error(rawErrorDetails(err));
136
+ function isCliEntrypoint() {
137
+ if (!process.argv[1]) return false;
138
+ const modulePath = fileURLToPath(import.meta.url);
139
+ try {
140
+ return realpathSync(modulePath) === realpathSync(process.argv[1]);
141
+ } catch {
142
+ return modulePath === process.argv[1];
127
143
  }
128
- process.exit(1);
129
- });
144
+ }
130
145
 
131
146
  async function dispatch() {
132
147
  if (command === "onboard") {
@@ -153,6 +168,10 @@ async function dispatch() {
153
168
  await runWriteMessagesConfig();
154
169
  } else if (command === "install-messages-agent") {
155
170
  await runInstallMessagesAgent();
171
+ } else if (command === "write-coding-sessions-config") {
172
+ await runWriteCodingSessionsConfig();
173
+ } else if (command === "install-coding-sessions-agent") {
174
+ await runInstallCodingSessionsAgent();
156
175
  } else if (command === "coding-sessions-agent") {
157
176
  await runCodingSessionsAgent();
158
177
  } else if (command === "coding-sessions-status") {
@@ -1412,6 +1431,7 @@ async function runWriteMessagesConfig() {
1412
1431
  apiUrl: trimTrailingSlash(requiredConfigString(input.apiUrl, "apiUrl")),
1413
1432
  userId: requiredConfigString(input.userId, "userId"),
1414
1433
  agentToken: requiredConfigString(input.agentToken, "agentToken"),
1434
+ handle: optionalString(input.handle),
1415
1435
  backfillDays: parseBackfillDays(input.backfillDays, null),
1416
1436
  allowedChatIds: input.allowedChatIds,
1417
1437
  selectedChats: Array.isArray(input.selectedChats) ? input.selectedChats : [],
@@ -1419,6 +1439,20 @@ async function runWriteMessagesConfig() {
1419
1439
  console.log(JSON.stringify({ configPath }, null, 2));
1420
1440
  }
1421
1441
 
1442
+ async function runWriteCodingSessionsConfig() {
1443
+ const input = await readJsonInput();
1444
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
1445
+ throw new Error("write-coding-sessions-config expects a JSON object on stdin.");
1446
+ }
1447
+ const configPath = await writeCodingSessionsConfig({
1448
+ apiUrl: trimTrailingSlash(requiredConfigString(input.apiUrl, "apiUrl")),
1449
+ userId: requiredConfigString(input.userId, "userId"),
1450
+ agentToken: requiredConfigString(input.agentToken, "agentToken"),
1451
+ intervalSeconds: Number(input.intervalSeconds ?? args["coding-sessions-interval-seconds"] ?? 60),
1452
+ });
1453
+ console.log(JSON.stringify({ configPath }, null, 2));
1454
+ }
1455
+
1422
1456
  async function runInstallMessagesAgent() {
1423
1457
  const configPath = stringArg("config");
1424
1458
  if (!configPath) throw new Error("install-messages-agent requires --config <path>");
@@ -1444,6 +1478,31 @@ async function runInstallMessagesAgent() {
1444
1478
  console.log(JSON.stringify(install, null, 2));
1445
1479
  }
1446
1480
 
1481
+ async function runInstallCodingSessionsAgent() {
1482
+ const configPath = stringArg("config");
1483
+ if (!configPath) throw new Error("install-coding-sessions-agent requires --config <path>");
1484
+ let config;
1485
+ try {
1486
+ config = JSON.parse(await readFile(configPath, "utf8"));
1487
+ } catch (err) {
1488
+ if (err && typeof err === "object" && "code" in err) throw err;
1489
+ throw new Error(`install-coding-sessions-agent: config file at ${configPath} does not contain valid JSON.`);
1490
+ }
1491
+ const userId = stringArg("user-id") ?? requiredConfigString(config.userId, "userId");
1492
+ const overrides = {
1493
+ programArguments: parseJsonArrayArg("program"),
1494
+ environment: parseJsonObjectArg("env"),
1495
+ };
1496
+
1497
+ if (args["dry-run"]) {
1498
+ console.log(JSON.stringify(buildCodingSessionsAgentInstall(configPath, userId, overrides), null, 2));
1499
+ return;
1500
+ }
1501
+
1502
+ const install = await installCodingSessionsAgent(configPath, userId, overrides);
1503
+ console.log(JSON.stringify(install, null, 2));
1504
+ }
1505
+
1447
1506
  async function readJsonInput() {
1448
1507
  const chunks = [];
1449
1508
  for await (const chunk of process.stdin) chunks.push(chunk);
@@ -1932,7 +1991,7 @@ Options:
1932
1991
  return;
1933
1992
  }
1934
1993
 
1935
- if (which === "write-agent-state" || which === "write-messages-config" || which === "install-messages-agent") {
1994
+ if (which === "write-agent-state" || which === "write-messages-config" || which === "install-messages-agent" || which === "write-coding-sessions-config" || which === "install-coding-sessions-agent") {
1936
1995
  console.log(`Shepherd onboarding engine commands
1937
1996
 
1938
1997
  These non-interactive commands are used by GUI onboarding apps (for example the
@@ -1944,9 +2003,12 @@ Usage:
1944
2003
  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}.
1945
2004
  shepherd-onboard install-messages-agent --config <path>
1946
2005
  Installs and verifies the Messages launchd agent for an existing config. Prints install metadata.
2006
+ shepherd-onboard write-coding-sessions-config JSON object on stdin ({apiUrl, userId, agentToken, intervalSeconds?}) is written to ~/.shepherd/coding-sessions/<userId>.json. Prints {configPath}.
2007
+ shepherd-onboard install-coding-sessions-agent --config <path>
2008
+ Installs and verifies the Coding Sessions launchd agent for an existing config. Prints install metadata.
1947
2009
 
1948
- install-messages-agent options:
1949
- --config <path> Messages agent config created by onboarding. Required.
2010
+ install agent options:
2011
+ --config <path> Agent config created by onboarding. Required.
1950
2012
  --user-id <id> Override the user ID. Defaults to the config's userId.
1951
2013
  --program <json_array> Replace the default npx launcher with custom ProgramArguments (e.g. a signed app binary). --config <path> is appended.
1952
2014
  --env <json_object> Extra EnvironmentVariables merged into the launchd plist (e.g. ELECTRON_RUN_AS_NODE).
@@ -2294,23 +2356,6 @@ function selectedSources() {
2294
2356
  return selected;
2295
2357
  }
2296
2358
 
2297
- function sourceSelectionFromSession(response, fallback) {
2298
- const raw = response?.sources;
2299
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
2300
- return fallback ? { ...fallback } : null;
2301
- }
2302
- return {
2303
- google: raw.google === true,
2304
- slack: raw.slack === true,
2305
- github: raw.github === true,
2306
- granola: raw.granola === true,
2307
- messages: raw.messages === true,
2308
- discord: raw.discord === true,
2309
- instagram: raw.instagram === true,
2310
- codingSessions: raw.codingSessions === true,
2311
- };
2312
- }
2313
-
2314
2359
  function sourceSelectionFromList(value) {
2315
2360
  const selected = {
2316
2361
  google: false,
@@ -2927,6 +2972,7 @@ async function writeMessagesConfig(input) {
2927
2972
  apiUrl: input.apiUrl,
2928
2973
  userId: input.userId,
2929
2974
  agentToken: input.agentToken,
2975
+ ...(input.handle ? { handle: input.handle } : {}),
2930
2976
  backfillDays: input.backfillDays,
2931
2977
  allChats,
2932
2978
  allowedChatIds: allChats ? [] : allowedChatIds,
@@ -3057,22 +3103,29 @@ async function writeCodingSessionsConfig(input) {
3057
3103
  return path;
3058
3104
  }
3059
3105
 
3060
- async function installCodingSessionsAgent(configPath, userId) {
3061
- if (platform() !== "darwin") {
3062
- throw new Error("automatic local coding-session sync is only supported on macOS");
3063
- }
3064
-
3106
+ function buildCodingSessionsAgentInstall(configPath, userId, overrides = {}) {
3065
3107
  const safeId = safeFileId(userId);
3066
3108
  const label = `ai.shepherd.coding-sessions.${safeId}`;
3067
3109
  const rawDir = join(homedir(), ".shepherd", "coding-sessions");
3068
3110
  const agentsDir = join(homedir(), "Library", "LaunchAgents");
3069
- await mkdir(rawDir, { recursive: true });
3070
- await mkdir(agentsDir, { recursive: true });
3071
-
3072
3111
  const plistPath = join(agentsDir, `${label}.plist`);
3073
3112
  const stdoutPath = join(rawDir, `${safeId}.out.log`);
3074
3113
  const stderrPath = join(rawDir, `${safeId}.err.log`);
3075
- const launchPath = launchAgentPath();
3114
+ const programPrefix = Array.isArray(overrides.programArguments) && overrides.programArguments.length > 0
3115
+ ? overrides.programArguments
3116
+ : ["/usr/bin/env", "npx", "-y", PACKAGE_SPEC, "coding-sessions-agent"];
3117
+ const programArguments = [...programPrefix, "--config", configPath];
3118
+ const environment = {
3119
+ PATH: launchAgentPath(),
3120
+ ...stringRecord(overrides.environment),
3121
+ };
3122
+
3123
+ const programArgumentsXml = programArguments
3124
+ .map((value) => ` <string>${xmlEscape(value)}</string>`)
3125
+ .join("\n");
3126
+ const environmentXml = Object.entries(environment)
3127
+ .map(([key, value]) => ` <key>${xmlEscape(key)}</key>\n <string>${xmlEscape(value)}</string>`)
3128
+ .join("\n");
3076
3129
 
3077
3130
  const plist = `<?xml version="1.0" encoding="UTF-8"?>
3078
3131
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -3082,39 +3135,50 @@ async function installCodingSessionsAgent(configPath, userId) {
3082
3135
  <string>${xmlEscape(label)}</string>
3083
3136
  <key>ProgramArguments</key>
3084
3137
  <array>
3085
- <string>/usr/bin/env</string>
3086
- <string>npx</string>
3087
- <string>-y</string>
3088
- <string>${PACKAGE_SPEC}</string>
3089
- <string>coding-sessions-agent</string>
3090
- <string>--config</string>
3091
- <string>${xmlEscape(configPath)}</string>
3138
+ ${programArgumentsXml}
3092
3139
  </array>
3093
3140
  <key>KeepAlive</key>
3094
3141
  <true/>
3095
3142
  <key>RunAtLoad</key>
3096
3143
  <true/>
3144
+ <key>ThrottleInterval</key>
3145
+ <integer>10</integer>
3146
+ <key>WorkingDirectory</key>
3147
+ <string>${xmlEscape(rawDir)}</string>
3097
3148
  <key>StandardOutPath</key>
3098
3149
  <string>${xmlEscape(stdoutPath)}</string>
3099
3150
  <key>StandardErrorPath</key>
3100
3151
  <string>${xmlEscape(stderrPath)}</string>
3101
3152
  <key>EnvironmentVariables</key>
3102
3153
  <dict>
3103
- <key>PATH</key>
3104
- <string>${xmlEscape(launchPath)}</string>
3154
+ ${environmentXml}
3105
3155
  </dict>
3106
3156
  </dict>
3107
3157
  </plist>
3108
3158
  `;
3109
3159
 
3160
+ return { label, rawDir, agentsDir, plistPath, stdoutPath, stderrPath, programArguments, environment, plist };
3161
+ }
3162
+
3163
+ async function installCodingSessionsAgent(configPath, userId, overrides = {}) {
3164
+ if (platform() !== "darwin") {
3165
+ throw new Error("automatic local coding-session sync is only supported on macOS");
3166
+ }
3167
+
3168
+ const install = buildCodingSessionsAgentInstall(configPath, userId, overrides);
3169
+ const { label, rawDir, agentsDir, plistPath, stdoutPath, stderrPath, plist } = install;
3170
+ await mkdir(rawDir, { recursive: true });
3171
+ await mkdir(agentsDir, { recursive: true });
3172
+
3110
3173
  await writeFile(plistPath, plist, { mode: 0o600 });
3174
+ await chmod(plistPath, 0o600);
3111
3175
  const stdoutOffset = await fileLength(stdoutPath);
3112
3176
  const stderrOffset = await fileLength(stderrPath);
3113
3177
  await execFileQuiet("launchctl", ["unload", plistPath], { ignoreError: true });
3114
3178
  await execFileQuiet("launchctl", ["load", plistPath]);
3115
3179
  await execFileQuiet("launchctl", ["start", label], { ignoreError: true });
3116
3180
  await verifyCodingSessionsAgentLaunch({ label, stdoutPath, stderrPath, stdoutOffset, stderrOffset });
3117
- return { label, plistPath, stdoutPath, stderrPath };
3181
+ return install;
3118
3182
  }
3119
3183
 
3120
3184
  async function verifyCodingSessionsAgentLaunch({ label, stdoutPath, stderrPath, stdoutOffset, stderrOffset }) {
@@ -4249,10 +4313,19 @@ function messageIdentity(msg) {
4249
4313
  }
4250
4314
 
4251
4315
  function messageState(msg) {
4316
+ const participant = messageParticipant(msg);
4252
4317
  return {
4253
4318
  text: msg?.text ?? null,
4319
+ chatId: msg?.chatId ?? null,
4320
+ chatKind: messageChatKind(msg),
4321
+ participant,
4322
+ isFromMe: Boolean(msg?.isFromMe),
4254
4323
  editedAt: isoDate(msg?.editedAt),
4255
4324
  retractedAt: isoDate(msg?.retractedAt),
4325
+ replyToMessageId: msg?.replyToMessageId ?? null,
4326
+ threadRootMessageId: msg?.threadRootMessageId ?? null,
4327
+ affectedParticipant: msg?.affectedParticipant ?? null,
4328
+ kind: msg?.kind ?? null,
4256
4329
  hasAttachments: Boolean(msg?.hasAttachments ?? (Array.isArray(msg?.attachments) && msg.attachments.length > 0)),
4257
4330
  attachments: messageAttachmentState(msg),
4258
4331
  };
@@ -4260,8 +4333,16 @@ function messageState(msg) {
4260
4333
 
4261
4334
  function sameMessageState(a, b) {
4262
4335
  return a.text === b.text
4336
+ && a.chatId === b.chatId
4337
+ && a.chatKind === b.chatKind
4338
+ && a.participant === b.participant
4339
+ && a.isFromMe === b.isFromMe
4263
4340
  && a.editedAt === b.editedAt
4264
4341
  && a.retractedAt === b.retractedAt
4342
+ && a.replyToMessageId === b.replyToMessageId
4343
+ && a.threadRootMessageId === b.threadRootMessageId
4344
+ && a.affectedParticipant === b.affectedParticipant
4345
+ && a.kind === b.kind
4265
4346
  && a.hasAttachments === b.hasAttachments
4266
4347
  && stableLocalJson(a.attachments) === stableLocalJson(b.attachments);
4267
4348
  }
@@ -4699,7 +4780,7 @@ function startMessagesContactSync(sender, contactLookup, opts = {}) {
4699
4780
  const observeMessages = (messages) => {
4700
4781
  let changed = false;
4701
4782
  for (const msg of messages ?? []) {
4702
- if (rememberObservedHandle(observedHandles, contactLookup, msg?.participant)) changed = true;
4783
+ if (rememberObservedHandle(observedHandles, contactLookup, messageParticipant(msg))) changed = true;
4703
4784
  if (msg?.affectedParticipant) {
4704
4785
  if (rememberObservedHandle(observedHandles, contactLookup, msg.affectedParticipant)) changed = true;
4705
4786
  }
@@ -4784,6 +4865,8 @@ function createMessageSerializer(kit, contactLookup = emptyContactLookup()) {
4784
4865
  },
4785
4866
  serialize(msg) {
4786
4867
  const chatId = msg.chatId ?? "unknown";
4868
+ const participant = messageParticipant(msg);
4869
+ const chatKind = messageChatKind(msg);
4787
4870
  const attachments = Array.isArray(msg.attachments) ? msg.attachments : [];
4788
4871
  return {
4789
4872
  messageId: String(msg.id ?? msg.messageId ?? msg.rowId),
@@ -4791,9 +4874,9 @@ function createMessageSerializer(kit, contactLookup = emptyContactLookup()) {
4791
4874
  text: msg.text ?? null,
4792
4875
  service: msg.service ?? "iMessage",
4793
4876
  chatId,
4794
- chatKind: msg.chatKind ?? "unknown",
4877
+ chatKind,
4795
4878
  chatName: chatNames.get(chatId) ?? null,
4796
- participant: msg.participant ?? null,
4879
+ participant,
4797
4880
  isFromMe: Boolean(msg.isFromMe),
4798
4881
  createdAt: isoDate(msg.createdAt) ?? new Date().toISOString(),
4799
4882
  deliveredAt: isoDate(msg.deliveredAt),
@@ -4835,13 +4918,31 @@ function createMessageSerializer(kit, contactLookup = emptyContactLookup()) {
4835
4918
  isForwarded: Boolean(msg.isForwarded),
4836
4919
  affectedParticipant: msg.affectedParticipant ?? null,
4837
4920
  newGroupName: msg.newGroupName ?? null,
4838
- _resolved_name: msg.participant ? contactLookup.resolveName(msg.participant) : null,
4839
- _is_self_handle: msg.participant ? contactLookup.isSelfHandle(msg.participant) : false,
4921
+ _resolved_name: participant ? contactLookup.resolveName(participant) : null,
4922
+ _is_self_handle: participant ? contactLookup.isSelfHandle(participant) : false,
4840
4923
  };
4841
4924
  },
4842
4925
  };
4843
4926
  }
4844
4927
 
4928
+ function messageParticipant(msg) {
4929
+ const explicit = typeof msg?.participant === "string" ? msg.participant.trim() : "";
4930
+ if (explicit) return explicit;
4931
+ if (!isDirectMessageKind(messageChatKind(msg))) return null;
4932
+ return parseDmHandleFromChatId(msg?.chatId);
4933
+ }
4934
+
4935
+ function messageChatKind(msg) {
4936
+ const explicit = typeof msg?.chatKind === "string" ? msg.chatKind.trim() : "";
4937
+ if (explicit === "direct") return "dm";
4938
+ if (explicit) return explicit;
4939
+ return parseDmHandleFromChatId(msg?.chatId) ? "dm" : "unknown";
4940
+ }
4941
+
4942
+ function isDirectMessageKind(kind) {
4943
+ return kind === "dm";
4944
+ }
4945
+
4845
4946
  function buildContactLookup(opts = {}) {
4846
4947
  const addressBook = platform() === "darwin" ? loadContactsFromAddressBookDb() : { contacts: [], myCard: null };
4847
4948
  if (opts.userId) {
@@ -5975,6 +6076,10 @@ function requiredConfigString(value, label) {
5975
6076
  return value.trim();
5976
6077
  }
5977
6078
 
6079
+ function optionalString(value) {
6080
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
6081
+ }
6082
+
5978
6083
  function execFileQuiet(file, argv, opts = {}) {
5979
6084
  return new Promise((resolve, reject) => {
5980
6085
  execFile(file, argv, { windowsHide: true }, (error) => {
@@ -6040,3 +6145,10 @@ function rawErrorDetails(err) {
6040
6145
  if (err instanceof Error) return err.stack ?? err.message;
6041
6146
  return String(err);
6042
6147
  }
6148
+
6149
+ export const __test = {
6150
+ messageChatKind,
6151
+ messageParticipant,
6152
+ messageState,
6153
+ sameMessageState,
6154
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "askshepherd",
3
- "version": "0.1.41",
3
+ "version": "0.1.42",
4
4
  "description": "Customer-facing Shepherd production onboarding and MCP CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,6 +15,7 @@
15
15
  "files": [
16
16
  "assets",
17
17
  "bin",
18
+ "source-selection.cjs",
18
19
  "README.md"
19
20
  ],
20
21
  "engines": {
@@ -0,0 +1,18 @@
1
+ function sourceSelectionFromSession(response, fallback) {
2
+ const raw = response?.sources;
3
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
4
+ return fallback ? { ...fallback } : null;
5
+ }
6
+ return {
7
+ google: raw.google === true,
8
+ slack: raw.slack === true,
9
+ github: raw.github === true,
10
+ granola: raw.granola === true,
11
+ messages: raw.messages === true,
12
+ discord: raw.discord === true,
13
+ instagram: raw.instagram === true,
14
+ codingSessions: raw.codingSessions === true,
15
+ };
16
+ }
17
+
18
+ module.exports = { sourceSelectionFromSession };