@monster-orange/den 0.4.5 → 0.4.6
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 +74 -0
- package/README.md +21 -7
- package/docs/distributions.md +97 -0
- package/package.json +4 -3
- package/skills/install-den/SKILL.md +114 -0
- package/src/shared/agent-setup.ts +9 -0
- package/src/shared/constants.ts +7 -0
- package/src/shared/dev-metrics/emit.ts +28 -1
- package/src/shared/port-pick.ts +60 -0
- package/src/ui/cli/create-main.ts +1 -1
- package/src/ui/cli/create.ts +263 -30
- package/src/ui/cli/doctor-command.ts +67 -4
- package/src/ui/cli/doctor-onboard.ts +291 -0
- package/src/ui/cli/flag-parser.ts +4 -0
- package/src/ui/cli/index.ts +246 -4
- package/src/ui/cli/json-out.ts +49 -0
- package/src/ui/cli/mcp-connect.ts +179 -15
- package/src/ui/cli/repo-command.ts +490 -0
- package/src/ui/cli/repo-detect.ts +192 -0
- package/src/ui/cli/web-command.ts +1 -0
- package/src/ui/mcp/connect.ts +134 -0
- package/src/ui/server/public/settings-registry.js +3 -3
- package/src/ui/server/request-guard.ts +1 -1
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `--json` for setup commands an agent drives (ON-2): stdout carries exactly
|
|
3
|
+
* one JSON object, and everything a command would print for a person goes
|
|
4
|
+
* to stderr instead, so an agent can parse stdout and still show the log.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Print one JSON object on stdout (one line, newline-terminated). */
|
|
8
|
+
export function printJson(value: unknown): void {
|
|
9
|
+
process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Thrown in place of process.exit while quietStdout runs. */
|
|
13
|
+
export class ExitRequested extends Error {
|
|
14
|
+
constructor(readonly code: number) {
|
|
15
|
+
super(`exit ${code}`);
|
|
16
|
+
this.name = "ExitRequested";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Run `fn` with console.log/info and process.stdout.write sent to stderr,
|
|
22
|
+
* and process.exit turned into a thrown ExitRequested (so the caller still
|
|
23
|
+
* prints its JSON). Everything is restored afterwards.
|
|
24
|
+
*/
|
|
25
|
+
export async function quietStdout<T>(fn: () => Promise<T>): Promise<T> {
|
|
26
|
+
const log = console.log;
|
|
27
|
+
const info = console.info;
|
|
28
|
+
const write = process.stdout.write;
|
|
29
|
+
const exit = process.exit;
|
|
30
|
+
const toErr = (...a: unknown[]) => console.error(...a);
|
|
31
|
+
console.log = toErr;
|
|
32
|
+
console.info = toErr;
|
|
33
|
+
process.stdout.write = ((chunk: unknown, ...rest: unknown[]) =>
|
|
34
|
+
(process.stderr.write as (...x: unknown[]) => boolean)(
|
|
35
|
+
chunk,
|
|
36
|
+
...rest,
|
|
37
|
+
)) as typeof process.stdout.write;
|
|
38
|
+
process.exit = ((code?: number) => {
|
|
39
|
+
throw new ExitRequested(code ?? 0);
|
|
40
|
+
}) as typeof process.exit;
|
|
41
|
+
try {
|
|
42
|
+
return await fn();
|
|
43
|
+
} finally {
|
|
44
|
+
console.log = log;
|
|
45
|
+
console.info = info;
|
|
46
|
+
process.stdout.write = write;
|
|
47
|
+
process.exit = exit;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -10,11 +10,15 @@ import * as path from "path";
|
|
|
10
10
|
import chalk from "chalk";
|
|
11
11
|
import {
|
|
12
12
|
MCP_CLIENTS,
|
|
13
|
+
RUNTIME_CLIENTS,
|
|
14
|
+
clientForRuntime,
|
|
13
15
|
findClient,
|
|
14
16
|
genericSnippet,
|
|
15
17
|
jsonHasEntry,
|
|
16
18
|
mergeJsonConfig,
|
|
19
|
+
mergeTomlConfig,
|
|
17
20
|
serverSpec,
|
|
21
|
+
tomlHasEntry,
|
|
18
22
|
type ConnectAction,
|
|
19
23
|
type ConnectEnv,
|
|
20
24
|
type McpClient,
|
|
@@ -33,6 +37,7 @@ import {
|
|
|
33
37
|
type OwnServer,
|
|
34
38
|
} from "./server-identity.js";
|
|
35
39
|
import { readDenConfig, readMcpConfig } from "../../shared/den-config.js";
|
|
40
|
+
import { printJson } from "./json-out.js";
|
|
36
41
|
|
|
37
42
|
export const MCP_CONNECT_SUBCOMMANDS = [
|
|
38
43
|
"connect",
|
|
@@ -77,15 +82,28 @@ interface Flags {
|
|
|
77
82
|
project: string | null;
|
|
78
83
|
port?: number;
|
|
79
84
|
dryRun: boolean;
|
|
85
|
+
json: boolean;
|
|
86
|
+
runtime?: string;
|
|
80
87
|
}
|
|
81
88
|
|
|
82
89
|
function parseFlags(argv: string[]): Flags {
|
|
83
|
-
const f: Flags = {
|
|
90
|
+
const f: Flags = {
|
|
91
|
+
clients: [],
|
|
92
|
+
all: false,
|
|
93
|
+
project: null,
|
|
94
|
+
dryRun: false,
|
|
95
|
+
json: false,
|
|
96
|
+
};
|
|
84
97
|
for (let i = 0; i < argv.length; i++) {
|
|
85
98
|
const a = argv[i];
|
|
86
99
|
if (a === "--all" || a === "-a") f.all = true;
|
|
87
100
|
else if (a === "--dry-run" || a === "-n") f.dryRun = true;
|
|
88
|
-
else if (a === "--
|
|
101
|
+
else if (a === "--json") f.json = true;
|
|
102
|
+
else if (a === "--runtime" || a.startsWith("--runtime=")) {
|
|
103
|
+
const v = a.includes("=") ? a.slice(a.indexOf("=") + 1) : argv[++i];
|
|
104
|
+
if (!v) throw new Error("--runtime needs a value");
|
|
105
|
+
f.runtime = v;
|
|
106
|
+
} else if (a === "--project" || a === "-p") {
|
|
89
107
|
const next = argv[i + 1];
|
|
90
108
|
if (next && !next.startsWith("-") && !findClient(next)) {
|
|
91
109
|
f.project = path.resolve(next);
|
|
@@ -150,19 +168,21 @@ function apply(
|
|
|
150
168
|
a: ConnectAction,
|
|
151
169
|
remove: boolean,
|
|
152
170
|
dryRun: boolean,
|
|
153
|
-
): { ok: boolean; said: string } {
|
|
154
|
-
if (a.kind === "json") {
|
|
171
|
+
): { ok: boolean; said: string; file?: string; changed?: boolean } {
|
|
172
|
+
if (a.kind === "json" || a.kind === "toml") {
|
|
155
173
|
const before = readText(a.file);
|
|
156
|
-
const after =
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
)
|
|
162
|
-
if (!dryRun) writeConfig(a.file, after);
|
|
174
|
+
const after =
|
|
175
|
+
a.kind === "json"
|
|
176
|
+
? mergeJsonConfig(before, a.key, a.name, remove ? null : a.entry)
|
|
177
|
+
: mergeTomlConfig(before, a.name, remove ? null : a.entry);
|
|
178
|
+
const changed = after !== (before ?? "");
|
|
179
|
+
// Unchanged is not rewritten: a re-run leaves the file (and its .bak) be.
|
|
180
|
+
if (!dryRun && changed) writeConfig(a.file, after);
|
|
163
181
|
return {
|
|
164
182
|
ok: true,
|
|
165
|
-
|
|
183
|
+
file: a.file,
|
|
184
|
+
changed,
|
|
185
|
+
said: `${dryRun ? "would write" : !changed ? "already in" : remove ? "removed from" : "wrote"} ${a.file}`,
|
|
166
186
|
};
|
|
167
187
|
}
|
|
168
188
|
if (remove) {
|
|
@@ -192,8 +212,11 @@ function status(
|
|
|
192
212
|
env: ConnectEnv,
|
|
193
213
|
): string {
|
|
194
214
|
const a = c.plan(s, scope, env);
|
|
195
|
-
if (a.kind === "json") {
|
|
196
|
-
const st =
|
|
215
|
+
if (a.kind === "json" || a.kind === "toml") {
|
|
216
|
+
const st =
|
|
217
|
+
a.kind === "json"
|
|
218
|
+
? jsonHasEntry(readText(a.file), a.key, a.name, a.entry)
|
|
219
|
+
: tomlHasEntry(readText(a.file), a.name, a.entry);
|
|
197
220
|
return st === "connected"
|
|
198
221
|
? chalk.green("connected")
|
|
199
222
|
: st === "different"
|
|
@@ -314,7 +337,8 @@ function usage(): string {
|
|
|
314
337
|
` ${cli} mcp clients which agents are installed and connected`,
|
|
315
338
|
` ${cli} mcp connect [<client>...|--all] register ${cli}'s MCP server (user scope)`,
|
|
316
339
|
` [--project [dir]] in a project's config instead (default: here)`,
|
|
317
|
-
` [--port <n>] [--dry-run]`,
|
|
340
|
+
` [--port <n>] [--dry-run] [--json]`,
|
|
341
|
+
` ${cli} mcp connect --runtime <runtime> by the agent's own name: ${Object.keys(RUNTIME_CLIENTS).join(", ")}`,
|
|
318
342
|
` ${cli} mcp disconnect <client>... [--project [dir]]`,
|
|
319
343
|
` ${cli} mcp config [--port <n>] JSON for any other MCP client`,
|
|
320
344
|
``,
|
|
@@ -332,9 +356,31 @@ export async function runMcpConnect(argv: string[]): Promise<number> {
|
|
|
332
356
|
try {
|
|
333
357
|
flags = parseFlags(rest);
|
|
334
358
|
} catch (e) {
|
|
359
|
+
if (rest.includes("--json")) {
|
|
360
|
+
printJson({
|
|
361
|
+
ok: false,
|
|
362
|
+
error: (e as Error).message,
|
|
363
|
+
fix: `${product().cli} mcp connect --runtime <${Object.keys(RUNTIME_CLIENTS).join("|")}> --json`,
|
|
364
|
+
});
|
|
365
|
+
return 1;
|
|
366
|
+
}
|
|
335
367
|
console.error(chalk.red(`${(e as Error).message}\n`) + usage());
|
|
336
368
|
return 1;
|
|
337
369
|
}
|
|
370
|
+
if (flags.json && (sub === "connect" || sub === "disconnect"))
|
|
371
|
+
return runMcpConnectJson(sub, flags);
|
|
372
|
+
if (flags.runtime) {
|
|
373
|
+
const c = clientForRuntime(flags.runtime);
|
|
374
|
+
if (!c) {
|
|
375
|
+
console.error(
|
|
376
|
+
chalk.red(
|
|
377
|
+
`unknown runtime: ${flags.runtime} (${Object.keys(RUNTIME_CLIENTS).join(", ")})\n`,
|
|
378
|
+
) + usage(),
|
|
379
|
+
);
|
|
380
|
+
return 1;
|
|
381
|
+
}
|
|
382
|
+
flags.clients.push(c.id);
|
|
383
|
+
}
|
|
338
384
|
const s = spec(flags.port);
|
|
339
385
|
const scope: Scope = flags.project ? "project" : "user";
|
|
340
386
|
const env = connectEnv(
|
|
@@ -441,3 +487,121 @@ export async function runMcpConnect(argv: string[]): Promise<number> {
|
|
|
441
487
|
}
|
|
442
488
|
return failed ? 1 : 0;
|
|
443
489
|
}
|
|
490
|
+
|
|
491
|
+
/** One client's result in `mcp connect --json`. */
|
|
492
|
+
export interface ConnectJsonClient {
|
|
493
|
+
id: string;
|
|
494
|
+
title: string;
|
|
495
|
+
ok: boolean;
|
|
496
|
+
said: string;
|
|
497
|
+
file?: string;
|
|
498
|
+
/** For a config file: whether this run changed it (false on a re-run). */
|
|
499
|
+
changed?: boolean;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* `mcp connect|disconnect … --json` (ON-2): one JSON object, `{ ok, server,
|
|
504
|
+
* clients, restart, web, fix? }`. Safe to re-run: a config file that
|
|
505
|
+
* already has the entry is left as it is (changed: false), and a client's
|
|
506
|
+
* own `mcp add` is preceded by its `mcp remove`.
|
|
507
|
+
*/
|
|
508
|
+
async function runMcpConnectJson(
|
|
509
|
+
sub: "connect" | "disconnect",
|
|
510
|
+
flags: Flags,
|
|
511
|
+
): Promise<number> {
|
|
512
|
+
const cli = product().cli;
|
|
513
|
+
const fixAll = `./bin/${cli} mcp connect --runtime <${Object.keys(RUNTIME_CLIENTS).join("|")}> --json`;
|
|
514
|
+
const fail = (error: string, extra: Record<string, unknown> = {}) => {
|
|
515
|
+
printJson({ ok: false, error, ...extra });
|
|
516
|
+
return 1;
|
|
517
|
+
};
|
|
518
|
+
let s: ServerSpec;
|
|
519
|
+
try {
|
|
520
|
+
s = spec(flags.port);
|
|
521
|
+
} catch (e) {
|
|
522
|
+
return fail((e as Error).message, {
|
|
523
|
+
ask: "Run this from your Den folder (the one with bin/ in it)?",
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
const scope: Scope = flags.project ? "project" : "user";
|
|
527
|
+
const env = connectEnv(
|
|
528
|
+
flags.project ?? process.env.DEN_INVOKE_CWD ?? process.cwd(),
|
|
529
|
+
);
|
|
530
|
+
const ids = [...flags.clients];
|
|
531
|
+
if (flags.runtime) {
|
|
532
|
+
const c = clientForRuntime(flags.runtime);
|
|
533
|
+
if (!c)
|
|
534
|
+
return fail(
|
|
535
|
+
`unknown runtime "${flags.runtime}": Den connects ${Object.keys(RUNTIME_CLIENTS).join(", ")}`,
|
|
536
|
+
{
|
|
537
|
+
fix: `./bin/${cli} mcp config`,
|
|
538
|
+
ask: "Your agent is not one Den knows. Add the JSON that `mcp config` prints to its MCP settings by hand?",
|
|
539
|
+
},
|
|
540
|
+
);
|
|
541
|
+
ids.push(c.id);
|
|
542
|
+
}
|
|
543
|
+
const unknown = ids.filter((id) => !findClient(id));
|
|
544
|
+
if (unknown.length)
|
|
545
|
+
return fail(`unknown client: ${unknown.join(", ")}`, { fix: fixAll });
|
|
546
|
+
const targets = flags.all
|
|
547
|
+
? MCP_CLIENTS.filter((c) => c.detect(env) && c.scopes.includes(scope))
|
|
548
|
+
: ids.map((id) => findClient(id)!);
|
|
549
|
+
if (targets.length === 0) return fail("no client named", { fix: fixAll });
|
|
550
|
+
|
|
551
|
+
const url = webUrl(flags.port);
|
|
552
|
+
const probe = await probeWebServer(url);
|
|
553
|
+
if (sub === "connect" && probe.kind === "foreign")
|
|
554
|
+
return fail(
|
|
555
|
+
`${probe.who} answers at ${url}, not this distribution's web server: agents would read it`,
|
|
556
|
+
{ ask: pickAnotherPortHint(cli) },
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
const clients: ConnectJsonClient[] = [];
|
|
560
|
+
for (const c of targets) {
|
|
561
|
+
if (!c.scopes.includes(scope)) {
|
|
562
|
+
clients.push({
|
|
563
|
+
id: c.id,
|
|
564
|
+
title: c.title,
|
|
565
|
+
ok: false,
|
|
566
|
+
said: `no ${scope} scope`,
|
|
567
|
+
});
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
try {
|
|
571
|
+
const r = apply(
|
|
572
|
+
c.plan(s, scope, env),
|
|
573
|
+
sub === "disconnect",
|
|
574
|
+
flags.dryRun,
|
|
575
|
+
);
|
|
576
|
+
clients.push({ id: c.id, title: c.title, ...r });
|
|
577
|
+
} catch (e) {
|
|
578
|
+
clients.push({
|
|
579
|
+
id: c.id,
|
|
580
|
+
title: c.title,
|
|
581
|
+
ok: false,
|
|
582
|
+
said: (e as Error).message,
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
const ok = clients.every((c) => c.ok);
|
|
587
|
+
const failed = clients.find((c) => !c.ok);
|
|
588
|
+
printJson({
|
|
589
|
+
ok,
|
|
590
|
+
server: { name: s.name, command: s.command, args: s.args },
|
|
591
|
+
clients,
|
|
592
|
+
dryRun: flags.dryRun,
|
|
593
|
+
// Most agents load a new MCP server only when they start.
|
|
594
|
+
restart: sub === "connect" && !flags.dryRun && ok,
|
|
595
|
+
web: { url, up: probe.kind === "up" },
|
|
596
|
+
...(failed
|
|
597
|
+
? {
|
|
598
|
+
error: `${failed.title}: ${failed.said}`,
|
|
599
|
+
fix: `./bin/${cli} mcp config`,
|
|
600
|
+
ask: `Add Den's MCP server to ${failed.title} by hand, with the JSON \`mcp config\` prints?`,
|
|
601
|
+
}
|
|
602
|
+
: probe.kind !== "up" && sub === "connect"
|
|
603
|
+
? { next: [`./bin/${cli} web up --no-open`] }
|
|
604
|
+
: {}),
|
|
605
|
+
});
|
|
606
|
+
return ok ? 0 : 1;
|
|
607
|
+
}
|