@obov/mext 0.5.0 → 0.7.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/dist/approval.d.ts +33 -0
- package/dist/approval.js +36 -0
- package/dist/approval.js.map +1 -0
- package/dist/audit.d.ts +123 -0
- package/dist/audit.js +221 -0
- package/dist/audit.js.map +1 -0
- package/dist/build-cli.d.ts +1 -0
- package/dist/build-cli.js.map +1 -1
- package/dist/build.d.ts +2 -0
- package/dist/build.js +46 -5
- package/dist/build.js.map +1 -1
- package/dist/caller.d.ts +53 -0
- package/dist/caller.js +51 -0
- package/dist/caller.js.map +1 -0
- package/dist/cli/args.js +1 -2
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/build.js +26 -4
- package/dist/cli/build.js.map +1 -1
- package/dist/cli/dev-audit.d.ts +25 -0
- package/dist/cli/dev-audit.js +84 -0
- package/dist/cli/dev-audit.js.map +1 -0
- package/dist/cli/dev-lock.d.ts +3 -1
- package/dist/cli/dev-lock.js +12 -5
- package/dist/cli/dev-lock.js.map +1 -1
- package/dist/cli/dev-server.d.ts +9 -0
- package/dist/cli/dev-server.js +27 -3
- package/dist/cli/dev-server.js.map +1 -1
- package/dist/cli/dev-tap.d.ts +20 -0
- package/dist/cli/dev-tap.js +70 -0
- package/dist/cli/dev-tap.js.map +1 -0
- package/dist/cli/dev.d.ts +6 -0
- package/dist/cli/dev.js +20 -8
- package/dist/cli/dev.js.map +1 -1
- package/dist/cli/help.js +2 -0
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/inspect.d.ts +7 -0
- package/dist/cli/inspect.js +699 -0
- package/dist/cli/inspect.js.map +1 -0
- package/dist/cli/main.js +10 -0
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/media.d.ts +19 -0
- package/dist/cli/media.js +77 -0
- package/dist/cli/media.js.map +1 -0
- package/dist/cli/output.d.ts +1 -1
- package/dist/cli/output.js +3 -2
- package/dist/cli/output.js.map +1 -1
- package/dist/cli/run.js +33 -4
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/serve.d.ts +10 -0
- package/dist/cli/serve.js +114 -0
- package/dist/cli/serve.js.map +1 -0
- package/dist/cli/types.d.ts +9 -0
- package/dist/cli/types.js +77 -0
- package/dist/cli/types.js.map +1 -0
- package/dist/client/index.d.ts +11 -0
- package/dist/client/index.js +7 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/manifest.js +6 -2
- package/dist/client/manifest.js.map +1 -1
- package/dist/client/runner.js +40 -3
- package/dist/client/runner.js.map +1 -1
- package/dist/client/typed.d.ts +56 -0
- package/dist/client/typed.js +526 -0
- package/dist/client/typed.js.map +1 -0
- package/dist/client/types.d.ts +225 -0
- package/dist/client/types.js +2 -0
- package/dist/client/types.js.map +1 -0
- package/dist/command.d.ts +61 -5
- package/dist/command.js +42 -1
- package/dist/command.js.map +1 -1
- package/dist/config.d.ts +59 -10
- package/dist/config.js +67 -7
- package/dist/config.js.map +1 -1
- package/dist/content.d.ts +65 -0
- package/dist/content.js +99 -0
- package/dist/content.js.map +1 -0
- package/dist/errors.d.ts +21 -3
- package/dist/errors.js +13 -0
- package/dist/errors.js.map +1 -1
- package/dist/expose.d.ts +9 -0
- package/dist/expose.js +52 -0
- package/dist/expose.js.map +1 -0
- package/dist/index.d.ts +18 -7
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/inspect/agent.d.ts +104 -0
- package/dist/inspect/agent.js +125 -0
- package/dist/inspect/agent.js.map +1 -0
- package/dist/inspect/audit.d.ts +46 -0
- package/dist/inspect/audit.js +133 -0
- package/dist/inspect/audit.js.map +1 -0
- package/dist/inspect/compare.d.ts +12 -0
- package/dist/inspect/compare.js +31 -0
- package/dist/inspect/compare.js.map +1 -0
- package/dist/inspect/index.d.ts +17 -0
- package/dist/inspect/index.js +12 -0
- package/dist/inspect/index.js.map +1 -0
- package/dist/inspect/inspector.d.ts +168 -0
- package/dist/inspect/inspector.js +392 -0
- package/dist/inspect/inspector.js.map +1 -0
- package/dist/inspect/profiles.d.ts +95 -0
- package/dist/inspect/profiles.js +235 -0
- package/dist/inspect/profiles.js.map +1 -0
- package/dist/inspect/scenario.d.ts +126 -0
- package/dist/inspect/scenario.js +555 -0
- package/dist/inspect/scenario.js.map +1 -0
- package/dist/inspect/ui-server.d.ts +31 -0
- package/dist/inspect/ui-server.js +378 -0
- package/dist/inspect/ui-server.js.map +1 -0
- package/dist/inspect/ui.html +917 -0
- package/dist/instrumentation.d.ts +13 -2
- package/dist/instrumentation.js +11 -1
- package/dist/instrumentation.js.map +1 -1
- package/dist/invoke.d.ts +36 -5
- package/dist/invoke.js +162 -21
- package/dist/invoke.js.map +1 -1
- package/dist/manifest.d.ts +19 -3
- package/dist/manifest.js +5 -1
- package/dist/manifest.js.map +1 -1
- package/dist/mcp/constants.d.ts +7 -0
- package/dist/mcp/constants.js +7 -0
- package/dist/mcp/constants.js.map +1 -1
- package/dist/mcp/dispatch.d.ts +10 -4
- package/dist/mcp/dispatch.js +24 -7
- package/dist/mcp/dispatch.js.map +1 -1
- package/dist/mcp/help.d.ts +2 -0
- package/dist/mcp/help.js +6 -1
- package/dist/mcp/help.js.map +1 -1
- package/dist/mcp/host.d.ts +14 -0
- package/dist/mcp/host.js +48 -0
- package/dist/mcp/host.js.map +1 -0
- package/dist/mcp/http.d.ts +38 -7
- package/dist/mcp/http.js +199 -23
- package/dist/mcp/http.js.map +1 -1
- package/dist/mcp/index.d.ts +2 -1
- package/dist/mcp/index.js +2 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/server.d.ts +12 -1
- package/dist/mcp/server.js +63 -17
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/stdio.d.ts +4 -0
- package/dist/mcp/stdio.js +3 -0
- package/dist/mcp/stdio.js.map +1 -1
- package/dist/mcp/tasks.d.ts +3 -1
- package/dist/mcp/tasks.js +7 -4
- package/dist/mcp/tasks.js.map +1 -1
- package/dist/principal.d.ts +22 -0
- package/dist/principal.js +29 -0
- package/dist/principal.js.map +1 -1
- package/dist/registry.d.ts +5 -0
- package/dist/registry.js +21 -0
- package/dist/registry.js.map +1 -1
- package/dist/remote/client.d.ts +26 -1
- package/dist/remote/client.js +180 -55
- package/dist/remote/client.js.map +1 -1
- package/dist/remote/index.d.ts +1 -1
- package/dist/remote/index.js.map +1 -1
- package/dist/remote/oauth.d.ts +1 -1
- package/dist/remote/oauth.js +1 -1
- package/dist/remote/oauth.js.map +1 -1
- package/dist/route.d.ts +40 -2
- package/dist/route.js +128 -8
- package/dist/route.js.map +1 -1
- package/dist/start.js +9 -4
- package/dist/start.js.map +1 -1
- package/dist/suggest.js +1 -1
- package/dist/suggest.js.map +1 -1
- package/dist/typegen.d.ts +27 -0
- package/dist/typegen.js +62 -0
- package/dist/typegen.js.map +1 -0
- package/dist/version-constant.d.ts +5 -0
- package/dist/version-constant.js +6 -0
- package/dist/version-constant.js.map +1 -0
- package/dist/version.d.ts +1 -2
- package/dist/version.js +1 -2
- package/dist/version.js.map +1 -1
- package/package.json +12 -2
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
10
|
+
import { basename, relative, resolve } from "node:path";
|
|
11
|
+
import { pathToFileURL } from "node:url";
|
|
12
|
+
import { entryFromHelp } from "../client/runner.js";
|
|
13
|
+
import { CallError } from "../client/typed.js";
|
|
14
|
+
import { isMextError } from "../errors.js";
|
|
15
|
+
import { parentNamespace } from "../ids.js";
|
|
16
|
+
import { createInspector, InspectUsageError, outcomeOf, resolveTarget, } from "../inspect/inspector.js";
|
|
17
|
+
import { isAgentClient } from "../inspect/agent.js";
|
|
18
|
+
import { resolveAudit } from "../inspect/audit.js";
|
|
19
|
+
import { startUiServer, UI_DEFAULT_PORT } from "../inspect/ui-server.js";
|
|
20
|
+
import { findProfile, INSPECT_DIR, loadProfiles, PROFILES_FILE, ProfileFileError } from "../inspect/profiles.js";
|
|
21
|
+
import { appendStep, listErrorBody, loadScenario, runScenario, ScenarioFileError, scenarioFiles, stepFromRecord, } from "../inspect/scenario.js";
|
|
22
|
+
import { parseArgs } from "./args.js";
|
|
23
|
+
import { table } from "./help.js";
|
|
24
|
+
import { readAll } from "./io.js";
|
|
25
|
+
import { EXIT, formatError, formatJson, formatProgress, formatResult } from "./output.js";
|
|
26
|
+
export const INSPECT_PROG = "mext inspect";
|
|
27
|
+
const VALUE_FLAGS = ["url", "profiles", "as", "answers", "save", "port", "agent", "name"];
|
|
28
|
+
const BOOLEAN_FLAGS = ["json", "confirmed", "decline", "raw", "bail"];
|
|
29
|
+
/** 하위 명령별로 받는 플래그 (공통 제외) */
|
|
30
|
+
const SUBCOMMAND_FLAGS = {
|
|
31
|
+
commands: [],
|
|
32
|
+
show: [],
|
|
33
|
+
call: ["confirmed", "decline", "raw", "answers", "save"],
|
|
34
|
+
run: ["bail", "agent"],
|
|
35
|
+
profiles: [],
|
|
36
|
+
ui: ["port", "agent"],
|
|
37
|
+
"mcp-config": ["name"],
|
|
38
|
+
};
|
|
39
|
+
const COMMON_FLAGS = ["url", "profiles", "as", "json"];
|
|
40
|
+
class UsageError extends Error {
|
|
41
|
+
}
|
|
42
|
+
export async function inspectCommand(argv, io) {
|
|
43
|
+
const json = argv.includes("--json");
|
|
44
|
+
try {
|
|
45
|
+
return await run(argv, io);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (error instanceof UsageError) {
|
|
49
|
+
io.stderr.write(`error: ${error.message}\nRun "${INSPECT_PROG} --help" for usage.\n`);
|
|
50
|
+
return EXIT.usage;
|
|
51
|
+
}
|
|
52
|
+
if (error instanceof InspectUsageError || error instanceof ProfileFileError || error instanceof ScenarioFileError) {
|
|
53
|
+
io.stderr.write(`error: ${error.message}\n`);
|
|
54
|
+
return EXIT.usage;
|
|
55
|
+
}
|
|
56
|
+
if (isMextError(error, "INVALID_ARGUMENT")) {
|
|
57
|
+
const lines = formatError(error, INSPECT_PROG).trimEnd().split("\n");
|
|
58
|
+
if (lines.at(-1)?.startsWith("Run "))
|
|
59
|
+
lines[lines.length - 1] = `Run "${INSPECT_PROG} show ${error.details.command}" for the input schema.`;
|
|
60
|
+
io.stderr.write(`${lines.join("\n")}\n`);
|
|
61
|
+
return EXIT.usage;
|
|
62
|
+
}
|
|
63
|
+
const body = listErrorBody(error);
|
|
64
|
+
io.stderr.write(json ? formatJson(body) : `error: ${body.code}: ${body.message}\n`);
|
|
65
|
+
return EXIT.error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function run(argv, io) {
|
|
69
|
+
const flags = { json: false, help: false, confirmed: false, decline: false, raw: false, bail: false };
|
|
70
|
+
const given = new Set();
|
|
71
|
+
const positionals = [];
|
|
72
|
+
/** call 의 <id> 뒤 = 명령어 입력 (mext run 과 같은 플래그). inspect 플래그는 <id> 앞에 둔다 */
|
|
73
|
+
let inputArgv = [];
|
|
74
|
+
for (let i = 0; i < argv.length; i++) {
|
|
75
|
+
const tok = argv[i];
|
|
76
|
+
if (tok === "--help" || tok === "-h") {
|
|
77
|
+
flags.help = true;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (!tok.startsWith("--")) {
|
|
81
|
+
positionals.push(tok);
|
|
82
|
+
if (positionals[0] === "call" && positionals.length === 2) {
|
|
83
|
+
inputArgv = argv.slice(i + 1);
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const eq = tok.indexOf("=");
|
|
89
|
+
const name = tok.slice(2, eq < 0 ? undefined : eq);
|
|
90
|
+
given.add(name);
|
|
91
|
+
if (BOOLEAN_FLAGS.includes(name) && eq < 0) {
|
|
92
|
+
flags[name] = true;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (!VALUE_FLAGS.includes(name))
|
|
96
|
+
throw new UsageError(`unknown flag --${name}`);
|
|
97
|
+
const value = eq < 0 ? argv[++i] : tok.slice(eq + 1);
|
|
98
|
+
if (value === undefined || value === "")
|
|
99
|
+
throw new UsageError(`--${name} requires a value`);
|
|
100
|
+
if (name === "as") {
|
|
101
|
+
flags.as = value
|
|
102
|
+
.split(",")
|
|
103
|
+
.map((s) => s.trim())
|
|
104
|
+
.filter((s) => s !== "");
|
|
105
|
+
}
|
|
106
|
+
else if (name === "answers")
|
|
107
|
+
flags.answers = parseAnswers(value);
|
|
108
|
+
else
|
|
109
|
+
flags[name] = value;
|
|
110
|
+
}
|
|
111
|
+
const [sub, ...args] = positionals;
|
|
112
|
+
if (sub === undefined || sub === "help") {
|
|
113
|
+
(flags.help || sub === "help" ? io.stdout : io.stderr).write(inspectHelp());
|
|
114
|
+
return flags.help || sub === "help" ? EXIT.ok : EXIT.usage;
|
|
115
|
+
}
|
|
116
|
+
if (flags.help) {
|
|
117
|
+
io.stdout.write(inspectHelp());
|
|
118
|
+
return EXIT.ok;
|
|
119
|
+
}
|
|
120
|
+
const allowed = SUBCOMMAND_FLAGS[sub];
|
|
121
|
+
if (!allowed)
|
|
122
|
+
throw new UsageError(`unknown subcommand "${sub}" (commands · show · call · run · profiles · ui · mcp-config)`);
|
|
123
|
+
for (const name of given) {
|
|
124
|
+
if (!COMMON_FLAGS.includes(name) && !allowed.includes(name))
|
|
125
|
+
throw new UsageError(`--${name} is not a flag of "${INSPECT_PROG} ${sub}"`);
|
|
126
|
+
}
|
|
127
|
+
const profiles = loadProfiles(io.cwd, { ...(flags.profiles !== undefined ? { file: flags.profiles } : {}), env: io.env });
|
|
128
|
+
if (sub === "profiles") {
|
|
129
|
+
if (args.length > 0)
|
|
130
|
+
throw new UsageError(`unexpected argument "${args[0]}"`);
|
|
131
|
+
return printProfiles(profiles, io, flags.json);
|
|
132
|
+
}
|
|
133
|
+
const target = resolveTarget({ ...(flags.url !== undefined ? { url: flags.url } : {}), env: io.env, cwd: io.cwd });
|
|
134
|
+
if (sub === "ui") {
|
|
135
|
+
if (args.length > 0)
|
|
136
|
+
throw new UsageError(`unexpected argument "${args[0]}"`);
|
|
137
|
+
if (flags.as || flags.json)
|
|
138
|
+
throw new UsageError(`--${flags.as ? "as" : "json"} is not a flag of "${INSPECT_PROG} ui" (pick profiles in the page)`);
|
|
139
|
+
return ui(target, profiles, flags, io);
|
|
140
|
+
}
|
|
141
|
+
// run = 감사 이벤트를 비교한다 (expect.audit · agent 단계): 대상이 이 프로젝트의 mext dev 면 감사 출처를 잇는다
|
|
142
|
+
if (sub === "mcp-config") {
|
|
143
|
+
if (args.length > 0)
|
|
144
|
+
throw new UsageError(`unexpected argument "${args[0]}"`);
|
|
145
|
+
return mcpConfig(target, profiles, flags, io);
|
|
146
|
+
}
|
|
147
|
+
const audit = sub === "run" ? resolveAudit(target, io.cwd) : undefined;
|
|
148
|
+
const inspector = createInspector({ url: target.url, profiles, env: io.env, ...(audit?.available ? { audit: audit.source } : {}) });
|
|
149
|
+
// 이름 확인 (없는 프로필 = 사용법 오류)
|
|
150
|
+
const names = (flags.as ?? [profiles.default]).map((name) => inspector.profile(name).name);
|
|
151
|
+
if (names.length === 0)
|
|
152
|
+
throw new UsageError("--as requires at least one profile name");
|
|
153
|
+
switch (sub) {
|
|
154
|
+
case "commands": {
|
|
155
|
+
if (args.length > 1)
|
|
156
|
+
throw new UsageError(`unexpected argument "${args[1]}"`);
|
|
157
|
+
return commands(inspector, target, names, args[0], io, flags.json);
|
|
158
|
+
}
|
|
159
|
+
case "show": {
|
|
160
|
+
if (args.length !== 1)
|
|
161
|
+
throw new UsageError(`usage: ${INSPECT_PROG} show <id>`);
|
|
162
|
+
return show(inspector, names, args[0], io, flags.json);
|
|
163
|
+
}
|
|
164
|
+
case "call": {
|
|
165
|
+
if (args.length !== 1)
|
|
166
|
+
throw new UsageError(`usage: ${INSPECT_PROG} call [flags] <id> [input flags]`);
|
|
167
|
+
return call(inspector, names, args[0], inputArgv, flags, io);
|
|
168
|
+
}
|
|
169
|
+
default: {
|
|
170
|
+
if (flags.as && flags.as.length > 1)
|
|
171
|
+
throw new UsageError("--as takes one profile for run (steps name their own profile with \"as\")");
|
|
172
|
+
try {
|
|
173
|
+
return await runScenarios(inspector, profiles, args, flags, io);
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
inspector.close();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function parseAnswers(text) {
|
|
182
|
+
let value;
|
|
183
|
+
try {
|
|
184
|
+
value = JSON.parse(text);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
throw new UsageError('--answers must be JSON, e.g. \'{"confirm": true, "form": {"reason": "ok"}}\'');
|
|
188
|
+
}
|
|
189
|
+
if (!isRecord(value))
|
|
190
|
+
throw new UsageError("--answers must be a JSON object");
|
|
191
|
+
const out = {};
|
|
192
|
+
for (const [key, v] of Object.entries(value)) {
|
|
193
|
+
if (key === "confirm" || key === "url") {
|
|
194
|
+
if (typeof v !== "boolean")
|
|
195
|
+
throw new UsageError(`--answers ${key} must be true or false`);
|
|
196
|
+
out[key] = v;
|
|
197
|
+
}
|
|
198
|
+
else if (key === "form") {
|
|
199
|
+
if (v !== false && !isRecord(v))
|
|
200
|
+
throw new UsageError("--answers form must be an object or false");
|
|
201
|
+
out.form = v;
|
|
202
|
+
}
|
|
203
|
+
else
|
|
204
|
+
throw new UsageError(`--answers: unknown key "${key}" (confirm · form · url)`);
|
|
205
|
+
}
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
208
|
+
/* profiles */
|
|
209
|
+
function printProfiles(profiles, io, json) {
|
|
210
|
+
const inspector = createInspector({ url: "http://unused.invalid/mcp", profiles, env: io.env });
|
|
211
|
+
const views = inspector.profiles();
|
|
212
|
+
if (json) {
|
|
213
|
+
io.stdout.write(formatJson({ file: profiles.file ?? null, default: profiles.default, profiles: views }));
|
|
214
|
+
return EXIT.ok;
|
|
215
|
+
}
|
|
216
|
+
const source = profiles.file ? shown(io.cwd, profiles.file) : `built-in (no ${INSPECT_DIR}/${PROFILES_FILE})`;
|
|
217
|
+
const rows = views.map((p) => [
|
|
218
|
+
`${p.default ? "*" : " "} ${p.name}`,
|
|
219
|
+
p.token ? `${p.token.env} (${p.token.set ? "set" : "not set"})` : "-",
|
|
220
|
+
p.headers.map((h) => (h.env ? `${h.name}=$${h.env}${h.set ? "" : " (not set)"}` : `${h.name}=${h.value}`)).join(", ") || "-",
|
|
221
|
+
p.description ?? "",
|
|
222
|
+
]);
|
|
223
|
+
io.stdout.write(`Profiles: ${source}\n${table([[" NAME", "TOKEN", "HEADERS", "DESCRIPTION"], ...rows]).join("\n")}\n`);
|
|
224
|
+
return EXIT.ok;
|
|
225
|
+
}
|
|
226
|
+
/* mcp-config */
|
|
227
|
+
/**
|
|
228
|
+
* 에이전트 (Happy 봇) 가 이 서버를 MCP 서버로 쓰게 하는 설정 스니펫 (mcp.toml 형식, §3). 출력만 한다 (파일을 쓰지 않는다).
|
|
229
|
+
* 토큰 = 환경변수 이름 (bearer_token_env_var). 값은 읽지도 출력하지도 않는다
|
|
230
|
+
*/
|
|
231
|
+
function mcpConfig(target, profiles, flags, io) {
|
|
232
|
+
if (!flags.as || flags.as.length !== 1) {
|
|
233
|
+
throw new UsageError(`"${INSPECT_PROG} mcp-config" needs --as <profile>: the profile whose token the agent will use (profiles: ${profiles.profiles.map((p) => p.name).join(", ")})`);
|
|
234
|
+
}
|
|
235
|
+
const profile = findProfile(profiles, flags.as[0]);
|
|
236
|
+
if (!profile)
|
|
237
|
+
throw new UsageError(`Unknown profile "${flags.as[0]}". Profiles: ${profiles.profiles.map((p) => p.name).join(", ")}`);
|
|
238
|
+
const name = flags.name ?? (basename(resolve(io.cwd)).replace(/[^A-Za-z0-9_-]+/g, "_") || "mext");
|
|
239
|
+
const fixed = Object.entries(profile.headers).filter((entry) => typeof entry[1] === "string");
|
|
240
|
+
const fromEnv = Object.entries(profile.headers).flatMap(([header, value]) => (typeof value === "string" ? [] : [{ name: header, env: value.env }]));
|
|
241
|
+
const key = /^[A-Za-z0-9_-]+$/.test(name) ? name : JSON.stringify(name);
|
|
242
|
+
const entry = [
|
|
243
|
+
`[mcp_servers.${key}]`,
|
|
244
|
+
`url = ${JSON.stringify(target.url)}`,
|
|
245
|
+
...(profile.token ? [`bearer_token_env_var = ${JSON.stringify(profile.token.env)}`] : []),
|
|
246
|
+
...(fixed.length > 0 ? [`http_headers = { ${fixed.map(([header, value]) => `${JSON.stringify(header)} = ${JSON.stringify(value)}`).join(", ")} }`] : []),
|
|
247
|
+
];
|
|
248
|
+
if (flags.json) {
|
|
249
|
+
io.stdout.write(formatJson({
|
|
250
|
+
name,
|
|
251
|
+
profile: profile.name,
|
|
252
|
+
url: target.url,
|
|
253
|
+
bearerTokenEnvVar: profile.token?.env ?? null,
|
|
254
|
+
httpHeaders: Object.fromEntries(fixed),
|
|
255
|
+
skippedHeaders: fromEnv,
|
|
256
|
+
toml: `${entry.join("\n")}\n`,
|
|
257
|
+
}));
|
|
258
|
+
return EXIT.ok;
|
|
259
|
+
}
|
|
260
|
+
const lines = [
|
|
261
|
+
`# MCP server entry for a Happy Agent bot, calling as profile "${profile.name}" (${INSPECT_PROG} mcp-config).`,
|
|
262
|
+
"# 1. Add it to the mcp.toml in the bot's folder (for example ~/Happy/Bots/<bot>/mcp.toml). This command does not write that file.",
|
|
263
|
+
profile.token
|
|
264
|
+
? `# 2. The Happy daemon reads the token from its own environment: ${profile.token.env} must be set where the daemon runs (not only in this shell).`
|
|
265
|
+
: "# 2. This profile has no token: the bot calls the server anonymously.",
|
|
266
|
+
"# 3. Ask the bot to reload its MCP servers.",
|
|
267
|
+
...entry,
|
|
268
|
+
...fromEnv.map((h) => `# Not included: header ${h.name} comes from $${h.env} in this profile, and mcp.toml headers are fixed values only.`),
|
|
269
|
+
...(target.source === "dev" ? [`# This URL is this project's mext dev server. Keep the port fixed (mext dev --port ${new URL(target.url).port}) so the entry stays valid.`] : []),
|
|
270
|
+
];
|
|
271
|
+
io.stdout.write(`${lines.join("\n")}\n`);
|
|
272
|
+
return EXIT.ok;
|
|
273
|
+
}
|
|
274
|
+
async function listFor(inspector, names, namespace) {
|
|
275
|
+
const out = [];
|
|
276
|
+
for (const profile of names) {
|
|
277
|
+
try {
|
|
278
|
+
const list = await inspector.commands(profile);
|
|
279
|
+
out.push({ profile, ok: true, list: namespace === undefined ? list : within(list, namespace) });
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
out.push({ profile, ok: false, error: listErrorBody(error) });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return out;
|
|
286
|
+
}
|
|
287
|
+
function within(list, namespace) {
|
|
288
|
+
const inside = (id) => id === namespace || id.startsWith(`${namespace}.`);
|
|
289
|
+
return { ...list, namespaces: list.namespaces.filter((n) => inside(n.id)), commands: list.commands.filter((c) => inside(c.id)) };
|
|
290
|
+
}
|
|
291
|
+
async function commands(inspector, target, names, namespace, io, json) {
|
|
292
|
+
const listings = await listFor(inspector, names, namespace);
|
|
293
|
+
const failed = listings.some((l) => !l.ok);
|
|
294
|
+
if (names.length === 1) {
|
|
295
|
+
const only = listings[0];
|
|
296
|
+
if (!only.ok) {
|
|
297
|
+
io.stderr.write(json ? formatJson(only.error) : `error: ${only.error.code}: ${only.error.message}\n`);
|
|
298
|
+
return EXIT.error;
|
|
299
|
+
}
|
|
300
|
+
if (json) {
|
|
301
|
+
io.stdout.write(formatJson(only.list));
|
|
302
|
+
return EXIT.ok;
|
|
303
|
+
}
|
|
304
|
+
io.stdout.write(`${targetLine(target)} · as ${only.profile}\n\n${commandLines(only.list)}`);
|
|
305
|
+
return EXIT.ok;
|
|
306
|
+
}
|
|
307
|
+
if (json) {
|
|
308
|
+
io.stdout.write(formatJson(listings));
|
|
309
|
+
return failed ? EXIT.error : EXIT.ok;
|
|
310
|
+
}
|
|
311
|
+
const ids = [...new Set(listings.flatMap((l) => (l.ok ? l.list.commands.map((c) => c.id) : [])))].sort();
|
|
312
|
+
const rows = ids.map((id) => [id, ...listings.map((l) => (l.ok ? (l.list.commands.some((c) => c.id === id) ? "visible" : "hidden") : l.error.code))]);
|
|
313
|
+
io.stdout.write(`${targetLine(target)}\n\n${table([["", ...names], ...rows]).join("\n")}\n`);
|
|
314
|
+
for (const l of listings)
|
|
315
|
+
if (!l.ok)
|
|
316
|
+
io.stderr.write(`${l.profile}: ${l.error.code}: ${l.error.message}\n`);
|
|
317
|
+
return failed ? EXIT.error : EXIT.ok;
|
|
318
|
+
}
|
|
319
|
+
function targetLine(target) {
|
|
320
|
+
return `${target.url}${target.source === "dev" ? " (mext dev)" : ""}`;
|
|
321
|
+
}
|
|
322
|
+
/** 네임스페이스별 명령어 + 안전성 */
|
|
323
|
+
function commandLines(list) {
|
|
324
|
+
if (list.commands.length === 0)
|
|
325
|
+
return "(no commands visible to this profile)\n";
|
|
326
|
+
const groups = new Map();
|
|
327
|
+
for (const c of list.commands) {
|
|
328
|
+
const ns = parentNamespace(c.id);
|
|
329
|
+
groups.set(ns, [...(groups.get(ns) ?? []), c]);
|
|
330
|
+
}
|
|
331
|
+
const rows = [];
|
|
332
|
+
const heads = new Set();
|
|
333
|
+
for (const [ns, items] of [...groups].sort(([a], [b]) => a.localeCompare(b))) {
|
|
334
|
+
heads.add(rows.length);
|
|
335
|
+
rows.push([ns === "" ? "(root)" : ns, "", list.namespaces.find((n) => n.id === ns)?.description ?? ""]);
|
|
336
|
+
for (const c of items)
|
|
337
|
+
rows.push([` ${c.id}`, safetyOf(c).join(", "), c.description]);
|
|
338
|
+
}
|
|
339
|
+
const lines = table(rows);
|
|
340
|
+
return `${lines.map((l, i) => (heads.has(i) && i > 0 ? `\n${l}` : l)).join("\n")}\n`;
|
|
341
|
+
}
|
|
342
|
+
/** 안전성 표시 (mext run --help 의 Safety 줄과 같은 말) */
|
|
343
|
+
export function safetyOf(c) {
|
|
344
|
+
const out = [c.readOnly ? "read-only" : "writes"];
|
|
345
|
+
if (c.destructive)
|
|
346
|
+
out.push("destructive");
|
|
347
|
+
if (c.requiresConfirmation)
|
|
348
|
+
out.push("requires confirmation");
|
|
349
|
+
else if (c.needsApproval === "conditional")
|
|
350
|
+
out.push("may require confirmation");
|
|
351
|
+
return out;
|
|
352
|
+
}
|
|
353
|
+
/* show */
|
|
354
|
+
async function show(inspector, names, id, io, json) {
|
|
355
|
+
let info;
|
|
356
|
+
const visibleTo = {};
|
|
357
|
+
let firstError;
|
|
358
|
+
for (const profile of names) {
|
|
359
|
+
try {
|
|
360
|
+
const found = await inspector.describe(id, profile);
|
|
361
|
+
info ??= found;
|
|
362
|
+
visibleTo[profile] = true;
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
if (!(error instanceof CallError && error.code === "UNKNOWN_COMMAND") && names.length === 1)
|
|
366
|
+
throw error;
|
|
367
|
+
firstError ??= error;
|
|
368
|
+
visibleTo[profile] = false;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
if (!info)
|
|
372
|
+
throw firstError;
|
|
373
|
+
if (json) {
|
|
374
|
+
io.stdout.write(formatJson(names.length === 1 ? info : { ...info, visibleTo }));
|
|
375
|
+
return EXIT.ok;
|
|
376
|
+
}
|
|
377
|
+
const safety = safetyOf(info);
|
|
378
|
+
const rows = [
|
|
379
|
+
["Safety", safety.join(", ")],
|
|
380
|
+
["Approval", info.requiresConfirmation ? "required" : info.needsApproval === "conditional" ? "conditional" : "none"],
|
|
381
|
+
["Scopes", info.scopes.join(", ") || "(none)"],
|
|
382
|
+
];
|
|
383
|
+
if (info.aliases.length > 0)
|
|
384
|
+
rows.push(["Aliases", info.aliases.join(", ")]);
|
|
385
|
+
if (info.tool !== undefined)
|
|
386
|
+
rows.push(["Tool", info.tool]);
|
|
387
|
+
if (info.task)
|
|
388
|
+
rows.push(["Task", `ttl ${info.task.ttlMs}ms`]);
|
|
389
|
+
if (info.errors.length > 0)
|
|
390
|
+
rows.push(["Errors", info.errors.join(", ")]);
|
|
391
|
+
if (names.length > 1)
|
|
392
|
+
rows.push(["Visible to", names.map((n) => `${n} ${visibleTo[n] ? "yes" : "no"}`).join(" · ")]);
|
|
393
|
+
const out = [`${info.id} — ${info.description}`, "", ...table(rows), "", "Input schema:", indent(JSON.stringify(info.inputSchema, null, 2))];
|
|
394
|
+
if (info.outputSchema)
|
|
395
|
+
out.push("", "Output schema:", indent(JSON.stringify(info.outputSchema, null, 2)));
|
|
396
|
+
if (info.examples.length > 0)
|
|
397
|
+
out.push("", "Examples:", ...info.examples.map((e) => ` ${JSON.stringify(e)}`));
|
|
398
|
+
io.stdout.write(`${out.join("\n")}\n`);
|
|
399
|
+
return EXIT.ok;
|
|
400
|
+
}
|
|
401
|
+
/* call */
|
|
402
|
+
async function call(inspector, names, id, inputArgv, flags, io) {
|
|
403
|
+
const parsed = await parseInput(inspector, names, id, inputArgv, io);
|
|
404
|
+
if (parsed.help)
|
|
405
|
+
return show(inspector, names, id, io, flags.json || parsed.json);
|
|
406
|
+
const json = flags.json || parsed.json;
|
|
407
|
+
const confirmed = flags.confirmed || parsed.yes;
|
|
408
|
+
const records = [];
|
|
409
|
+
for (const profile of names) {
|
|
410
|
+
records.push(await inspector.call(id, parsed.input, {
|
|
411
|
+
profile,
|
|
412
|
+
...(confirmed ? { confirmed: true } : {}),
|
|
413
|
+
...(flags.answers ? { answers: flags.answers } : {}),
|
|
414
|
+
...(flags.decline ? { decline: true } : {}),
|
|
415
|
+
...(io.signal ? { signal: io.signal } : {}),
|
|
416
|
+
onProgress: (update) => {
|
|
417
|
+
const line = formatProgress({ ...update, progress: update.progress ?? 0 });
|
|
418
|
+
io.stderr.write(names.length > 1 ? `${profile}: ${line}` : line);
|
|
419
|
+
},
|
|
420
|
+
}));
|
|
421
|
+
}
|
|
422
|
+
if (flags.save !== undefined) {
|
|
423
|
+
const file = resolve(io.cwd, flags.save);
|
|
424
|
+
let count = 0;
|
|
425
|
+
for (const record of records) {
|
|
426
|
+
count = appendStep(file, stepFromRecord(record, { ...(flags.answers ? { answers: flags.answers } : {}), ...(flags.decline ? { decline: true } : {}) }));
|
|
427
|
+
}
|
|
428
|
+
io.stderr.write(`Saved ${records.length === 1 ? "step" : `${records.length} steps`} to ${shown(io.cwd, file)} (${count} step${count === 1 ? "" : "s"})\n`);
|
|
429
|
+
}
|
|
430
|
+
const failed = records.some((r) => !r.ok);
|
|
431
|
+
if (json) {
|
|
432
|
+
io.stdout.write(formatJson(records.map((r) => ({
|
|
433
|
+
profile: r.profile,
|
|
434
|
+
...outcomeOf(r),
|
|
435
|
+
durationMs: r.durationMs,
|
|
436
|
+
...(r.task ? { task: r.task } : {}),
|
|
437
|
+
...(flags.raw ? { exchanges: r.exchanges } : {}),
|
|
438
|
+
}))));
|
|
439
|
+
return failed ? EXIT.error : EXIT.ok;
|
|
440
|
+
}
|
|
441
|
+
const blocks = records.map((r) => {
|
|
442
|
+
const lines = [recordLine(r)];
|
|
443
|
+
const body = r.ok ? formatResult(r.value).trimEnd() : JSON.stringify(r.error, null, 2);
|
|
444
|
+
if (body !== "")
|
|
445
|
+
lines.push(indent(body));
|
|
446
|
+
if (flags.raw) {
|
|
447
|
+
lines.push(" Requests:");
|
|
448
|
+
for (const x of r.exchanges) {
|
|
449
|
+
lines.push(` → ${x.request.method} ${x.request.url}${x.rpc ? ` ${x.rpc}` : ""} (${x.response ? x.response.status : (x.error ?? "no response")}, ${x.durationMs}ms)`);
|
|
450
|
+
lines.push(indent(JSON.stringify({ request: x.request, response: x.response }, null, 2), 4));
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return lines.join("\n");
|
|
454
|
+
});
|
|
455
|
+
io.stdout.write(`${blocks.join("\n\n")}\n`);
|
|
456
|
+
return failed ? EXIT.error : EXIT.ok;
|
|
457
|
+
}
|
|
458
|
+
/** 한 줄 요약: ✓ posts.get as editor (12ms) · ✗ posts.publish as agent (8ms) APPROVAL_REQUIRED (human_required) */
|
|
459
|
+
function recordLine(r) {
|
|
460
|
+
const head = `${r.ok ? "✓" : "✗"} ${r.command} as ${r.profile} (${r.durationMs}ms)`;
|
|
461
|
+
if (r.ok || !r.error)
|
|
462
|
+
return head;
|
|
463
|
+
return `${head} ${r.error.code}${typeof r.error.reason === "string" ? ` (${r.error.reason})` : ""}`;
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* <id> 뒤 argv → 입력. 스키마 = 이 명령어가 보이는 첫 프로필의 것 (mext run 과 같은 플래그 해석).
|
|
467
|
+
* 어느 프로필에도 보이지 않으면 (숨긴 명령어를 호출해 보는 경우) --input-json 만 받는다
|
|
468
|
+
*/
|
|
469
|
+
async function parseInput(inspector, names, id, argv, io) {
|
|
470
|
+
let info;
|
|
471
|
+
for (const profile of names) {
|
|
472
|
+
try {
|
|
473
|
+
info = await inspector.describe(id, profile);
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
// 다음 프로필
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (info) {
|
|
481
|
+
const entry = entryFromHelp({ ...info, kind: "command" });
|
|
482
|
+
const parsed = await parseArgs(entry, argv, { readStdin: () => readAll(io.stdin) });
|
|
483
|
+
return { input: isRecord(parsed.input) ? parsed.input : {}, json: parsed.json, yes: parsed.yes, help: parsed.help };
|
|
484
|
+
}
|
|
485
|
+
const out = { input: {}, json: false, yes: false, help: false };
|
|
486
|
+
for (let i = 0; i < argv.length; i++) {
|
|
487
|
+
const tok = argv[i];
|
|
488
|
+
if (tok === "--json")
|
|
489
|
+
out.json = true;
|
|
490
|
+
else if (tok === "--yes")
|
|
491
|
+
out.yes = true;
|
|
492
|
+
else if (tok === "--help" || tok === "-h")
|
|
493
|
+
out.help = true;
|
|
494
|
+
else if (tok === "--input-json" || tok.startsWith("--input-json=")) {
|
|
495
|
+
const text = tok === "--input-json" ? argv[++i] : tok.slice("--input-json=".length);
|
|
496
|
+
let value;
|
|
497
|
+
try {
|
|
498
|
+
value = JSON.parse(text === "-" ? await readAll(io.stdin) : (text ?? ""));
|
|
499
|
+
}
|
|
500
|
+
catch {
|
|
501
|
+
throw new UsageError("--input-json must be a JSON object");
|
|
502
|
+
}
|
|
503
|
+
if (!isRecord(value))
|
|
504
|
+
throw new UsageError("--input-json must be a JSON object");
|
|
505
|
+
out.input = value;
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
throw new UsageError(`"${id}" is not visible to ${names.join(", ")}, so its flags are unknown. Pass the input with --input-json '<json>'`);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return out;
|
|
512
|
+
}
|
|
513
|
+
/* run */
|
|
514
|
+
async function runScenarios(inspector, profiles, paths, flags, io) {
|
|
515
|
+
const scenarios = scenarioFiles(io.cwd, paths).map((file) => loadScenario(file, profiles));
|
|
516
|
+
const needsAgent = scenarios.some((scenario) => scenario.steps.some((step) => "agent" in step));
|
|
517
|
+
if (needsAgent && flags.agent === undefined) {
|
|
518
|
+
throw new UsageError('these scenarios have "agent" steps: pass --agent <module> (a module whose default export is an AgentClient, or a function that makes one)');
|
|
519
|
+
}
|
|
520
|
+
const agent = flags.agent !== undefined ? await loadAgent(flags.agent, io) : undefined;
|
|
521
|
+
try {
|
|
522
|
+
return await runLoaded(inspector, scenarios, agent, flags, io);
|
|
523
|
+
}
|
|
524
|
+
finally {
|
|
525
|
+
await agent?.close?.().catch(() => { });
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
/** --agent <모듈>: default export = AgentClient, 또는 그것을 만드는 함수 ({ cwd, env }) */
|
|
529
|
+
async function loadAgent(path, io) {
|
|
530
|
+
const file = resolve(io.cwd, path);
|
|
531
|
+
if (!existsSync(file))
|
|
532
|
+
throw new UsageError(`--agent: ${shown(io.cwd, file)} not found`);
|
|
533
|
+
const module = (await import(__rewriteRelativeImportExtension(pathToFileURL(file).href)));
|
|
534
|
+
const plugin = module.default;
|
|
535
|
+
let client = plugin;
|
|
536
|
+
if (typeof plugin === "function") {
|
|
537
|
+
try {
|
|
538
|
+
client = await plugin({ cwd: io.cwd, env: io.env });
|
|
539
|
+
}
|
|
540
|
+
catch (error) {
|
|
541
|
+
// 설정이 빠진 plugin (환경변수 등) = 사용법 오류
|
|
542
|
+
throw new UsageError(`--agent: ${error instanceof Error ? error.message : String(error)}`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
if (!isAgentClient(client)) {
|
|
546
|
+
throw new UsageError(`--agent: the default export of ${shown(io.cwd, file)} must be an AgentClient ({ connect, send, onEvent }) or a function that returns one`);
|
|
547
|
+
}
|
|
548
|
+
return client;
|
|
549
|
+
}
|
|
550
|
+
async function runLoaded(inspector, scenarios, agent, flags, io) {
|
|
551
|
+
const results = [];
|
|
552
|
+
for (const scenario of scenarios) {
|
|
553
|
+
// --as = 시나리오 · 단계가 프로필을 적지 않았을 때의 기본값
|
|
554
|
+
const effective = scenario.as === undefined && flags.as ? { ...scenario, as: flags.as[0] } : scenario;
|
|
555
|
+
if (!flags.json)
|
|
556
|
+
io.stdout.write(`${scenario.name}${scenario.file ? ` (${shown(io.cwd, scenario.file)})` : ""}\n`);
|
|
557
|
+
const result = await runScenario(inspector, effective, {
|
|
558
|
+
bail: flags.bail,
|
|
559
|
+
...(agent ? { agent } : {}),
|
|
560
|
+
...(io.signal ? { signal: io.signal } : {}),
|
|
561
|
+
onStep: (step) => {
|
|
562
|
+
if (!flags.json)
|
|
563
|
+
io.stdout.write(stepLines(step));
|
|
564
|
+
},
|
|
565
|
+
});
|
|
566
|
+
results.push(result);
|
|
567
|
+
if (!flags.json && result.steps.length < scenario.steps.length) {
|
|
568
|
+
io.stdout.write(` (stopped: ${scenario.steps.length - result.steps.length} step(s) not run)\n`);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
const pass = results.every((r) => r.pass);
|
|
572
|
+
if (flags.json) {
|
|
573
|
+
io.stdout.write(formatJson({ pass, scenarios: results }));
|
|
574
|
+
return pass ? EXIT.ok : EXIT.error;
|
|
575
|
+
}
|
|
576
|
+
const steps = results.flatMap((r) => r.steps);
|
|
577
|
+
const passed = results.filter((r) => r.pass).length;
|
|
578
|
+
io.stdout.write(`\n${passed}/${results.length} scenario${results.length === 1 ? "" : "s"} passed, ${steps.filter((s) => s.pass).length}/${steps.length} steps passed\n`);
|
|
579
|
+
return pass ? EXIT.ok : EXIT.error;
|
|
580
|
+
}
|
|
581
|
+
function stepLines(step) {
|
|
582
|
+
const lines = [` ${step.pass ? "✓" : "✗"} ${step.step}. ${step.name} (as ${step.as}, ${step.durationMs}ms)`];
|
|
583
|
+
for (const f of step.failures)
|
|
584
|
+
lines.push(` ${f.path}: expected ${JSON.stringify(f.expected)}, got ${JSON.stringify(f.actual)}`);
|
|
585
|
+
const outcome = step.outcome;
|
|
586
|
+
if (outcome && "events" in outcome) {
|
|
587
|
+
// agent 단계: 실패하면 서버가 본 것 (감사 이벤트) 과 에이전트가 한 말을 같이 보여 준다
|
|
588
|
+
if (!step.pass) {
|
|
589
|
+
lines.push(` server saw ${outcome.events.length} call${outcome.events.length === 1 ? "" : "s"} from the agent${outcome.events.length > 0 ? ":" : ""}`);
|
|
590
|
+
for (const event of outcome.events)
|
|
591
|
+
lines.push(` ${event.outcome} ${event.command}${event.error ? ` (${event.error.code})` : ""}`);
|
|
592
|
+
const said = outcome.messages.at(-1);
|
|
593
|
+
if (said !== undefined)
|
|
594
|
+
lines.push(` agent said: ${said.length > 200 ? `${said.slice(0, 197)}…` : said}`);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
else if (!step.pass && outcome && !outcome.ok)
|
|
598
|
+
lines.push(` result: ${outcome.error.code}: ${outcome.error.message}`);
|
|
599
|
+
return `${lines.join("\n")}\n`;
|
|
600
|
+
}
|
|
601
|
+
/* ui */
|
|
602
|
+
/** 웹 UI (§5): 127.0.0.1 에 페이지 · 중계 API 를 열고 SIGINT 까지 실행 */
|
|
603
|
+
async function ui(target, profiles, flags, io) {
|
|
604
|
+
const port = flags.port === undefined ? UI_DEFAULT_PORT : Number(flags.port);
|
|
605
|
+
if (!Number.isInteger(port) || port < 0 || port > 65_535)
|
|
606
|
+
throw new UsageError("--port must be a port number (0 = any free port)");
|
|
607
|
+
const audit = resolveAudit(target, io.cwd);
|
|
608
|
+
const agent = flags.agent !== undefined ? await loadAgent(flags.agent, io) : undefined;
|
|
609
|
+
const inspector = createInspector({ url: target.url, profiles, env: io.env, ...(audit.available ? { audit: audit.source } : {}) });
|
|
610
|
+
let server;
|
|
611
|
+
try {
|
|
612
|
+
server = await startUiServer({ inspector, target, cwd: io.cwd, port, ...(audit.available ? {} : { auditReason: audit.reason }), ...(agent ? { agent } : {}) });
|
|
613
|
+
}
|
|
614
|
+
catch (error) {
|
|
615
|
+
inspector.close();
|
|
616
|
+
if (error.code === "EADDRINUSE") {
|
|
617
|
+
io.stderr.write(`error: port ${port} is in use. Pass --port <n> (0 = any free port).\n`);
|
|
618
|
+
return EXIT.error;
|
|
619
|
+
}
|
|
620
|
+
throw error;
|
|
621
|
+
}
|
|
622
|
+
io.stderr.write(`${INSPECT_PROG}: ${server.url}\n` +
|
|
623
|
+
` target ${targetLine(target)}\n` +
|
|
624
|
+
` audit ${audit.available ? "streaming from mext dev" : "not available for this target"}\n` +
|
|
625
|
+
(agent ? ` agent ${agent.name ?? "connected on the first agent step"}\n` : "") +
|
|
626
|
+
"Press Ctrl-C to stop.\n");
|
|
627
|
+
await new Promise((done) => {
|
|
628
|
+
if (io.signal?.aborted)
|
|
629
|
+
return done();
|
|
630
|
+
io.signal?.addEventListener("abort", () => done(), { once: true });
|
|
631
|
+
});
|
|
632
|
+
await server.close();
|
|
633
|
+
inspector.close();
|
|
634
|
+
await agent?.close?.().catch(() => { });
|
|
635
|
+
return EXIT.ok;
|
|
636
|
+
}
|
|
637
|
+
/* help */
|
|
638
|
+
export function inspectHelp() {
|
|
639
|
+
const lines = [
|
|
640
|
+
"Inspect a mext server command by command: who (a caller profile) calls what, and what comes back.",
|
|
641
|
+
"",
|
|
642
|
+
"Usage:",
|
|
643
|
+
` ${INSPECT_PROG} commands [<namespace>] List commands with safety metadata`,
|
|
644
|
+
` ${INSPECT_PROG} show <id> One command: safety, approval, scopes, input schema, examples, errors`,
|
|
645
|
+
` ${INSPECT_PROG} call [flags] <id> [input] Call a command (input: --flag value, or --input-json '<json>')`,
|
|
646
|
+
` ${INSPECT_PROG} run [<scenario|folder>...] Run scenario files (default: ${INSPECT_DIR}/scenarios)`,
|
|
647
|
+
` ${INSPECT_PROG} profiles List caller profiles (values are never printed)`,
|
|
648
|
+
` ${INSPECT_PROG} ui [--port <n>] [--agent <m>] Web UI on http://127.0.0.1:${UI_DEFAULT_PORT} (calls go through this process)`,
|
|
649
|
+
` ${INSPECT_PROG} mcp-config --as <profile> Print the mcp.toml entry that lets an agent (a Happy bot) use this server`,
|
|
650
|
+
"",
|
|
651
|
+
"Server: --url <url>, or MEXT_URL, or the mext dev server running in this project.",
|
|
652
|
+
`Profiles: ${INSPECT_DIR}/${PROFILES_FILE} names tokens and headers by environment variable, e.g.`,
|
|
653
|
+
' { "profiles": { "editor": { "token": { "env": "EDITOR_TOKEN" } }, "anonymous": {} } }',
|
|
654
|
+
"Without the file there is one profile, default (token from MEXT_TOKEN if set).",
|
|
655
|
+
"",
|
|
656
|
+
"Flags:",
|
|
657
|
+
...table([
|
|
658
|
+
["--url <url>", "MCP endpoint of the server to inspect"],
|
|
659
|
+
["--profiles <path>", `Profiles file (default: ${INSPECT_DIR}/${PROFILES_FILE})`],
|
|
660
|
+
["--as <a>[,<b>...]", "Caller profile. Several = run the same request as each and compare"],
|
|
661
|
+
["--json", "Print JSON"],
|
|
662
|
+
["-h, --help", "Show help"],
|
|
663
|
+
]).map((l) => ` ${l}`),
|
|
664
|
+
"",
|
|
665
|
+
`Flags of call (before <id>; everything after <id> is the command's input):`,
|
|
666
|
+
...table([
|
|
667
|
+
["--confirmed", "Declare that a human confirmed this call (same as --yes after <id>)"],
|
|
668
|
+
["--answers <json>", 'Answers for input requests: {"confirm": true, "form": {…}, "url": true}'],
|
|
669
|
+
["--decline", "Decline input and confirmation requests"],
|
|
670
|
+
["--save <file>", "Append this call and its observed result to a scenario file"],
|
|
671
|
+
["--raw", "Also print the raw requests and responses (secrets redacted)"],
|
|
672
|
+
]).map((l) => ` ${l}`),
|
|
673
|
+
"",
|
|
674
|
+
"Flags of run (--agent also works for ui):",
|
|
675
|
+
...table([
|
|
676
|
+
["--bail", "Stop a scenario at its first failing step"],
|
|
677
|
+
["--agent <module>", 'Agent client for "agent" steps (default export: { connect, send, onEvent })'],
|
|
678
|
+
]).map((l) => ` ${l}`),
|
|
679
|
+
"",
|
|
680
|
+
"Exit code: 0 = every call succeeded / every scenario passed, 1 = an error result or a failed scenario, 2 = usage or file format error.",
|
|
681
|
+
];
|
|
682
|
+
return `${lines.join("\n")}\n`;
|
|
683
|
+
}
|
|
684
|
+
/** cwd 안 = 상대 경로 · 밖 = 절대 경로 */
|
|
685
|
+
function shown(cwd, file) {
|
|
686
|
+
const rel = relative(cwd, file);
|
|
687
|
+
return rel === "" || rel.startsWith("..") ? file : rel;
|
|
688
|
+
}
|
|
689
|
+
function indent(text, by = 2) {
|
|
690
|
+
const pad = " ".repeat(by);
|
|
691
|
+
return text
|
|
692
|
+
.split("\n")
|
|
693
|
+
.map((l) => (l === "" ? l : `${pad}${l}`))
|
|
694
|
+
.join("\n");
|
|
695
|
+
}
|
|
696
|
+
function isRecord(value) {
|
|
697
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
698
|
+
}
|
|
699
|
+
//# sourceMappingURL=inspect.js.map
|