@skyelight/mcp 0.1.4 → 0.2.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 +22 -5
- package/package.json +1 -1
- package/src/cli.js +200 -0
- package/src/config.js +3 -2
- package/src/index.js +18 -1
- package/src/install.js +173 -0
- package/src/tools.js +5 -8
package/README.md
CHANGED
|
@@ -9,7 +9,24 @@ Works with anything that speaks MCP over stdio: Claude Code, Cursor, Codex.
|
|
|
9
9
|
## Setup
|
|
10
10
|
|
|
11
11
|
**Get a personal access token.** In the Skyelight web app, go to your account
|
|
12
|
-
settings → **
|
|
12
|
+
settings → **API Keys** → create one. It is shown once, and you can give
|
|
13
|
+
it an expiry.
|
|
14
|
+
|
|
15
|
+
## The short way
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx @skyelight/mcp init
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Finds your coding agent, registers the remote server with it, and leaves the
|
|
22
|
+
sign-in to OAuth — nothing is pasted and no key is stored on disk. It shows
|
|
23
|
+
what it will change and waits; `--yes` skips that once you have read it.
|
|
24
|
+
|
|
25
|
+
It knows Claude Code, Cursor, Windsurf and Codex. Name one with `--client
|
|
26
|
+
cursor` when more than one is installed, and point at another deployment with
|
|
27
|
+
`SKYELIGHT_URL=https://…`. The deployment tells the installer its own MCP URL
|
|
28
|
+
and OAuth client over `/mcp/install-config`, so this package holds no
|
|
29
|
+
environment-specific constants.
|
|
13
30
|
|
|
14
31
|
The token is yours, not a service account's: anything the agent writes is
|
|
15
32
|
attributed to you, marked with the tool it came through. That is deliberate —
|
|
@@ -119,10 +136,10 @@ There is no service-account mode. Synthetic agent identities existed and were
|
|
|
119
136
|
removed: a workspace should not grow a member for every tool somebody plugs in,
|
|
120
137
|
and a reply nobody is accountable for is worse than one attributed plainly.
|
|
121
138
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
139
|
+
Workspace API keys (`sk_live_...`) used to be a second way in, for scripts and
|
|
140
|
+
CI. They are gone — a credential that outlives whoever minted it is one nobody
|
|
141
|
+
is accountable for, and the write tools already refused them for exactly that
|
|
142
|
+
reason. A personal token is the only credential the API accepts.
|
|
126
143
|
|
|
127
144
|
## Permissions
|
|
128
145
|
|
package/package.json
CHANGED
package/src/cli.js
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `npx @skyelight/mcp init`
|
|
5
|
+
*
|
|
6
|
+
* Finds your coding agent, registers Skyelight's MCP server with it, and
|
|
7
|
+
* leaves the sign-in to the agent — the server speaks OAuth, so the first
|
|
8
|
+
* time the agent connects it opens a browser and you approve it there. No
|
|
9
|
+
* token is pasted anywhere, and none is stored on disk by this.
|
|
10
|
+
*
|
|
11
|
+
* It shows the change and waits, like `@skyelight/build init` does. Editing
|
|
12
|
+
* a config somebody else wrote is the kind of help that has to ask first.
|
|
13
|
+
* `--yes` exists for people who have already read it once.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
17
|
+
import { dirname } from "node:path";
|
|
18
|
+
import { createInterface } from "node:readline/promises";
|
|
19
|
+
import { spawnSync } from "node:child_process";
|
|
20
|
+
import {
|
|
21
|
+
CLIENTS,
|
|
22
|
+
SERVER_NAME,
|
|
23
|
+
detectClients,
|
|
24
|
+
findClient,
|
|
25
|
+
addToJson,
|
|
26
|
+
addToToml,
|
|
27
|
+
} from "./install.js";
|
|
28
|
+
|
|
29
|
+
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
30
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
31
|
+
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
32
|
+
const amber = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Which deployment to register. Production unless told otherwise.
|
|
36
|
+
*/
|
|
37
|
+
const ORIGIN = (
|
|
38
|
+
process.env.SKYELIGHT_URL ?? "https://mellow-zebra-383.convex.site"
|
|
39
|
+
).replace(/\/+$/, "");
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Asked for rather than baked in.
|
|
43
|
+
*
|
|
44
|
+
* The OAuth client id is per deployment, so a published package cannot hold
|
|
45
|
+
* one — that would be an npm release per environment, and the first person
|
|
46
|
+
* to run it against the wrong one gets an OAuth error naming a client that
|
|
47
|
+
* is not theirs. `/mcp/install-config` is the deployment saying what it is.
|
|
48
|
+
*
|
|
49
|
+
* A deployment that answers without a client id is one doing dynamic client
|
|
50
|
+
* registration, which is the correct fallback rather than a failure: the
|
|
51
|
+
* flags that carry the id are simply left off.
|
|
52
|
+
*/
|
|
53
|
+
async function fetchConfig() {
|
|
54
|
+
const res = await fetch(`${ORIGIN}/mcp/install-config`, {
|
|
55
|
+
headers: { accept: "application/json" },
|
|
56
|
+
});
|
|
57
|
+
if (!res.ok) {
|
|
58
|
+
throw new Error(`${ORIGIN} answered ${res.status}`);
|
|
59
|
+
}
|
|
60
|
+
const body = await res.json();
|
|
61
|
+
if (typeof body?.url !== "string") {
|
|
62
|
+
throw new Error(`${ORIGIN} did not say where its MCP server is`);
|
|
63
|
+
}
|
|
64
|
+
return body;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function confirm(question) {
|
|
68
|
+
if (!process.stdin.isTTY) return false;
|
|
69
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
70
|
+
try {
|
|
71
|
+
return /^y(es)?$/i.test((await rl.question(`${question} [y/N] `)).trim());
|
|
72
|
+
} finally {
|
|
73
|
+
rl.close();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function listClients() {
|
|
78
|
+
console.log("Say which one with:\n");
|
|
79
|
+
for (const c of CLIENTS) {
|
|
80
|
+
console.log(` ${bold(`npx @skyelight/mcp init --client ${c.id}`)} ${dim(c.label)}`);
|
|
81
|
+
}
|
|
82
|
+
console.log("");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function init({ yes, clientFlag }) {
|
|
86
|
+
console.log(`\n${bold("Skyelight MCP")}\n`);
|
|
87
|
+
|
|
88
|
+
let config;
|
|
89
|
+
try {
|
|
90
|
+
config = await fetchConfig();
|
|
91
|
+
} catch (err) {
|
|
92
|
+
console.log(amber(`Could not reach Skyelight: ${err.message}\n`));
|
|
93
|
+
console.log(
|
|
94
|
+
dim(" Point at another deployment with SKYELIGHT_URL=https://…\n"),
|
|
95
|
+
);
|
|
96
|
+
process.exitCode = 1;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const { url: MCP_URL, clientId: CLIENT_ID, callbackPort } = config;
|
|
100
|
+
|
|
101
|
+
let client;
|
|
102
|
+
if (clientFlag) {
|
|
103
|
+
client = findClient(clientFlag);
|
|
104
|
+
if (!client) {
|
|
105
|
+
console.log(amber(`Unknown agent "${clientFlag}".\n`));
|
|
106
|
+
listClients();
|
|
107
|
+
process.exitCode = 1;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
const found = detectClients();
|
|
112
|
+
if (found.length === 0) {
|
|
113
|
+
console.log(amber("No coding agent found on this machine.\n"));
|
|
114
|
+
listClients();
|
|
115
|
+
process.exitCode = 1;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (found.length > 1) {
|
|
119
|
+
console.log(
|
|
120
|
+
`Found ${found.map((c) => bold(c.label)).join(", ")}.\n`,
|
|
121
|
+
);
|
|
122
|
+
listClients();
|
|
123
|
+
process.exitCode = 1;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
client = found[0];
|
|
127
|
+
console.log(`Found ${bold(client.label)}.\n`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Claude Code registers through its own CLI — see `install.js`.
|
|
131
|
+
if (client.command) {
|
|
132
|
+
const [bin, args] = client.command(MCP_URL, CLIENT_ID, callbackPort);
|
|
133
|
+
console.log(dim(` ${bin} ${args.join(" ")}\n`));
|
|
134
|
+
if (!yes && !(await confirm("Run it?"))) {
|
|
135
|
+
console.log(dim("\nNothing changed.\n"));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const res = spawnSync(bin, args, { stdio: "inherit" });
|
|
139
|
+
if (res.status !== 0) {
|
|
140
|
+
console.log(amber("\nThat command failed. Run it yourself to see why.\n"));
|
|
141
|
+
process.exitCode = 1;
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
done(client);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const file = client.file();
|
|
149
|
+
const before = existsSync(file) ? readFileSync(file, "utf8") : "";
|
|
150
|
+
const after =
|
|
151
|
+
client.format === "json"
|
|
152
|
+
? addToJson(before, client.entry(MCP_URL, CLIENT_ID))
|
|
153
|
+
: addToToml(before, client.entry(MCP_URL, CLIENT_ID));
|
|
154
|
+
|
|
155
|
+
if (after === null) {
|
|
156
|
+
console.log(green(`Already set up in ${file}\n`));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
console.log(`${bold(file)}\n`);
|
|
161
|
+
console.log(
|
|
162
|
+
dim(
|
|
163
|
+
client.format === "json"
|
|
164
|
+
? ` mcpServers.${SERVER_NAME} → ${MCP_URL}`
|
|
165
|
+
: ` [mcp_servers.${SERVER_NAME}] → ${MCP_URL}`,
|
|
166
|
+
),
|
|
167
|
+
);
|
|
168
|
+
console.log("");
|
|
169
|
+
|
|
170
|
+
if (!yes && !(await confirm("Write it?"))) {
|
|
171
|
+
console.log(dim("\nNothing written.\n"));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
176
|
+
writeFileSync(file, after, "utf8");
|
|
177
|
+
done(client);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function done(client) {
|
|
181
|
+
console.log(green(`\n✓ Skyelight added to ${client.label}\n`));
|
|
182
|
+
console.log("Restart it, then ask it to list your Skyelight items.");
|
|
183
|
+
console.log(
|
|
184
|
+
dim("It will open a browser to sign you in the first time.\n"),
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const argv = process.argv.slice(2);
|
|
189
|
+
const cmd = argv[0];
|
|
190
|
+
|
|
191
|
+
if (cmd === "init") {
|
|
192
|
+
const yes = argv.includes("--yes") || argv.includes("-y");
|
|
193
|
+
const at = argv.indexOf("--client");
|
|
194
|
+
await init({ yes, clientFlag: at === -1 ? null : argv[at + 1] });
|
|
195
|
+
} else {
|
|
196
|
+
console.log(`\n${bold("@skyelight/mcp")}\n`);
|
|
197
|
+
console.log(" npx @skyelight/mcp init set up your coding agent");
|
|
198
|
+
console.log(" npx @skyelight/mcp init --client cursor\n");
|
|
199
|
+
console.log(dim(" Running with no command starts the stdio server.\n"));
|
|
200
|
+
}
|
package/src/config.js
CHANGED
|
@@ -92,9 +92,10 @@ export function resolveConfig({
|
|
|
92
92
|
"No Skyelight credentials found. Either:",
|
|
93
93
|
" • export SKYELIGHT_API_URL and SKYELIGHT_API_TOKEN, or",
|
|
94
94
|
" • put them in .env.local, or",
|
|
95
|
-
' • save ~/.skyelight/credentials as {"apiUrl": "...", "token": "
|
|
95
|
+
' • save ~/.skyelight/credentials as {"apiUrl": "...", "token": "sky_..."}',
|
|
96
96
|
"",
|
|
97
|
-
"Create
|
|
97
|
+
"Create one under Account settings → API Keys in the",
|
|
98
|
+
"Skyelight web app. Workspace API keys (sk_live_...) no longer work.",
|
|
98
99
|
].join("\n"),
|
|
99
100
|
);
|
|
100
101
|
}
|
package/src/index.js
CHANGED
|
@@ -1,8 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Entry point
|
|
3
|
+
* Entry point, wearing two hats.
|
|
4
|
+
*
|
|
5
|
+
* With no arguments this is the stdio MCP server, which is what a client
|
|
6
|
+
* spawns. With `init` it is the installer that registers the *remote* server
|
|
7
|
+
* with a coding agent — see `cli.js`.
|
|
8
|
+
*
|
|
9
|
+
* One binary rather than two, because npm resolves `npx @skyelight/mcp` to
|
|
10
|
+
* the package's only bin whatever it is called; a second bin would make that
|
|
11
|
+
* resolution ambiguous and `npx @skyelight/mcp init` would stop working.
|
|
12
|
+
* Dispatching here keeps the one-command install one command.
|
|
4
13
|
*/
|
|
5
14
|
|
|
15
|
+
// The static import below is hoisted, so `config.js` is evaluated either
|
|
16
|
+
// way — but `resolveConfig()` is only *called* past this point, and calling
|
|
17
|
+
// it is what throws when there are no credentials. The installer needs none.
|
|
18
|
+
if (process.argv[2] === "init" || process.argv[2] === "--help") {
|
|
19
|
+
await import("./cli.js");
|
|
20
|
+
process.exit(process.exitCode ?? 0);
|
|
21
|
+
}
|
|
22
|
+
|
|
6
23
|
import { resolveConfig, ConfigError } from "./config.js";
|
|
7
24
|
import { createClient } from "./client.js";
|
|
8
25
|
import { createServer, serveStdio } from "./server.js";
|
package/src/install.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where each agent keeps its MCP servers, and what to write there.
|
|
3
|
+
*
|
|
4
|
+
* Pure functions, so the CLI is the part that touches the disk and this is
|
|
5
|
+
* the part that can be tested. Every entry answers three questions: how do
|
|
6
|
+
* we know this agent is on the machine, what file holds its servers, and
|
|
7
|
+
* what shape does an entry take in it.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { existsSync } from "node:fs";
|
|
13
|
+
|
|
14
|
+
/** The name the server is registered under, in every client. */
|
|
15
|
+
export const SERVER_NAME = "skyelight";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The loopback port every client is told to listen on.
|
|
19
|
+
*
|
|
20
|
+
* Fixed rather than random because the redirect URI is registered with Clerk
|
|
21
|
+
* in advance, and only a stable one can be. Loopback, so it only ever
|
|
22
|
+
* resolves on the machine running the client.
|
|
23
|
+
*/
|
|
24
|
+
export const CALLBACK_PORT = 54545;
|
|
25
|
+
|
|
26
|
+
const home = () => homedir();
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The agents this can configure.
|
|
30
|
+
*
|
|
31
|
+
* Ordered by how likely a given machine is to have one, because the first
|
|
32
|
+
* match wins when nothing is named. That is a guess, and `--client` exists
|
|
33
|
+
* for when it guesses wrong.
|
|
34
|
+
*/
|
|
35
|
+
export const CLIENTS = [
|
|
36
|
+
{
|
|
37
|
+
id: "claude",
|
|
38
|
+
label: "Claude Code",
|
|
39
|
+
/**
|
|
40
|
+
* Registered by its own CLI rather than by editing a file.
|
|
41
|
+
*
|
|
42
|
+
* `claude mcp add` knows where the config lives on this machine and what
|
|
43
|
+
* the current schema is; writing the file ourselves would be a guess at
|
|
44
|
+
* both, and a wrong guess silently produces an agent with no tools.
|
|
45
|
+
*/
|
|
46
|
+
command: (url, clientId, port = CALLBACK_PORT) => [
|
|
47
|
+
"claude",
|
|
48
|
+
[
|
|
49
|
+
"mcp", "add", "-s", "user", "--transport", "http", SERVER_NAME,
|
|
50
|
+
// Only when the deployment named a client. Without them Claude Code
|
|
51
|
+
// registers itself dynamically, which is the right thing to do and
|
|
52
|
+
// the wrong thing to prevent by passing an empty id.
|
|
53
|
+
...(clientId
|
|
54
|
+
? ["--client-id", clientId, "--callback-port", String(port)]
|
|
55
|
+
: []),
|
|
56
|
+
url,
|
|
57
|
+
],
|
|
58
|
+
],
|
|
59
|
+
detect: () => hasBinary("claude"),
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "cursor",
|
|
63
|
+
label: "Cursor",
|
|
64
|
+
file: () => join(home(), ".cursor", "mcp.json"),
|
|
65
|
+
format: "json",
|
|
66
|
+
entry: (url) => ({ url }),
|
|
67
|
+
detect: () => existsSync(join(home(), ".cursor")),
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: "windsurf",
|
|
71
|
+
label: "Windsurf",
|
|
72
|
+
file: () => join(home(), ".codeium", "windsurf", "mcp_config.json"),
|
|
73
|
+
format: "json",
|
|
74
|
+
entry: (url) => ({ serverUrl: url }),
|
|
75
|
+
detect: () => existsSync(join(home(), ".codeium", "windsurf")),
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: "codex",
|
|
79
|
+
label: "Codex",
|
|
80
|
+
file: () => join(home(), ".codex", "config.toml"),
|
|
81
|
+
format: "toml",
|
|
82
|
+
entry: (url, clientId, port = CALLBACK_PORT) =>
|
|
83
|
+
[
|
|
84
|
+
`[mcp_servers.${SERVER_NAME}]`,
|
|
85
|
+
`url = "${url}"`,
|
|
86
|
+
'auth = "oauth"',
|
|
87
|
+
// The oauth table only when there is a client to name.
|
|
88
|
+
...(clientId
|
|
89
|
+
? [
|
|
90
|
+
"",
|
|
91
|
+
`[mcp_servers.${SERVER_NAME}.oauth]`,
|
|
92
|
+
`client_id = "${clientId}"`,
|
|
93
|
+
`callback_url = "http://127.0.0.1:${port}/callback"`,
|
|
94
|
+
]
|
|
95
|
+
: []),
|
|
96
|
+
].join("\n"),
|
|
97
|
+
detect: () => existsSync(join(home(), ".codex")),
|
|
98
|
+
},
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
/** On PATH, without running the thing. */
|
|
102
|
+
function hasBinary(name) {
|
|
103
|
+
const paths = (process.env.PATH ?? "").split(":").filter(Boolean);
|
|
104
|
+
return paths.some((p) => existsSync(join(p, name)));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Which agents are on this machine.
|
|
109
|
+
*
|
|
110
|
+
* All of them, not the first: somebody with Cursor and Claude Code installed
|
|
111
|
+
* should be told both were found rather than have one picked for them.
|
|
112
|
+
*/
|
|
113
|
+
export function detectClients(clients = CLIENTS) {
|
|
114
|
+
return clients.filter((c) => {
|
|
115
|
+
try {
|
|
116
|
+
return c.detect();
|
|
117
|
+
} catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function findClient(id, clients = CLIENTS) {
|
|
124
|
+
return clients.find((c) => c.id === id) ?? null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Add the server to a JSON config, preserving whatever else is in it.
|
|
129
|
+
*
|
|
130
|
+
* Returns the new text, or null when the server is already there and points
|
|
131
|
+
* at the same place — nothing to write is a better answer than an identical
|
|
132
|
+
* rewrite that churns the file's mtime.
|
|
133
|
+
*/
|
|
134
|
+
export function addToJson(existing, entry) {
|
|
135
|
+
const parsed = existing.trim() ? JSON.parse(existing) : {};
|
|
136
|
+
const servers = parsed.mcpServers ?? {};
|
|
137
|
+
const current = servers[SERVER_NAME];
|
|
138
|
+
if (current && JSON.stringify(current) === JSON.stringify(entry)) return null;
|
|
139
|
+
return `${JSON.stringify(
|
|
140
|
+
{ ...parsed, mcpServers: { ...servers, [SERVER_NAME]: entry } },
|
|
141
|
+
null,
|
|
142
|
+
2,
|
|
143
|
+
)}\n`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Append a TOML block, replacing an existing one for the same server.
|
|
148
|
+
*
|
|
149
|
+
* A parser would be the right tool for arbitrary TOML. This only ever writes
|
|
150
|
+
* two tables it wrote itself, so it finds them by their headers and swaps
|
|
151
|
+
* them; anything it does not recognise it leaves alone and appends after.
|
|
152
|
+
*/
|
|
153
|
+
export function addToToml(existing, block) {
|
|
154
|
+
const header = `[mcp_servers.${SERVER_NAME}]`;
|
|
155
|
+
if (!existing.includes(header)) {
|
|
156
|
+
const sep = existing.trim() ? "\n\n" : "";
|
|
157
|
+
return `${existing.trimEnd()}${sep}${block}\n`;
|
|
158
|
+
}
|
|
159
|
+
if (existing.includes(block)) return null;
|
|
160
|
+
|
|
161
|
+
const lines = existing.split("\n");
|
|
162
|
+
const start = lines.findIndex((l) => l.trim() === header);
|
|
163
|
+
// Ours runs to the next table that is not one of ours.
|
|
164
|
+
let end = start + 1;
|
|
165
|
+
while (end < lines.length) {
|
|
166
|
+
const t = lines[end].trim();
|
|
167
|
+
if (t.startsWith("[") && !t.startsWith(`[mcp_servers.${SERVER_NAME}`)) break;
|
|
168
|
+
end++;
|
|
169
|
+
}
|
|
170
|
+
return `${[...lines.slice(0, start), block, ...lines.slice(end)]
|
|
171
|
+
.join("\n")
|
|
172
|
+
.trimEnd()}\n`;
|
|
173
|
+
}
|
package/src/tools.js
CHANGED
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
const PROJECT_HINT =
|
|
11
|
-
'No project. Pass projectId, add .skyelight.json with {"projectId": "..."}
|
|
12
|
-
"or use a project-bound API key.";
|
|
11
|
+
'No project. Pass projectId, or add .skyelight.json with {"projectId": "..."}.';
|
|
13
12
|
|
|
14
13
|
/**
|
|
15
14
|
* @param opts.uiResourceUri When set, `get_item` advertises a `ui://`
|
|
@@ -337,13 +336,11 @@ export function renderItem(item) {
|
|
|
337
336
|
// The repo, when the project names one. Stated plainly and early: it is
|
|
338
337
|
// the difference between an agent knowing where to work and inferring it
|
|
339
338
|
// from a URL, which is how work lands in the wrong codebase.
|
|
339
|
+
// The repository and nothing else. Which branch to work on is the reader's
|
|
340
|
+
// own checkout to answer; which branch the page came from is on the source
|
|
341
|
+
// line below, and that is the half an agent cannot work out for itself.
|
|
340
342
|
const repo = item.project && item.project.repo;
|
|
341
|
-
if (repo) {
|
|
342
|
-
const where = [repo.url];
|
|
343
|
-
if (repo.branch) where.push(`branch ${repo.branch}`);
|
|
344
|
-
if (repo.pathPrefix) where.push(`under ${repo.pathPrefix}`);
|
|
345
|
-
lines.push(`Code: ${where.join(", ")}`);
|
|
346
|
-
}
|
|
343
|
+
if (repo) lines.push(`Code: ${repo.url}`);
|
|
347
344
|
if (item.assignee?.name) {
|
|
348
345
|
// The mode is the ask. An agent that reads "investigate" and opens a
|
|
349
346
|
// pull request has done the wrong job well.
|