@korso/shepherd 0.11.1 → 0.11.3
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/inboxExtension.js +56 -2
- package/dist/inboxHook.js +11 -2
- package/dist/index.js +831 -208
- 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/inboxExtension.js
CHANGED
|
@@ -139,6 +139,15 @@ var MAILBOX_FRESH_MS = 15 * 60 * 1e3;
|
|
|
139
139
|
function sessionMailboxPath(dir, serverPid) {
|
|
140
140
|
return join3(dir, `agent-${serverPid}.jsonl`);
|
|
141
141
|
}
|
|
142
|
+
var HOOK_CHAIN_REACH = { codex: 8 };
|
|
143
|
+
var DEFAULT_HOOK_CHAIN_REACH = 3;
|
|
144
|
+
var MAX_HOOK_CHAIN_REACH = Math.max(
|
|
145
|
+
DEFAULT_HOOK_CHAIN_REACH,
|
|
146
|
+
...Object.values(HOOK_CHAIN_REACH)
|
|
147
|
+
);
|
|
148
|
+
function hookChainReach(client) {
|
|
149
|
+
return (client === void 0 ? void 0 : HOOK_CHAIN_REACH[client]) ?? DEFAULT_HOOK_CHAIN_REACH;
|
|
150
|
+
}
|
|
142
151
|
function normalizeCwd(cwd) {
|
|
143
152
|
let normalized = resolve3(cwd);
|
|
144
153
|
if (process.platform === "win32") normalized = normalized.toLowerCase();
|
|
@@ -146,7 +155,7 @@ function normalizeCwd(cwd) {
|
|
|
146
155
|
}
|
|
147
156
|
function selectSessionMailboxes(dir, hookChain, hookCwd, staleMs = MAILBOX_FRESH_MS, nowMs = Date.now()) {
|
|
148
157
|
try {
|
|
149
|
-
const chain = hookChain.slice(0,
|
|
158
|
+
const chain = hookChain.slice(0, MAX_HOOK_CHAIN_REACH);
|
|
150
159
|
const wantedCwd = hookCwd === null ? null : normalizeCwd(hookCwd);
|
|
151
160
|
const candidates = [];
|
|
152
161
|
for (const name of readdirSync(dir)) {
|
|
@@ -180,7 +189,7 @@ function selectSessionMailboxes(dir, hookChain, hookCwd, staleMs = MAILBOX_FRESH
|
|
|
180
189
|
}
|
|
181
190
|
if (!Array.isArray(meta.chain) || typeof meta.cwd !== "string") continue;
|
|
182
191
|
const i = chain.findIndex((pid) => meta.chain.includes(pid));
|
|
183
|
-
if (i === -1) continue;
|
|
192
|
+
if (i === -1 || i >= hookChainReach(meta.client)) continue;
|
|
184
193
|
const j = meta.chain.indexOf(chain[i]);
|
|
185
194
|
if (i >= 2 && (j > 2 || wantedCwd === null || meta.cwd !== wantedCwd))
|
|
186
195
|
continue;
|
|
@@ -280,6 +289,51 @@ function mergeAnnouncements(...lists) {
|
|
|
280
289
|
return [...byId.values()].sort((x, y) => x.id - y.id);
|
|
281
290
|
}
|
|
282
291
|
|
|
292
|
+
// src/codexHookMigration.ts
|
|
293
|
+
import { z as z2 } from "zod";
|
|
294
|
+
|
|
295
|
+
// src/codexHookInstall.ts
|
|
296
|
+
import { parse, TomlDate } from "smol-toml";
|
|
297
|
+
|
|
298
|
+
// src/codexHookFs.ts
|
|
299
|
+
import { z } from "zod";
|
|
300
|
+
var lockSchema = z.object({
|
|
301
|
+
pid: z.number().int().positive(),
|
|
302
|
+
createdAt: z.string(),
|
|
303
|
+
owner: z.string().min(1).optional()
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// src/codexHookMigration.ts
|
|
307
|
+
var migrationOutcomeSchema = z2.enum([
|
|
308
|
+
"migrated",
|
|
309
|
+
"already-canonical",
|
|
310
|
+
"user-removed",
|
|
311
|
+
"ambiguous",
|
|
312
|
+
"opted-out",
|
|
313
|
+
"unsupported-shape"
|
|
314
|
+
]);
|
|
315
|
+
var recordSchema = z2.object({
|
|
316
|
+
status: z2.string(),
|
|
317
|
+
at: z2.string(),
|
|
318
|
+
migrationVersion: z2.number().int().nonnegative().optional(),
|
|
319
|
+
migrationOutcome: migrationOutcomeSchema.optional()
|
|
320
|
+
}).passthrough();
|
|
321
|
+
|
|
322
|
+
// src/version.ts
|
|
323
|
+
import { createRequire } from "node:module";
|
|
324
|
+
var PACKAGE_VERSION = (() => {
|
|
325
|
+
try {
|
|
326
|
+
const req = createRequire(import.meta.url);
|
|
327
|
+
const pkg = req("../package.json");
|
|
328
|
+
return pkg.version ?? "0.0.0";
|
|
329
|
+
} catch {
|
|
330
|
+
return "0.0.0";
|
|
331
|
+
}
|
|
332
|
+
})();
|
|
333
|
+
|
|
334
|
+
// src/hookInstall.ts
|
|
335
|
+
var HOOK_COMMAND = `npx -y --package=@korso/shepherd@${PACKAGE_VERSION} shepherd-inbox-hook`;
|
|
336
|
+
|
|
283
337
|
// src/instructions.ts
|
|
284
338
|
function sanitizeWorkspace(workspace) {
|
|
285
339
|
return workspace.replace(/\s+/g, " ").slice(0, 64);
|
package/dist/inboxHook.js
CHANGED
|
@@ -141,6 +141,15 @@ var MAILBOX_FRESH_MS = 15 * 60 * 1e3;
|
|
|
141
141
|
function sessionMailboxPath(dir, serverPid) {
|
|
142
142
|
return join3(dir, `agent-${serverPid}.jsonl`);
|
|
143
143
|
}
|
|
144
|
+
var HOOK_CHAIN_REACH = { codex: 8 };
|
|
145
|
+
var DEFAULT_HOOK_CHAIN_REACH = 3;
|
|
146
|
+
var MAX_HOOK_CHAIN_REACH = Math.max(
|
|
147
|
+
DEFAULT_HOOK_CHAIN_REACH,
|
|
148
|
+
...Object.values(HOOK_CHAIN_REACH)
|
|
149
|
+
);
|
|
150
|
+
function hookChainReach(client) {
|
|
151
|
+
return (client === void 0 ? void 0 : HOOK_CHAIN_REACH[client]) ?? DEFAULT_HOOK_CHAIN_REACH;
|
|
152
|
+
}
|
|
144
153
|
function normalizeCwd(cwd) {
|
|
145
154
|
let normalized = resolve3(cwd);
|
|
146
155
|
if (process.platform === "win32") normalized = normalized.toLowerCase();
|
|
@@ -163,7 +172,7 @@ function hasFreshSessionMeta(dir, staleMs = MAILBOX_FRESH_MS, nowMs = Date.now()
|
|
|
163
172
|
}
|
|
164
173
|
function selectSessionMailboxes(dir, hookChain, hookCwd, staleMs = MAILBOX_FRESH_MS, nowMs = Date.now()) {
|
|
165
174
|
try {
|
|
166
|
-
const chain = hookChain.slice(0,
|
|
175
|
+
const chain = hookChain.slice(0, MAX_HOOK_CHAIN_REACH);
|
|
167
176
|
const wantedCwd = hookCwd === null ? null : normalizeCwd(hookCwd);
|
|
168
177
|
const candidates = [];
|
|
169
178
|
for (const name of readdirSync(dir)) {
|
|
@@ -197,7 +206,7 @@ function selectSessionMailboxes(dir, hookChain, hookCwd, staleMs = MAILBOX_FRESH
|
|
|
197
206
|
}
|
|
198
207
|
if (!Array.isArray(meta.chain) || typeof meta.cwd !== "string") continue;
|
|
199
208
|
const i = chain.findIndex((pid) => meta.chain.includes(pid));
|
|
200
|
-
if (i === -1) continue;
|
|
209
|
+
if (i === -1 || i >= hookChainReach(meta.client)) continue;
|
|
201
210
|
const j = meta.chain.indexOf(chain[i]);
|
|
202
211
|
if (i >= 2 && (j > 2 || wantedCwd === null || meta.cwd !== wantedCwd))
|
|
203
212
|
continue;
|