@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.
- package/dist/inboxExtension.js +102 -5
- package/dist/inboxHook.js +301 -7
- package/dist/index.js +367 -79
- 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
|
@@ -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
|
-
|
|
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
|
-
|
|
1172
|
-
|
|
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: [
|
|
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: [
|
|
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
|
|
2042
|
-
import { homedir as
|
|
2043
|
-
import { dirname as
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
2253
|
+
mkdirSync4(dirname5(filePath), { recursive: true });
|
|
2073
2254
|
const payload = JSON.stringify({ human });
|
|
2074
|
-
|
|
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
|
|
2233
|
-
writeFileSync as
|
|
2234
|
-
mkdirSync as
|
|
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
|
|
2240
|
-
import { dirname as
|
|
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(
|
|
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 =
|
|
2273
|
-
const current = existsSync4(dest) ?
|
|
2545
|
+
const next = readFileSync6(source);
|
|
2546
|
+
const current = existsSync4(dest) ? readFileSync6(dest) : null;
|
|
2274
2547
|
if (current === null || !current.equals(next)) {
|
|
2275
|
-
|
|
2548
|
+
mkdirSync5(dirname6(dest), { recursive: true });
|
|
2276
2549
|
const tmp = dest + ".tmp";
|
|
2277
|
-
|
|
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 =
|
|
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
|
-
|
|
2326
|
-
|
|
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 =
|
|
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
|
-
|
|
2392
|
-
|
|
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
|
-
|
|
2401
|
-
|
|
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 =
|
|
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
|
-
|
|
2700
|
+
writeFileSync6(configFile, updated + hookBlock, "utf8");
|
|
2428
2701
|
return "installed";
|
|
2429
2702
|
}
|
|
2430
|
-
|
|
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 =
|
|
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
|
-
|
|
2480
|
-
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
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
|
|
2521
|
-
//
|
|
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.
|
|
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": {
|