@korso/shepherd 0.9.1 → 0.10.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.
- package/dist/inboxExtension.js +102 -5
- package/dist/inboxHook.js +301 -7
- package/dist/index.js +218 -35
- package/package.json +1 -1
package/dist/inboxExtension.js
CHANGED
|
@@ -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
|
-
|
|
130
|
-
|
|
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}
|
|
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 =
|
|
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
|
-
|
|
132
|
-
|
|
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}
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
@@ -712,6 +712,34 @@ var FeedbackResponse = z2.object({
|
|
|
712
712
|
// uuid PK (the feedback table, like workspaces, uses gen_random_uuid()).
|
|
713
713
|
id: z2.string()
|
|
714
714
|
});
|
|
715
|
+
var NullableCap = z2.number().int().positive().nullable();
|
|
716
|
+
var EntitlementLimits = z2.object({
|
|
717
|
+
seatsLimit: NullableCap,
|
|
718
|
+
reposLimit: NullableCap,
|
|
719
|
+
retentionDays: NullableCap
|
|
720
|
+
});
|
|
721
|
+
var LimitExceededErrorBody = z2.object({
|
|
722
|
+
error: z2.string(),
|
|
723
|
+
code: z2.literal("limit_exceeded"),
|
|
724
|
+
limit: z2.enum(["seats", "repos"]),
|
|
725
|
+
current: z2.number().int(),
|
|
726
|
+
max: z2.number().int()
|
|
727
|
+
});
|
|
728
|
+
var WorkspaceEntitlements = EntitlementLimits.extend({
|
|
729
|
+
expiresAt: IsoTimestamp.nullable(),
|
|
730
|
+
updatedAt: IsoTimestamp
|
|
731
|
+
});
|
|
732
|
+
var PutEntitlementsRequest = EntitlementLimits.extend({
|
|
733
|
+
expiresAt: IsoTimestamp.nullable()
|
|
734
|
+
});
|
|
735
|
+
var EntitlementsStatusResponse = z2.object({
|
|
736
|
+
record: WorkspaceEntitlements.nullable(),
|
|
737
|
+
effective: EntitlementLimits,
|
|
738
|
+
usage: z2.object({
|
|
739
|
+
seatsUsed: z2.number().int(),
|
|
740
|
+
reposUsed: z2.number().int()
|
|
741
|
+
})
|
|
742
|
+
});
|
|
715
743
|
var TrendPoint = z2.object({
|
|
716
744
|
// `YYYY-MM-DD` (UTC day).
|
|
717
745
|
date: z2.string(),
|
|
@@ -1144,13 +1172,15 @@ async function buildChangeReport(cwd, config) {
|
|
|
1144
1172
|
}
|
|
1145
1173
|
|
|
1146
1174
|
// src/inbox.ts
|
|
1147
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
1148
1175
|
import {
|
|
1149
1176
|
appendFileSync,
|
|
1150
1177
|
mkdirSync as mkdirSync2,
|
|
1151
1178
|
readFileSync as readFileSync3,
|
|
1179
|
+
readdirSync,
|
|
1152
1180
|
renameSync,
|
|
1153
1181
|
rmSync as rmSync3,
|
|
1182
|
+
statSync,
|
|
1183
|
+
writeFileSync as writeFileSync3,
|
|
1154
1184
|
existsSync as existsSync3
|
|
1155
1185
|
} from "node:fs";
|
|
1156
1186
|
import { homedir as homedir2, tmpdir as tmpdir2 } from "node:os";
|
|
@@ -1165,11 +1195,37 @@ function defaultInboxDir() {
|
|
|
1165
1195
|
if (!base) base = tmpdir2();
|
|
1166
1196
|
return join3(base, ".shepherd", "inbox");
|
|
1167
1197
|
}
|
|
1168
|
-
|
|
1198
|
+
var MAILBOX_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
1199
|
+
var MAILBOX_FRESH_MS = 15 * 60 * 1e3;
|
|
1200
|
+
function sessionMailboxPath(dir, serverPid) {
|
|
1201
|
+
return join3(dir, `agent-${serverPid}.jsonl`);
|
|
1202
|
+
}
|
|
1203
|
+
function sessionMetaPath(dir, serverPid) {
|
|
1204
|
+
return join3(dir, `agent-${serverPid}.json`);
|
|
1205
|
+
}
|
|
1206
|
+
function normalizeCwd(cwd) {
|
|
1169
1207
|
let normalized = resolve3(cwd);
|
|
1170
1208
|
if (process.platform === "win32") normalized = normalized.toLowerCase();
|
|
1171
|
-
|
|
1172
|
-
|
|
1209
|
+
return normalized;
|
|
1210
|
+
}
|
|
1211
|
+
function writeMailboxMeta(dir, serverPid, meta) {
|
|
1212
|
+
try {
|
|
1213
|
+
mkdirSync2(dir, { recursive: true });
|
|
1214
|
+
const dest = sessionMetaPath(dir, serverPid);
|
|
1215
|
+
const tmp = `${dest}.tmp`;
|
|
1216
|
+
writeFileSync3(
|
|
1217
|
+
tmp,
|
|
1218
|
+
JSON.stringify({ v: 1, cwd: normalizeCwd(meta.cwd), chain: meta.chain })
|
|
1219
|
+
);
|
|
1220
|
+
renameSync(tmp, dest);
|
|
1221
|
+
} catch {
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
function removeMailboxMeta(dir, serverPid) {
|
|
1225
|
+
try {
|
|
1226
|
+
rmSync3(sessionMetaPath(dir, serverPid), { force: true });
|
|
1227
|
+
} catch {
|
|
1228
|
+
}
|
|
1173
1229
|
}
|
|
1174
1230
|
function appendAnnouncements(filePath, announcements) {
|
|
1175
1231
|
if (!announcements || announcements.length === 0) return;
|
|
@@ -1221,6 +1277,19 @@ function oneLine(text) {
|
|
|
1221
1277
|
function indentContinuation(text) {
|
|
1222
1278
|
return text.replace(/\r?\n/g, "\n ");
|
|
1223
1279
|
}
|
|
1280
|
+
function relativeAge(iso) {
|
|
1281
|
+
const then = Date.parse(iso);
|
|
1282
|
+
if (Number.isNaN(then)) return "recently";
|
|
1283
|
+
const ms = Date.now() - then;
|
|
1284
|
+
if (ms < 0) return "just now";
|
|
1285
|
+
const mins = Math.floor(ms / 6e4);
|
|
1286
|
+
if (mins < 1) return "just now";
|
|
1287
|
+
if (mins < 60) return `${mins}m ago`;
|
|
1288
|
+
const hours = Math.floor(mins / 60);
|
|
1289
|
+
if (hours < 24) return `${hours}h ago`;
|
|
1290
|
+
const days = Math.floor(hours / 24);
|
|
1291
|
+
return `${days}d ago`;
|
|
1292
|
+
}
|
|
1224
1293
|
function mergeAnnouncements(...lists) {
|
|
1225
1294
|
const byId = /* @__PURE__ */ new Map();
|
|
1226
1295
|
for (const list of lists) {
|
|
@@ -1419,7 +1488,7 @@ function formatLandscape(landscape) {
|
|
|
1419
1488
|
for (const a of landscape.announcements) {
|
|
1420
1489
|
const target = a.targetAgentName ? ` \u2192 ${oneLine(a.targetAgentName)}` : " (broadcast)";
|
|
1421
1490
|
lines.push(
|
|
1422
|
-
` [${oneLine(a.fromAgentName)}${target}] ${indentContinuation(a.body)}`
|
|
1491
|
+
` [${oneLine(a.fromAgentName)}${target}, ${relativeAge(a.createdAt)}] ${indentContinuation(a.body)}`
|
|
1423
1492
|
);
|
|
1424
1493
|
}
|
|
1425
1494
|
lines.push(REPLY_ROUTING_HINT);
|
|
@@ -1434,25 +1503,12 @@ function formatAnnouncements(announcements) {
|
|
|
1434
1503
|
for (const a of announcements) {
|
|
1435
1504
|
const target = a.targetAgentName ? ` \u2192 ${oneLine(a.targetAgentName)}` : " (broadcast)";
|
|
1436
1505
|
lines.push(
|
|
1437
|
-
` [${oneLine(a.fromAgentName)}${target}] ${indentContinuation(a.body)}`
|
|
1506
|
+
` [${oneLine(a.fromAgentName)}${target}, ${relativeAge(a.createdAt)}] ${indentContinuation(a.body)}`
|
|
1438
1507
|
);
|
|
1439
1508
|
}
|
|
1440
1509
|
lines.push(REPLY_ROUTING_HINT);
|
|
1441
1510
|
return lines.join("\n");
|
|
1442
1511
|
}
|
|
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
1512
|
function presence(rec) {
|
|
1457
1513
|
return rec.authorIsLive ? "active now" : `offline, last seen ${relativeAge(rec.authorLastActiveAt)}`;
|
|
1458
1514
|
}
|
|
@@ -1529,6 +1585,7 @@ function registerTools(server, deps) {
|
|
|
1529
1585
|
const repoRoot = findRepoRoot(markerCwd);
|
|
1530
1586
|
let sessionId = null;
|
|
1531
1587
|
let agentName = null;
|
|
1588
|
+
let activeWorkspaceSlug = null;
|
|
1532
1589
|
const isHosted = Boolean(config.SHEPHERD_TOKEN);
|
|
1533
1590
|
const selfHostMismatch = !isHosted && context.linked && config.WORKSPACE !== void 0 && config.WORKSPACE !== context.workspace;
|
|
1534
1591
|
let hostedWorkspaceRejected = false;
|
|
@@ -1561,6 +1618,9 @@ function registerTools(server, deps) {
|
|
|
1561
1618
|
let joinFailure = null;
|
|
1562
1619
|
let joinInFlight = Promise.resolve();
|
|
1563
1620
|
async function activate(workspaceSlug) {
|
|
1621
|
+
if (sessionId !== null && activeWorkspaceSlug === workspaceSlug) {
|
|
1622
|
+
return { ok: true };
|
|
1623
|
+
}
|
|
1564
1624
|
const joinBody = {
|
|
1565
1625
|
workspace: workspaceSlug,
|
|
1566
1626
|
repo: context.repo,
|
|
@@ -1586,6 +1646,7 @@ function registerTools(server, deps) {
|
|
|
1586
1646
|
heartbeat.start(newSessionId);
|
|
1587
1647
|
sessionId = newSessionId;
|
|
1588
1648
|
agentName = parsed.data.agentName;
|
|
1649
|
+
activeWorkspaceSlug = workspaceSlug;
|
|
1589
1650
|
linked = true;
|
|
1590
1651
|
hostedWorkspaceRejected = false;
|
|
1591
1652
|
joinFailure = null;
|
|
@@ -1932,6 +1993,7 @@ ${msgs}` : base }]
|
|
|
1932
1993
|
await leave();
|
|
1933
1994
|
sessionId = null;
|
|
1934
1995
|
agentName = null;
|
|
1996
|
+
activeWorkspaceSlug = null;
|
|
1935
1997
|
}
|
|
1936
1998
|
return advisory(
|
|
1937
1999
|
"Unlinked \u2014 this repo will stay uncoordinated and won't ask again. Run `link` to re-enable."
|
|
@@ -2038,7 +2100,7 @@ function postLinkGuidance(workspace) {
|
|
|
2038
2100
|
}
|
|
2039
2101
|
|
|
2040
2102
|
// src/identityCache.ts
|
|
2041
|
-
import { mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as
|
|
2103
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
2042
2104
|
import { homedir as homedir3, tmpdir as tmpdir3 } from "node:os";
|
|
2043
2105
|
import { dirname as dirname4, join as join4 } from "node:path";
|
|
2044
2106
|
function defaultIdentityCachePath() {
|
|
@@ -2071,7 +2133,7 @@ function writeCachedHuman(human, filePath = defaultIdentityCachePath()) {
|
|
|
2071
2133
|
try {
|
|
2072
2134
|
mkdirSync3(dirname4(filePath), { recursive: true });
|
|
2073
2135
|
const payload = JSON.stringify({ human });
|
|
2074
|
-
|
|
2136
|
+
writeFileSync4(filePath, payload + "\n", "utf8");
|
|
2075
2137
|
} catch {
|
|
2076
2138
|
}
|
|
2077
2139
|
}
|
|
@@ -2130,13 +2192,20 @@ function createHeartbeat({
|
|
|
2130
2192
|
hubClient,
|
|
2131
2193
|
intervalSeconds,
|
|
2132
2194
|
buildReport,
|
|
2133
|
-
announcementSink
|
|
2195
|
+
announcementSink,
|
|
2196
|
+
liveness
|
|
2134
2197
|
}) {
|
|
2135
2198
|
let timer = null;
|
|
2136
2199
|
function stop() {
|
|
2137
2200
|
if (timer !== null) {
|
|
2138
2201
|
clearInterval(timer);
|
|
2139
2202
|
timer = null;
|
|
2203
|
+
if (liveness) {
|
|
2204
|
+
try {
|
|
2205
|
+
liveness.remove();
|
|
2206
|
+
} catch {
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2140
2209
|
}
|
|
2141
2210
|
}
|
|
2142
2211
|
async function beat(sessionId) {
|
|
@@ -2150,6 +2219,12 @@ function createHeartbeat({
|
|
|
2150
2219
|
}
|
|
2151
2220
|
const body = { sessionId };
|
|
2152
2221
|
if (changeReport) body.changeReport = changeReport;
|
|
2222
|
+
if (liveness) {
|
|
2223
|
+
try {
|
|
2224
|
+
liveness.refresh();
|
|
2225
|
+
} catch {
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2153
2228
|
if (announcementSink) body.deliverAnnouncements = true;
|
|
2154
2229
|
const parsed = HeartbeatResponse.safeParse(
|
|
2155
2230
|
await hubClient.post("/heartbeat", body)
|
|
@@ -2177,6 +2252,12 @@ function createHeartbeat({
|
|
|
2177
2252
|
}
|
|
2178
2253
|
function start(sessionId) {
|
|
2179
2254
|
stop();
|
|
2255
|
+
if (liveness) {
|
|
2256
|
+
try {
|
|
2257
|
+
liveness.refresh();
|
|
2258
|
+
} catch {
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2180
2261
|
timer = setInterval(() => {
|
|
2181
2262
|
void beat(sessionId).catch((err) => {
|
|
2182
2263
|
console.error(
|
|
@@ -2227,10 +2308,97 @@ Skip \`work\` entirely for read-only exploration \u2014 reading, searching, or t
|
|
|
2227
2308
|
|
|
2228
2309
|
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
2310
|
|
|
2311
|
+
// src/processTree.ts
|
|
2312
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
2313
|
+
import { promisify } from "node:util";
|
|
2314
|
+
var execFileAsync = promisify(execFile2);
|
|
2315
|
+
function pidChainFromMap(startPid, parentOf, maxDepth = 32) {
|
|
2316
|
+
const chain = [];
|
|
2317
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2318
|
+
let pid = startPid;
|
|
2319
|
+
while (chain.length < maxDepth && pid > 0 && !seen.has(pid)) {
|
|
2320
|
+
chain.push(pid);
|
|
2321
|
+
seen.add(pid);
|
|
2322
|
+
const parent = parentOf.get(pid);
|
|
2323
|
+
if (parent === void 0) break;
|
|
2324
|
+
pid = parent;
|
|
2325
|
+
}
|
|
2326
|
+
return chain;
|
|
2327
|
+
}
|
|
2328
|
+
function quickChain() {
|
|
2329
|
+
return [process.pid, process.ppid];
|
|
2330
|
+
}
|
|
2331
|
+
function parseWmicProcessList(text) {
|
|
2332
|
+
const map = /* @__PURE__ */ new Map();
|
|
2333
|
+
const lines = text.split(/\r?\n/).filter((l) => l.trim().length > 0);
|
|
2334
|
+
if (lines.length === 0) return map;
|
|
2335
|
+
const header = lines[0].trimStart();
|
|
2336
|
+
let pidFirst;
|
|
2337
|
+
if (header.startsWith("ParentProcessId")) pidFirst = false;
|
|
2338
|
+
else if (header.startsWith("ProcessId")) pidFirst = true;
|
|
2339
|
+
else return map;
|
|
2340
|
+
for (const line of lines.slice(1)) {
|
|
2341
|
+
const nums = line.trim().split(/\s+/).map(Number);
|
|
2342
|
+
if (nums.length !== 2 || nums.some((n) => !Number.isInteger(n))) continue;
|
|
2343
|
+
const [a, b] = nums;
|
|
2344
|
+
const [pid, ppid] = pidFirst ? [a, b] : [b, a];
|
|
2345
|
+
map.set(pid, ppid);
|
|
2346
|
+
}
|
|
2347
|
+
return map;
|
|
2348
|
+
}
|
|
2349
|
+
function parsePidPpidLines(text) {
|
|
2350
|
+
const map = /* @__PURE__ */ new Map();
|
|
2351
|
+
for (const line of text.split(/\r?\n/)) {
|
|
2352
|
+
const m = /^\s*(\d+)\s+(\d+)\s*$/.exec(line);
|
|
2353
|
+
if (m) map.set(Number(m[1]), Number(m[2]));
|
|
2354
|
+
}
|
|
2355
|
+
return map;
|
|
2356
|
+
}
|
|
2357
|
+
async function snapshotParentMap() {
|
|
2358
|
+
if (process.platform === "win32") {
|
|
2359
|
+
try {
|
|
2360
|
+
const { stdout: stdout3 } = await execFileAsync(
|
|
2361
|
+
"wmic",
|
|
2362
|
+
["process", "get", "ProcessId,ParentProcessId"],
|
|
2363
|
+
{ windowsHide: true, timeout: 1e4, maxBuffer: 8 * 1024 * 1024 }
|
|
2364
|
+
);
|
|
2365
|
+
const map = parseWmicProcessList(stdout3);
|
|
2366
|
+
if (map.size > 0) return map;
|
|
2367
|
+
} catch {
|
|
2368
|
+
}
|
|
2369
|
+
const { stdout: stdout2 } = await execFileAsync(
|
|
2370
|
+
"powershell.exe",
|
|
2371
|
+
[
|
|
2372
|
+
"-NoProfile",
|
|
2373
|
+
"-NonInteractive",
|
|
2374
|
+
"-Command",
|
|
2375
|
+
'Get-CimInstance -Query "SELECT ProcessId,ParentProcessId FROM Win32_Process" | ForEach-Object { "$($_.ProcessId) $($_.ParentProcessId)" }'
|
|
2376
|
+
],
|
|
2377
|
+
{ windowsHide: true, timeout: 15e3, maxBuffer: 8 * 1024 * 1024 }
|
|
2378
|
+
);
|
|
2379
|
+
return parsePidPpidLines(stdout2);
|
|
2380
|
+
}
|
|
2381
|
+
const { stdout } = await execFileAsync(
|
|
2382
|
+
"ps",
|
|
2383
|
+
["-eo", "pid=,ppid="],
|
|
2384
|
+
{ timeout: 1e4, maxBuffer: 8 * 1024 * 1024 }
|
|
2385
|
+
);
|
|
2386
|
+
return parsePidPpidLines(stdout);
|
|
2387
|
+
}
|
|
2388
|
+
async function ancestorChain(maxDepth = 32, snapshot = snapshotParentMap) {
|
|
2389
|
+
try {
|
|
2390
|
+
const map = await snapshot();
|
|
2391
|
+
const chain = pidChainFromMap(process.pid, map, maxDepth);
|
|
2392
|
+
return chain.length >= 2 ? chain : quickChain();
|
|
2393
|
+
} catch {
|
|
2394
|
+
return quickChain();
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2230
2398
|
// src/hookInstall.ts
|
|
2231
2399
|
import {
|
|
2232
2400
|
readFileSync as readFileSync5,
|
|
2233
|
-
writeFileSync as
|
|
2401
|
+
writeFileSync as writeFileSync5,
|
|
2234
2402
|
mkdirSync as mkdirSync4,
|
|
2235
2403
|
copyFileSync,
|
|
2236
2404
|
existsSync as existsSync4,
|
|
@@ -2274,7 +2442,7 @@ function ensureHookScript(homeDir, hookScriptSource) {
|
|
|
2274
2442
|
if (current === null || !current.equals(next)) {
|
|
2275
2443
|
mkdirSync4(dirname5(dest), { recursive: true });
|
|
2276
2444
|
const tmp = dest + ".tmp";
|
|
2277
|
-
|
|
2445
|
+
writeFileSync5(tmp, next);
|
|
2278
2446
|
renameSync2(tmp, dest);
|
|
2279
2447
|
}
|
|
2280
2448
|
return dest;
|
|
@@ -2283,7 +2451,7 @@ function ensureHookScript(homeDir, hookScriptSource) {
|
|
|
2283
2451
|
}
|
|
2284
2452
|
}
|
|
2285
2453
|
function hookCommandFor(scriptPath) {
|
|
2286
|
-
return scriptPath === null ? HOOK_COMMAND : `node "${scriptPath}"`;
|
|
2454
|
+
return scriptPath === null ? HOOK_COMMAND : `node "${scriptPath.replace(/\\/g, "/")}"`;
|
|
2287
2455
|
}
|
|
2288
2456
|
function codexHookBlock(scriptPath) {
|
|
2289
2457
|
const command = scriptPath === null ? `["npx", "-y", "--package=@korso/shepherd@${PACKAGE_VERSION}", "shepherd-inbox-hook"]` : `["node", ${JSON.stringify(scriptPath)}]`;
|
|
@@ -2323,7 +2491,7 @@ async function autoInstallHooks({
|
|
|
2323
2491
|
status = installPi(homeDir, extensionSource, log);
|
|
2324
2492
|
}
|
|
2325
2493
|
mkdirSync4(dirname5(recordFile), { recursive: true });
|
|
2326
|
-
|
|
2494
|
+
writeFileSync5(
|
|
2327
2495
|
recordFile,
|
|
2328
2496
|
JSON.stringify({ status, at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n",
|
|
2329
2497
|
"utf8"
|
|
@@ -2389,7 +2557,7 @@ function installClaude(homeDir, scriptPath, log) {
|
|
|
2389
2557
|
hooks: [{ type: "command", command }]
|
|
2390
2558
|
});
|
|
2391
2559
|
mkdirSync4(dirname5(settingsFile), { recursive: true });
|
|
2392
|
-
|
|
2560
|
+
writeFileSync5(settingsFile, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
2393
2561
|
return "installed";
|
|
2394
2562
|
}
|
|
2395
2563
|
function installCodex(homeDir, scriptPath, log) {
|
|
@@ -2398,7 +2566,7 @@ function installCodex(homeDir, scriptPath, log) {
|
|
|
2398
2566
|
const hookBlock = codexHookBlock(scriptPath);
|
|
2399
2567
|
if (!existsSync4(configFile)) {
|
|
2400
2568
|
mkdirSync4(dirname5(configFile), { recursive: true });
|
|
2401
|
-
|
|
2569
|
+
writeFileSync5(configFile, `[features]
|
|
2402
2570
|
hooks = true
|
|
2403
2571
|
${hookBlock}`, "utf8");
|
|
2404
2572
|
return "installed";
|
|
@@ -2424,10 +2592,10 @@ ${hookBlock}`, "utf8");
|
|
|
2424
2592
|
updated = toml.replace(/^(\s*\[features\]\s*)$/m, `$1
|
|
2425
2593
|
hooks = true`);
|
|
2426
2594
|
}
|
|
2427
|
-
|
|
2595
|
+
writeFileSync5(configFile, updated + hookBlock, "utf8");
|
|
2428
2596
|
return "installed";
|
|
2429
2597
|
}
|
|
2430
|
-
|
|
2598
|
+
writeFileSync5(
|
|
2431
2599
|
configFile,
|
|
2432
2600
|
`${toml}
|
|
2433
2601
|
[features]
|
|
@@ -2477,7 +2645,7 @@ function installCursor(homeDir, scriptPath, log) {
|
|
|
2477
2645
|
}
|
|
2478
2646
|
entries.push({ command: hookCommandFor(scriptPath) });
|
|
2479
2647
|
mkdirSync4(dirname5(hooksFile), { recursive: true });
|
|
2480
|
-
|
|
2648
|
+
writeFileSync5(hooksFile, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
2481
2649
|
return "installed";
|
|
2482
2650
|
}
|
|
2483
2651
|
function installPi(homeDir, extensionSource, log) {
|
|
@@ -2504,7 +2672,21 @@ async function main() {
|
|
|
2504
2672
|
});
|
|
2505
2673
|
const context = await resolveContext(config);
|
|
2506
2674
|
const inboxDir = config.SHEPHERD_INBOX_DIR ?? defaultInboxDir();
|
|
2507
|
-
const inboxFile =
|
|
2675
|
+
const inboxFile = sessionMailboxPath(inboxDir, process.pid);
|
|
2676
|
+
const launchCwd = process.cwd();
|
|
2677
|
+
let serverChain = quickChain();
|
|
2678
|
+
const liveness = {
|
|
2679
|
+
refresh: () => writeMailboxMeta(inboxDir, process.pid, {
|
|
2680
|
+
cwd: launchCwd,
|
|
2681
|
+
chain: serverChain
|
|
2682
|
+
}),
|
|
2683
|
+
remove: () => removeMailboxMeta(inboxDir, process.pid)
|
|
2684
|
+
};
|
|
2685
|
+
void ancestorChain().then((chain) => {
|
|
2686
|
+
serverChain = chain;
|
|
2687
|
+
liveness.refresh();
|
|
2688
|
+
}).catch(() => {
|
|
2689
|
+
});
|
|
2508
2690
|
const heartbeat = createHeartbeat({
|
|
2509
2691
|
hubClient,
|
|
2510
2692
|
intervalSeconds: config.HEARTBEAT_INTERVAL_SECONDS,
|
|
@@ -2517,10 +2699,11 @@ async function main() {
|
|
|
2517
2699
|
return void 0;
|
|
2518
2700
|
}
|
|
2519
2701
|
},
|
|
2520
|
-
// A model-visible sink (this
|
|
2521
|
-
//
|
|
2702
|
+
// A model-visible sink (this session's mailbox). Its presence opts the
|
|
2703
|
+
// heartbeat into two-phase announcement delivery: append locally, then
|
|
2522
2704
|
// ack the hub. appendAnnouncements is itself fail-open.
|
|
2523
|
-
announcementSink: (announcements) => appendAnnouncements(inboxFile, announcements)
|
|
2705
|
+
announcementSink: (announcements) => appendAnnouncements(inboxFile, announcements),
|
|
2706
|
+
liveness
|
|
2524
2707
|
});
|
|
2525
2708
|
const server = new McpServer(
|
|
2526
2709
|
{ name: "shepherd", version: PACKAGE_VERSION },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@korso/shepherd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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": {
|