@korso/shepherd 0.9.1 → 0.11.0

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.
@@ -4,8 +4,11 @@ import {
4
4
  appendFileSync,
5
5
  mkdirSync as mkdirSync2,
6
6
  readFileSync as readFileSync3,
7
+ readdirSync,
7
8
  renameSync,
8
9
  rmSync as rmSync3,
10
+ statSync,
11
+ writeFileSync as writeFileSync3,
9
12
  existsSync as existsSync3
10
13
  } from "node:fs";
11
14
  import { homedir as homedir2, tmpdir as tmpdir2 } from "node:os";
@@ -123,11 +126,79 @@ function defaultInboxDir() {
123
126
  if (!base) base = tmpdir2();
124
127
  return join3(base, ".shepherd", "inbox");
125
128
  }
129
+ function cwdHash(cwd) {
130
+ let normalized = resolve3(cwd);
131
+ if (process.platform === "win32") normalized = normalized.toLowerCase();
132
+ return createHash2("sha256").update(normalized).digest("hex").slice(0, 16);
133
+ }
126
134
  function inboxFilePath(dir, cwd) {
135
+ return join3(dir, `${cwdHash(cwd)}.jsonl`);
136
+ }
137
+ var MAILBOX_TTL_MS = 24 * 60 * 60 * 1e3;
138
+ var MAILBOX_FRESH_MS = 15 * 60 * 1e3;
139
+ function sessionMailboxPath(dir, serverPid) {
140
+ return join3(dir, `agent-${serverPid}.jsonl`);
141
+ }
142
+ function normalizeCwd(cwd) {
127
143
  let normalized = resolve3(cwd);
128
144
  if (process.platform === "win32") normalized = normalized.toLowerCase();
129
- const hash = createHash2("sha256").update(normalized).digest("hex").slice(0, 16);
130
- return join3(dir, `${hash}.jsonl`);
145
+ return normalized;
146
+ }
147
+ function selectSessionMailboxes(dir, hookChain, hookCwd, staleMs = MAILBOX_FRESH_MS, nowMs = Date.now()) {
148
+ try {
149
+ const chain = hookChain.slice(0, 3);
150
+ const wantedCwd = hookCwd === null ? null : normalizeCwd(hookCwd);
151
+ const candidates = [];
152
+ for (const name of readdirSync(dir)) {
153
+ const m = /^agent-(\d+)\.json$/.exec(name);
154
+ if (!m) continue;
155
+ const serverPid = Number(m[1]);
156
+ const metaFile = join3(dir, name);
157
+ let mtimeMs;
158
+ try {
159
+ mtimeMs = statSync(metaFile).mtimeMs;
160
+ } catch {
161
+ continue;
162
+ }
163
+ if (nowMs - mtimeMs > MAILBOX_TTL_MS) {
164
+ try {
165
+ rmSync3(metaFile, { force: true });
166
+ rmSync3(sessionMailboxPath(dir, serverPid), { force: true });
167
+ rmSync3(`${sessionMailboxPath(dir, serverPid)}.draining`, {
168
+ force: true
169
+ });
170
+ } catch {
171
+ }
172
+ continue;
173
+ }
174
+ if (nowMs - mtimeMs > staleMs) continue;
175
+ let meta;
176
+ try {
177
+ meta = JSON.parse(readFileSync3(metaFile, "utf8"));
178
+ } catch {
179
+ continue;
180
+ }
181
+ if (!Array.isArray(meta.chain) || typeof meta.cwd !== "string") continue;
182
+ const i = chain.findIndex((pid) => meta.chain.includes(pid));
183
+ if (i === -1) continue;
184
+ const j = meta.chain.indexOf(chain[i]);
185
+ if (i >= 2 && (j > 2 || wantedCwd === null || meta.cwd !== wantedCwd))
186
+ continue;
187
+ candidates.push({ pid: serverPid, i, j, cwd: meta.cwd });
188
+ }
189
+ if (candidates.length === 0) return [];
190
+ const best = candidates.reduce(
191
+ (a, b) => b.i < a.i || b.i === a.i && b.j < a.j ? b : a
192
+ );
193
+ let winners = candidates.filter((c) => c.i === best.i && c.j === best.j);
194
+ if (winners.length > 1 && new Set(winners.map((w) => w.cwd)).size > 1) {
195
+ if (wantedCwd === null) return [];
196
+ winners = winners.filter((w) => w.cwd === wantedCwd);
197
+ }
198
+ return winners.map((w) => sessionMailboxPath(dir, w.pid));
199
+ } catch {
200
+ return [];
201
+ }
131
202
  }
132
203
  function drainInbox(filePath) {
133
204
  const tmp = `${filePath}.draining`;
@@ -170,21 +241,44 @@ function oneLine(text) {
170
241
  function indentContinuation(text) {
171
242
  return text.replace(/\r?\n/g, "\n ");
172
243
  }
244
+ function relativeAge(iso) {
245
+ const then = Date.parse(iso);
246
+ if (Number.isNaN(then)) return "recently";
247
+ const ms = Date.now() - then;
248
+ if (ms < 0) return "just now";
249
+ const mins = Math.floor(ms / 6e4);
250
+ if (mins < 1) return "just now";
251
+ if (mins < 60) return `${mins}m ago`;
252
+ const hours = Math.floor(mins / 60);
253
+ if (hours < 24) return `${hours}h ago`;
254
+ const days = Math.floor(hours / 24);
255
+ return `${days}d ago`;
256
+ }
173
257
  function formatInboxAnnouncements(announcements) {
174
258
  if (!announcements || announcements.length === 0) return "";
175
259
  const count = announcements.length;
176
260
  const lines = [
177
- `[Shepherd] ${count} new announcement${count === 1 ? "" : "s"} from your teammates:`
261
+ `[Shepherd] ${count} announcement${count === 1 ? "" : "s"} from your teammates:`
178
262
  ];
179
263
  for (const a of announcements) {
180
264
  const target = a.targetAgentName ? ` \u2192 ${oneLine(a.targetAgentName)}` : " (broadcast)";
181
265
  lines.push(
182
- ` [${oneLine(a.fromAgentName)}${target}] ${indentContinuation(a.body)}`
266
+ ` [${oneLine(a.fromAgentName)}${target}, ${relativeAge(a.createdAt)}] ${indentContinuation(a.body)}`
183
267
  );
184
268
  }
185
269
  lines.push(REPLY_ROUTING_HINT);
186
270
  return lines.join("\n");
187
271
  }
272
+ function mergeAnnouncements(...lists) {
273
+ const byId = /* @__PURE__ */ new Map();
274
+ for (const list of lists) {
275
+ if (!list) continue;
276
+ for (const a of list) {
277
+ if (!byId.has(a.id)) byId.set(a.id, a);
278
+ }
279
+ }
280
+ return [...byId.values()].sort((x, y) => x.id - y.id);
281
+ }
188
282
 
189
283
  // src/instructions.ts
190
284
  function sanitizeWorkspace(workspace) {
@@ -235,7 +329,10 @@ function shepherdInbox(pi) {
235
329
  try {
236
330
  const dir = process.env["SHEPHERD_INBOX_DIR"] || defaultInboxDir();
237
331
  const cwd = ctx?.cwd ?? process.cwd();
238
- const announcements = drainInbox(inboxFilePath(dir, cwd));
332
+ const announcements = mergeAnnouncements(
333
+ ...selectSessionMailboxes(dir, [process.pid], cwd).map(drainInbox),
334
+ drainInbox(inboxFilePath(dir, cwd))
335
+ );
239
336
  const messageContent = [
240
337
  buildLinkNudge(cwd),
241
338
  formatInboxAnnouncements(announcements)
package/dist/inboxHook.js CHANGED
@@ -6,8 +6,11 @@ import {
6
6
  appendFileSync,
7
7
  mkdirSync as mkdirSync2,
8
8
  readFileSync as readFileSync3,
9
+ readdirSync,
9
10
  renameSync,
10
11
  rmSync as rmSync3,
12
+ statSync,
13
+ writeFileSync as writeFileSync3,
11
14
  existsSync as existsSync3
12
15
  } from "node:fs";
13
16
  import { homedir as homedir2, tmpdir as tmpdir2 } from "node:os";
@@ -125,11 +128,94 @@ function defaultInboxDir() {
125
128
  if (!base) base = tmpdir2();
126
129
  return join3(base, ".shepherd", "inbox");
127
130
  }
131
+ function cwdHash(cwd) {
132
+ let normalized = resolve3(cwd);
133
+ if (process.platform === "win32") normalized = normalized.toLowerCase();
134
+ return createHash2("sha256").update(normalized).digest("hex").slice(0, 16);
135
+ }
128
136
  function inboxFilePath(dir, cwd) {
137
+ return join3(dir, `${cwdHash(cwd)}.jsonl`);
138
+ }
139
+ var MAILBOX_TTL_MS = 24 * 60 * 60 * 1e3;
140
+ var MAILBOX_FRESH_MS = 15 * 60 * 1e3;
141
+ function sessionMailboxPath(dir, serverPid) {
142
+ return join3(dir, `agent-${serverPid}.jsonl`);
143
+ }
144
+ function normalizeCwd(cwd) {
129
145
  let normalized = resolve3(cwd);
130
146
  if (process.platform === "win32") normalized = normalized.toLowerCase();
131
- const hash = createHash2("sha256").update(normalized).digest("hex").slice(0, 16);
132
- return join3(dir, `${hash}.jsonl`);
147
+ return normalized;
148
+ }
149
+ function hasFreshSessionMeta(dir, staleMs = MAILBOX_FRESH_MS, nowMs = Date.now()) {
150
+ try {
151
+ for (const name of readdirSync(dir)) {
152
+ if (!/^agent-\d+\.json$/.test(name)) continue;
153
+ try {
154
+ if (nowMs - statSync(join3(dir, name)).mtimeMs <= staleMs) return true;
155
+ } catch {
156
+ continue;
157
+ }
158
+ }
159
+ return false;
160
+ } catch {
161
+ return false;
162
+ }
163
+ }
164
+ function selectSessionMailboxes(dir, hookChain, hookCwd, staleMs = MAILBOX_FRESH_MS, nowMs = Date.now()) {
165
+ try {
166
+ const chain = hookChain.slice(0, 3);
167
+ const wantedCwd = hookCwd === null ? null : normalizeCwd(hookCwd);
168
+ const candidates = [];
169
+ for (const name of readdirSync(dir)) {
170
+ const m = /^agent-(\d+)\.json$/.exec(name);
171
+ if (!m) continue;
172
+ const serverPid = Number(m[1]);
173
+ const metaFile = join3(dir, name);
174
+ let mtimeMs;
175
+ try {
176
+ mtimeMs = statSync(metaFile).mtimeMs;
177
+ } catch {
178
+ continue;
179
+ }
180
+ if (nowMs - mtimeMs > MAILBOX_TTL_MS) {
181
+ try {
182
+ rmSync3(metaFile, { force: true });
183
+ rmSync3(sessionMailboxPath(dir, serverPid), { force: true });
184
+ rmSync3(`${sessionMailboxPath(dir, serverPid)}.draining`, {
185
+ force: true
186
+ });
187
+ } catch {
188
+ }
189
+ continue;
190
+ }
191
+ if (nowMs - mtimeMs > staleMs) continue;
192
+ let meta;
193
+ try {
194
+ meta = JSON.parse(readFileSync3(metaFile, "utf8"));
195
+ } catch {
196
+ continue;
197
+ }
198
+ if (!Array.isArray(meta.chain) || typeof meta.cwd !== "string") continue;
199
+ const i = chain.findIndex((pid) => meta.chain.includes(pid));
200
+ if (i === -1) continue;
201
+ const j = meta.chain.indexOf(chain[i]);
202
+ if (i >= 2 && (j > 2 || wantedCwd === null || meta.cwd !== wantedCwd))
203
+ continue;
204
+ candidates.push({ pid: serverPid, i, j, cwd: meta.cwd });
205
+ }
206
+ if (candidates.length === 0) return [];
207
+ const best = candidates.reduce(
208
+ (a, b) => b.i < a.i || b.i === a.i && b.j < a.j ? b : a
209
+ );
210
+ let winners = candidates.filter((c) => c.i === best.i && c.j === best.j);
211
+ if (winners.length > 1 && new Set(winners.map((w) => w.cwd)).size > 1) {
212
+ if (wantedCwd === null) return [];
213
+ winners = winners.filter((w) => w.cwd === wantedCwd);
214
+ }
215
+ return winners.map((w) => sessionMailboxPath(dir, w.pid));
216
+ } catch {
217
+ return [];
218
+ }
133
219
  }
134
220
  function drainInbox(filePath) {
135
221
  const tmp = `${filePath}.draining`;
@@ -172,26 +258,60 @@ function oneLine(text) {
172
258
  function indentContinuation(text) {
173
259
  return text.replace(/\r?\n/g, "\n ");
174
260
  }
261
+ function relativeAge(iso) {
262
+ const then = Date.parse(iso);
263
+ if (Number.isNaN(then)) return "recently";
264
+ const ms = Date.now() - then;
265
+ if (ms < 0) return "just now";
266
+ const mins = Math.floor(ms / 6e4);
267
+ if (mins < 1) return "just now";
268
+ if (mins < 60) return `${mins}m ago`;
269
+ const hours = Math.floor(mins / 60);
270
+ if (hours < 24) return `${hours}h ago`;
271
+ const days = Math.floor(hours / 24);
272
+ return `${days}d ago`;
273
+ }
175
274
  function formatInboxAnnouncements(announcements) {
176
275
  if (!announcements || announcements.length === 0) return "";
177
276
  const count = announcements.length;
178
277
  const lines = [
179
- `[Shepherd] ${count} new announcement${count === 1 ? "" : "s"} from your teammates:`
278
+ `[Shepherd] ${count} announcement${count === 1 ? "" : "s"} from your teammates:`
180
279
  ];
181
280
  for (const a of announcements) {
182
281
  const target = a.targetAgentName ? ` \u2192 ${oneLine(a.targetAgentName)}` : " (broadcast)";
183
282
  lines.push(
184
- ` [${oneLine(a.fromAgentName)}${target}] ${indentContinuation(a.body)}`
283
+ ` [${oneLine(a.fromAgentName)}${target}, ${relativeAge(a.createdAt)}] ${indentContinuation(a.body)}`
185
284
  );
186
285
  }
187
286
  lines.push(REPLY_ROUTING_HINT);
188
287
  return lines.join("\n");
189
288
  }
289
+ function mergeAnnouncements(...lists) {
290
+ const byId = /* @__PURE__ */ new Map();
291
+ for (const list of lists) {
292
+ if (!list) continue;
293
+ for (const a of list) {
294
+ if (!byId.has(a.id)) byId.set(a.id, a);
295
+ }
296
+ }
297
+ return [...byId.values()].sort((x, y) => x.id - y.id);
298
+ }
190
299
  function nativeWorkspacePath(root) {
191
300
  const win = /^\/([A-Za-z]:[/\\].*)$/.exec(root);
192
301
  return win ? win[1] : root;
193
302
  }
194
- function buildHookOutput(rawStdin, inboxDir, drain = drainInbox, nudge = buildLinkNudge) {
303
+ function parseHookPairingInput(rawStdin) {
304
+ try {
305
+ const input = JSON.parse(rawStdin.replace(/^\uFEFF/, ""));
306
+ const sessionId = typeof input.session_id === "string" && input.session_id.length > 0 ? input.session_id : void 0;
307
+ const firstRoot = Array.isArray(input.workspace_roots) ? input.workspace_roots[0] : void 0;
308
+ const cwd = typeof input.cwd === "string" && input.cwd.length > 0 ? input.cwd : typeof firstRoot === "string" && firstRoot.length > 0 ? nativeWorkspacePath(firstRoot) : null;
309
+ return { sessionId, cwd };
310
+ } catch {
311
+ return { sessionId: void 0, cwd: null };
312
+ }
313
+ }
314
+ function buildHookOutput(rawStdin, inboxDir, drain = drainInbox, nudge = buildLinkNudge, sessionMailboxes = []) {
195
315
  let input;
196
316
  try {
197
317
  input = JSON.parse(rawStdin.replace(/^\uFEFF/, ""));
@@ -210,7 +330,11 @@ function buildHookOutput(rawStdin, inboxDir, drain = drainInbox, nudge = buildLi
210
330
  );
211
331
  if (nudgeText) parts.push(nudgeText);
212
332
  if (inboxDir) {
213
- const text = formatInboxAnnouncements(drain(inboxFilePath(inboxDir, cwd)));
333
+ const drained = mergeAnnouncements(
334
+ ...sessionMailboxes.map((box) => drain(box)),
335
+ drain(inboxFilePath(inboxDir, cwd))
336
+ );
337
+ const text = formatInboxAnnouncements(drained);
214
338
  if (text) parts.push(text);
215
339
  }
216
340
  if (parts.length === 0) return "";
@@ -228,6 +352,168 @@ function buildHookOutput(rawStdin, inboxDir, drain = drainInbox, nudge = buildLi
228
352
  });
229
353
  }
230
354
 
355
+ // src/hookPairing.ts
356
+ import { createHash as createHash3 } from "node:crypto";
357
+ import {
358
+ readFileSync as readFileSync4,
359
+ writeFileSync as writeFileSync4,
360
+ renameSync as renameSync2,
361
+ readdirSync as readdirSync2,
362
+ rmSync as rmSync4,
363
+ statSync as statSync2
364
+ } from "node:fs";
365
+ import { join as join4 } from "node:path";
366
+
367
+ // src/processTree.ts
368
+ import { execFile } from "node:child_process";
369
+ import { promisify } from "node:util";
370
+ var execFileAsync = promisify(execFile);
371
+ function pidChainFromMap(startPid, parentOf, maxDepth = 32) {
372
+ const chain = [];
373
+ const seen = /* @__PURE__ */ new Set();
374
+ let pid = startPid;
375
+ while (chain.length < maxDepth && pid > 0 && !seen.has(pid)) {
376
+ chain.push(pid);
377
+ seen.add(pid);
378
+ const parent = parentOf.get(pid);
379
+ if (parent === void 0) break;
380
+ pid = parent;
381
+ }
382
+ return chain;
383
+ }
384
+ function quickChain() {
385
+ return [process.pid, process.ppid];
386
+ }
387
+ function parseWmicProcessList(text) {
388
+ const map = /* @__PURE__ */ new Map();
389
+ const lines = text.split(/\r?\n/).filter((l) => l.trim().length > 0);
390
+ if (lines.length === 0) return map;
391
+ const header = lines[0].trimStart();
392
+ let pidFirst;
393
+ if (header.startsWith("ParentProcessId")) pidFirst = false;
394
+ else if (header.startsWith("ProcessId")) pidFirst = true;
395
+ else return map;
396
+ for (const line of lines.slice(1)) {
397
+ const nums = line.trim().split(/\s+/).map(Number);
398
+ if (nums.length !== 2 || nums.some((n) => !Number.isInteger(n))) continue;
399
+ const [a, b] = nums;
400
+ const [pid, ppid] = pidFirst ? [a, b] : [b, a];
401
+ map.set(pid, ppid);
402
+ }
403
+ return map;
404
+ }
405
+ function parsePidPpidLines(text) {
406
+ const map = /* @__PURE__ */ new Map();
407
+ for (const line of text.split(/\r?\n/)) {
408
+ const m = /^\s*(\d+)\s+(\d+)\s*$/.exec(line);
409
+ if (m) map.set(Number(m[1]), Number(m[2]));
410
+ }
411
+ return map;
412
+ }
413
+ async function snapshotParentMap() {
414
+ if (process.platform === "win32") {
415
+ try {
416
+ const { stdout: stdout3 } = await execFileAsync(
417
+ "wmic",
418
+ ["process", "get", "ProcessId,ParentProcessId"],
419
+ { windowsHide: true, timeout: 1e4, maxBuffer: 8 * 1024 * 1024 }
420
+ );
421
+ const map = parseWmicProcessList(stdout3);
422
+ if (map.size > 0) return map;
423
+ } catch {
424
+ }
425
+ const { stdout: stdout2 } = await execFileAsync(
426
+ "powershell.exe",
427
+ [
428
+ "-NoProfile",
429
+ "-NonInteractive",
430
+ "-Command",
431
+ 'Get-CimInstance -Query "SELECT ProcessId,ParentProcessId FROM Win32_Process" | ForEach-Object { "$($_.ProcessId) $($_.ParentProcessId)" }'
432
+ ],
433
+ { windowsHide: true, timeout: 15e3, maxBuffer: 8 * 1024 * 1024 }
434
+ );
435
+ return parsePidPpidLines(stdout2);
436
+ }
437
+ const { stdout } = await execFileAsync(
438
+ "ps",
439
+ ["-eo", "pid=,ppid="],
440
+ { timeout: 1e4, maxBuffer: 8 * 1024 * 1024 }
441
+ );
442
+ return parsePidPpidLines(stdout);
443
+ }
444
+ async function ancestorChain(maxDepth = 32, snapshot = snapshotParentMap) {
445
+ try {
446
+ const map = await snapshot();
447
+ const chain = pidChainFromMap(process.pid, map, maxDepth);
448
+ return chain.length >= 2 ? chain : quickChain();
449
+ } catch {
450
+ return quickChain();
451
+ }
452
+ }
453
+
454
+ // src/hookPairing.ts
455
+ function pairingCachePath(dir, sessionId) {
456
+ const key = createHash3("sha256").update(sessionId).digest("hex").slice(0, 16);
457
+ return join4(dir, `pairing-${key}.json`);
458
+ }
459
+ function readCachedChain(file, nowMs) {
460
+ try {
461
+ const parsed = JSON.parse(readFileSync4(file, "utf8"));
462
+ if (!Array.isArray(parsed.chain) || typeof parsed.ts !== "number")
463
+ return null;
464
+ if (nowMs - parsed.ts > MAILBOX_TTL_MS) return null;
465
+ return parsed.chain;
466
+ } catch {
467
+ return null;
468
+ }
469
+ }
470
+ function writeCachedChain(dir, file, chain, nowMs) {
471
+ try {
472
+ for (const name of readdirSync2(dir)) {
473
+ if (!name.startsWith("pairing-") || !name.endsWith(".json")) continue;
474
+ try {
475
+ if (nowMs - statSync2(join4(dir, name)).mtimeMs > MAILBOX_TTL_MS) {
476
+ rmSync4(join4(dir, name), { force: true });
477
+ }
478
+ } catch {
479
+ }
480
+ }
481
+ const tmp = `${file}.tmp`;
482
+ writeFileSync4(tmp, JSON.stringify({ chain, ts: nowMs }));
483
+ renameSync2(tmp, file);
484
+ } catch {
485
+ }
486
+ }
487
+ async function resolveHookMailboxes(inboxDir, { sessionId, cwd }, deps = {}) {
488
+ const quick = deps.quick ?? quickChain;
489
+ const full = deps.full ?? (() => ancestorChain());
490
+ const nowMs = deps.nowMs ?? Date.now();
491
+ try {
492
+ let boxes = selectSessionMailboxes(
493
+ inboxDir,
494
+ quick(),
495
+ cwd,
496
+ void 0,
497
+ nowMs
498
+ );
499
+ if (boxes.length > 0) return boxes;
500
+ if (!hasFreshSessionMeta(inboxDir, void 0, nowMs)) return [];
501
+ const cacheFile = sessionId ? pairingCachePath(inboxDir, sessionId) : null;
502
+ if (cacheFile) {
503
+ const cached = readCachedChain(cacheFile, nowMs);
504
+ if (cached) {
505
+ boxes = selectSessionMailboxes(inboxDir, cached, cwd, void 0, nowMs);
506
+ if (boxes.length > 0) return boxes;
507
+ }
508
+ }
509
+ const chain = await full();
510
+ if (cacheFile) writeCachedChain(inboxDir, cacheFile, chain, nowMs);
511
+ return selectSessionMailboxes(inboxDir, chain, cwd, void 0, nowMs);
512
+ } catch {
513
+ return [];
514
+ }
515
+ }
516
+
231
517
  // src/inboxHook.ts
232
518
  async function readStdin() {
233
519
  const chunks = [];
@@ -240,7 +526,15 @@ async function main() {
240
526
  try {
241
527
  const raw = await readStdin();
242
528
  const inboxDir = process.argv[2] || process.env["SHEPHERD_INBOX_DIR"] || defaultInboxDir();
243
- const out = buildHookOutput(raw, inboxDir);
529
+ let mailboxes = [];
530
+ try {
531
+ mailboxes = await resolveHookMailboxes(
532
+ inboxDir,
533
+ parseHookPairingInput(raw)
534
+ );
535
+ } catch {
536
+ }
537
+ const out = buildHookOutput(raw, inboxDir, void 0, void 0, mailboxes);
244
538
  if (out) process.stdout.write(out);
245
539
  } catch {
246
540
  }
package/dist/index.js CHANGED
@@ -481,7 +481,12 @@ var JoinRequest = z2.object({
481
481
  });
482
482
  var JoinResponse = z2.object({
483
483
  agentName: z2.string(),
484
- sessionId: z2.string().uuid()
484
+ sessionId: z2.string().uuid(),
485
+ // Advertised so clients can nudge their humans to update. Optional: older
486
+ // hubs omit them, and a hub that cannot determine its bundled client
487
+ // version fails open by leaving them out.
488
+ latestClientVersion: z2.string().optional(),
489
+ minimumClientVersion: z2.string().optional()
485
490
  });
486
491
  var WorkRequest = z2.object({
487
492
  sessionId: z2.string().uuid(),
@@ -712,6 +717,34 @@ var FeedbackResponse = z2.object({
712
717
  // uuid PK (the feedback table, like workspaces, uses gen_random_uuid()).
713
718
  id: z2.string()
714
719
  });
720
+ var NullableCap = z2.number().int().positive().nullable();
721
+ var EntitlementLimits = z2.object({
722
+ seatsLimit: NullableCap,
723
+ reposLimit: NullableCap,
724
+ retentionDays: NullableCap
725
+ });
726
+ var LimitExceededErrorBody = z2.object({
727
+ error: z2.string(),
728
+ code: z2.literal("limit_exceeded"),
729
+ limit: z2.enum(["seats", "repos"]),
730
+ current: z2.number().int(),
731
+ max: z2.number().int()
732
+ });
733
+ var WorkspaceEntitlements = EntitlementLimits.extend({
734
+ expiresAt: IsoTimestamp.nullable(),
735
+ updatedAt: IsoTimestamp
736
+ });
737
+ var PutEntitlementsRequest = EntitlementLimits.extend({
738
+ expiresAt: IsoTimestamp.nullable()
739
+ });
740
+ var EntitlementsStatusResponse = z2.object({
741
+ record: WorkspaceEntitlements.nullable(),
742
+ effective: EntitlementLimits,
743
+ usage: z2.object({
744
+ seatsUsed: z2.number().int(),
745
+ reposUsed: z2.number().int()
746
+ })
747
+ });
715
748
  var TrendPoint = z2.object({
716
749
  // `YYYY-MM-DD` (UTC day).
717
750
  date: z2.string(),
@@ -1144,13 +1177,15 @@ async function buildChangeReport(cwd, config) {
1144
1177
  }
1145
1178
 
1146
1179
  // src/inbox.ts
1147
- import { createHash as createHash2 } from "node:crypto";
1148
1180
  import {
1149
1181
  appendFileSync,
1150
1182
  mkdirSync as mkdirSync2,
1151
1183
  readFileSync as readFileSync3,
1184
+ readdirSync,
1152
1185
  renameSync,
1153
1186
  rmSync as rmSync3,
1187
+ statSync,
1188
+ writeFileSync as writeFileSync3,
1154
1189
  existsSync as existsSync3
1155
1190
  } from "node:fs";
1156
1191
  import { homedir as homedir2, tmpdir as tmpdir2 } from "node:os";
@@ -1165,11 +1200,37 @@ function defaultInboxDir() {
1165
1200
  if (!base) base = tmpdir2();
1166
1201
  return join3(base, ".shepherd", "inbox");
1167
1202
  }
1168
- function inboxFilePath(dir, cwd) {
1203
+ var MAILBOX_TTL_MS = 24 * 60 * 60 * 1e3;
1204
+ var MAILBOX_FRESH_MS = 15 * 60 * 1e3;
1205
+ function sessionMailboxPath(dir, serverPid) {
1206
+ return join3(dir, `agent-${serverPid}.jsonl`);
1207
+ }
1208
+ function sessionMetaPath(dir, serverPid) {
1209
+ return join3(dir, `agent-${serverPid}.json`);
1210
+ }
1211
+ function normalizeCwd(cwd) {
1169
1212
  let normalized = resolve3(cwd);
1170
1213
  if (process.platform === "win32") normalized = normalized.toLowerCase();
1171
- const hash = createHash2("sha256").update(normalized).digest("hex").slice(0, 16);
1172
- return join3(dir, `${hash}.jsonl`);
1214
+ return normalized;
1215
+ }
1216
+ function writeMailboxMeta(dir, serverPid, meta) {
1217
+ try {
1218
+ mkdirSync2(dir, { recursive: true });
1219
+ const dest = sessionMetaPath(dir, serverPid);
1220
+ const tmp = `${dest}.tmp`;
1221
+ writeFileSync3(
1222
+ tmp,
1223
+ JSON.stringify({ v: 1, cwd: normalizeCwd(meta.cwd), chain: meta.chain })
1224
+ );
1225
+ renameSync(tmp, dest);
1226
+ } catch {
1227
+ }
1228
+ }
1229
+ function removeMailboxMeta(dir, serverPid) {
1230
+ try {
1231
+ rmSync3(sessionMetaPath(dir, serverPid), { force: true });
1232
+ } catch {
1233
+ }
1173
1234
  }
1174
1235
  function appendAnnouncements(filePath, announcements) {
1175
1236
  if (!announcements || announcements.length === 0) return;
@@ -1221,6 +1282,19 @@ function oneLine(text) {
1221
1282
  function indentContinuation(text) {
1222
1283
  return text.replace(/\r?\n/g, "\n ");
1223
1284
  }
1285
+ function relativeAge(iso) {
1286
+ const then = Date.parse(iso);
1287
+ if (Number.isNaN(then)) return "recently";
1288
+ const ms = Date.now() - then;
1289
+ if (ms < 0) return "just now";
1290
+ const mins = Math.floor(ms / 6e4);
1291
+ if (mins < 1) return "just now";
1292
+ if (mins < 60) return `${mins}m ago`;
1293
+ const hours = Math.floor(mins / 60);
1294
+ if (hours < 24) return `${hours}h ago`;
1295
+ const days = Math.floor(hours / 24);
1296
+ return `${days}d ago`;
1297
+ }
1224
1298
  function mergeAnnouncements(...lists) {
1225
1299
  const byId = /* @__PURE__ */ new Map();
1226
1300
  for (const list of lists) {
@@ -1299,6 +1373,81 @@ function defaultRunGitStatus(cwd) {
1299
1373
  });
1300
1374
  }
1301
1375
 
1376
+ // src/updateNudge.ts
1377
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
1378
+ import { dirname as dirname4 } from "node:path";
1379
+ var NUDGE_COOLDOWN_MS = 24 * 60 * 60 * 1e3;
1380
+ function parseVersion(v) {
1381
+ const m = /^v?(\d+(?:\.\d+)*)/.exec(v.trim());
1382
+ if (!m) return null;
1383
+ return m[1].split(".").map(Number);
1384
+ }
1385
+ function compareVersions(a, b) {
1386
+ const pa = parseVersion(a) ?? [];
1387
+ const pb = parseVersion(b) ?? [];
1388
+ const len = Math.max(pa.length, pb.length);
1389
+ for (let i = 0; i < len; i++) {
1390
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
1391
+ if (d !== 0) return d < 0 ? -1 : 1;
1392
+ }
1393
+ return 0;
1394
+ }
1395
+ function readStamp(stampFile) {
1396
+ try {
1397
+ const parsed = JSON.parse(readFileSync4(stampFile, "utf8"));
1398
+ if (typeof parsed === "object" && parsed !== null && typeof parsed.latest === "string" && typeof parsed.at === "number") {
1399
+ return parsed;
1400
+ }
1401
+ } catch {
1402
+ }
1403
+ return null;
1404
+ }
1405
+ function writeStamp(stampFile, stamp) {
1406
+ try {
1407
+ mkdirSync3(dirname4(stampFile), { recursive: true });
1408
+ writeFileSync4(stampFile, JSON.stringify(stamp), "utf8");
1409
+ } catch {
1410
+ }
1411
+ }
1412
+ var SUGGEST = "Let your human know, and suggest the update command that matches how Shepherd is installed on this machine (global npm, an npx cache, a version manager, \u2026).";
1413
+ function maybeUpdateNudge(opts) {
1414
+ const now = opts.nowMs ?? Date.now();
1415
+ if (!parseVersion(opts.current)) return "";
1416
+ const latest = opts.latest !== void 0 && parseVersion(opts.latest) ? opts.latest : void 0;
1417
+ const minimum = opts.minimum !== void 0 && parseVersion(opts.minimum) ? opts.minimum : void 0;
1418
+ const belowMinimum = minimum !== void 0 && compareVersions(opts.current, minimum) < 0;
1419
+ const behind = latest !== void 0 && compareVersions(opts.current, latest) < 0;
1420
+ if (!belowMinimum && !behind) return "";
1421
+ if (!belowMinimum) {
1422
+ const stamp = readStamp(opts.stampFile);
1423
+ if (stamp !== null && compareVersions(latest, stamp.latest) <= 0 && now - stamp.at < NUDGE_COOLDOWN_MS) {
1424
+ return "";
1425
+ }
1426
+ }
1427
+ writeStamp(opts.stampFile, { latest: latest ?? opts.current, at: now });
1428
+ if (belowMinimum) {
1429
+ const latestPart = latest !== void 0 ? ` (latest: ${latest})` : "";
1430
+ return `[shepherd] This client (${opts.current}) is below the minimum supported version ${minimum}${latestPart} \u2014 coordination may misbehave until it is updated. ${SUGGEST}`;
1431
+ }
1432
+ return `[shepherd] Update available: @korso/shepherd ${latest} (this machine runs ${opts.current}). ${SUGGEST}`;
1433
+ }
1434
+
1435
+ // src/version.ts
1436
+ import { createRequire } from "node:module";
1437
+ var PACKAGE_VERSION = (() => {
1438
+ try {
1439
+ const req = createRequire(import.meta.url);
1440
+ const pkg = req("../package.json");
1441
+ return pkg.version ?? "0.0.0";
1442
+ } catch {
1443
+ return "0.0.0";
1444
+ }
1445
+ })();
1446
+
1447
+ // src/tools.ts
1448
+ import { homedir as homedir3 } from "node:os";
1449
+ import nodePath from "node:path";
1450
+
1302
1451
  // src/linkPopup.ts
1303
1452
  var NEVER_ASK_CHOICE = "No \u2014 don't ask again";
1304
1453
  async function offerLinkPopup({
@@ -1419,7 +1568,7 @@ function formatLandscape(landscape) {
1419
1568
  for (const a of landscape.announcements) {
1420
1569
  const target = a.targetAgentName ? ` \u2192 ${oneLine(a.targetAgentName)}` : " (broadcast)";
1421
1570
  lines.push(
1422
- ` [${oneLine(a.fromAgentName)}${target}] ${indentContinuation(a.body)}`
1571
+ ` [${oneLine(a.fromAgentName)}${target}, ${relativeAge(a.createdAt)}] ${indentContinuation(a.body)}`
1423
1572
  );
1424
1573
  }
1425
1574
  lines.push(REPLY_ROUTING_HINT);
@@ -1434,25 +1583,12 @@ function formatAnnouncements(announcements) {
1434
1583
  for (const a of announcements) {
1435
1584
  const target = a.targetAgentName ? ` \u2192 ${oneLine(a.targetAgentName)}` : " (broadcast)";
1436
1585
  lines.push(
1437
- ` [${oneLine(a.fromAgentName)}${target}] ${indentContinuation(a.body)}`
1586
+ ` [${oneLine(a.fromAgentName)}${target}, ${relativeAge(a.createdAt)}] ${indentContinuation(a.body)}`
1438
1587
  );
1439
1588
  }
1440
1589
  lines.push(REPLY_ROUTING_HINT);
1441
1590
  return lines.join("\n");
1442
1591
  }
1443
- function relativeAge(iso) {
1444
- const then = Date.parse(iso);
1445
- if (Number.isNaN(then)) return "recently";
1446
- const ms = Date.now() - then;
1447
- if (ms < 0) return "just now";
1448
- const mins = Math.floor(ms / 6e4);
1449
- if (mins < 1) return "just now";
1450
- if (mins < 60) return `${mins}m ago`;
1451
- const hours = Math.floor(mins / 60);
1452
- if (hours < 24) return `${hours}h ago`;
1453
- const days = Math.floor(hours / 24);
1454
- return `${days}d ago`;
1455
- }
1456
1592
  function presence(rec) {
1457
1593
  return rec.authorIsLive ? "active now" : `offline, last seen ${relativeAge(rec.authorLastActiveAt)}`;
1458
1594
  }
@@ -1526,9 +1662,37 @@ function registerTools(server, deps) {
1526
1662
  const { hubClient, config, context, heartbeat, inboxFile } = deps;
1527
1663
  const markerCwd = deps.cwd ?? process.cwd();
1528
1664
  const declinedDir = deps.declinedDir;
1665
+ const updateNudge = deps.updateNudge ?? ((versions) => maybeUpdateNudge({
1666
+ current: PACKAGE_VERSION,
1667
+ latest: versions.latest,
1668
+ minimum: versions.minimum,
1669
+ stampFile: nodePath.join(homedir3(), ".shepherd", "update-nudge.json")
1670
+ }));
1671
+ let latestClientVersion;
1672
+ let minimumClientVersion;
1673
+ let nudgeDelivered = false;
1674
+ function withUpdateNudge(text) {
1675
+ if (nudgeDelivered) return text;
1676
+ if (latestClientVersion === void 0 && minimumClientVersion === void 0) {
1677
+ return text;
1678
+ }
1679
+ nudgeDelivered = true;
1680
+ let nudge = "";
1681
+ try {
1682
+ nudge = updateNudge({
1683
+ latest: latestClientVersion,
1684
+ minimum: minimumClientVersion
1685
+ });
1686
+ } catch {
1687
+ }
1688
+ return nudge ? `${text}
1689
+
1690
+ ${nudge}` : text;
1691
+ }
1529
1692
  const repoRoot = findRepoRoot(markerCwd);
1530
1693
  let sessionId = null;
1531
1694
  let agentName = null;
1695
+ let activeWorkspaceSlug = null;
1532
1696
  const isHosted = Boolean(config.SHEPHERD_TOKEN);
1533
1697
  const selfHostMismatch = !isHosted && context.linked && config.WORKSPACE !== void 0 && config.WORKSPACE !== context.workspace;
1534
1698
  let hostedWorkspaceRejected = false;
@@ -1561,6 +1725,9 @@ function registerTools(server, deps) {
1561
1725
  let joinFailure = null;
1562
1726
  let joinInFlight = Promise.resolve();
1563
1727
  async function activate(workspaceSlug) {
1728
+ if (sessionId !== null && activeWorkspaceSlug === workspaceSlug) {
1729
+ return { ok: true };
1730
+ }
1564
1731
  const joinBody = {
1565
1732
  workspace: workspaceSlug,
1566
1733
  repo: context.repo,
@@ -1586,6 +1753,9 @@ function registerTools(server, deps) {
1586
1753
  heartbeat.start(newSessionId);
1587
1754
  sessionId = newSessionId;
1588
1755
  agentName = parsed.data.agentName;
1756
+ latestClientVersion = parsed.data.latestClientVersion;
1757
+ minimumClientVersion = parsed.data.minimumClientVersion;
1758
+ activeWorkspaceSlug = workspaceSlug;
1589
1759
  linked = true;
1590
1760
  hostedWorkspaceRejected = false;
1591
1761
  joinFailure = null;
@@ -1726,7 +1896,7 @@ ${section}` : body;
1726
1896
  You hold this claim until you call done (workItemId: ${result.workItemId}) or it expires (~60 min). Calling work or sync renews it.`
1727
1897
  )
1728
1898
  );
1729
- return { content: [{ type: "text", text }] };
1899
+ return { content: [{ type: "text", text: withUpdateNudge(text) }] };
1730
1900
  } catch (err) {
1731
1901
  if (err instanceof HubUnreachable || err instanceof HubRequestError) {
1732
1902
  return degradedResult(err);
@@ -1757,9 +1927,14 @@ You hold this claim until you call done (workItemId: ${result.workItemId}) or it
1757
1927
  mergeAnnouncements(result.announcements, drainLocalInbox())
1758
1928
  );
1759
1929
  return {
1760
- content: [{ type: "text", text: msgs ? `${base}
1930
+ content: [
1931
+ {
1932
+ type: "text",
1933
+ text: withUpdateNudge(msgs ? `${base}
1761
1934
 
1762
- ${msgs}` : base }]
1935
+ ${msgs}` : base)
1936
+ }
1937
+ ]
1763
1938
  };
1764
1939
  } catch (err) {
1765
1940
  if (err instanceof HubUnreachable || err instanceof HubRequestError) {
@@ -1791,9 +1966,14 @@ ${msgs}` : base }]
1791
1966
  mergeAnnouncements(result.announcements, drainLocalInbox())
1792
1967
  );
1793
1968
  return {
1794
- content: [{ type: "text", text: msgs ? `${base}
1969
+ content: [
1970
+ {
1971
+ type: "text",
1972
+ text: withUpdateNudge(msgs ? `${base}
1795
1973
 
1796
- ${msgs}` : base }]
1974
+ ${msgs}` : base)
1975
+ }
1976
+ ]
1797
1977
  };
1798
1978
  } catch (err) {
1799
1979
  if (err instanceof HubUnreachable || err instanceof HubRequestError) {
@@ -1831,7 +2011,7 @@ ${msgs}` : base }]
1831
2011
  formatLandscape(result.landscape)
1832
2012
  )
1833
2013
  );
1834
- return { content: [{ type: "text", text }] };
2014
+ return { content: [{ type: "text", text: withUpdateNudge(text) }] };
1835
2015
  } catch (err) {
1836
2016
  if (err instanceof HubUnreachable || err instanceof HubRequestError) {
1837
2017
  return degradedResult(err);
@@ -1932,6 +2112,7 @@ ${msgs}` : base }]
1932
2112
  await leave();
1933
2113
  sessionId = null;
1934
2114
  agentName = null;
2115
+ activeWorkspaceSlug = null;
1935
2116
  }
1936
2117
  return advisory(
1937
2118
  "Unlinked \u2014 this repo will stay uncoordinated and won't ask again. Run `link` to re-enable."
@@ -2038,13 +2219,13 @@ function postLinkGuidance(workspace) {
2038
2219
  }
2039
2220
 
2040
2221
  // src/identityCache.ts
2041
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
2042
- import { homedir as homedir3, tmpdir as tmpdir3 } from "node:os";
2043
- import { dirname as dirname4, join as join4 } from "node:path";
2222
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "node:fs";
2223
+ import { homedir as homedir4, tmpdir as tmpdir3 } from "node:os";
2224
+ import { dirname as dirname5, join as join4 } from "node:path";
2044
2225
  function defaultIdentityCachePath() {
2045
2226
  let base = "";
2046
2227
  try {
2047
- base = homedir3();
2228
+ base = homedir4();
2048
2229
  } catch {
2049
2230
  base = "";
2050
2231
  }
@@ -2054,7 +2235,7 @@ function defaultIdentityCachePath() {
2054
2235
  function readCachedHuman(filePath = defaultIdentityCachePath()) {
2055
2236
  let raw;
2056
2237
  try {
2057
- raw = readFileSync4(filePath, "utf8");
2238
+ raw = readFileSync5(filePath, "utf8");
2058
2239
  } catch {
2059
2240
  return null;
2060
2241
  }
@@ -2069,9 +2250,9 @@ function readCachedHuman(filePath = defaultIdentityCachePath()) {
2069
2250
  function writeCachedHuman(human, filePath = defaultIdentityCachePath()) {
2070
2251
  if (typeof human !== "string" || human.trim().length === 0) return;
2071
2252
  try {
2072
- mkdirSync3(dirname4(filePath), { recursive: true });
2253
+ mkdirSync4(dirname5(filePath), { recursive: true });
2073
2254
  const payload = JSON.stringify({ human });
2074
- writeFileSync3(filePath, payload + "\n", "utf8");
2255
+ writeFileSync5(filePath, payload + "\n", "utf8");
2075
2256
  } catch {
2076
2257
  }
2077
2258
  }
@@ -2130,13 +2311,20 @@ function createHeartbeat({
2130
2311
  hubClient,
2131
2312
  intervalSeconds,
2132
2313
  buildReport,
2133
- announcementSink
2314
+ announcementSink,
2315
+ liveness
2134
2316
  }) {
2135
2317
  let timer = null;
2136
2318
  function stop() {
2137
2319
  if (timer !== null) {
2138
2320
  clearInterval(timer);
2139
2321
  timer = null;
2322
+ if (liveness) {
2323
+ try {
2324
+ liveness.remove();
2325
+ } catch {
2326
+ }
2327
+ }
2140
2328
  }
2141
2329
  }
2142
2330
  async function beat(sessionId) {
@@ -2150,6 +2338,12 @@ function createHeartbeat({
2150
2338
  }
2151
2339
  const body = { sessionId };
2152
2340
  if (changeReport) body.changeReport = changeReport;
2341
+ if (liveness) {
2342
+ try {
2343
+ liveness.refresh();
2344
+ } catch {
2345
+ }
2346
+ }
2153
2347
  if (announcementSink) body.deliverAnnouncements = true;
2154
2348
  const parsed = HeartbeatResponse.safeParse(
2155
2349
  await hubClient.post("/heartbeat", body)
@@ -2177,6 +2371,12 @@ function createHeartbeat({
2177
2371
  }
2178
2372
  function start(sessionId) {
2179
2373
  stop();
2374
+ if (liveness) {
2375
+ try {
2376
+ liveness.refresh();
2377
+ } catch {
2378
+ }
2379
+ }
2180
2380
  timer = setInterval(() => {
2181
2381
  void beat(sessionId).catch((err) => {
2182
2382
  console.error(
@@ -2227,32 +2427,105 @@ Skip \`work\` entirely for read-only exploration \u2014 reading, searching, or t
2227
2427
 
2228
2428
  Commit work-in-progress as you go rather than sitting on a large dirty tree: committed work becomes a precise, presence-independent signal to teammates (with line-level detail and automatic resolution once it lands), whereas uncommitted edits are only a best-effort, decaying hint.`;
2229
2429
 
2430
+ // src/processTree.ts
2431
+ import { execFile as execFile2 } from "node:child_process";
2432
+ import { promisify } from "node:util";
2433
+ var execFileAsync = promisify(execFile2);
2434
+ function pidChainFromMap(startPid, parentOf, maxDepth = 32) {
2435
+ const chain = [];
2436
+ const seen = /* @__PURE__ */ new Set();
2437
+ let pid = startPid;
2438
+ while (chain.length < maxDepth && pid > 0 && !seen.has(pid)) {
2439
+ chain.push(pid);
2440
+ seen.add(pid);
2441
+ const parent = parentOf.get(pid);
2442
+ if (parent === void 0) break;
2443
+ pid = parent;
2444
+ }
2445
+ return chain;
2446
+ }
2447
+ function quickChain() {
2448
+ return [process.pid, process.ppid];
2449
+ }
2450
+ function parseWmicProcessList(text) {
2451
+ const map = /* @__PURE__ */ new Map();
2452
+ const lines = text.split(/\r?\n/).filter((l) => l.trim().length > 0);
2453
+ if (lines.length === 0) return map;
2454
+ const header = lines[0].trimStart();
2455
+ let pidFirst;
2456
+ if (header.startsWith("ParentProcessId")) pidFirst = false;
2457
+ else if (header.startsWith("ProcessId")) pidFirst = true;
2458
+ else return map;
2459
+ for (const line of lines.slice(1)) {
2460
+ const nums = line.trim().split(/\s+/).map(Number);
2461
+ if (nums.length !== 2 || nums.some((n) => !Number.isInteger(n))) continue;
2462
+ const [a, b] = nums;
2463
+ const [pid, ppid] = pidFirst ? [a, b] : [b, a];
2464
+ map.set(pid, ppid);
2465
+ }
2466
+ return map;
2467
+ }
2468
+ function parsePidPpidLines(text) {
2469
+ const map = /* @__PURE__ */ new Map();
2470
+ for (const line of text.split(/\r?\n/)) {
2471
+ const m = /^\s*(\d+)\s+(\d+)\s*$/.exec(line);
2472
+ if (m) map.set(Number(m[1]), Number(m[2]));
2473
+ }
2474
+ return map;
2475
+ }
2476
+ async function snapshotParentMap() {
2477
+ if (process.platform === "win32") {
2478
+ try {
2479
+ const { stdout: stdout3 } = await execFileAsync(
2480
+ "wmic",
2481
+ ["process", "get", "ProcessId,ParentProcessId"],
2482
+ { windowsHide: true, timeout: 1e4, maxBuffer: 8 * 1024 * 1024 }
2483
+ );
2484
+ const map = parseWmicProcessList(stdout3);
2485
+ if (map.size > 0) return map;
2486
+ } catch {
2487
+ }
2488
+ const { stdout: stdout2 } = await execFileAsync(
2489
+ "powershell.exe",
2490
+ [
2491
+ "-NoProfile",
2492
+ "-NonInteractive",
2493
+ "-Command",
2494
+ 'Get-CimInstance -Query "SELECT ProcessId,ParentProcessId FROM Win32_Process" | ForEach-Object { "$($_.ProcessId) $($_.ParentProcessId)" }'
2495
+ ],
2496
+ { windowsHide: true, timeout: 15e3, maxBuffer: 8 * 1024 * 1024 }
2497
+ );
2498
+ return parsePidPpidLines(stdout2);
2499
+ }
2500
+ const { stdout } = await execFileAsync(
2501
+ "ps",
2502
+ ["-eo", "pid=,ppid="],
2503
+ { timeout: 1e4, maxBuffer: 8 * 1024 * 1024 }
2504
+ );
2505
+ return parsePidPpidLines(stdout);
2506
+ }
2507
+ async function ancestorChain(maxDepth = 32, snapshot = snapshotParentMap) {
2508
+ try {
2509
+ const map = await snapshot();
2510
+ const chain = pidChainFromMap(process.pid, map, maxDepth);
2511
+ return chain.length >= 2 ? chain : quickChain();
2512
+ } catch {
2513
+ return quickChain();
2514
+ }
2515
+ }
2516
+
2230
2517
  // src/hookInstall.ts
2231
2518
  import {
2232
- readFileSync as readFileSync5,
2233
- writeFileSync as writeFileSync4,
2234
- mkdirSync as mkdirSync4,
2519
+ readFileSync as readFileSync6,
2520
+ writeFileSync as writeFileSync6,
2521
+ mkdirSync as mkdirSync5,
2235
2522
  copyFileSync,
2236
2523
  existsSync as existsSync4,
2237
2524
  renameSync as renameSync2
2238
2525
  } from "node:fs";
2239
- import { homedir as homedir4 } from "node:os";
2240
- import { dirname as dirname5, join as join5 } from "node:path";
2526
+ import { homedir as homedir5 } from "node:os";
2527
+ import { dirname as dirname6, join as join5 } from "node:path";
2241
2528
  import { fileURLToPath } from "node:url";
2242
-
2243
- // src/version.ts
2244
- import { createRequire } from "node:module";
2245
- var PACKAGE_VERSION = (() => {
2246
- try {
2247
- const req = createRequire(import.meta.url);
2248
- const pkg = req("../package.json");
2249
- return pkg.version ?? "0.0.0";
2250
- } catch {
2251
- return "0.0.0";
2252
- }
2253
- })();
2254
-
2255
- // src/hookInstall.ts
2256
2529
  function detectClient(clientName) {
2257
2530
  const name = (clientName ?? "").toLowerCase();
2258
2531
  if (!name) return "unknown";
@@ -2265,16 +2538,16 @@ function detectClient(clientName) {
2265
2538
  var HOOK_COMMAND = `npx -y --package=@korso/shepherd@${PACKAGE_VERSION} shepherd-inbox-hook`;
2266
2539
  var HOOK_MARKER = "shepherd-inbox-hook";
2267
2540
  function ensureHookScript(homeDir, hookScriptSource) {
2268
- const source = hookScriptSource ?? join5(dirname5(fileURLToPath(import.meta.url)), "inboxHook.js");
2541
+ const source = hookScriptSource ?? join5(dirname6(fileURLToPath(import.meta.url)), "inboxHook.js");
2269
2542
  try {
2270
2543
  if (!existsSync4(source)) return null;
2271
2544
  const dest = join5(homeDir, ".shepherd", "hooks", "shepherd-inbox-hook.mjs");
2272
- const next = readFileSync5(source);
2273
- const current = existsSync4(dest) ? readFileSync5(dest) : null;
2545
+ const next = readFileSync6(source);
2546
+ const current = existsSync4(dest) ? readFileSync6(dest) : null;
2274
2547
  if (current === null || !current.equals(next)) {
2275
- mkdirSync4(dirname5(dest), { recursive: true });
2548
+ mkdirSync5(dirname6(dest), { recursive: true });
2276
2549
  const tmp = dest + ".tmp";
2277
- writeFileSync4(tmp, next);
2550
+ writeFileSync6(tmp, next);
2278
2551
  renameSync2(tmp, dest);
2279
2552
  }
2280
2553
  return dest;
@@ -2283,7 +2556,7 @@ function ensureHookScript(homeDir, hookScriptSource) {
2283
2556
  }
2284
2557
  }
2285
2558
  function hookCommandFor(scriptPath) {
2286
- return scriptPath === null ? HOOK_COMMAND : `node "${scriptPath}"`;
2559
+ return scriptPath === null ? HOOK_COMMAND : `node "${scriptPath.replace(/\\/g, "/")}"`;
2287
2560
  }
2288
2561
  function codexHookBlock(scriptPath) {
2289
2562
  const command = scriptPath === null ? `["npx", "-y", "--package=@korso/shepherd@${PACKAGE_VERSION}", "shepherd-inbox-hook"]` : `["node", ${JSON.stringify(scriptPath)}]`;
@@ -2297,7 +2570,7 @@ function codexHookBlock(scriptPath) {
2297
2570
  }
2298
2571
  async function autoInstallHooks({
2299
2572
  clientName,
2300
- homeDir = homedir4(),
2573
+ homeDir = homedir5(),
2301
2574
  disabled = false,
2302
2575
  extensionSource,
2303
2576
  hookScriptSource,
@@ -2322,8 +2595,8 @@ async function autoInstallHooks({
2322
2595
  } else {
2323
2596
  status = installPi(homeDir, extensionSource, log);
2324
2597
  }
2325
- mkdirSync4(dirname5(recordFile), { recursive: true });
2326
- writeFileSync4(
2598
+ mkdirSync5(dirname6(recordFile), { recursive: true });
2599
+ writeFileSync6(
2327
2600
  recordFile,
2328
2601
  JSON.stringify({ status, at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n",
2329
2602
  "utf8"
@@ -2345,7 +2618,7 @@ function installClaude(homeDir, scriptPath, log) {
2345
2618
  const settingsFile = join5(homeDir, ".claude", "settings.json");
2346
2619
  let raw = "";
2347
2620
  if (existsSync4(settingsFile)) {
2348
- raw = readFileSync5(settingsFile, "utf8");
2621
+ raw = readFileSync6(settingsFile, "utf8");
2349
2622
  if (raw.includes(HOOK_MARKER)) return "already-present";
2350
2623
  }
2351
2624
  let settings = {};
@@ -2388,8 +2661,8 @@ function installClaude(homeDir, scriptPath, log) {
2388
2661
  matcher: "*",
2389
2662
  hooks: [{ type: "command", command }]
2390
2663
  });
2391
- mkdirSync4(dirname5(settingsFile), { recursive: true });
2392
- writeFileSync4(settingsFile, JSON.stringify(settings, null, 2) + "\n", "utf8");
2664
+ mkdirSync5(dirname6(settingsFile), { recursive: true });
2665
+ writeFileSync6(settingsFile, JSON.stringify(settings, null, 2) + "\n", "utf8");
2393
2666
  return "installed";
2394
2667
  }
2395
2668
  function installCodex(homeDir, scriptPath, log) {
@@ -2397,13 +2670,13 @@ function installCodex(homeDir, scriptPath, log) {
2397
2670
  const manualHint = "Add the hook manually (see the dashboard's Connect screen).";
2398
2671
  const hookBlock = codexHookBlock(scriptPath);
2399
2672
  if (!existsSync4(configFile)) {
2400
- mkdirSync4(dirname5(configFile), { recursive: true });
2401
- writeFileSync4(configFile, `[features]
2673
+ mkdirSync5(dirname6(configFile), { recursive: true });
2674
+ writeFileSync6(configFile, `[features]
2402
2675
  hooks = true
2403
2676
  ${hookBlock}`, "utf8");
2404
2677
  return "installed";
2405
2678
  }
2406
- const toml = readFileSync5(configFile, "utf8");
2679
+ const toml = readFileSync6(configFile, "utf8");
2407
2680
  if (toml.includes(HOOK_MARKER)) return "already-present";
2408
2681
  if (/^\s*\[hooks\.UserPromptSubmit\]\s*$/m.test(toml)) {
2409
2682
  log(
@@ -2424,10 +2697,10 @@ ${hookBlock}`, "utf8");
2424
2697
  updated = toml.replace(/^(\s*\[features\]\s*)$/m, `$1
2425
2698
  hooks = true`);
2426
2699
  }
2427
- writeFileSync4(configFile, updated + hookBlock, "utf8");
2700
+ writeFileSync6(configFile, updated + hookBlock, "utf8");
2428
2701
  return "installed";
2429
2702
  }
2430
- writeFileSync4(
2703
+ writeFileSync6(
2431
2704
  configFile,
2432
2705
  `${toml}
2433
2706
  [features]
@@ -2441,7 +2714,7 @@ function installCursor(homeDir, scriptPath, log) {
2441
2714
  const hooksFile = join5(homeDir, ".cursor", "hooks.json");
2442
2715
  let raw = "";
2443
2716
  if (existsSync4(hooksFile)) {
2444
- raw = readFileSync5(hooksFile, "utf8");
2717
+ raw = readFileSync6(hooksFile, "utf8");
2445
2718
  if (raw.includes(HOOK_MARKER)) return "already-present";
2446
2719
  }
2447
2720
  let config = {};
@@ -2476,12 +2749,12 @@ function installCursor(homeDir, scriptPath, log) {
2476
2749
  return "skipped";
2477
2750
  }
2478
2751
  entries.push({ command: hookCommandFor(scriptPath) });
2479
- mkdirSync4(dirname5(hooksFile), { recursive: true });
2480
- writeFileSync4(hooksFile, JSON.stringify(config, null, 2) + "\n", "utf8");
2752
+ mkdirSync5(dirname6(hooksFile), { recursive: true });
2753
+ writeFileSync6(hooksFile, JSON.stringify(config, null, 2) + "\n", "utf8");
2481
2754
  return "installed";
2482
2755
  }
2483
2756
  function installPi(homeDir, extensionSource, log) {
2484
- const source = extensionSource ?? join5(dirname5(fileURLToPath(import.meta.url)), "inboxExtension.js");
2757
+ const source = extensionSource ?? join5(dirname6(fileURLToPath(import.meta.url)), "inboxExtension.js");
2485
2758
  const dest = join5(homeDir, ".pi", "agent", "extensions", "shepherd-inbox.js");
2486
2759
  if (existsSync4(dest)) return "already-present";
2487
2760
  if (!existsSync4(source)) {
@@ -2490,7 +2763,7 @@ function installPi(homeDir, extensionSource, log) {
2490
2763
  );
2491
2764
  return "skipped";
2492
2765
  }
2493
- mkdirSync4(dirname5(dest), { recursive: true });
2766
+ mkdirSync5(dirname6(dest), { recursive: true });
2494
2767
  copyFileSync(source, dest);
2495
2768
  return "installed";
2496
2769
  }
@@ -2504,7 +2777,21 @@ async function main() {
2504
2777
  });
2505
2778
  const context = await resolveContext(config);
2506
2779
  const inboxDir = config.SHEPHERD_INBOX_DIR ?? defaultInboxDir();
2507
- const inboxFile = inboxFilePath(inboxDir, process.cwd());
2780
+ const inboxFile = sessionMailboxPath(inboxDir, process.pid);
2781
+ const launchCwd = process.cwd();
2782
+ let serverChain = quickChain();
2783
+ const liveness = {
2784
+ refresh: () => writeMailboxMeta(inboxDir, process.pid, {
2785
+ cwd: launchCwd,
2786
+ chain: serverChain
2787
+ }),
2788
+ remove: () => removeMailboxMeta(inboxDir, process.pid)
2789
+ };
2790
+ void ancestorChain().then((chain) => {
2791
+ serverChain = chain;
2792
+ liveness.refresh();
2793
+ }).catch(() => {
2794
+ });
2508
2795
  const heartbeat = createHeartbeat({
2509
2796
  hubClient,
2510
2797
  intervalSeconds: config.HEARTBEAT_INTERVAL_SECONDS,
@@ -2517,10 +2804,11 @@ async function main() {
2517
2804
  return void 0;
2518
2805
  }
2519
2806
  },
2520
- // A model-visible sink (this working dir's inbox file). Its presence opts
2521
- // the heartbeat into two-phase announcement delivery: append locally, then
2807
+ // A model-visible sink (this session's mailbox). Its presence opts the
2808
+ // heartbeat into two-phase announcement delivery: append locally, then
2522
2809
  // ack the hub. appendAnnouncements is itself fail-open.
2523
- announcementSink: (announcements) => appendAnnouncements(inboxFile, announcements)
2810
+ announcementSink: (announcements) => appendAnnouncements(inboxFile, announcements),
2811
+ liveness
2524
2812
  });
2525
2813
  const server = new McpServer(
2526
2814
  { name: "shepherd", version: PACKAGE_VERSION },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@korso/shepherd",
3
- "version": "0.9.1",
3
+ "version": "0.11.0",
4
4
  "description": "Shepherd MCP server — gives any MCP-capable agent (Claude Code, Codex, etc.) advisory cross-session coordination tools (work/done/announce/sync, plus link/unlink/decline) backed by the shared Shepherd hub. Joins the workspace automatically and ships standing instructions so the agent self-coordinates.",
5
5
  "homepage": "https://github.com/Korso-AI/shepherd#readme",
6
6
  "bugs": {