@korso/shepherd-ui 0.6.0 → 0.8.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/config/ConnectAgent.js +76 -12
- package/dist/lib/index.js +374 -329
- package/dist/lib/styles.css +12 -5
- package/dist/selfhost/assets/{index-C78oV7tm.css → index-dBmCZSQx.css} +1 -1
- package/dist/selfhost/index.html +2 -2
- package/package.json +1 -1
- /package/dist/selfhost/assets/{index-B_B7vc7a.js → index--_PPE03M.js} +0 -0
|
@@ -7,18 +7,63 @@ const TOOLS = [
|
|
|
7
7
|
{ id: "claude", label: "Claude Code" },
|
|
8
8
|
{ id: "codex", label: "Codex" },
|
|
9
9
|
{ id: "pi", label: "Pi" },
|
|
10
|
+
{ id: "cursor", label: "Cursor" },
|
|
10
11
|
{ id: "generic", label: "Generic (JSON)" },
|
|
11
12
|
];
|
|
12
13
|
// The token placeholder shown before a real token is minted. Switching tools or
|
|
13
14
|
// reading the command pre-mint shows this, never a real secret.
|
|
14
15
|
const TOKEN_PLACEHOLDER = "shp_<paste-after-generating>";
|
|
15
|
-
//
|
|
16
|
-
//
|
|
16
|
+
// Tools with an `mcp add` CLI. Pi, Cursor, and generic get a JSON config
|
|
17
|
+
// block instead: Pi reads `~/.pi/agent/mcp.json` (or `.pi/mcp.json`), Cursor
|
|
18
|
+
// reads `~/.cursor/mcp.json` (or `.cursor/mcp.json`) — same `mcpServers` shape.
|
|
17
19
|
const CLI_PREFIX = {
|
|
18
20
|
claude: "claude mcp add shepherd -s user",
|
|
19
21
|
codex: "codex mcp add shepherd",
|
|
20
|
-
pi: "pi mcp add shepherd",
|
|
21
22
|
};
|
|
23
|
+
// `claude mcp add` takes `-e KEY=val`; `codex mcp add` only accepts `--env`.
|
|
24
|
+
const ENV_FLAG = {
|
|
25
|
+
claude: "-e",
|
|
26
|
+
codex: "--env",
|
|
27
|
+
};
|
|
28
|
+
// The shepherd-inbox-hook bin: delivers teammate announcements out-of-band AND
|
|
29
|
+
// nudges the agent to run `link` before its first write in an unlinked repo.
|
|
30
|
+
// Neither `claude mcp add` nor `codex mcp add` can install hooks, so the
|
|
31
|
+
// snippet is a paste-into-config block surfaced next to the install command.
|
|
32
|
+
// Cursor has no hook mechanism; Pi uses the bundled extension (see the
|
|
33
|
+
// @korso/shepherd README) — neither gets a snippet here.
|
|
34
|
+
const HOOK_COMMAND = "npx -y --package=@korso/shepherd shepherd-inbox-hook";
|
|
35
|
+
function hookSnippet(tool) {
|
|
36
|
+
if (tool === "claude") {
|
|
37
|
+
// SessionStart front-loads the link ask; PreToolUse delivers announcements
|
|
38
|
+
// and re-nudges right before a write in a still-unlinked repo.
|
|
39
|
+
return {
|
|
40
|
+
target: "~/.claude/settings.json",
|
|
41
|
+
snippet: JSON.stringify({
|
|
42
|
+
hooks: {
|
|
43
|
+
SessionStart: [{ hooks: [{ type: "command", command: HOOK_COMMAND }] }],
|
|
44
|
+
PreToolUse: [
|
|
45
|
+
{ matcher: "*", hooks: [{ type: "command", command: HOOK_COMMAND }] },
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
}, null, 2),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (tool === "codex") {
|
|
52
|
+
// Codex's PreToolUse only fires for Bash, so UserPromptSubmit is the
|
|
53
|
+
// frequent event there; hooks must be feature-flagged on.
|
|
54
|
+
return {
|
|
55
|
+
target: "~/.codex/config.toml",
|
|
56
|
+
snippet: [
|
|
57
|
+
"[features]",
|
|
58
|
+
"hooks = true",
|
|
59
|
+
"",
|
|
60
|
+
"[[hooks.UserPromptSubmit]]",
|
|
61
|
+
'command = ["npx", "-y", "--package=@korso/shepherd", "shepherd-inbox-hook"]',
|
|
62
|
+
].join("\n"),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
22
67
|
// "created 3d ago · never used" / "created 3d ago · last used 2h ago" — helps
|
|
23
68
|
// an operator tell which tokens are still active before revoking one.
|
|
24
69
|
function tokenMeta(token, nowMs) {
|
|
@@ -27,13 +72,18 @@ function tokenMeta(token, nowMs) {
|
|
|
27
72
|
return `${created} · ${used}`;
|
|
28
73
|
}
|
|
29
74
|
function installCommand(tool, hubUrl, token) {
|
|
30
|
-
if (tool === "generic") {
|
|
75
|
+
if (tool === "generic" || tool === "pi" || tool === "cursor") {
|
|
76
|
+
// PROGRAM names the tool in the presence feed; it defaults to
|
|
77
|
+
// `claude-code`, so JSON-config tools set it explicitly.
|
|
78
|
+
const env = { HUB_URL: hubUrl, SHEPHERD_TOKEN: token };
|
|
79
|
+
if (tool !== "generic")
|
|
80
|
+
env["PROGRAM"] = tool;
|
|
31
81
|
return JSON.stringify({
|
|
32
82
|
mcpServers: {
|
|
33
83
|
shepherd: {
|
|
34
84
|
command: "npx",
|
|
35
|
-
args: ["-y", "
|
|
36
|
-
env
|
|
85
|
+
args: ["-y", "--package=@korso/shepherd", "shepherd-mcp"],
|
|
86
|
+
env,
|
|
37
87
|
},
|
|
38
88
|
},
|
|
39
89
|
}, null, 2);
|
|
@@ -41,12 +91,19 @@ function installCommand(tool, hubUrl, token) {
|
|
|
41
91
|
// A single line with no shell-specific continuation character (`\` in
|
|
42
92
|
// bash/zsh, backtick in PowerShell, `^` in cmd) so the command pastes
|
|
43
93
|
// cleanly into any terminal.
|
|
44
|
-
|
|
94
|
+
const envFlag = ENV_FLAG[tool];
|
|
95
|
+
const parts = [
|
|
45
96
|
CLI_PREFIX[tool],
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
97
|
+
`${envFlag} HUB_URL=${hubUrl}`,
|
|
98
|
+
`${envFlag} SHEPHERD_TOKEN=${token}`,
|
|
99
|
+
];
|
|
100
|
+
// PROGRAM defaults to claude-code, so only non-Claude tools set it.
|
|
101
|
+
if (tool === "codex")
|
|
102
|
+
parts.push(`${envFlag} PROGRAM=codex`);
|
|
103
|
+
// NOTE: long `--package=` form on purpose — `claude mcp add` (CLI) fails to
|
|
104
|
+
// parse a bare `-p` after the `--` separator ("error: unknown option '-s'").
|
|
105
|
+
parts.push("-- npx -y --package=@korso/shepherd shepherd-mcp");
|
|
106
|
+
return parts.join(" ");
|
|
50
107
|
}
|
|
51
108
|
export function ConnectAgent({ hubUrl }) {
|
|
52
109
|
const client = useShepherdClient();
|
|
@@ -87,6 +144,7 @@ export function ConnectAgent({ hubUrl }) {
|
|
|
87
144
|
async function generate() {
|
|
88
145
|
setBusy(true);
|
|
89
146
|
setError(null);
|
|
147
|
+
setStatus(null);
|
|
90
148
|
try {
|
|
91
149
|
const res = await client.mintAccountToken(name.trim() ? { name: name.trim() } : {});
|
|
92
150
|
setRawToken(res.token);
|
|
@@ -119,12 +177,18 @@ export function ConnectAgent({ hubUrl }) {
|
|
|
119
177
|
}
|
|
120
178
|
}
|
|
121
179
|
const command = installCommand(tool, directHubUrl, rawToken ?? TOKEN_PLACEHOLDER);
|
|
180
|
+
const hook = hookSnippet(tool);
|
|
122
181
|
async function copyCommand() {
|
|
123
182
|
await navigator.clipboard.writeText(command);
|
|
124
183
|
setCopied(true);
|
|
125
184
|
setTimeout(() => setCopied(false), 2000);
|
|
126
185
|
}
|
|
127
|
-
return (_jsxs("section", { className: "shepherd-connect-agent", "aria-labelledby": headingId, children: [_jsxs("div", { className: "card-head", children: [_jsx("h3", { id: headingId, children: "Connect your agent" }), _jsx("p", { className: "card-sub", children: "Generate a token \u2014 it works across all your workspaces \u2014 then paste the command into your coding tool." }), _jsx("p", { className: "card-sub", children: "The first time the agent opens a repo, it'll ask which workspace to coordinate that repo with." })] }), _jsxs("div", { className: "card-body", children: [_jsxs("div", { className: "field", children: [_jsx("label", { htmlFor: "connect-tool", children: "Tool" }), _jsx("select", { id: "connect-tool", value: tool, onChange: (e) => setTool(e.target.value), children: TOOLS.map((t) => (_jsx("option", { value: t.id, children: t.label }, t.id))) })] }), _jsxs("div", { className: "field generate", children: [_jsx("label", { htmlFor: "token-name", children: "Token name (optional)" }), _jsxs("div", { className: "field__row", children: [_jsx("input", { id: "token-name", type: "text", value: name, onChange: (e) => setName(e.target.value),
|
|
186
|
+
return (_jsxs("section", { className: "shepherd-connect-agent", "aria-labelledby": headingId, children: [_jsxs("div", { className: "card-head", children: [_jsx("h3", { id: headingId, children: "Connect your agent" }), _jsx("p", { className: "card-sub", children: "Generate a token \u2014 it works across all your workspaces \u2014 then paste the command into your coding tool." }), _jsx("p", { className: "card-sub", children: "The first time the agent opens a repo, it'll ask which workspace to coordinate that repo with." })] }), _jsxs("div", { className: "card-body", children: [_jsxs("div", { className: "field", children: [_jsx("label", { htmlFor: "connect-tool", children: "Tool" }), _jsx("select", { id: "connect-tool", value: tool, onChange: (e) => setTool(e.target.value), children: TOOLS.map((t) => (_jsx("option", { value: t.id, children: t.label }, t.id))) })] }), _jsxs("div", { className: "field generate", children: [_jsx("label", { htmlFor: "token-name", children: "Token name (optional)" }), _jsxs("div", { className: "field__row", children: [_jsx("input", { id: "token-name", type: "text", value: name, onChange: (e) => setName(e.target.value), onKeyDown: (e) => {
|
|
187
|
+
if (e.key === "Enter" && !busy) {
|
|
188
|
+
e.preventDefault();
|
|
189
|
+
void generate();
|
|
190
|
+
}
|
|
191
|
+
}, placeholder: "e.g. laptop" }), _jsx("button", { type: "button", onClick: () => void generate(), disabled: busy, children: "Generate token" })] })] }), error && _jsx("p", { role: "alert", children: error }), status && _jsx("p", { role: "status", children: status }), rawToken && (_jsx("p", { className: "token-once", role: "status", children: "Copy this token now \u2014 it won't be shown again." })), _jsxs("div", { className: "install-command", children: [_jsx("pre", { "data-testid": "install-command", children: command }), _jsx("button", { type: "button", className: "install-command__copy", "aria-label": copied ? "Copied" : "Copy command", title: copied ? "Copied" : "Copy", onClick: () => void copyCommand(), children: copied ? (_jsx("svg", { width: "15", height: "15", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: _jsx("path", { d: "M20 6 9 17l-5-5" }) })) : (_jsxs("svg", { width: "15", height: "15", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [_jsx("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2" }), _jsx("path", { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" })] })) })] }), hook && (_jsxs("div", { className: "hook-setup", children: [_jsxs("p", { className: "card-sub", children: ["Recommended: merge this into ", _jsx("code", { children: hook.target }), " \u2014 it delivers teammate announcements to the agent and reminds it to link each repo before its first write."] }), _jsx("div", { className: "install-command", children: _jsx("pre", { "data-testid": "hook-snippet", children: hook.snippet }) })] })), _jsx("h4", { children: "Existing tokens" }), loading ? (_jsx("p", { role: "status", children: "Loading\u2026" })) : tokens.length === 0 ? (_jsx("p", { children: "No tokens yet." })) : (_jsx("ul", { children: tokens.map((t) => {
|
|
128
192
|
const name = t.name ?? t.id;
|
|
129
193
|
return (_jsxs("li", { children: [_jsx("span", { children: name }), _jsx("span", { className: "token-meta", children: tokenMeta(t, Date.now()) }), t.revokedAt ? (_jsx("span", { className: "revoked", children: "revoked" })) : (_jsx("button", { type: "button", "aria-label": `Revoke token ${name}`, onClick: () => void revoke(t.id), disabled: revokingId === t.id, children: "Revoke" }))] }, t.id));
|
|
130
194
|
}) }))] })] }));
|