@korso/shepherd 0.6.2 → 0.7.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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Shepherd's stdio MCP server. Gives any MCP-capable agent (Claude Code, Codex, etc.) four advisory coordination tools backed by the shared hub: `work`, `done`, `announce`, and `sync`. The agent **joins the workspace automatically** on startup (no `join` tool), and the server ships standing instructions so the agent self-coordinates without the user prompting it.
4
4
 
5
- > **New here?** The [developer quickstart](https://github.com/Korsoai/shepherd/blob/main/docs/shepherd-mcp-quickstart.md) is the fastest path. TL;DR: `npx -y @korso/shepherd` with the env vars below.
5
+ > **New here?** The [developer quickstart](https://github.com/Korso-AI/shepherd/blob/main/docs/shepherd-mcp-quickstart.md) is the fastest path. TL;DR: `npx -y --package=@korso/shepherd shepherd-mcp` with the env vars below.
6
6
 
7
7
  ---
8
8
 
@@ -20,7 +20,7 @@ The server is published to npm and runs via `npx` — no clone or build required
20
20
  (Node 18+):
21
21
 
22
22
  ```sh
23
- npx -y @korso/shepherd
23
+ npx -y --package=@korso/shepherd shepherd-mcp
24
24
  ```
25
25
 
26
26
  You won't normally run that by hand; you put it in your MCP client config (below)
@@ -45,14 +45,14 @@ Missing either causes an immediate startup failure with a clear error on stderr
45
45
  listing which vars are absent. (No other var triggers this.)
46
46
 
47
47
  **Everything else is optional** — each identity field is resolved at startup as
48
- **env var → git detection → fallback**, so a plain `npx -y @korso/shepherd` with
48
+ **env var → git detection → fallback**, so a plain `npx -y --package=@korso/shepherd shepherd-mcp` with
49
49
  just the two required vars produces a valid, fully-identified session. Set an
50
50
  override only to replace what's detected:
51
51
 
52
52
  | Variable | If omitted | Example |
53
53
  |---|---|---|
54
54
  | `WORKSPACE` | defaults to `default` (**must match hub's `ALLOWED_WORKSPACE` if overridden**) | `shepherd` |
55
- | `REPO` | `git remote origin` → `owner/repo`, else repo folder name, else `unknown-repo` | `Korsoai/shepherd` |
55
+ | `REPO` | `git remote origin` → `owner/repo`, else repo folder name, else `unknown-repo` | `Korso-AI/shepherd` |
56
56
  | `BRANCH` | `git rev-parse --abbrev-ref HEAD`, else `HEAD` | `main` |
57
57
  | `BASE_BRANCH` | `origin/HEAD`, else `origin/main` / `origin/master` (used for the change-awareness heads-up) | `origin/main` |
58
58
  | `HUMAN` | git `user.name`, else local-part of `user.email`, else this device's **cached** last-detected name, else a generated name | `daichi` |
@@ -99,9 +99,22 @@ It delivers to an agent **while it's active**; an idle agent picks messages up t
99
99
  moment it next does anything. (Waking a fully-idle agent is out of scope — for
100
100
  Claude Code that needs Channels; Codex/Pi have no equivalent.)
101
101
 
102
- ### Claude Code — `PreToolUse` hook
103
-
104
- `PreToolUse` fires before every tool, giving the most frequent passive delivery.
102
+ **The hook also carries the unlinked-repo nudge.** The server instructions tell
103
+ the agent to ask about linking on its first write in a new repo, but instructions
104
+ sitting passively in context don't trigger themselves — an agent focused on the
105
+ task can skip straight to editing. So on every invocation the hook also checks
106
+ this repo's link state and, when the repo is **neither linked (`.shepherd`
107
+ marker) nor declined**, injects a reminder to run `link`/`decline` — on
108
+ `SessionStart` (front-loads the ask), right before a file-writing tool
109
+ (`Edit`/`Write`/`MultiEdit`/`NotebookEdit`; read-only tools never nudge), and on
110
+ tool-less events like Codex's `UserPromptSubmit` or a Pi turn. The
111
+ nudge is advisory and self-extinguishing: the moment the repo is linked or
112
+ declined it goes quiet, and like everything else here it fails open.
113
+
114
+ ### Claude Code — `PreToolUse` + `SessionStart` hooks
115
+
116
+ `PreToolUse` fires before every tool, giving the most frequent passive delivery;
117
+ `SessionStart` surfaces the link ask at the top of a session in an unlinked repo.
105
118
  The hook needs no arguments — it resolves the same default inbox dir the server
106
119
  uses (override both with `SHEPHERD_INBOX_DIR` if you relocated it):
107
120
 
@@ -110,7 +123,7 @@ uses (override both with `SHEPHERD_INBOX_DIR` if you relocated it):
110
123
  "mcpServers": {
111
124
  "shepherd": {
112
125
  "command": "npx",
113
- "args": ["-y", "@korso/shepherd"],
126
+ "args": ["-y", "--package=@korso/shepherd", "shepherd-mcp"],
114
127
  "env": {
115
128
  "HUB_URL": "https://shepherd.example.com",
116
129
  "TEAM_TOKEN": "tok_abc123"
@@ -118,11 +131,18 @@ uses (override both with `SHEPHERD_INBOX_DIR` if you relocated it):
118
131
  }
119
132
  },
120
133
  "hooks": {
134
+ "SessionStart": [
135
+ {
136
+ "hooks": [
137
+ { "type": "command", "command": "npx -y --package=@korso/shepherd shepherd-inbox-hook" }
138
+ ]
139
+ }
140
+ ],
121
141
  "PreToolUse": [
122
142
  {
123
143
  "matcher": "*",
124
144
  "hooks": [
125
- { "type": "command", "command": "npx -y -p @korso/shepherd shepherd-inbox-hook" }
145
+ { "type": "command", "command": "npx -y --package=@korso/shepherd shepherd-inbox-hook" }
126
146
  ]
127
147
  }
128
148
  ]
@@ -143,9 +163,9 @@ or MCP calls. Hooks must be enabled with `features.hooks = true`. In
143
163
  hooks = true
144
164
 
145
165
  [[hooks.UserPromptSubmit]]
146
- command = ["npx", "-y", "-p", "@korso/shepherd", "shepherd-inbox-hook"]
166
+ command = ["npx", "-y", "--package=@korso/shepherd", "shepherd-inbox-hook"]
147
167
  # On Windows use command_windows instead:
148
- # command_windows = ["cmd", "/c", "npx -y -p @korso/shepherd shepherd-inbox-hook"]
168
+ # command_windows = ["cmd", "/c", "npx -y --package=@korso/shepherd shepherd-inbox-hook"]
149
169
  ```
150
170
 
151
171
  ### Pi — extension
@@ -161,8 +181,8 @@ cp "$(npm root -g)/@korso/shepherd/dist/inboxExtension.js" ~/.pi/agent/extension
161
181
  ```
162
182
 
163
183
  It runs on every user turn (`before_agent_start`), drains the same inbox, and
164
- injects pending announcements. (Or load it ad hoc with
165
- `pi -e /abs/path/to/dist/inboxExtension.js`.)
184
+ injects pending announcements plus the unlinked-repo nudge. (Or load it ad hoc
185
+ with `pi -e /abs/path/to/dist/inboxExtension.js`.)
166
186
 
167
187
  ### Notes
168
188
 
@@ -191,7 +211,7 @@ line-continuation does not work). Minimal: just the two required vars (identity
191
211
  is auto-detected from git):
192
212
 
193
213
  ```powershell
194
- claude mcp add shepherd -s user -e HUB_URL=https://shepherd.example.com -e TEAM_TOKEN=tok_abc123 -- npx -y @korso/shepherd
214
+ claude mcp add shepherd -s user -e HUB_URL=https://shepherd.example.com -e TEAM_TOKEN=tok_abc123 -- npx -y --package=@korso/shepherd shepherd-mcp
195
215
  ```
196
216
 
197
217
  Add any optional overrides from §2 with extra `-e` flags (e.g. `-e MODEL=claude-sonnet-4-6 -e HUMAN=daichi`).
@@ -204,7 +224,7 @@ Alternative — a `.mcp.json` at the **root of the repo you're working in**
204
224
  "mcpServers": {
205
225
  "shepherd": {
206
226
  "command": "npx",
207
- "args": ["-y", "@korso/shepherd"],
227
+ "args": ["-y", "--package=@korso/shepherd", "shepherd-mcp"],
208
228
  "env": {
209
229
  "HUB_URL": "https://shepherd.example.com",
210
230
  "TEAM_TOKEN": "tok_abc123",
@@ -228,7 +248,7 @@ The table is `mcp_servers` with an **underscore** (`mcp-servers`/`mcpServers` ar
228
248
  silently ignored). Either run `codex mcp add`:
229
249
 
230
250
  ```sh
231
- codex mcp add shepherd --env HUB_URL=https://shepherd.example.com --env TEAM_TOKEN=tok_abc123 --env PROGRAM=codex -- npx -y @korso/shepherd
251
+ codex mcp add shepherd --env HUB_URL=https://shepherd.example.com --env TEAM_TOKEN=tok_abc123 --env PROGRAM=codex -- npx -y --package=@korso/shepherd shepherd-mcp
232
252
  ```
233
253
 
234
254
  …or add the table directly:
@@ -236,7 +256,7 @@ codex mcp add shepherd --env HUB_URL=https://shepherd.example.com --env TEAM_TOK
236
256
  ```toml
237
257
  [mcp_servers.shepherd]
238
258
  command = "npx"
239
- args = ["-y", "@korso/shepherd"]
259
+ args = ["-y", "--package=@korso/shepherd", "shepherd-mcp"]
240
260
  env = { HUB_URL = "https://shepherd.example.com", TEAM_TOKEN = "tok_abc123", PROGRAM = "codex", MODEL = "o4-mini" }
241
261
  ```
242
262
 
@@ -249,7 +269,7 @@ Pi uses a JSON `mcpServers` block (project config overrides global):
249
269
  "mcpServers": {
250
270
  "shepherd": {
251
271
  "command": "npx",
252
- "args": ["-y", "@korso/shepherd"],
272
+ "args": ["-y", "--package=@korso/shepherd", "shepherd-mcp"],
253
273
  "env": {
254
274
  "HUB_URL": "https://shepherd.example.com",
255
275
  "TEAM_TOKEN": "tok_abc123",
@@ -260,6 +280,32 @@ Pi uses a JSON `mcpServers` block (project config overrides global):
260
280
  }
261
281
  ```
262
282
 
283
+ ### Cursor (`~/.cursor/mcp.json` or `.cursor/mcp.json`)
284
+
285
+ Cursor reads the same JSON `mcpServers` shape — global at `~/.cursor/mcp.json`,
286
+ or per-project at `.cursor/mcp.json`:
287
+
288
+ ```json
289
+ {
290
+ "mcpServers": {
291
+ "shepherd": {
292
+ "command": "npx",
293
+ "args": ["-y", "--package=@korso/shepherd", "shepherd-mcp"],
294
+ "env": {
295
+ "HUB_URL": "https://shepherd.example.com",
296
+ "TEAM_TOKEN": "tok_abc123",
297
+ "PROGRAM": "cursor"
298
+ }
299
+ }
300
+ }
301
+ }
302
+ ```
303
+
304
+ Confirm under **Settings → MCP** that `shepherd` is listed with its tools.
305
+ Announcement push: Cursor has no hook equivalent (see the hooks above for
306
+ Claude Code/Codex/Pi), so announcements arrive via the universal drainer — on
307
+ the agent's next Shepherd tool call — rather than pushed between calls.
308
+
263
309
  ---
264
310
 
265
311
  ## 4. Verify the server starts (quick smoke test)
@@ -270,10 +316,10 @@ PowerShell (set env vars, then run):
270
316
  ```powershell
271
317
  $env:HUB_URL = "https://shepherd.example.com"
272
318
  $env:TEAM_TOKEN = "tok_abc123"
273
- npx -y @korso/shepherd
319
+ npx -y --package=@korso/shepherd shepherd-mcp
274
320
  ```
275
321
 
276
- bash/zsh: `HUB_URL=https://shepherd.example.com TEAM_TOKEN=tok_abc123 npx -y @korso/shepherd`
322
+ bash/zsh: `HUB_URL=https://shepherd.example.com TEAM_TOKEN=tok_abc123 npx -y --package=@korso/shepherd shepherd-mcp`
277
323
 
278
324
  No stderr output and the process blocking on stdin = healthy. Press Ctrl+C to exit.
279
325
 
@@ -298,7 +344,7 @@ Only needed if you're changing the MCP server itself. Clone the monorepo and
298
344
  point your client at a local build instead of npx:
299
345
 
300
346
  ```sh
301
- git clone https://github.com/Korsoai/shepherd.git
347
+ git clone https://github.com/Korso-AI/shepherd.git
302
348
  cd shepherd
303
349
  npm install
304
350
  npm run build # tsc -b — compiles the workspace for dev + tests
@@ -1,16 +1,59 @@
1
1
  // src/inbox.ts
2
- import { createHash } from "crypto";
2
+ import { createHash as createHash2 } from "crypto";
3
3
  import {
4
4
  appendFileSync,
5
- mkdirSync,
6
- readFileSync,
5
+ mkdirSync as mkdirSync2,
6
+ readFileSync as readFileSync3,
7
7
  renameSync,
8
- rmSync,
9
- existsSync
8
+ rmSync as rmSync3,
9
+ existsSync as existsSync3
10
10
  } from "fs";
11
+ import { homedir as homedir2, tmpdir as tmpdir2 } from "os";
12
+ import { dirname as dirname3, join as join3, resolve as resolve3 } from "path";
13
+
14
+ // src/marker.ts
15
+ import * as fs from "fs";
16
+ import * as path from "path";
17
+ var MARKER_FILENAME = ".shepherd";
18
+ function findRepoRoot(cwd) {
19
+ let dir = path.resolve(cwd);
20
+ for (; ; ) {
21
+ if (fs.existsSync(path.join(dir, ".git"))) return dir;
22
+ const parent = path.dirname(dir);
23
+ if (parent === dir) return null;
24
+ dir = parent;
25
+ }
26
+ }
27
+ function markerPath(cwd) {
28
+ const root = findRepoRoot(cwd);
29
+ return root === null ? null : path.join(root, MARKER_FILENAME);
30
+ }
31
+ function readMarker(cwd = process.cwd()) {
32
+ const file = markerPath(cwd);
33
+ if (file === null) return null;
34
+ let raw;
35
+ try {
36
+ raw = fs.readFileSync(file, "utf8");
37
+ } catch {
38
+ return null;
39
+ }
40
+ try {
41
+ const parsed = JSON.parse(raw);
42
+ if (parsed !== null && typeof parsed === "object" && typeof parsed.workspace === "string" && parsed.workspace.length > 0) {
43
+ return { workspace: parsed.workspace };
44
+ }
45
+ return null;
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ // src/declined.ts
52
+ import { createHash } from "crypto";
53
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
11
54
  import { homedir, tmpdir } from "os";
12
- import { dirname, join, resolve } from "path";
13
- function defaultInboxDir() {
55
+ import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
56
+ function defaultDeclinedDir() {
14
57
  let base = "";
15
58
  try {
16
59
  base = homedir();
@@ -18,29 +61,78 @@ function defaultInboxDir() {
18
61
  base = "";
19
62
  }
20
63
  if (!base) base = tmpdir();
21
- return join(base, ".shepherd", "inbox");
64
+ return join2(base, ".shepherd", "declined");
22
65
  }
23
- function inboxFilePath(dir, cwd) {
24
- let normalized = resolve(cwd);
66
+ function declinedFilePath(repoRoot, dir = defaultDeclinedDir()) {
67
+ let normalized = resolve2(repoRoot);
25
68
  if (process.platform === "win32") normalized = normalized.toLowerCase();
26
69
  const hash = createHash("sha256").update(normalized).digest("hex").slice(0, 16);
27
- return join(dir, `${hash}.jsonl`);
70
+ return join2(dir, hash);
71
+ }
72
+ function isDeclined(repoRoot, dir = defaultDeclinedDir()) {
73
+ const file = declinedFilePath(repoRoot, dir);
74
+ let raw;
75
+ try {
76
+ raw = readFileSync2(file, "utf8");
77
+ } catch {
78
+ return false;
79
+ }
80
+ try {
81
+ const parsed = JSON.parse(raw);
82
+ return typeof parsed?.declinedAt === "string";
83
+ } catch {
84
+ return false;
85
+ }
86
+ }
87
+
88
+ // src/linkNudge.ts
89
+ var WRITE_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
90
+ var LINK_NUDGE_TEXT = '[Shepherd] This repo is not linked to a Shepherd workspace and has not been declined. Before writing files here, call the shepherd `link` tool with no argument: it auto-links if the user belongs to exactly one workspace, or lists workspaces \u2014 then ask the user "Coordinate this repo with Shepherd? Which workspace?" and call `link <workspace>` with their answer, or `decline` if they say no. Ask at most once per repo.';
91
+ function buildLinkNudge(cwd, toolName, deps = {}) {
92
+ try {
93
+ if (toolName !== void 0 && !WRITE_TOOLS.has(toolName)) return "";
94
+ const repoRoot = findRepoRoot(cwd);
95
+ if (repoRoot === null) return "";
96
+ if (readMarker(cwd) !== null) return "";
97
+ if (isDeclined(repoRoot, deps.declinedDir)) return "";
98
+ return LINK_NUDGE_TEXT;
99
+ } catch {
100
+ return "";
101
+ }
102
+ }
103
+
104
+ // src/inbox.ts
105
+ function defaultInboxDir() {
106
+ let base = "";
107
+ try {
108
+ base = homedir2();
109
+ } catch {
110
+ base = "";
111
+ }
112
+ if (!base) base = tmpdir2();
113
+ return join3(base, ".shepherd", "inbox");
114
+ }
115
+ function inboxFilePath(dir, cwd) {
116
+ let normalized = resolve3(cwd);
117
+ if (process.platform === "win32") normalized = normalized.toLowerCase();
118
+ const hash = createHash2("sha256").update(normalized).digest("hex").slice(0, 16);
119
+ return join3(dir, `${hash}.jsonl`);
28
120
  }
29
121
  function drainInbox(filePath) {
30
122
  const tmp = `${filePath}.draining`;
31
123
  let raw = "";
32
124
  try {
33
- if (existsSync(tmp)) {
34
- raw += readFileSync(tmp, "utf8");
35
- rmSync(tmp, { force: true });
125
+ if (existsSync3(tmp)) {
126
+ raw += readFileSync3(tmp, "utf8");
127
+ rmSync3(tmp, { force: true });
36
128
  }
37
129
  } catch {
38
130
  }
39
131
  try {
40
- if (existsSync(filePath)) {
132
+ if (existsSync3(filePath)) {
41
133
  renameSync(filePath, tmp);
42
- raw += readFileSync(tmp, "utf8");
43
- rmSync(tmp, { force: true });
134
+ raw += readFileSync3(tmp, "utf8");
135
+ rmSync3(tmp, { force: true });
44
136
  }
45
137
  } catch {
46
138
  }
@@ -80,7 +172,7 @@ function shepherdInbox(pi) {
80
172
  const dir = process.env["SHEPHERD_INBOX_DIR"] || defaultInboxDir();
81
173
  const cwd = ctx?.cwd ?? process.cwd();
82
174
  const announcements = drainInbox(inboxFilePath(dir, cwd));
83
- const content = formatInboxAnnouncements(announcements);
175
+ const content = [buildLinkNudge(cwd), formatInboxAnnouncements(announcements)].filter(Boolean).join("\n\n");
84
176
  if (!content) return void 0;
85
177
  return { message: { customType: "shepherd-inbox", content, display: true } };
86
178
  } catch {
package/dist/inboxHook.js CHANGED
@@ -1,18 +1,61 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/inbox.ts
4
- import { createHash } from "crypto";
4
+ import { createHash as createHash2 } from "crypto";
5
5
  import {
6
6
  appendFileSync,
7
- mkdirSync,
8
- readFileSync,
7
+ mkdirSync as mkdirSync2,
8
+ readFileSync as readFileSync3,
9
9
  renameSync,
10
- rmSync,
11
- existsSync
10
+ rmSync as rmSync3,
11
+ existsSync as existsSync3
12
12
  } from "fs";
13
+ import { homedir as homedir2, tmpdir as tmpdir2 } from "os";
14
+ import { dirname as dirname3, join as join3, resolve as resolve3 } from "path";
15
+
16
+ // src/marker.ts
17
+ import * as fs from "fs";
18
+ import * as path from "path";
19
+ var MARKER_FILENAME = ".shepherd";
20
+ function findRepoRoot(cwd) {
21
+ let dir = path.resolve(cwd);
22
+ for (; ; ) {
23
+ if (fs.existsSync(path.join(dir, ".git"))) return dir;
24
+ const parent = path.dirname(dir);
25
+ if (parent === dir) return null;
26
+ dir = parent;
27
+ }
28
+ }
29
+ function markerPath(cwd) {
30
+ const root = findRepoRoot(cwd);
31
+ return root === null ? null : path.join(root, MARKER_FILENAME);
32
+ }
33
+ function readMarker(cwd = process.cwd()) {
34
+ const file = markerPath(cwd);
35
+ if (file === null) return null;
36
+ let raw;
37
+ try {
38
+ raw = fs.readFileSync(file, "utf8");
39
+ } catch {
40
+ return null;
41
+ }
42
+ try {
43
+ const parsed = JSON.parse(raw);
44
+ if (parsed !== null && typeof parsed === "object" && typeof parsed.workspace === "string" && parsed.workspace.length > 0) {
45
+ return { workspace: parsed.workspace };
46
+ }
47
+ return null;
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ // src/declined.ts
54
+ import { createHash } from "crypto";
55
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
13
56
  import { homedir, tmpdir } from "os";
14
- import { dirname, join, resolve } from "path";
15
- function defaultInboxDir() {
57
+ import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
58
+ function defaultDeclinedDir() {
16
59
  let base = "";
17
60
  try {
18
61
  base = homedir();
@@ -20,29 +63,78 @@ function defaultInboxDir() {
20
63
  base = "";
21
64
  }
22
65
  if (!base) base = tmpdir();
23
- return join(base, ".shepherd", "inbox");
66
+ return join2(base, ".shepherd", "declined");
24
67
  }
25
- function inboxFilePath(dir, cwd) {
26
- let normalized = resolve(cwd);
68
+ function declinedFilePath(repoRoot, dir = defaultDeclinedDir()) {
69
+ let normalized = resolve2(repoRoot);
27
70
  if (process.platform === "win32") normalized = normalized.toLowerCase();
28
71
  const hash = createHash("sha256").update(normalized).digest("hex").slice(0, 16);
29
- return join(dir, `${hash}.jsonl`);
72
+ return join2(dir, hash);
73
+ }
74
+ function isDeclined(repoRoot, dir = defaultDeclinedDir()) {
75
+ const file = declinedFilePath(repoRoot, dir);
76
+ let raw;
77
+ try {
78
+ raw = readFileSync2(file, "utf8");
79
+ } catch {
80
+ return false;
81
+ }
82
+ try {
83
+ const parsed = JSON.parse(raw);
84
+ return typeof parsed?.declinedAt === "string";
85
+ } catch {
86
+ return false;
87
+ }
88
+ }
89
+
90
+ // src/linkNudge.ts
91
+ var WRITE_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
92
+ var LINK_NUDGE_TEXT = '[Shepherd] This repo is not linked to a Shepherd workspace and has not been declined. Before writing files here, call the shepherd `link` tool with no argument: it auto-links if the user belongs to exactly one workspace, or lists workspaces \u2014 then ask the user "Coordinate this repo with Shepherd? Which workspace?" and call `link <workspace>` with their answer, or `decline` if they say no. Ask at most once per repo.';
93
+ function buildLinkNudge(cwd, toolName, deps = {}) {
94
+ try {
95
+ if (toolName !== void 0 && !WRITE_TOOLS.has(toolName)) return "";
96
+ const repoRoot = findRepoRoot(cwd);
97
+ if (repoRoot === null) return "";
98
+ if (readMarker(cwd) !== null) return "";
99
+ if (isDeclined(repoRoot, deps.declinedDir)) return "";
100
+ return LINK_NUDGE_TEXT;
101
+ } catch {
102
+ return "";
103
+ }
104
+ }
105
+
106
+ // src/inbox.ts
107
+ function defaultInboxDir() {
108
+ let base = "";
109
+ try {
110
+ base = homedir2();
111
+ } catch {
112
+ base = "";
113
+ }
114
+ if (!base) base = tmpdir2();
115
+ return join3(base, ".shepherd", "inbox");
116
+ }
117
+ function inboxFilePath(dir, cwd) {
118
+ let normalized = resolve3(cwd);
119
+ if (process.platform === "win32") normalized = normalized.toLowerCase();
120
+ const hash = createHash2("sha256").update(normalized).digest("hex").slice(0, 16);
121
+ return join3(dir, `${hash}.jsonl`);
30
122
  }
31
123
  function drainInbox(filePath) {
32
124
  const tmp = `${filePath}.draining`;
33
125
  let raw = "";
34
126
  try {
35
- if (existsSync(tmp)) {
36
- raw += readFileSync(tmp, "utf8");
37
- rmSync(tmp, { force: true });
127
+ if (existsSync3(tmp)) {
128
+ raw += readFileSync3(tmp, "utf8");
129
+ rmSync3(tmp, { force: true });
38
130
  }
39
131
  } catch {
40
132
  }
41
133
  try {
42
- if (existsSync(filePath)) {
134
+ if (existsSync3(filePath)) {
43
135
  renameSync(filePath, tmp);
44
- raw += readFileSync(tmp, "utf8");
45
- rmSync(tmp, { force: true });
136
+ raw += readFileSync3(tmp, "utf8");
137
+ rmSync3(tmp, { force: true });
46
138
  }
47
139
  } catch {
48
140
  }
@@ -74,8 +166,7 @@ function formatInboxAnnouncements(announcements) {
74
166
  }
75
167
  return lines.join("\n");
76
168
  }
77
- function buildHookOutput(rawStdin, inboxDir, drain = drainInbox) {
78
- if (!inboxDir) return "";
169
+ function buildHookOutput(rawStdin, inboxDir, drain = drainInbox, nudge = buildLinkNudge) {
79
170
  let input;
80
171
  try {
81
172
  input = JSON.parse(rawStdin);
@@ -83,13 +174,21 @@ function buildHookOutput(rawStdin, inboxDir, drain = drainInbox) {
83
174
  return "";
84
175
  }
85
176
  if (!input || typeof input.cwd !== "string" || input.cwd.length === 0) return "";
86
- const announcements = drain(inboxFilePath(inboxDir, input.cwd));
87
- const text = formatInboxAnnouncements(announcements);
88
- if (!text) return "";
177
+ const parts = [];
178
+ const nudgeText = nudge(
179
+ input.cwd,
180
+ typeof input.tool_name === "string" ? input.tool_name : void 0
181
+ );
182
+ if (nudgeText) parts.push(nudgeText);
183
+ if (inboxDir) {
184
+ const text = formatInboxAnnouncements(drain(inboxFilePath(inboxDir, input.cwd)));
185
+ if (text) parts.push(text);
186
+ }
187
+ if (parts.length === 0) return "";
89
188
  return JSON.stringify({
90
189
  hookSpecificOutput: {
91
190
  hookEventName: input.hook_event_name || "PreToolUse",
92
- additionalContext: text
191
+ additionalContext: parts.join("\n\n")
93
192
  }
94
193
  });
95
194
  }
package/dist/index.js CHANGED
@@ -783,7 +783,10 @@ function detectHuman(cwd = process.cwd()) {
783
783
  return null;
784
784
  }
785
785
  const name = runGit(cwd, ["config", "user.name"]);
786
- if (name) return name;
786
+ if (name) {
787
+ const local = name.includes("@") ? name.split("@")[0] : name;
788
+ if (local) return local;
789
+ }
787
790
  const email = runGit(cwd, ["config", "user.email"]);
788
791
  if (email) {
789
792
  const local = email.split("@")[0];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@korso/shepherd",
3
- "version": "0.6.2",
3
+ "version": "0.7.0",
4
4
  "description": "Shepherd MCP server — gives any MCP-capable agent (Claude Code, Codex, etc.) four advisory cross-session coordination tools (work/done/announce/sync) backed by the shared Shepherd hub. Joins the workspace automatically and ships standing instructions so the agent self-coordinates.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",