@skyelight/mcp 0.4.1 → 0.5.1
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/CHANGELOG.md +78 -0
- package/README.md +64 -48
- package/package.json +3 -11
- package/src/brand.d.ts +9 -0
- package/src/brand.js +64 -0
- package/src/cli.js +355 -82
- package/src/client.js +9 -0
- package/src/config.js +5 -7
- package/src/index.js +11 -1
- package/src/insights.d.ts +15 -0
- package/src/insights.js +688 -0
- package/src/install.js +59 -2
- package/src/server.js +11 -2
- package/src/theme.js +120 -0
- package/src/tools.d.ts +10 -0
- package/src/tools.js +236 -14
- package/src/version.js +1 -1
package/src/cli.js
CHANGED
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
* It shows the change and waits, like `@skyelight/build init` does. Editing
|
|
12
12
|
* a config somebody else wrote is the kind of help that has to ask first.
|
|
13
13
|
* `--yes` exists for people who have already read it once.
|
|
14
|
+
*
|
|
15
|
+
* Several agents used to be a dead end: it printed the list and exited 1, as
|
|
16
|
+
* though having two editors were an error. Most developers have more than
|
|
17
|
+
* one, so the commonest case on the first command anybody runs was a refusal.
|
|
18
|
+
* It asks now, and `--client` still answers in advance for a script.
|
|
14
19
|
*/
|
|
15
20
|
|
|
16
21
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
@@ -24,18 +29,26 @@ import {
|
|
|
24
29
|
findClient,
|
|
25
30
|
addToJson,
|
|
26
31
|
addToToml,
|
|
32
|
+
removeFromJson,
|
|
33
|
+
removeFromToml,
|
|
27
34
|
} from "./install.js";
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
import {
|
|
36
|
+
bold,
|
|
37
|
+
dim,
|
|
38
|
+
green,
|
|
39
|
+
amber,
|
|
40
|
+
brand,
|
|
41
|
+
header,
|
|
42
|
+
heading,
|
|
43
|
+
pad,
|
|
44
|
+
MARK,
|
|
45
|
+
} from "./theme.js";
|
|
33
46
|
|
|
34
47
|
/**
|
|
35
48
|
* Which deployment to register. Production unless told otherwise.
|
|
36
49
|
*/
|
|
37
50
|
const ORIGIN = (
|
|
38
|
-
process.env.SKYELIGHT_URL ?? "https://
|
|
51
|
+
process.env.SKYELIGHT_URL ?? "https://app.skyelight.ai"
|
|
39
52
|
).replace(/\/+$/, "");
|
|
40
53
|
|
|
41
54
|
/**
|
|
@@ -64,85 +77,102 @@ async function fetchConfig() {
|
|
|
64
77
|
return body;
|
|
65
78
|
}
|
|
66
79
|
|
|
67
|
-
|
|
68
|
-
|
|
80
|
+
/** The host being configured, for the header. Not the whole URL — it is the */
|
|
81
|
+
/** part that tells you whether you are pointed somewhere unexpected. */
|
|
82
|
+
function originLabel() {
|
|
83
|
+
try {
|
|
84
|
+
return new URL(ORIGIN).host;
|
|
85
|
+
} catch {
|
|
86
|
+
return ORIGIN;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function ask(question) {
|
|
69
91
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
70
92
|
try {
|
|
71
|
-
return
|
|
93
|
+
return (await rl.question(` ${question} ${brand(MARK.arrow)} `)).trim();
|
|
72
94
|
} finally {
|
|
73
95
|
rl.close();
|
|
74
96
|
}
|
|
75
97
|
}
|
|
76
98
|
|
|
77
|
-
function
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
99
|
+
async function confirm(question) {
|
|
100
|
+
// No terminal, no question. A CI run should not hang on a prompt nobody is
|
|
101
|
+
// there to answer, and `--yes` is how it says so deliberately.
|
|
102
|
+
if (!process.stdin.isTTY) return false;
|
|
103
|
+
return /^y(es)?$/i.test(await ask(`${question} ${dim("[y/N]")}`));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Which agent, when several are on the machine.
|
|
108
|
+
*
|
|
109
|
+
* A numbered prompt rather than a list of commands to go and retype. The
|
|
110
|
+
* commands are still printed underneath, because somebody scripting this
|
|
111
|
+
* needs them and because the prompt is unavailable without a terminal.
|
|
112
|
+
*/
|
|
113
|
+
async function choose(found) {
|
|
114
|
+
console.log(` Found ${bold(found.length)} agents.\n`);
|
|
115
|
+
found.forEach((c, i) => {
|
|
116
|
+
console.log(` ${brand(String(i + 1))} ${c.label}`);
|
|
117
|
+
});
|
|
118
|
+
console.log(` ${brand("a")} ${dim("All of them")}\n`);
|
|
119
|
+
|
|
120
|
+
if (!process.stdin.isTTY) {
|
|
121
|
+
console.log(dim(" Not a terminal, so nothing was chosen. Name one:\n"));
|
|
122
|
+
listClients();
|
|
123
|
+
return null;
|
|
81
124
|
}
|
|
82
|
-
|
|
125
|
+
|
|
126
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
127
|
+
const answer = (
|
|
128
|
+
await ask(`Which one? ${dim(`[1-${found.length}, a]`)}`)
|
|
129
|
+
).toLowerCase();
|
|
130
|
+
if (answer === "a" || answer === "all") return found;
|
|
131
|
+
const n = Number(answer);
|
|
132
|
+
if (Number.isInteger(n) && n >= 1 && n <= found.length) {
|
|
133
|
+
return [found[n - 1]];
|
|
134
|
+
}
|
|
135
|
+
console.log(dim(` Not one of them.\n`));
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
83
138
|
}
|
|
84
139
|
|
|
85
|
-
|
|
86
|
-
|
|
140
|
+
/** The widest agent id, so the labels line up whatever is in the registry. */
|
|
141
|
+
const ID_WIDTH = Math.max(...CLIENTS.map((c) => c.id.length));
|
|
87
142
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
config = await fetchConfig();
|
|
91
|
-
} catch (err) {
|
|
92
|
-
console.log(amber(`Could not reach Skyelight: ${err.message}\n`));
|
|
143
|
+
function listClients() {
|
|
144
|
+
for (const c of CLIENTS) {
|
|
93
145
|
console.log(
|
|
94
|
-
dim("
|
|
146
|
+
` ${dim("--client")} ${bold(pad(c.id, ID_WIDTH))} ${dim(c.label)}`,
|
|
95
147
|
);
|
|
96
|
-
process.exitCode = 1;
|
|
97
|
-
return;
|
|
98
148
|
}
|
|
99
|
-
|
|
149
|
+
console.log("");
|
|
150
|
+
}
|
|
100
151
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
}
|
|
152
|
+
/**
|
|
153
|
+
* Register one agent. Returns whether anything changed, so the summary at
|
|
154
|
+
* the end can tell "added" from "already there" without guessing.
|
|
155
|
+
*/
|
|
156
|
+
async function register(client, config, { yes, quiet }) {
|
|
157
|
+
const { url: MCP_URL, clientId: CLIENT_ID, callbackPort } = config;
|
|
129
158
|
|
|
130
159
|
// Claude Code registers through its own CLI — see `install.js`.
|
|
131
160
|
if (client.command) {
|
|
132
161
|
const [bin, args] = client.command(MCP_URL, CLIENT_ID, callbackPort);
|
|
133
|
-
console.log(
|
|
162
|
+
console.log(` ${bold(client.label)}`);
|
|
163
|
+
console.log(dim(` ${bin} ${args.join(" ")}\n`));
|
|
134
164
|
if (!yes && !(await confirm("Run it?"))) {
|
|
135
|
-
console.log(dim("\
|
|
136
|
-
return;
|
|
165
|
+
console.log(dim("\n Nothing changed.\n"));
|
|
166
|
+
return "skipped";
|
|
137
167
|
}
|
|
138
|
-
const res = spawnSync(bin, args, { stdio: "inherit" });
|
|
168
|
+
const res = spawnSync(bin, args, { stdio: quiet ? "ignore" : "inherit" });
|
|
139
169
|
if (res.status !== 0) {
|
|
140
|
-
console.log(
|
|
141
|
-
|
|
142
|
-
|
|
170
|
+
console.log(
|
|
171
|
+
amber(`\n That command failed. Run it yourself to see why.\n`),
|
|
172
|
+
);
|
|
173
|
+
return "failed";
|
|
143
174
|
}
|
|
144
|
-
|
|
145
|
-
return;
|
|
175
|
+
return "added";
|
|
146
176
|
}
|
|
147
177
|
|
|
148
178
|
const file = client.file();
|
|
@@ -153,48 +183,291 @@ async function init({ yes, clientFlag }) {
|
|
|
153
183
|
: addToToml(before, client.entry(MCP_URL, CLIENT_ID));
|
|
154
184
|
|
|
155
185
|
if (after === null) {
|
|
156
|
-
console.log(
|
|
157
|
-
|
|
186
|
+
console.log(
|
|
187
|
+
` ${green(MARK.tick)} ${client.label} ${dim("— already set up")}`,
|
|
188
|
+
);
|
|
189
|
+
return "already";
|
|
158
190
|
}
|
|
159
191
|
|
|
160
|
-
console.log(
|
|
192
|
+
console.log(` ${bold(client.label)}`);
|
|
193
|
+
console.log(dim(` ${file}`));
|
|
161
194
|
console.log(
|
|
162
195
|
dim(
|
|
163
196
|
client.format === "json"
|
|
164
|
-
? `
|
|
165
|
-
: `
|
|
197
|
+
? ` mcpServers.${SERVER_NAME} → ${MCP_URL}\n`
|
|
198
|
+
: ` [mcp_servers.${SERVER_NAME}] → ${MCP_URL}\n`,
|
|
166
199
|
),
|
|
167
200
|
);
|
|
168
|
-
console.log("");
|
|
169
201
|
|
|
170
202
|
if (!yes && !(await confirm("Write it?"))) {
|
|
171
|
-
console.log(dim("\
|
|
172
|
-
return;
|
|
203
|
+
console.log(dim("\n Nothing written.\n"));
|
|
204
|
+
return "skipped";
|
|
173
205
|
}
|
|
174
206
|
|
|
175
207
|
mkdirSync(dirname(file), { recursive: true });
|
|
176
208
|
writeFileSync(file, after, "utf8");
|
|
177
|
-
|
|
209
|
+
return "added";
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Take the server back out of one agent.
|
|
214
|
+
*
|
|
215
|
+
* Symmetrical with `register` on purpose, including the confirmation: the
|
|
216
|
+
* file belongs to whoever configured it, and a command that edits it without
|
|
217
|
+
* showing what it is about to do is the thing `init` deliberately is not.
|
|
218
|
+
*/
|
|
219
|
+
async function unregister(client, { yes, quiet }) {
|
|
220
|
+
if (client.removeCommand) {
|
|
221
|
+
const [bin, args] = client.removeCommand();
|
|
222
|
+
console.log(` ${bold(client.label)}`);
|
|
223
|
+
console.log(dim(` ${bin} ${args.join(" ")}\n`));
|
|
224
|
+
if (!yes && !(await confirm("Run it?"))) {
|
|
225
|
+
console.log(dim("\n Nothing changed.\n"));
|
|
226
|
+
return "skipped";
|
|
227
|
+
}
|
|
228
|
+
const res = spawnSync(bin, args, { stdio: quiet ? "ignore" : "inherit" });
|
|
229
|
+
// A client that had no such server exits non-zero saying so, which is
|
|
230
|
+
// the answer rather than a failure — `remove` is meant to be safe to
|
|
231
|
+
// run twice.
|
|
232
|
+
return res.status === 0 ? "removed" : "absent";
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const file = client.file();
|
|
236
|
+
if (!existsSync(file)) {
|
|
237
|
+
console.log(
|
|
238
|
+
` ${dim(MARK.dot)} ${client.label} ${dim("— nothing to remove")}`,
|
|
239
|
+
);
|
|
240
|
+
return "absent";
|
|
241
|
+
}
|
|
242
|
+
const before = readFileSync(file, "utf8");
|
|
243
|
+
const after =
|
|
244
|
+
client.format === "json" ? removeFromJson(before) : removeFromToml(before);
|
|
245
|
+
|
|
246
|
+
if (after === null) {
|
|
247
|
+
console.log(
|
|
248
|
+
` ${dim(MARK.dot)} ${client.label} ${dim("— nothing to remove")}`,
|
|
249
|
+
);
|
|
250
|
+
return "absent";
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
console.log(` ${bold(client.label)}`);
|
|
254
|
+
console.log(dim(` ${file}`));
|
|
255
|
+
console.log(
|
|
256
|
+
dim(
|
|
257
|
+
client.format === "json"
|
|
258
|
+
? ` mcpServers.${SERVER_NAME} ${MARK.arrow} removed\n`
|
|
259
|
+
: ` [mcp_servers.${SERVER_NAME}] ${MARK.arrow} removed\n`,
|
|
260
|
+
),
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
if (!yes && !(await confirm("Write it?"))) {
|
|
264
|
+
console.log(dim("\n Nothing written.\n"));
|
|
265
|
+
return "skipped";
|
|
266
|
+
}
|
|
267
|
+
writeFileSync(file, after, "utf8");
|
|
268
|
+
return "removed";
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Which agents to act on. Shared by `init` and `remove` so the two cannot
|
|
273
|
+
* disagree about what `--client all` means or how a tie is broken.
|
|
274
|
+
*
|
|
275
|
+
* `known` is for removal: an agent whose config directory has since been
|
|
276
|
+
* deleted still fails detection, and somebody uninstalling wants every place
|
|
277
|
+
* we ever wrote considered rather than only the ones still standing.
|
|
278
|
+
*/
|
|
279
|
+
async function pickTargets({ clientFlag, known = false }) {
|
|
280
|
+
if (clientFlag === "all") {
|
|
281
|
+
const all = known ? CLIENTS : detectClients();
|
|
282
|
+
if (all.length === 0) {
|
|
283
|
+
console.log(amber(" No coding agent found on this machine.\n"));
|
|
284
|
+
listClients();
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
return all;
|
|
288
|
+
}
|
|
289
|
+
if (clientFlag) {
|
|
290
|
+
const one = findClient(clientFlag);
|
|
291
|
+
if (!one) {
|
|
292
|
+
console.log(amber(` Unknown agent "${clientFlag}".\n`));
|
|
293
|
+
listClients();
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
return [one];
|
|
297
|
+
}
|
|
298
|
+
const found = detectClients();
|
|
299
|
+
if (found.length === 0) {
|
|
300
|
+
console.log(amber(" No coding agent found on this machine.\n"));
|
|
301
|
+
listClients();
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
if (found.length === 1) {
|
|
305
|
+
console.log(` Found ${bold(found[0].label)}.\n`);
|
|
306
|
+
return found;
|
|
307
|
+
}
|
|
308
|
+
const chosen = await choose(found);
|
|
309
|
+
if (chosen) console.log("");
|
|
310
|
+
return chosen;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function remove({ yes, clientFlag }) {
|
|
314
|
+
console.log(header());
|
|
315
|
+
|
|
316
|
+
// Default to every agent we know, not every agent detected. "Uninstall
|
|
317
|
+
// Skyelight" means everywhere, and asking which one to forget is a
|
|
318
|
+
// question with no useful wrong answer.
|
|
319
|
+
const targets = await pickTargets({
|
|
320
|
+
clientFlag: clientFlag ?? "all",
|
|
321
|
+
known: true,
|
|
322
|
+
});
|
|
323
|
+
if (!targets) {
|
|
324
|
+
process.exitCode = 1;
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const results = [];
|
|
329
|
+
for (const client of targets) {
|
|
330
|
+
results.push({
|
|
331
|
+
client,
|
|
332
|
+
outcome: await unregister(client, { yes, quiet: targets.length > 1 }),
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const removed = results.filter((r) => r.outcome === "removed");
|
|
337
|
+
const skipped = results.filter((r) => r.outcome === "skipped");
|
|
338
|
+
if (removed.length === 0) {
|
|
339
|
+
// "Not registered anywhere" and "you declined every prompt" are
|
|
340
|
+
// different facts, and saying the first when the second happened tells
|
|
341
|
+
// somebody their agent is clean when it is not.
|
|
342
|
+
console.log(
|
|
343
|
+
skipped.length
|
|
344
|
+
? `\n ${dim("Nothing was removed.")}\n`
|
|
345
|
+
: `\n ${dim("Skyelight was not registered anywhere.")}\n`,
|
|
346
|
+
);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
console.log("");
|
|
350
|
+
for (const r of removed) {
|
|
351
|
+
console.log(
|
|
352
|
+
` ${green(MARK.tick)} ${bold(r.client.label)} ${dim("— removed")}`,
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
console.log(
|
|
356
|
+
`\n${heading("Note")}\n ${dim(MARK.dot)} Restart the agent for it to notice.`,
|
|
357
|
+
);
|
|
358
|
+
console.log(
|
|
359
|
+
` ${dim(MARK.dot)} Your sign-in is still valid; revoke it under Account ${MARK.arrow} API Keys\n or in the agent's own OAuth settings.\n`,
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function init({ yes, clientFlag }) {
|
|
364
|
+
console.log(header(originLabel()));
|
|
365
|
+
|
|
366
|
+
let config;
|
|
367
|
+
try {
|
|
368
|
+
config = await fetchConfig();
|
|
369
|
+
} catch (err) {
|
|
370
|
+
console.log(amber(` Could not reach Skyelight: ${err.message}\n`));
|
|
371
|
+
console.log(
|
|
372
|
+
dim(" Point at another deployment with SKYELIGHT_URL=https://…\n"),
|
|
373
|
+
);
|
|
374
|
+
process.exitCode = 1;
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const targets = await pickTargets({ clientFlag });
|
|
379
|
+
if (!targets) {
|
|
380
|
+
process.exitCode = 1;
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const results = [];
|
|
385
|
+
for (const client of targets) {
|
|
386
|
+
const outcome = await register(client, config, {
|
|
387
|
+
yes,
|
|
388
|
+
quiet: targets.length > 1,
|
|
389
|
+
});
|
|
390
|
+
results.push({ client, outcome });
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
done(results);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function done(results) {
|
|
397
|
+
const added = results.filter((r) => r.outcome === "added");
|
|
398
|
+
const already = results.filter((r) => r.outcome === "already");
|
|
399
|
+
const failed = results.filter((r) => r.outcome === "failed");
|
|
400
|
+
|
|
401
|
+
if (added.length === 0 && already.length === 0) {
|
|
402
|
+
if (failed.length) process.exitCode = 1;
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
console.log("");
|
|
407
|
+
for (const r of [...added, ...already]) {
|
|
408
|
+
console.log(` ${green(MARK.tick)} ${bold(r.client.label)}`);
|
|
409
|
+
}
|
|
410
|
+
for (const r of failed) {
|
|
411
|
+
console.log(
|
|
412
|
+
` ${amber(MARK.bullet)} ${bold(r.client.label)} ${dim("— not changed")}`,
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
console.log(`\n${heading("Next")}`);
|
|
417
|
+
console.log(
|
|
418
|
+
` ${dim(MARK.dot)} Restart ${added.length + already.length > 1 ? "them" : (added[0] ?? already[0]).client.label}, then ask for your Skyelight items.`,
|
|
419
|
+
);
|
|
420
|
+
console.log(
|
|
421
|
+
` ${dim(MARK.dot)} A browser opens to sign you in the first time.\n`,
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
if (failed.length) process.exitCode = 1;
|
|
178
425
|
}
|
|
179
426
|
|
|
180
|
-
function
|
|
181
|
-
console.log(
|
|
182
|
-
console.log("Restart it, then ask it to list your Skyelight items.");
|
|
427
|
+
function help() {
|
|
428
|
+
console.log(header());
|
|
183
429
|
console.log(
|
|
184
|
-
dim("
|
|
430
|
+
` ${dim("MCP server — your feedback, in your coding agent.")}\n`,
|
|
185
431
|
);
|
|
432
|
+
console.log(heading("Usage"));
|
|
433
|
+
const usage = [
|
|
434
|
+
["npx @skyelight/mcp init", "set up your coding agent"],
|
|
435
|
+
["npx @skyelight/mcp init --client <id>", "skip the question"],
|
|
436
|
+
["npx @skyelight/mcp init --client all", "every agent on this machine"],
|
|
437
|
+
["npx @skyelight/mcp init --yes", "don't ask before writing"],
|
|
438
|
+
["npx @skyelight/mcp remove", "take it out of every agent"],
|
|
439
|
+
];
|
|
440
|
+
const w = Math.max(...usage.map(([u]) => u.length));
|
|
441
|
+
for (const [u, what] of usage) {
|
|
442
|
+
console.log(` ${bold(pad(u, w))} ${dim(what)}`);
|
|
443
|
+
}
|
|
444
|
+
console.log("");
|
|
445
|
+
console.log(heading("Agents"));
|
|
446
|
+
listClients();
|
|
447
|
+
console.log(heading("Environment"));
|
|
448
|
+
const env = [
|
|
449
|
+
["SKYELIGHT_URL", `which deployment to register (${originLabel()})`],
|
|
450
|
+
["NO_COLOR", "plain output"],
|
|
451
|
+
];
|
|
452
|
+
const ew = Math.max(...env.map(([n]) => n.length));
|
|
453
|
+
for (const [n, what] of env) {
|
|
454
|
+
console.log(` ${bold(pad(n, ew))} ${dim(what)}`);
|
|
455
|
+
}
|
|
456
|
+
console.log("");
|
|
457
|
+
console.log(dim(" Running with no command starts the stdio server.\n"));
|
|
186
458
|
}
|
|
187
459
|
|
|
188
460
|
const argv = process.argv.slice(2);
|
|
189
461
|
const cmd = argv[0];
|
|
190
462
|
|
|
463
|
+
const yes = argv.includes("--yes") || argv.includes("-y");
|
|
464
|
+
const at = argv.indexOf("--client");
|
|
465
|
+
const clientFlag = at === -1 ? null : argv[at + 1];
|
|
466
|
+
|
|
191
467
|
if (cmd === "init") {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
await
|
|
468
|
+
await init({ yes, clientFlag });
|
|
469
|
+
} else if (cmd === "remove" || cmd === "uninstall") {
|
|
470
|
+
await remove({ yes, clientFlag });
|
|
195
471
|
} else {
|
|
196
|
-
|
|
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"));
|
|
472
|
+
help();
|
|
200
473
|
}
|
package/src/client.js
CHANGED
|
@@ -101,5 +101,14 @@ export function createClient({ apiUrl, token, fetchImpl = fetch }) {
|
|
|
101
101
|
postUpdate: (body) => post("/api/v1/items/update", body),
|
|
102
102
|
createItem: (body) => post("/api/v1/items/create", body),
|
|
103
103
|
setStatus: (body) => post("/api/v1/items/status", body),
|
|
104
|
+
listMembers: (params) => request("/api/v1/members", params),
|
|
105
|
+
assign: (body) => post("/api/v1/items/assign", body),
|
|
106
|
+
projectReview: (params) => request("/api/v1/review", params),
|
|
107
|
+
whatsNew: (params) => request("/api/v1/whats-new", params),
|
|
108
|
+
findBySource: (params) => request("/api/v1/source", params),
|
|
109
|
+
findSimilar: (body) => post("/api/v1/similar", body),
|
|
110
|
+
mergeItems: (body) => post("/api/v1/items/merge", body),
|
|
111
|
+
decisionLog: (params) => request("/api/v1/decisions", params),
|
|
112
|
+
saveRule: (body) => post("/api/v1/rules", body),
|
|
104
113
|
};
|
|
105
114
|
}
|
package/src/config.js
CHANGED
|
@@ -11,11 +11,9 @@
|
|
|
11
11
|
*
|
|
12
12
|
* The project binding is separate and comes from `.skyelight.json` in the
|
|
13
13
|
* working directory, so a repo can declare which Skyelight project it is
|
|
14
|
-
* without every developer configuring it.
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* knows its project. `.skyelight.json` is for workspace-wide keys used
|
|
18
|
-
* against several repos.
|
|
14
|
+
* without every developer configuring it. A personal token reaches every
|
|
15
|
+
* project its owner can, so the binding is what saves passing a projectId on
|
|
16
|
+
* every call.
|
|
19
17
|
*/
|
|
20
18
|
|
|
21
19
|
import { readFileSync, existsSync } from "node:fs";
|
|
@@ -105,8 +103,8 @@ export function resolveConfig({
|
|
|
105
103
|
return {
|
|
106
104
|
token,
|
|
107
105
|
apiUrl: apiUrl.replace(/\/+$/, ""),
|
|
108
|
-
// Named projectId to match the API. A
|
|
109
|
-
//
|
|
106
|
+
// Named projectId to match the API. A projectId the token's owner cannot
|
|
107
|
+
// reach is refused by the server, whatever this says.
|
|
110
108
|
projectId: project.projectId ?? null,
|
|
111
109
|
projectName: project.projectName ?? null,
|
|
112
110
|
};
|
package/src/index.js
CHANGED
|
@@ -15,7 +15,17 @@
|
|
|
15
15
|
// The static import below is hoisted, so `config.js` is evaluated either
|
|
16
16
|
// way — but `resolveConfig()` is only *called* past this point, and calling
|
|
17
17
|
// it is what throws when there are no credentials. The installer needs none.
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Every word that is a command rather than "be the server".
|
|
20
|
+
*
|
|
21
|
+
* Listed rather than tested for "not empty", because an unknown word has to
|
|
22
|
+
* reach the server path: that is where a client passes flags we have never
|
|
23
|
+
* heard of, and turning those into a help screen would break the client
|
|
24
|
+
* rather than the typo.
|
|
25
|
+
*/
|
|
26
|
+
const COMMANDS = new Set(["init", "remove", "uninstall", "--help", "-h"]);
|
|
27
|
+
|
|
28
|
+
if (COMMANDS.has(process.argv[2])) {
|
|
19
29
|
await import("./cli.js");
|
|
20
30
|
process.exit(process.exitCode ?? 0);
|
|
21
31
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Types for insights.js; see tools.d.ts for why the package ships these. */
|
|
2
|
+
|
|
3
|
+
import type { ToolDefinition } from "./tools.js";
|
|
4
|
+
|
|
5
|
+
export declare function insightToolDefinitions(): ToolDefinition[];
|
|
6
|
+
export declare const INSIGHT_TOOLS: Set<string>;
|
|
7
|
+
export declare function renderInsight(
|
|
8
|
+
name: string,
|
|
9
|
+
data: unknown,
|
|
10
|
+
): string | null;
|
|
11
|
+
export declare function callInsightTool(
|
|
12
|
+
name: string,
|
|
13
|
+
args: Record<string, any>,
|
|
14
|
+
opts: { client: any; projectId?: string },
|
|
15
|
+
): Promise<unknown> | undefined;
|