@korso/shepherd 0.11.1 → 0.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -30
- package/dist/index.js +596 -90
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -105,34 +105,45 @@ default `~/.shepherd/inbox`). That file is then drained by two paths:
|
|
|
105
105
|
> given client on this machine it **writes to that client's own configuration
|
|
106
106
|
> file in your home directory**, without a separate prompt:
|
|
107
107
|
>
|
|
108
|
-
> | Client | File it edits/creates | What it adds
|
|
109
|
-
> | ----------- | ------------------------------------------ |
|
|
110
|
-
> | Claude Code | `~/.claude/settings.json` | `SessionStart` + `PreToolUse` hook entries
|
|
111
|
-
> | Codex | `~/.codex/config.toml` |
|
|
112
|
-
> | Cursor | `~/.cursor/hooks.json` | a `beforeSubmitPrompt` entry
|
|
113
|
-
> | Pi | `~/.pi/agent/extensions/shepherd-inbox.js` | copies the bundled extension
|
|
108
|
+
> | Client | File it edits/creates | What it adds |
|
|
109
|
+
> | ----------- | ------------------------------------------ | --------------------------------------------------------- |
|
|
110
|
+
> | Claude Code | `~/.claude/settings.json` | `SessionStart` + `PreToolUse` hook entries |
|
|
111
|
+
> | Codex | `~/.codex/config.toml` | the hooks feature plus all three canonical event handlers |
|
|
112
|
+
> | Cursor | `~/.cursor/hooks.json` | a `beforeSubmitPrompt` entry |
|
|
113
|
+
> | Pi | `~/.pi/agent/extensions/shepherd-inbox.js` | copies the bundled extension |
|
|
114
114
|
>
|
|
115
|
-
>
|
|
116
|
-
>
|
|
117
|
-
>
|
|
118
|
-
>
|
|
119
|
-
>
|
|
120
|
-
>
|
|
115
|
+
> Fresh installs are **additive only** (existing keys/entries are never removed
|
|
116
|
+
> or reordered), **record-guarded** under `~/.shepherd/hooks/`,
|
|
117
|
+
> **version-pinned** (the installed command runs the exact shipped build, not a
|
|
118
|
+
> floating `npx latest`). Unsupported client config shapes are left untouched;
|
|
119
|
+
> installer and migration failures never block or break the agent session, and
|
|
120
|
+
> failure logs may be emitted to stderr. Codex also has a one-time, versioned
|
|
121
|
+
> migration, but only when it finds both a legacy auto-install record at
|
|
122
|
+
> `~/.shepherd/hooks/codex.json` and the exact Shepherd-owned legacy block. It
|
|
123
|
+
> preserves that block and appends only the missing handlers after saving a
|
|
124
|
+
> persistent backup at
|
|
125
|
+
> `~/.shepherd/hooks/backups/codex-config-before-v2.toml`. Ambiguous or manually
|
|
126
|
+
> removed hooks are left alone. Existing users receive the migration after they
|
|
127
|
+
> update `@korso/shepherd` and restart Codex so the updated MCP server starts.
|
|
121
128
|
>
|
|
122
129
|
> **To opt out entirely, set `SHEPHERD_NO_AUTO_HOOKS=1`** — the server then never
|
|
123
130
|
> touches any client config, and you can wire the hook manually using the
|
|
124
131
|
> per-client snippets below.
|
|
125
132
|
|
|
126
133
|
On its first `initialize` handshake the server detects the connecting client
|
|
127
|
-
and
|
|
128
|
-
machine**:
|
|
134
|
+
and installs the delivery hook **once per machine**:
|
|
129
135
|
|
|
130
136
|
- **Claude Code** — merges the `SessionStart` + `PreToolUse` hook entries into
|
|
131
137
|
`~/.claude/settings.json` (additive JSON merge; an unparseable file is left
|
|
132
138
|
untouched).
|
|
133
|
-
- **Codex** —
|
|
134
|
-
`
|
|
135
|
-
|
|
139
|
+
- **Codex** — enables `features.hooks` and appends canonical
|
|
140
|
+
`UserPromptSubmit`, `SessionStart`, and wildcard `PreToolUse` handlers to
|
|
141
|
+
`~/.codex/config.toml`. An explicit `hooks = false` is respected. Existing
|
|
142
|
+
installs are migrated only when a legacy `~/.shepherd/hooks/codex.json`
|
|
143
|
+
auto-install record and Shepherd's exact legacy `UserPromptSubmit` block are
|
|
144
|
+
both present. The migration retains that block, appends the two missing
|
|
145
|
+
handlers, and first writes a persistent backup. Update `@korso/shepherd` and
|
|
146
|
+
restart Codex to run it.
|
|
136
147
|
- **Pi** — copies the bundled extension to
|
|
137
148
|
`~/.pi/agent/extensions/shepherd-inbox.js`.
|
|
138
149
|
- **Cursor** — merges a `beforeSubmitPrompt` entry into `~/.cursor/hooks.json`
|
|
@@ -140,10 +151,11 @@ machine**:
|
|
|
140
151
|
verified to inject hook output into the agent's context, and wiring an
|
|
141
152
|
unverified event would consume announcements without delivering them.
|
|
142
153
|
|
|
143
|
-
A record under `~/.shepherd/hooks/` guarantees at-most-once
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
154
|
+
A record under `~/.shepherd/hooks/` guarantees at-most-once installation (and
|
|
155
|
+
tracks the current Codex migration version): if you remove the hook, Shepherd
|
|
156
|
+
won't re-add it. Installation and migration are fail-open: a failure never
|
|
157
|
+
blocks or breaks the agent session. `SHEPHERD_NO_AUTO_HOOKS=1` disables the
|
|
158
|
+
whole mechanism.
|
|
147
159
|
|
|
148
160
|
Both paths read the **same** inbox file and de-duplicate by announcement id, so
|
|
149
161
|
running both is safe (the hub hands each announcement to exactly one drain; the
|
|
@@ -225,24 +237,41 @@ uses (override both with `SHEPHERD_INBOX_DIR` if you relocated it):
|
|
|
225
237
|
}
|
|
226
238
|
```
|
|
227
239
|
|
|
228
|
-
### Codex — `UserPromptSubmit`
|
|
240
|
+
### Codex — `UserPromptSubmit` + `SessionStart` + `PreToolUse` hooks
|
|
229
241
|
|
|
230
|
-
_(Installed automatically
|
|
242
|
+
_(Installed automatically, with legacy Shepherd configs migrated once — shown
|
|
243
|
+
for reference/manual setup.)_
|
|
231
244
|
|
|
232
245
|
Codex uses the **same** hook contract as Claude Code (JSON on stdin, a
|
|
233
|
-
`hookSpecificOutput.additionalContext` reply), so the **same bin** serves it.
|
|
234
|
-
`UserPromptSubmit`
|
|
235
|
-
|
|
236
|
-
|
|
246
|
+
`hookSpecificOutput.additionalContext` reply), so the **same bin** serves it.
|
|
247
|
+
`UserPromptSubmit` and `SessionStart` cover turn and session boundaries. In
|
|
248
|
+
local Codex testing, wildcard `PreToolUse` delivered before Bash,
|
|
249
|
+
`apply_patch`, and MCP calls. Other richer tool paths, including WebSearch, are
|
|
250
|
+
not guaranteed by the current Codex hook coverage. Hooks must be enabled with
|
|
251
|
+
`features.hooks = true`. In `~/.codex/config.toml`:
|
|
237
252
|
|
|
238
253
|
```toml
|
|
239
254
|
[features]
|
|
240
255
|
hooks = true
|
|
241
256
|
|
|
242
257
|
[[hooks.UserPromptSubmit]]
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
258
|
+
[[hooks.UserPromptSubmit.hooks]]
|
|
259
|
+
type = "command"
|
|
260
|
+
command = "npx -y --package=@korso/shepherd shepherd-inbox-hook"
|
|
261
|
+
timeout = 20
|
|
262
|
+
|
|
263
|
+
[[hooks.SessionStart]]
|
|
264
|
+
[[hooks.SessionStart.hooks]]
|
|
265
|
+
type = "command"
|
|
266
|
+
command = "npx -y --package=@korso/shepherd shepherd-inbox-hook"
|
|
267
|
+
timeout = 20
|
|
268
|
+
|
|
269
|
+
[[hooks.PreToolUse]]
|
|
270
|
+
matcher = "*"
|
|
271
|
+
[[hooks.PreToolUse.hooks]]
|
|
272
|
+
type = "command"
|
|
273
|
+
command = "npx -y --package=@korso/shepherd shepherd-inbox-hook"
|
|
274
|
+
timeout = 20
|
|
246
275
|
```
|
|
247
276
|
|
|
248
277
|
### Pi — extension
|
package/dist/index.js
CHANGED
|
@@ -2530,16 +2530,565 @@ async function ancestorChain(maxDepth = 32, snapshot = snapshotParentMap) {
|
|
|
2530
2530
|
|
|
2531
2531
|
// src/hookInstall.ts
|
|
2532
2532
|
import {
|
|
2533
|
-
readFileSync as
|
|
2534
|
-
writeFileSync as
|
|
2535
|
-
mkdirSync as
|
|
2533
|
+
readFileSync as readFileSync8,
|
|
2534
|
+
writeFileSync as writeFileSync7,
|
|
2535
|
+
mkdirSync as mkdirSync6,
|
|
2536
2536
|
copyFileSync,
|
|
2537
|
-
existsSync as
|
|
2538
|
-
renameSync as
|
|
2537
|
+
existsSync as existsSync6,
|
|
2538
|
+
renameSync as renameSync3
|
|
2539
2539
|
} from "node:fs";
|
|
2540
2540
|
import { homedir as homedir5 } from "node:os";
|
|
2541
|
-
import { dirname as
|
|
2541
|
+
import { dirname as dirname7, join as join7 } from "node:path";
|
|
2542
2542
|
import { fileURLToPath } from "node:url";
|
|
2543
|
+
|
|
2544
|
+
// src/codexHookMigration.ts
|
|
2545
|
+
import { existsSync as existsSync5, readFileSync as readFileSync7 } from "node:fs";
|
|
2546
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
2547
|
+
import { join as join6 } from "node:path";
|
|
2548
|
+
import { z as z5 } from "zod";
|
|
2549
|
+
|
|
2550
|
+
// src/codexHookInstall.ts
|
|
2551
|
+
import { parse, TomlDate } from "smol-toml";
|
|
2552
|
+
var CODEX_SHEPHERD_COMMENT = "# Added by Shepherd: delivers teammate announcements to the agent. Remove to disable.";
|
|
2553
|
+
function canonicalHandlerBlock(event, command, matcher) {
|
|
2554
|
+
return [
|
|
2555
|
+
"[[hooks." + event + "]]",
|
|
2556
|
+
...matcher === void 0 ? [] : ["matcher = " + JSON.stringify(matcher)],
|
|
2557
|
+
"[[hooks." + event + ".hooks]]",
|
|
2558
|
+
'type = "command"',
|
|
2559
|
+
"command = " + JSON.stringify(command),
|
|
2560
|
+
"timeout = 20",
|
|
2561
|
+
""
|
|
2562
|
+
].join("\n");
|
|
2563
|
+
}
|
|
2564
|
+
function canonicalHookBlock(command) {
|
|
2565
|
+
return [
|
|
2566
|
+
"",
|
|
2567
|
+
CODEX_SHEPHERD_COMMENT,
|
|
2568
|
+
canonicalHandlerBlock("UserPromptSubmit", command),
|
|
2569
|
+
canonicalHandlerBlock("SessionStart", command),
|
|
2570
|
+
canonicalHandlerBlock("PreToolUse", command, "*")
|
|
2571
|
+
].join("\n");
|
|
2572
|
+
}
|
|
2573
|
+
function parseConfig2(source) {
|
|
2574
|
+
try {
|
|
2575
|
+
return parse(source, { integersAsBigInt: false });
|
|
2576
|
+
} catch {
|
|
2577
|
+
return null;
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
function isTomlTable(value) {
|
|
2581
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof TomlDate);
|
|
2582
|
+
}
|
|
2583
|
+
function hasEnabledHooks(config) {
|
|
2584
|
+
const features = config["features"];
|
|
2585
|
+
return isTomlTable(features) && features["hooks"] === true;
|
|
2586
|
+
}
|
|
2587
|
+
function insertHooksFeature(source) {
|
|
2588
|
+
const featuresHeader = /^(\s*\[features\]\s*(?:#.*)?)$/gm;
|
|
2589
|
+
let match;
|
|
2590
|
+
while ((match = featuresHeader.exec(source)) !== null) {
|
|
2591
|
+
const insertionPoint = match.index + match[0].length;
|
|
2592
|
+
const candidate = source.slice(0, insertionPoint) + "\nhooks = true" + source.slice(insertionPoint);
|
|
2593
|
+
const config = parseConfig2(candidate);
|
|
2594
|
+
if (config !== null && hasEnabledHooks(config)) return candidate;
|
|
2595
|
+
}
|
|
2596
|
+
return null;
|
|
2597
|
+
}
|
|
2598
|
+
function installCandidate(source, config, command) {
|
|
2599
|
+
const features = config["features"];
|
|
2600
|
+
let candidate;
|
|
2601
|
+
if (isTomlTable(features)) {
|
|
2602
|
+
if (Object.prototype.hasOwnProperty.call(features, "hooks")) {
|
|
2603
|
+
candidate = features["hooks"] === true ? source + canonicalHookBlock(command) : null;
|
|
2604
|
+
} else {
|
|
2605
|
+
const withFeature = insertHooksFeature(source);
|
|
2606
|
+
candidate = withFeature === null ? null : withFeature + canonicalHookBlock(command);
|
|
2607
|
+
}
|
|
2608
|
+
} else if (features === void 0) {
|
|
2609
|
+
candidate = source + (source.length === 0 ? "" : "\n") + "[features]\nhooks = true\n" + canonicalHookBlock(command);
|
|
2610
|
+
} else {
|
|
2611
|
+
candidate = null;
|
|
2612
|
+
}
|
|
2613
|
+
const parsed = candidate === null ? null : parseConfig2(candidate);
|
|
2614
|
+
return parsed !== null && hasEnabledHooks(parsed) ? candidate : null;
|
|
2615
|
+
}
|
|
2616
|
+
function planCodexConfig(source, command) {
|
|
2617
|
+
const config = parseConfig2(source);
|
|
2618
|
+
if (config === null) return { kind: "skip", outcome: "unsupported-shape" };
|
|
2619
|
+
const features = config["features"];
|
|
2620
|
+
if (isTomlTable(features) && features["hooks"] !== void 0) {
|
|
2621
|
+
if (features["hooks"] === false) {
|
|
2622
|
+
return { kind: "skip", outcome: "opted-out" };
|
|
2623
|
+
}
|
|
2624
|
+
if (features["hooks"] !== true) {
|
|
2625
|
+
return { kind: "skip", outcome: "unsupported-shape" };
|
|
2626
|
+
}
|
|
2627
|
+
} else if (features !== void 0 && !isTomlTable(features)) {
|
|
2628
|
+
return { kind: "skip", outcome: "unsupported-shape" };
|
|
2629
|
+
}
|
|
2630
|
+
if (source.includes(canonicalHookBlock(command))) {
|
|
2631
|
+
return { kind: "already-canonical" };
|
|
2632
|
+
}
|
|
2633
|
+
const candidate = installCandidate(source, config, command);
|
|
2634
|
+
return candidate === null ? { kind: "skip", outcome: "unsupported-shape" } : { kind: "install", candidate };
|
|
2635
|
+
}
|
|
2636
|
+
function appendMissingCodexHandlers(source, command) {
|
|
2637
|
+
const handlers = [
|
|
2638
|
+
canonicalHandlerBlock("SessionStart", command),
|
|
2639
|
+
canonicalHandlerBlock("PreToolUse", command, "*")
|
|
2640
|
+
].filter((handler) => !source.includes(handler));
|
|
2641
|
+
const candidate = handlers.length === 0 ? source : source + (source.endsWith("\n") ? "" : "\n") + handlers.join("");
|
|
2642
|
+
return parseConfig2(candidate) === null ? null : candidate;
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2645
|
+
// src/codexHookFs.ts
|
|
2646
|
+
import {
|
|
2647
|
+
chmodSync,
|
|
2648
|
+
closeSync,
|
|
2649
|
+
existsSync as existsSync4,
|
|
2650
|
+
fchmodSync,
|
|
2651
|
+
fsyncSync,
|
|
2652
|
+
linkSync,
|
|
2653
|
+
mkdirSync as mkdirSync5,
|
|
2654
|
+
openSync,
|
|
2655
|
+
readFileSync as readFileSync6,
|
|
2656
|
+
renameSync as renameSync2,
|
|
2657
|
+
statSync as statSync2,
|
|
2658
|
+
unlinkSync,
|
|
2659
|
+
writeFileSync as writeFileSync6
|
|
2660
|
+
} from "node:fs";
|
|
2661
|
+
import { createHash as createHash2, randomUUID } from "node:crypto";
|
|
2662
|
+
import { basename as basename2, dirname as dirname6, join as join5 } from "node:path";
|
|
2663
|
+
import { z as z4 } from "zod";
|
|
2664
|
+
var STALE_LOCK_MS = 3e4;
|
|
2665
|
+
var lockSchema = z4.object({
|
|
2666
|
+
pid: z4.number().int().positive(),
|
|
2667
|
+
createdAt: z4.string(),
|
|
2668
|
+
owner: z4.string().min(1).optional()
|
|
2669
|
+
});
|
|
2670
|
+
function errorCode(error) {
|
|
2671
|
+
return error instanceof Error && "code" in error ? String(error.code) : void 0;
|
|
2672
|
+
}
|
|
2673
|
+
function modeOf(path3) {
|
|
2674
|
+
try {
|
|
2675
|
+
return statSync2(path3).mode & 511;
|
|
2676
|
+
} catch {
|
|
2677
|
+
return 384;
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
function syncParent(path3) {
|
|
2681
|
+
if (process.platform === "win32") return;
|
|
2682
|
+
const descriptor = openSync(dirname6(path3), "r");
|
|
2683
|
+
try {
|
|
2684
|
+
fsyncSync(descriptor);
|
|
2685
|
+
} finally {
|
|
2686
|
+
closeSync(descriptor);
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
function durableTemp(path3, contents, mode) {
|
|
2690
|
+
const descriptor = openSync(path3, "wx", mode);
|
|
2691
|
+
try {
|
|
2692
|
+
writeFileSync6(descriptor, contents);
|
|
2693
|
+
fchmodSync(descriptor, mode);
|
|
2694
|
+
fsyncSync(descriptor);
|
|
2695
|
+
} finally {
|
|
2696
|
+
closeSync(descriptor);
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
function atomicWrite(path3, contents) {
|
|
2700
|
+
mkdirSync5(dirname6(path3), { recursive: true });
|
|
2701
|
+
const temporary = join5(
|
|
2702
|
+
dirname6(path3),
|
|
2703
|
+
"." + basename2(path3) + "." + process.pid + "." + randomUUID() + ".tmp"
|
|
2704
|
+
);
|
|
2705
|
+
try {
|
|
2706
|
+
durableTemp(temporary, contents, modeOf(path3));
|
|
2707
|
+
renameSync2(temporary, path3);
|
|
2708
|
+
syncParent(path3);
|
|
2709
|
+
} catch (error) {
|
|
2710
|
+
try {
|
|
2711
|
+
unlinkSync(temporary);
|
|
2712
|
+
} catch {
|
|
2713
|
+
}
|
|
2714
|
+
throw error;
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
function processIsLive(pid) {
|
|
2718
|
+
try {
|
|
2719
|
+
process.kill(pid, 0);
|
|
2720
|
+
return true;
|
|
2721
|
+
} catch (error) {
|
|
2722
|
+
return errorCode(error) !== "ESRCH";
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
function lockSnapshot(lockFile) {
|
|
2726
|
+
try {
|
|
2727
|
+
const bytes = readFileSync6(lockFile);
|
|
2728
|
+
const modifiedAt = statSync2(lockFile).mtimeMs;
|
|
2729
|
+
let owner = null;
|
|
2730
|
+
let reclaimable = false;
|
|
2731
|
+
try {
|
|
2732
|
+
const decoded = JSON.parse(bytes.toString("utf8"));
|
|
2733
|
+
const parsed = lockSchema.safeParse(decoded);
|
|
2734
|
+
if (parsed.success) {
|
|
2735
|
+
owner = parsed.data.owner ?? null;
|
|
2736
|
+
const createdAt = Date.parse(parsed.data.createdAt);
|
|
2737
|
+
const ageBasis = Number.isFinite(createdAt) ? createdAt : modifiedAt;
|
|
2738
|
+
reclaimable = Date.now() - ageBasis > STALE_LOCK_MS && !processIsLive(parsed.data.pid);
|
|
2739
|
+
}
|
|
2740
|
+
} catch {
|
|
2741
|
+
}
|
|
2742
|
+
return { bytes, owner, reclaimable };
|
|
2743
|
+
} catch {
|
|
2744
|
+
return null;
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
function lockContents(owner) {
|
|
2748
|
+
return JSON.stringify({
|
|
2749
|
+
pid: process.pid,
|
|
2750
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2751
|
+
owner
|
|
2752
|
+
});
|
|
2753
|
+
}
|
|
2754
|
+
function createOwnedLock(lockFile, owner) {
|
|
2755
|
+
const temporary = lockFile + ".owner-" + owner + ".tmp";
|
|
2756
|
+
try {
|
|
2757
|
+
durableTemp(temporary, lockContents(owner), 384);
|
|
2758
|
+
linkSync(temporary, lockFile);
|
|
2759
|
+
return true;
|
|
2760
|
+
} catch {
|
|
2761
|
+
return false;
|
|
2762
|
+
} finally {
|
|
2763
|
+
try {
|
|
2764
|
+
unlinkSync(temporary);
|
|
2765
|
+
} catch {
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
function ownedBy(lockFile, owner) {
|
|
2770
|
+
try {
|
|
2771
|
+
const decoded = JSON.parse(readFileSync6(lockFile, "utf8"));
|
|
2772
|
+
const parsed = lockSchema.safeParse(decoded);
|
|
2773
|
+
return parsed.success && parsed.data.owner === owner;
|
|
2774
|
+
} catch {
|
|
2775
|
+
return false;
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
function removeOwnedLock(lockFile, owner) {
|
|
2779
|
+
if (ownedBy(lockFile, owner)) unlinkSync(lockFile);
|
|
2780
|
+
}
|
|
2781
|
+
function snapshotMatches(current, expected) {
|
|
2782
|
+
return current !== null && current.reclaimable && current.bytes.equals(expected.bytes);
|
|
2783
|
+
}
|
|
2784
|
+
function nextReclaimClaim(rootClaim, snapshot) {
|
|
2785
|
+
const generation = createHash2("sha256").update(snapshot.bytes).digest("hex");
|
|
2786
|
+
return rootClaim + ".generation-" + generation;
|
|
2787
|
+
}
|
|
2788
|
+
function acquireReclaimClaim(rootClaim, owner) {
|
|
2789
|
+
let claimFile = rootClaim;
|
|
2790
|
+
for (let generation = 0; generation < 32; generation += 1) {
|
|
2791
|
+
if (createOwnedLock(claimFile, owner)) return claimFile;
|
|
2792
|
+
const snapshot = lockSnapshot(claimFile);
|
|
2793
|
+
if (snapshot === null || !snapshot.reclaimable || snapshot.owner === null) {
|
|
2794
|
+
return null;
|
|
2795
|
+
}
|
|
2796
|
+
claimFile = nextReclaimClaim(rootClaim, snapshot);
|
|
2797
|
+
}
|
|
2798
|
+
return null;
|
|
2799
|
+
}
|
|
2800
|
+
function replaceStaleLock(lockFile, claimFile, owner, expected) {
|
|
2801
|
+
const replacement = lockFile + ".replacement-" + owner;
|
|
2802
|
+
let published = false;
|
|
2803
|
+
try {
|
|
2804
|
+
durableTemp(replacement, lockContents(owner), 384);
|
|
2805
|
+
if (!ownedBy(claimFile, owner)) return false;
|
|
2806
|
+
if (!snapshotMatches(lockSnapshot(lockFile), expected)) return false;
|
|
2807
|
+
renameSync2(replacement, lockFile);
|
|
2808
|
+
published = true;
|
|
2809
|
+
syncParent(lockFile);
|
|
2810
|
+
if (ownedBy(claimFile, owner)) return true;
|
|
2811
|
+
removeOwnedLock(lockFile, owner);
|
|
2812
|
+
return false;
|
|
2813
|
+
} catch {
|
|
2814
|
+
if (published) removeOwnedLock(lockFile, owner);
|
|
2815
|
+
return false;
|
|
2816
|
+
} finally {
|
|
2817
|
+
try {
|
|
2818
|
+
unlinkSync(replacement);
|
|
2819
|
+
} catch {
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
function acquireMigrationLock(lockFile) {
|
|
2824
|
+
mkdirSync5(dirname6(lockFile), { recursive: true });
|
|
2825
|
+
const owner = randomUUID();
|
|
2826
|
+
if (createOwnedLock(lockFile, owner)) return owner;
|
|
2827
|
+
const snapshot = lockSnapshot(lockFile);
|
|
2828
|
+
if (snapshot === null || !snapshot.reclaimable) return null;
|
|
2829
|
+
const claimFile = acquireReclaimClaim(lockFile + ".reclaim", owner);
|
|
2830
|
+
if (claimFile === null) return null;
|
|
2831
|
+
try {
|
|
2832
|
+
return replaceStaleLock(lockFile, claimFile, owner, snapshot) ? owner : null;
|
|
2833
|
+
} finally {
|
|
2834
|
+
try {
|
|
2835
|
+
removeOwnedLock(claimFile, owner);
|
|
2836
|
+
} catch {
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
function releaseMigrationLock(lockFile, owner, log) {
|
|
2841
|
+
try {
|
|
2842
|
+
removeOwnedLock(lockFile, owner);
|
|
2843
|
+
} catch (error) {
|
|
2844
|
+
log(
|
|
2845
|
+
"[shepherd] Codex hook migration lock cleanup failed: " + String(error)
|
|
2846
|
+
);
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
function validateBackup(backupFile, source) {
|
|
2850
|
+
if (!readFileSync6(backupFile).equals(source)) {
|
|
2851
|
+
throw new Error("existing Codex migration backup does not match config");
|
|
2852
|
+
}
|
|
2853
|
+
chmodSync(backupFile, 384);
|
|
2854
|
+
}
|
|
2855
|
+
function ensureMigrationBackup(backupFile, source) {
|
|
2856
|
+
const backupDirectory = dirname6(backupFile);
|
|
2857
|
+
mkdirSync5(backupDirectory, { recursive: true });
|
|
2858
|
+
syncParent(backupDirectory);
|
|
2859
|
+
if (existsSync4(backupFile)) {
|
|
2860
|
+
validateBackup(backupFile, source);
|
|
2861
|
+
syncParent(backupFile);
|
|
2862
|
+
return;
|
|
2863
|
+
}
|
|
2864
|
+
const temporary = backupFile + "." + process.pid + "." + randomUUID() + ".tmp";
|
|
2865
|
+
try {
|
|
2866
|
+
durableTemp(temporary, source, 384);
|
|
2867
|
+
try {
|
|
2868
|
+
linkSync(temporary, backupFile);
|
|
2869
|
+
} catch (error) {
|
|
2870
|
+
if (errorCode(error) !== "EEXIST") throw error;
|
|
2871
|
+
validateBackup(backupFile, source);
|
|
2872
|
+
}
|
|
2873
|
+
syncParent(backupFile);
|
|
2874
|
+
} finally {
|
|
2875
|
+
try {
|
|
2876
|
+
unlinkSync(temporary);
|
|
2877
|
+
} catch {
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
// src/codexHookMigration.ts
|
|
2883
|
+
var MIGRATION_VERSION = 2;
|
|
2884
|
+
var migrationOutcomeSchema = z5.enum([
|
|
2885
|
+
"migrated",
|
|
2886
|
+
"already-canonical",
|
|
2887
|
+
"user-removed",
|
|
2888
|
+
"ambiguous",
|
|
2889
|
+
"opted-out",
|
|
2890
|
+
"unsupported-shape"
|
|
2891
|
+
]);
|
|
2892
|
+
var recordSchema = z5.object({
|
|
2893
|
+
status: z5.string(),
|
|
2894
|
+
at: z5.string(),
|
|
2895
|
+
migrationVersion: z5.number().int().nonnegative().optional(),
|
|
2896
|
+
migrationOutcome: migrationOutcomeSchema.optional()
|
|
2897
|
+
}).passthrough();
|
|
2898
|
+
function migrationPaths(homeDir) {
|
|
2899
|
+
const hooksDir = join6(homeDir, ".shepherd", "hooks");
|
|
2900
|
+
return {
|
|
2901
|
+
hooksDir,
|
|
2902
|
+
recordFile: join6(hooksDir, "codex.json"),
|
|
2903
|
+
lockFile: join6(hooksDir, "codex-migration-v2.lock"),
|
|
2904
|
+
backupFile: join6(hooksDir, "backups", "codex-config-before-v2.toml"),
|
|
2905
|
+
configFile: join6(homeDir, ".codex", "config.toml")
|
|
2906
|
+
};
|
|
2907
|
+
}
|
|
2908
|
+
function readRecord(recordFile) {
|
|
2909
|
+
if (!existsSync5(recordFile)) return { kind: "none" };
|
|
2910
|
+
try {
|
|
2911
|
+
const decoded = JSON.parse(readFileSync7(recordFile, "utf8"));
|
|
2912
|
+
const parsed = recordSchema.safeParse(decoded);
|
|
2913
|
+
if (!parsed.success) return { kind: "corrupt" };
|
|
2914
|
+
const record = parsed.data;
|
|
2915
|
+
const version = parsed.data.migrationVersion;
|
|
2916
|
+
if (version === void 0 || version < MIGRATION_VERSION) {
|
|
2917
|
+
return { kind: "legacy", record };
|
|
2918
|
+
}
|
|
2919
|
+
if (version > MIGRATION_VERSION) return { kind: "future", record };
|
|
2920
|
+
return parsed.data.migrationOutcome === void 0 ? { kind: "corrupt" } : { kind: "current", record };
|
|
2921
|
+
} catch {
|
|
2922
|
+
return { kind: "corrupt" };
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
function migrationRecord(prior, status, outcome) {
|
|
2926
|
+
return JSON.stringify(
|
|
2927
|
+
{
|
|
2928
|
+
...prior,
|
|
2929
|
+
status: typeof prior?.["status"] === "string" ? prior["status"] : status,
|
|
2930
|
+
at: typeof prior?.["at"] === "string" ? prior["at"] : (/* @__PURE__ */ new Date()).toISOString(),
|
|
2931
|
+
migrationVersion: MIGRATION_VERSION,
|
|
2932
|
+
migrationOutcome: outcome
|
|
2933
|
+
},
|
|
2934
|
+
null,
|
|
2935
|
+
2
|
|
2936
|
+
) + "\n";
|
|
2937
|
+
}
|
|
2938
|
+
function fingerprint(bytes) {
|
|
2939
|
+
return createHash3("sha256").update(bytes).digest("hex");
|
|
2940
|
+
}
|
|
2941
|
+
function readConfigBytes(configFile) {
|
|
2942
|
+
return existsSync5(configFile) ? readFileSync7(configFile) : Buffer.alloc(0);
|
|
2943
|
+
}
|
|
2944
|
+
function decodeConfig(bytes) {
|
|
2945
|
+
try {
|
|
2946
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
2947
|
+
} catch {
|
|
2948
|
+
return null;
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
function advanceRecord(recordFile, state, status, outcome) {
|
|
2952
|
+
atomicWrite(recordFile, migrationRecord(state.record, status, outcome));
|
|
2953
|
+
}
|
|
2954
|
+
function legacyHookBlock(commandValue) {
|
|
2955
|
+
return [
|
|
2956
|
+
"",
|
|
2957
|
+
CODEX_SHEPHERD_COMMENT,
|
|
2958
|
+
"[[hooks.UserPromptSubmit]]",
|
|
2959
|
+
"command = " + commandValue,
|
|
2960
|
+
""
|
|
2961
|
+
].join("\n");
|
|
2962
|
+
}
|
|
2963
|
+
function exactOwnedLegacyBlock(source, hooksDir) {
|
|
2964
|
+
const semver = "(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)";
|
|
2965
|
+
const pinned = new RegExp(
|
|
2966
|
+
'^command = (\\["npx", "-y", "--package=@korso/shepherd@' + semver + '", "shepherd-inbox-hook"\\])$',
|
|
2967
|
+
"gm"
|
|
2968
|
+
);
|
|
2969
|
+
const candidates = Array.from(
|
|
2970
|
+
source.matchAll(pinned),
|
|
2971
|
+
(match) => legacyHookBlock(match[1])
|
|
2972
|
+
);
|
|
2973
|
+
candidates.push(
|
|
2974
|
+
legacyHookBlock(
|
|
2975
|
+
'["node", ' + JSON.stringify(join6(hooksDir, "shepherd-inbox-hook.mjs")) + "]"
|
|
2976
|
+
)
|
|
2977
|
+
);
|
|
2978
|
+
const exact = candidates.filter(
|
|
2979
|
+
(block) => source.indexOf(block) >= 0 && source.indexOf(block) === source.lastIndexOf(block)
|
|
2980
|
+
);
|
|
2981
|
+
return exact.length === 1 ? exact[0] : void 0;
|
|
2982
|
+
}
|
|
2983
|
+
function migrateLegacy(context, state, sourceBytes, source) {
|
|
2984
|
+
const { paths, command, log } = context;
|
|
2985
|
+
const candidate = appendMissingCodexHandlers(source, command);
|
|
2986
|
+
if (candidate === null) {
|
|
2987
|
+
advanceRecord(paths.recordFile, state, "skipped", "unsupported-shape");
|
|
2988
|
+
return "skipped";
|
|
2989
|
+
}
|
|
2990
|
+
const recordBytes = readFileSync7(paths.recordFile);
|
|
2991
|
+
const configChanged = candidate !== source;
|
|
2992
|
+
ensureMigrationBackup(paths.backupFile, sourceBytes);
|
|
2993
|
+
if (!readConfigBytes(paths.configFile).equals(sourceBytes)) return "skipped";
|
|
2994
|
+
try {
|
|
2995
|
+
if (configChanged) atomicWrite(paths.configFile, candidate);
|
|
2996
|
+
advanceRecord(paths.recordFile, state, "installed", "migrated");
|
|
2997
|
+
} catch (error) {
|
|
2998
|
+
if (!readConfigBytes(paths.configFile).equals(sourceBytes)) {
|
|
2999
|
+
atomicWrite(paths.configFile, sourceBytes);
|
|
3000
|
+
}
|
|
3001
|
+
if (!readFileSync7(paths.recordFile).equals(recordBytes)) {
|
|
3002
|
+
atomicWrite(paths.recordFile, recordBytes);
|
|
3003
|
+
}
|
|
3004
|
+
throw error;
|
|
3005
|
+
}
|
|
3006
|
+
log(
|
|
3007
|
+
"[shepherd] Migrated Codex hooks. Persistent backup: " + paths.backupFile + "; you may remove it after validation."
|
|
3008
|
+
);
|
|
3009
|
+
return "installed";
|
|
3010
|
+
}
|
|
3011
|
+
function processLockedConfig(context, expectedFingerprint) {
|
|
3012
|
+
const { paths, command, hookMarker } = context;
|
|
3013
|
+
const state = readRecord(paths.recordFile);
|
|
3014
|
+
if (state.kind === "current" || state.kind === "future") {
|
|
3015
|
+
return "already-attempted";
|
|
3016
|
+
}
|
|
3017
|
+
if (state.kind === "corrupt") return "skipped";
|
|
3018
|
+
const sourceBytes = readConfigBytes(paths.configFile);
|
|
3019
|
+
if (fingerprint(sourceBytes) !== expectedFingerprint) return "skipped";
|
|
3020
|
+
const source = decodeConfig(sourceBytes);
|
|
3021
|
+
if (source === null) {
|
|
3022
|
+
advanceRecord(paths.recordFile, state, "skipped", "unsupported-shape");
|
|
3023
|
+
return "skipped";
|
|
3024
|
+
}
|
|
3025
|
+
const plan = planCodexConfig(source, command);
|
|
3026
|
+
if (plan.kind === "skip") {
|
|
3027
|
+
advanceRecord(paths.recordFile, state, "skipped", plan.outcome);
|
|
3028
|
+
return "skipped";
|
|
3029
|
+
}
|
|
3030
|
+
if (plan.kind === "already-canonical") {
|
|
3031
|
+
advanceRecord(
|
|
3032
|
+
paths.recordFile,
|
|
3033
|
+
state,
|
|
3034
|
+
"already-present",
|
|
3035
|
+
"already-canonical"
|
|
3036
|
+
);
|
|
3037
|
+
return "already-present";
|
|
3038
|
+
}
|
|
3039
|
+
const ownedBlock = exactOwnedLegacyBlock(source, paths.hooksDir);
|
|
3040
|
+
if (state.kind === "legacy" && ownedBlock !== void 0) {
|
|
3041
|
+
return migrateLegacy(context, state, sourceBytes, source);
|
|
3042
|
+
}
|
|
3043
|
+
if (ownedBlock !== void 0 || source.includes(CODEX_SHEPHERD_COMMENT) || source.includes(hookMarker)) {
|
|
3044
|
+
advanceRecord(paths.recordFile, state, "already-present", "ambiguous");
|
|
3045
|
+
return "already-present";
|
|
3046
|
+
}
|
|
3047
|
+
if (state.kind === "legacy") {
|
|
3048
|
+
advanceRecord(paths.recordFile, state, "skipped", "user-removed");
|
|
3049
|
+
return "skipped";
|
|
3050
|
+
}
|
|
3051
|
+
atomicWrite(paths.configFile, plan.candidate);
|
|
3052
|
+
advanceRecord(paths.recordFile, state, "installed", "already-canonical");
|
|
3053
|
+
return "installed";
|
|
3054
|
+
}
|
|
3055
|
+
async function installCodexHooks({
|
|
3056
|
+
homeDir,
|
|
3057
|
+
command,
|
|
3058
|
+
hookMarker,
|
|
3059
|
+
log
|
|
3060
|
+
}) {
|
|
3061
|
+
const paths = migrationPaths(homeDir);
|
|
3062
|
+
const context = { paths, command, hookMarker, log };
|
|
3063
|
+
const initialRecord = readRecord(paths.recordFile);
|
|
3064
|
+
if (initialRecord.kind === "current" || initialRecord.kind === "future") {
|
|
3065
|
+
return "already-attempted";
|
|
3066
|
+
}
|
|
3067
|
+
if (initialRecord.kind === "corrupt") return "skipped";
|
|
3068
|
+
let initialBytes;
|
|
3069
|
+
try {
|
|
3070
|
+
initialBytes = readConfigBytes(paths.configFile);
|
|
3071
|
+
} catch (error) {
|
|
3072
|
+
log(
|
|
3073
|
+
"[shepherd] Codex hook migration could not read config: " + String(error)
|
|
3074
|
+
);
|
|
3075
|
+
return "skipped";
|
|
3076
|
+
}
|
|
3077
|
+
const expectedFingerprint = fingerprint(initialBytes);
|
|
3078
|
+
const owner = acquireMigrationLock(paths.lockFile);
|
|
3079
|
+
if (owner === null) return "skipped";
|
|
3080
|
+
try {
|
|
3081
|
+
await Promise.resolve();
|
|
3082
|
+
return processLockedConfig(context, expectedFingerprint);
|
|
3083
|
+
} catch (error) {
|
|
3084
|
+
log("[shepherd] Codex hook migration skipped: " + String(error));
|
|
3085
|
+
return "skipped";
|
|
3086
|
+
} finally {
|
|
3087
|
+
releaseMigrationLock(paths.lockFile, owner, log);
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
|
|
3091
|
+
// src/hookInstall.ts
|
|
2543
3092
|
function detectClient(clientName) {
|
|
2544
3093
|
const name = (clientName ?? "").toLowerCase();
|
|
2545
3094
|
if (!name) return "unknown";
|
|
@@ -2552,17 +3101,17 @@ function detectClient(clientName) {
|
|
|
2552
3101
|
var HOOK_COMMAND = `npx -y --package=@korso/shepherd@${PACKAGE_VERSION} shepherd-inbox-hook`;
|
|
2553
3102
|
var HOOK_MARKER = "shepherd-inbox-hook";
|
|
2554
3103
|
function ensureHookScript(homeDir, hookScriptSource) {
|
|
2555
|
-
const source = hookScriptSource ??
|
|
3104
|
+
const source = hookScriptSource ?? join7(dirname7(fileURLToPath(import.meta.url)), "inboxHook.js");
|
|
2556
3105
|
try {
|
|
2557
|
-
if (!
|
|
2558
|
-
const dest =
|
|
2559
|
-
const next =
|
|
2560
|
-
const current =
|
|
3106
|
+
if (!existsSync6(source)) return null;
|
|
3107
|
+
const dest = join7(homeDir, ".shepherd", "hooks", "shepherd-inbox-hook.mjs");
|
|
3108
|
+
const next = readFileSync8(source);
|
|
3109
|
+
const current = existsSync6(dest) ? readFileSync8(dest) : null;
|
|
2561
3110
|
if (current === null || !current.equals(next)) {
|
|
2562
|
-
|
|
3111
|
+
mkdirSync6(dirname7(dest), { recursive: true });
|
|
2563
3112
|
const tmp = dest + ".tmp";
|
|
2564
|
-
|
|
2565
|
-
|
|
3113
|
+
writeFileSync7(tmp, next);
|
|
3114
|
+
renameSync3(tmp, dest);
|
|
2566
3115
|
}
|
|
2567
3116
|
return dest;
|
|
2568
3117
|
} catch {
|
|
@@ -2572,16 +3121,6 @@ function ensureHookScript(homeDir, hookScriptSource) {
|
|
|
2572
3121
|
function hookCommandFor(scriptPath) {
|
|
2573
3122
|
return scriptPath === null ? HOOK_COMMAND : `node "${scriptPath.replace(/\\/g, "/")}"`;
|
|
2574
3123
|
}
|
|
2575
|
-
function codexHookBlock(scriptPath) {
|
|
2576
|
-
const command = scriptPath === null ? `["npx", "-y", "--package=@korso/shepherd@${PACKAGE_VERSION}", "shepherd-inbox-hook"]` : `["node", ${JSON.stringify(scriptPath)}]`;
|
|
2577
|
-
return [
|
|
2578
|
-
"",
|
|
2579
|
-
"# Added by Shepherd: delivers teammate announcements to the agent. Remove to disable.",
|
|
2580
|
-
"[[hooks.UserPromptSubmit]]",
|
|
2581
|
-
`command = ${command}`,
|
|
2582
|
-
""
|
|
2583
|
-
].join("\n");
|
|
2584
|
-
}
|
|
2585
3124
|
async function autoInstallHooks({
|
|
2586
3125
|
clientName,
|
|
2587
3126
|
homeDir = homedir5(),
|
|
@@ -2597,20 +3136,32 @@ async function autoInstallHooks({
|
|
|
2597
3136
|
return { client, status: "unsupported" };
|
|
2598
3137
|
}
|
|
2599
3138
|
const scriptPath = ensureHookScript(homeDir, hookScriptSource);
|
|
2600
|
-
const recordFile =
|
|
2601
|
-
if (
|
|
3139
|
+
const recordFile = join7(homeDir, ".shepherd", "hooks", `${client}.json`);
|
|
3140
|
+
if (client === "codex") {
|
|
3141
|
+
const status2 = await installCodexHooks({
|
|
3142
|
+
homeDir,
|
|
3143
|
+
command: hookCommandFor(scriptPath),
|
|
3144
|
+
hookMarker: HOOK_MARKER,
|
|
3145
|
+
log
|
|
3146
|
+
});
|
|
3147
|
+
if (status2 === "installed") {
|
|
3148
|
+
log(
|
|
3149
|
+
"[shepherd] Installed the announcement-delivery hook for codex (disable by removing it, or set SHEPHERD_NO_AUTO_HOOKS=1 to never auto-install)."
|
|
3150
|
+
);
|
|
3151
|
+
}
|
|
3152
|
+
return { client, status: status2 };
|
|
3153
|
+
}
|
|
3154
|
+
if (existsSync6(recordFile)) return { client, status: "already-attempted" };
|
|
2602
3155
|
let status;
|
|
2603
3156
|
if (client === "claude") {
|
|
2604
3157
|
status = installClaude(homeDir, scriptPath, log);
|
|
2605
|
-
} else if (client === "codex") {
|
|
2606
|
-
status = installCodex(homeDir, scriptPath, log);
|
|
2607
3158
|
} else if (client === "cursor") {
|
|
2608
3159
|
status = installCursor(homeDir, scriptPath, log);
|
|
2609
3160
|
} else {
|
|
2610
3161
|
status = installPi(homeDir, extensionSource, log);
|
|
2611
3162
|
}
|
|
2612
|
-
|
|
2613
|
-
|
|
3163
|
+
mkdirSync6(dirname7(recordFile), { recursive: true });
|
|
3164
|
+
writeFileSync7(
|
|
2614
3165
|
recordFile,
|
|
2615
3166
|
JSON.stringify({ status, at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n",
|
|
2616
3167
|
"utf8"
|
|
@@ -2629,10 +3180,10 @@ async function autoInstallHooks({
|
|
|
2629
3180
|
}
|
|
2630
3181
|
}
|
|
2631
3182
|
function installClaude(homeDir, scriptPath, log) {
|
|
2632
|
-
const settingsFile =
|
|
3183
|
+
const settingsFile = join7(homeDir, ".claude", "settings.json");
|
|
2633
3184
|
let raw = "";
|
|
2634
|
-
if (
|
|
2635
|
-
raw =
|
|
3185
|
+
if (existsSync6(settingsFile)) {
|
|
3186
|
+
raw = readFileSync8(settingsFile, "utf8");
|
|
2636
3187
|
if (raw.includes(HOOK_MARKER)) return "already-present";
|
|
2637
3188
|
}
|
|
2638
3189
|
let settings = {};
|
|
@@ -2675,60 +3226,15 @@ function installClaude(homeDir, scriptPath, log) {
|
|
|
2675
3226
|
matcher: "*",
|
|
2676
3227
|
hooks: [{ type: "command", command }]
|
|
2677
3228
|
});
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
return "installed";
|
|
2681
|
-
}
|
|
2682
|
-
function installCodex(homeDir, scriptPath, log) {
|
|
2683
|
-
const configFile = join5(homeDir, ".codex", "config.toml");
|
|
2684
|
-
const manualHint = "Add the hook manually (see the dashboard's Connect screen).";
|
|
2685
|
-
const hookBlock = codexHookBlock(scriptPath);
|
|
2686
|
-
if (!existsSync4(configFile)) {
|
|
2687
|
-
mkdirSync5(dirname6(configFile), { recursive: true });
|
|
2688
|
-
writeFileSync6(configFile, `[features]
|
|
2689
|
-
hooks = true
|
|
2690
|
-
${hookBlock}`, "utf8");
|
|
2691
|
-
return "installed";
|
|
2692
|
-
}
|
|
2693
|
-
const toml = readFileSync6(configFile, "utf8");
|
|
2694
|
-
if (toml.includes(HOOK_MARKER)) return "already-present";
|
|
2695
|
-
if (/^\s*\[hooks\.UserPromptSubmit\]\s*$/m.test(toml)) {
|
|
2696
|
-
log(
|
|
2697
|
-
`[shepherd] ${configFile} defines [hooks.UserPromptSubmit] \u2014 not touching it. ${manualHint}`
|
|
2698
|
-
);
|
|
2699
|
-
return "skipped";
|
|
2700
|
-
}
|
|
2701
|
-
if (/^\s*\[features\]/m.test(toml)) {
|
|
2702
|
-
const hooksKey = /^\s*hooks\s*=\s*(.+)$/m.exec(toml);
|
|
2703
|
-
if (hooksKey && hooksKey[1].trim() !== "true") {
|
|
2704
|
-
log(
|
|
2705
|
-
`[shepherd] ${configFile} sets hooks = ${hooksKey[1].trim()} \u2014 respecting it. ${manualHint}`
|
|
2706
|
-
);
|
|
2707
|
-
return "skipped";
|
|
2708
|
-
}
|
|
2709
|
-
let updated = toml;
|
|
2710
|
-
if (!hooksKey) {
|
|
2711
|
-
updated = toml.replace(/^(\s*\[features\]\s*)$/m, `$1
|
|
2712
|
-
hooks = true`);
|
|
2713
|
-
}
|
|
2714
|
-
writeFileSync6(configFile, updated + hookBlock, "utf8");
|
|
2715
|
-
return "installed";
|
|
2716
|
-
}
|
|
2717
|
-
writeFileSync6(
|
|
2718
|
-
configFile,
|
|
2719
|
-
`${toml}
|
|
2720
|
-
[features]
|
|
2721
|
-
hooks = true
|
|
2722
|
-
${hookBlock}`,
|
|
2723
|
-
"utf8"
|
|
2724
|
-
);
|
|
3229
|
+
mkdirSync6(dirname7(settingsFile), { recursive: true });
|
|
3230
|
+
writeFileSync7(settingsFile, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
2725
3231
|
return "installed";
|
|
2726
3232
|
}
|
|
2727
3233
|
function installCursor(homeDir, scriptPath, log) {
|
|
2728
|
-
const hooksFile =
|
|
3234
|
+
const hooksFile = join7(homeDir, ".cursor", "hooks.json");
|
|
2729
3235
|
let raw = "";
|
|
2730
|
-
if (
|
|
2731
|
-
raw =
|
|
3236
|
+
if (existsSync6(hooksFile)) {
|
|
3237
|
+
raw = readFileSync8(hooksFile, "utf8");
|
|
2732
3238
|
if (raw.includes(HOOK_MARKER)) return "already-present";
|
|
2733
3239
|
}
|
|
2734
3240
|
let config = {};
|
|
@@ -2763,21 +3269,21 @@ function installCursor(homeDir, scriptPath, log) {
|
|
|
2763
3269
|
return "skipped";
|
|
2764
3270
|
}
|
|
2765
3271
|
entries.push({ command: hookCommandFor(scriptPath) });
|
|
2766
|
-
|
|
2767
|
-
|
|
3272
|
+
mkdirSync6(dirname7(hooksFile), { recursive: true });
|
|
3273
|
+
writeFileSync7(hooksFile, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
2768
3274
|
return "installed";
|
|
2769
3275
|
}
|
|
2770
3276
|
function installPi(homeDir, extensionSource, log) {
|
|
2771
|
-
const source = extensionSource ??
|
|
2772
|
-
const dest =
|
|
2773
|
-
if (
|
|
2774
|
-
if (!
|
|
3277
|
+
const source = extensionSource ?? join7(dirname7(fileURLToPath(import.meta.url)), "inboxExtension.js");
|
|
3278
|
+
const dest = join7(homeDir, ".pi", "agent", "extensions", "shepherd-inbox.js");
|
|
3279
|
+
if (existsSync6(dest)) return "already-present";
|
|
3280
|
+
if (!existsSync6(source)) {
|
|
2775
3281
|
log(
|
|
2776
3282
|
`[shepherd] bundled Pi extension not found at ${source} \u2014 skipping auto-install.`
|
|
2777
3283
|
);
|
|
2778
3284
|
return "skipped";
|
|
2779
3285
|
}
|
|
2780
|
-
|
|
3286
|
+
mkdirSync6(dirname7(dest), { recursive: true });
|
|
2781
3287
|
copyFileSync(source, dest);
|
|
2782
3288
|
return "installed";
|
|
2783
3289
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@korso/shepherd",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.2",
|
|
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": {
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@modelcontextprotocol/sdk": "^1.29",
|
|
49
|
+
"smol-toml": "1.7.0",
|
|
49
50
|
"zod": "^3"
|
|
50
51
|
},
|
|
51
52
|
"devDependencies": {
|