@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,235 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { isSegment } from "../ids.js";
|
|
4
|
+
/**
|
|
5
|
+
* 호출자 프로필 (specs/inspector.md §2): 토큰 · 헤더 묶음에 이름을 붙인 것.
|
|
6
|
+
* 파일에는 환경변수 이름만 적는다 (토큰 평문 금지). 값은 호출할 때마다 환경변수에서 읽고 저장 · 출력하지 않는다
|
|
7
|
+
*/
|
|
8
|
+
/** <프로젝트>/inspect */
|
|
9
|
+
export const INSPECT_DIR = "inspect";
|
|
10
|
+
export const PROFILES_FILE = "profiles.json";
|
|
11
|
+
/** 프로필 파일이 없을 때의 내장 프로필 이름 · 토큰 환경변수 */
|
|
12
|
+
export const DEFAULT_PROFILE = "default";
|
|
13
|
+
export const DEFAULT_TOKEN_ENV = "MEXT_TOKEN";
|
|
14
|
+
/** 가린 값의 표시 */
|
|
15
|
+
export const REDACTED = "[redacted]";
|
|
16
|
+
/** 프로필 파일 형식 오류. issues = "<경로>: <문제>" */
|
|
17
|
+
export class ProfileFileError extends Error {
|
|
18
|
+
file;
|
|
19
|
+
issues;
|
|
20
|
+
constructor(file, issues) {
|
|
21
|
+
super(`${file}: invalid profiles file\n${issues.map((i) => ` ${i}`).join("\n")}`);
|
|
22
|
+
this.name = "ProfileFileError";
|
|
23
|
+
this.file = file;
|
|
24
|
+
this.issues = issues;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** 환경변수가 없어 그 프로필로 호출할 수 없다. 호출을 보내지 않는다 */
|
|
28
|
+
export class ProfileUnavailableError extends Error {
|
|
29
|
+
code = "PROFILE_UNAVAILABLE";
|
|
30
|
+
profile;
|
|
31
|
+
/** 없는 환경변수 이름 */
|
|
32
|
+
missing;
|
|
33
|
+
constructor(profile, missing) {
|
|
34
|
+
super(`Profile "${profile}" needs environment variable${missing.length === 1 ? "" : "s"} ${missing.join(", ")} (not set).`);
|
|
35
|
+
this.name = "ProfileUnavailableError";
|
|
36
|
+
this.profile = profile;
|
|
37
|
+
this.missing = missing;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** 문자열 값을 허용하지 않는 헤더 (비밀): 환경변수 참조만 */
|
|
41
|
+
const SECRET_HEADERS = new Set(["authorization", "cookie", "proxy-authorization"]);
|
|
42
|
+
const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
43
|
+
const HEADER_NAME = /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/;
|
|
44
|
+
/** <cwd>/inspect/profiles.json 또는 지정한 경로 */
|
|
45
|
+
export function profilesPath(cwd, override) {
|
|
46
|
+
return override === undefined ? join(resolve(cwd), INSPECT_DIR, PROFILES_FILE) : resolve(cwd, override);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 프로필 파일 읽기. 파일 없음 = 내장 프로필 default (MEXT_TOKEN 이 있으면 그 토큰. override 로 지정한 파일이 없으면 오류).
|
|
50
|
+
* 형식 오류 = ProfileFileError (문제를 모두 모아 한 번에)
|
|
51
|
+
*/
|
|
52
|
+
export function loadProfiles(cwd, options = {}) {
|
|
53
|
+
const file = profilesPath(cwd, options.file);
|
|
54
|
+
if (!existsSync(file)) {
|
|
55
|
+
if (options.file !== undefined)
|
|
56
|
+
throw new ProfileFileError(file, ["file not found"]);
|
|
57
|
+
const env = options.env ?? process.env;
|
|
58
|
+
const hasToken = env[DEFAULT_TOKEN_ENV] !== undefined && env[DEFAULT_TOKEN_ENV] !== "";
|
|
59
|
+
return {
|
|
60
|
+
file: undefined,
|
|
61
|
+
default: DEFAULT_PROFILE,
|
|
62
|
+
profiles: [
|
|
63
|
+
{
|
|
64
|
+
name: DEFAULT_PROFILE,
|
|
65
|
+
description: hasToken ? `Token from ${DEFAULT_TOKEN_ENV}` : `No token (set ${DEFAULT_TOKEN_ENV}, or add ${INSPECT_DIR}/${PROFILES_FILE})`,
|
|
66
|
+
...(hasToken ? { token: { env: DEFAULT_TOKEN_ENV } } : {}),
|
|
67
|
+
headers: {},
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
let value;
|
|
73
|
+
try {
|
|
74
|
+
value = JSON.parse(readFileSync(file, "utf8"));
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
throw new ProfileFileError(file, [`not valid JSON (${error instanceof Error ? error.message : String(error)})`]);
|
|
78
|
+
}
|
|
79
|
+
return parseProfiles(value, file);
|
|
80
|
+
}
|
|
81
|
+
/** 프로필 파일 내용 검증 */
|
|
82
|
+
export function parseProfiles(value, file) {
|
|
83
|
+
const issues = [];
|
|
84
|
+
if (!isObject(value))
|
|
85
|
+
throw new ProfileFileError(file, ['must be an object ({ "profiles": { … } })']);
|
|
86
|
+
for (const key of Object.keys(value))
|
|
87
|
+
if (key !== "profiles" && key !== "default")
|
|
88
|
+
issues.push(`${key}: unknown key`);
|
|
89
|
+
const profiles = [];
|
|
90
|
+
if (!isObject(value.profiles) || Object.keys(value.profiles).length === 0) {
|
|
91
|
+
issues.push("profiles: must be an object with at least one profile");
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
for (const [name, raw] of Object.entries(value.profiles)) {
|
|
95
|
+
const profile = parseProfile(name, raw, issues);
|
|
96
|
+
if (profile)
|
|
97
|
+
profiles.push(profile);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
let fallback = profiles[0]?.name ?? DEFAULT_PROFILE;
|
|
101
|
+
if (value.default !== undefined) {
|
|
102
|
+
if (typeof value.default !== "string" || !profiles.some((p) => p.name === value.default)) {
|
|
103
|
+
issues.push(`default: must be one of the profile names (${profiles.map((p) => p.name).join(", ")})`);
|
|
104
|
+
}
|
|
105
|
+
else
|
|
106
|
+
fallback = value.default;
|
|
107
|
+
}
|
|
108
|
+
if (issues.length > 0)
|
|
109
|
+
throw new ProfileFileError(file, issues);
|
|
110
|
+
return { file, default: fallback, profiles };
|
|
111
|
+
}
|
|
112
|
+
function parseProfile(name, raw, issues) {
|
|
113
|
+
const at = `profiles.${name}`;
|
|
114
|
+
const before = issues.length;
|
|
115
|
+
if (!isSegment(name))
|
|
116
|
+
issues.push(`${at}: profile name must be kebab-case`);
|
|
117
|
+
if (!isObject(raw)) {
|
|
118
|
+
issues.push(`${at}: must be an object`);
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
for (const key of Object.keys(raw)) {
|
|
122
|
+
if (!["description", "token", "headers", "clientInfo"].includes(key))
|
|
123
|
+
issues.push(`${at}.${key}: unknown key`);
|
|
124
|
+
}
|
|
125
|
+
const profile = { name, headers: {} };
|
|
126
|
+
if (raw.description !== undefined) {
|
|
127
|
+
if (typeof raw.description !== "string")
|
|
128
|
+
issues.push(`${at}.description: must be a string`);
|
|
129
|
+
else
|
|
130
|
+
profile.description = raw.description;
|
|
131
|
+
}
|
|
132
|
+
if (raw.token !== undefined) {
|
|
133
|
+
const ref = envRef(raw.token);
|
|
134
|
+
if (ref)
|
|
135
|
+
profile.token = ref;
|
|
136
|
+
else if (typeof raw.token === "string") {
|
|
137
|
+
issues.push(`${at}.token: must be { "env": "<variable name>" }. Do not write token values in this file`);
|
|
138
|
+
}
|
|
139
|
+
else
|
|
140
|
+
issues.push(`${at}.token: must be { "env": "<variable name>" }`);
|
|
141
|
+
}
|
|
142
|
+
if (raw.headers !== undefined) {
|
|
143
|
+
if (!isObject(raw.headers))
|
|
144
|
+
issues.push(`${at}.headers: must be an object`);
|
|
145
|
+
else {
|
|
146
|
+
for (const [header, v] of Object.entries(raw.headers)) {
|
|
147
|
+
const where = `${at}.headers.${header}`;
|
|
148
|
+
if (!HEADER_NAME.test(header))
|
|
149
|
+
issues.push(`${where}: not a valid header name`);
|
|
150
|
+
const ref = envRef(v);
|
|
151
|
+
if (ref)
|
|
152
|
+
profile.headers[header] = ref;
|
|
153
|
+
else if (typeof v !== "string")
|
|
154
|
+
issues.push(`${where}: must be a string or { "env": "<variable name>" }`);
|
|
155
|
+
else if (SECRET_HEADERS.has(header.toLowerCase())) {
|
|
156
|
+
issues.push(`${where}: must be { "env": "<variable name>" }. Do not write secret header values in this file`);
|
|
157
|
+
}
|
|
158
|
+
else
|
|
159
|
+
profile.headers[header] = v;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (raw.clientInfo !== undefined) {
|
|
164
|
+
const c = raw.clientInfo;
|
|
165
|
+
if (!isObject(c) || typeof c.name !== "string" || c.name === "" || (c.version !== undefined && typeof c.version !== "string")) {
|
|
166
|
+
issues.push(`${at}.clientInfo: must be { "name": string, "version"?: string }`);
|
|
167
|
+
}
|
|
168
|
+
else
|
|
169
|
+
profile.clientInfo = { name: c.name, version: typeof c.version === "string" ? c.version : "0.0.0" };
|
|
170
|
+
}
|
|
171
|
+
return issues.length > before ? undefined : profile;
|
|
172
|
+
}
|
|
173
|
+
function envRef(value) {
|
|
174
|
+
if (!isObject(value))
|
|
175
|
+
return undefined;
|
|
176
|
+
const keys = Object.keys(value);
|
|
177
|
+
return keys.length === 1 && typeof value.env === "string" && ENV_NAME.test(value.env) ? { env: value.env } : undefined;
|
|
178
|
+
}
|
|
179
|
+
export function findProfile(set, name) {
|
|
180
|
+
return set.profiles.find((p) => p.name === (name ?? set.default));
|
|
181
|
+
}
|
|
182
|
+
/** 환경변수에서 값 읽기. 없는 변수 = ProfileUnavailableError */
|
|
183
|
+
export function resolveProfile(profile, env = process.env) {
|
|
184
|
+
const missing = [];
|
|
185
|
+
const secrets = [];
|
|
186
|
+
const read = (ref) => {
|
|
187
|
+
const value = env[ref.env];
|
|
188
|
+
if (value === undefined || value === "") {
|
|
189
|
+
if (!missing.includes(ref.env))
|
|
190
|
+
missing.push(ref.env);
|
|
191
|
+
return "";
|
|
192
|
+
}
|
|
193
|
+
secrets.push(value);
|
|
194
|
+
return value;
|
|
195
|
+
};
|
|
196
|
+
const token = profile.token ? read(profile.token) : undefined;
|
|
197
|
+
const headers = {};
|
|
198
|
+
for (const [name, value] of Object.entries(profile.headers))
|
|
199
|
+
headers[name] = typeof value === "string" ? value : read(value);
|
|
200
|
+
if (missing.length > 0)
|
|
201
|
+
throw new ProfileUnavailableError(profile.name, missing);
|
|
202
|
+
return { token, headers, secrets };
|
|
203
|
+
}
|
|
204
|
+
export function describeProfiles(set, env = process.env) {
|
|
205
|
+
const isSet = (name) => env[name] !== undefined && env[name] !== "";
|
|
206
|
+
return set.profiles.map((p) => {
|
|
207
|
+
const headers = Object.entries(p.headers).map(([name, value]) => typeof value === "string" ? { name, value } : { name, env: value.env, set: isSet(value.env) });
|
|
208
|
+
const view = {
|
|
209
|
+
name: p.name,
|
|
210
|
+
default: p.name === set.default,
|
|
211
|
+
headers,
|
|
212
|
+
available: (p.token === undefined || isSet(p.token.env)) && headers.every((h) => h.set !== false),
|
|
213
|
+
};
|
|
214
|
+
if (p.description !== undefined)
|
|
215
|
+
view.description = p.description;
|
|
216
|
+
if (p.token)
|
|
217
|
+
view.token = { env: p.token.env, set: isSet(p.token.env) };
|
|
218
|
+
if (p.clientInfo)
|
|
219
|
+
view.clientInfo = p.clientInfo;
|
|
220
|
+
return view;
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
/** 문자열 안의 비밀 값을 [redacted] 로 (긴 값부터: 한 값이 다른 값의 일부일 때 긴 쪽이 남지 않게) */
|
|
224
|
+
export function redact(text, secrets) {
|
|
225
|
+
let out = text;
|
|
226
|
+
for (const secret of [...secrets].sort((a, b) => b.length - a.length)) {
|
|
227
|
+
if (secret !== "")
|
|
228
|
+
out = out.split(secret).join(REDACTED);
|
|
229
|
+
}
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
function isObject(value) {
|
|
233
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
234
|
+
}
|
|
235
|
+
//# sourceMappingURL=profiles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profiles.js","sourceRoot":"","sources":["../../src/inspect/profiles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAClD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAErC;;;GAGG;AAEH,qBAAqB;AACrB,MAAM,CAAC,MAAM,WAAW,GAAG,SAAS,CAAA;AACpC,MAAM,CAAC,MAAM,aAAa,GAAG,eAAe,CAAA;AAC5C,wCAAwC;AACxC,MAAM,CAAC,MAAM,eAAe,GAAG,SAAS,CAAA;AACxC,MAAM,CAAC,MAAM,iBAAiB,GAAG,YAAY,CAAA;AAC7C,eAAe;AACf,MAAM,CAAC,MAAM,QAAQ,GAAG,YAAY,CAAA;AAyBpC,0CAA0C;AAC1C,MAAM,OAAO,gBAAiB,SAAQ,KAAK;IAChC,IAAI,CAAQ;IACZ,MAAM,CAAmB;IAClC,YAAY,IAAY,EAAE,MAAyB;QACjD,KAAK,CAAC,GAAG,IAAI,4BAA4B,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QAClF,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAA;QAC9B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QAChB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;IACtB,CAAC;CACF;AAED,4CAA4C;AAC5C,MAAM,OAAO,uBAAwB,SAAQ,KAAK;IACvC,IAAI,GAAG,qBAAqB,CAAA;IAC5B,OAAO,CAAQ;IACxB,iBAAiB;IACR,OAAO,CAAmB;IACnC,YAAY,OAAe,EAAE,OAA0B;QACrD,KAAK,CAAC,YAAY,OAAO,+BAA+B,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,CAAA;QAC3H,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAA;QACrC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;QACtB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;IACxB,CAAC;CACF;AAED,uCAAuC;AACvC,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC,CAAC,eAAe,EAAE,QAAQ,EAAE,qBAAqB,CAAC,CAAC,CAAA;AAClF,MAAM,QAAQ,GAAG,0BAA0B,CAAA;AAC3C,MAAM,WAAW,GAAG,+BAA+B,CAAA;AAEnD,4CAA4C;AAC5C,MAAM,UAAU,YAAY,CAAC,GAAW,EAAE,QAAiB;IACzD,OAAO,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,WAAW,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAA;AACzG,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,GAAW,EAAE,OAAO,GAAgE,EAAE;IACjH,MAAM,IAAI,GAAG,YAAY,CAAC,GAAG,EAAE,OAAO,CAAC,IAAI,CAAC,CAAA;IAC5C,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACtB,IAAI,OAAO,CAAC,IAAI,KAAK,SAAS;YAAE,MAAM,IAAI,gBAAgB,CAAC,IAAI,EAAE,CAAC,gBAAgB,CAAC,CAAC,CAAA;QACpF,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAA;QACtC,MAAM,QAAQ,GAAG,GAAG,CAAC,iBAAiB,CAAC,KAAK,SAAS,IAAI,GAAG,CAAC,iBAAiB,CAAC,KAAK,EAAE,CAAA;QACtF,OAAO;YACL,IAAI,EAAE,SAAS;YACf,OAAO,EAAE,eAAe;YACxB,QAAQ,EAAE;gBACR;oBACE,IAAI,EAAE,eAAe;oBACrB,WAAW,EAAE,QAAQ,CAAC,CAAC,CAAC,cAAc,iBAAiB,EAAE,CAAC,CAAC,CAAC,iBAAiB,iBAAiB,YAAY,WAAW,IAAI,aAAa,GAAG;oBACzI,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,GAAG,EAAE,iBAAiB,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;oBAC1D,OAAO,EAAE,EAAE;iBACZ;aACF;SACF,CAAA;IACH,CAAC;IACD,IAAI,KAAc,CAAA;IAClB,IAAI,CAAC;QACH,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAA;IAChD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,gBAAgB,CAAC,IAAI,EAAE,CAAC,mBAAmB,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAA;IAClH,CAAC;IACD,OAAO,aAAa,CAAC,KAAK,EAAE,IAAI,CAAC,CAAA;AACnC,CAAC;AAED,mBAAmB;AACnB,MAAM,UAAU,aAAa,CAAC,KAAc,EAAE,IAAY;IACxD,MAAM,MAAM,GAAa,EAAE,CAAA;IAC3B,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,MAAM,IAAI,gBAAgB,CAAC,IAAI,EAAE,CAAC,2CAA2C,CAAC,CAAC,CAAA;IACrG,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,IAAI,GAAG,KAAK,UAAU,IAAI,GAAG,KAAK,SAAS;YAAE,MAAM,CAAC,IAAI,CAAC,GAAG,GAAG,eAAe,CAAC,CAAA;IACrH,MAAM,QAAQ,GAAc,EAAE,CAAA;IAC9B,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1E,MAAM,CAAC,IAAI,CAAC,uDAAuD,CAAC,CAAA;IACtE,CAAC;SAAM,CAAC;QACN,KAAK,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;YACzD,MAAM,OAAO,GAAG,YAAY,CAAC,IAAI,EAAE,GAAG,EAAE,MAAM,CAAC,CAAA;YAC/C,IAAI,OAAO;gBAAE,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QACrC,CAAC;IACH,CAAC;IACD,IAAI,QAAQ,GAAG,QAAQ,CAAC,CAAC,CAAC,EAAE,IAAI,IAAI,eAAe,CAAA;IACnD,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;QAChC,IAAI,OAAO,KAAK,CAAC,OAAO,KAAK,QAAQ,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;YACzF,MAAM,CAAC,IAAI,CAAC,8CAA8C,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QACtG,CAAC;;YAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAA;IACjC,CAAC;IACD,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;QAAE,MAAM,IAAI,gBAAgB,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;IAC/D,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAA;AAC9C,CAAC;AAED,SAAS,YAAY,CAAC,IAAY,EAAE,GAAY,EAAE,MAAgB;IAChE,MAAM,EAAE,GAAG,YAAY,IAAI,EAAE,CAAA;IAC7B,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAA;IAC5B,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;QAAE,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,mCAAmC,CAAC,CAAA;IAC3E,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACnB,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,qBAAqB,CAAC,CAAA;QACvC,OAAO,SAAS,CAAA;IAClB,CAAC;IACD,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QACnC,IAAI,CAAC,CAAC,aAAa,EAAE,OAAO,EAAE,SAAS,EAAE,YAAY,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC;YAAE,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,GAAG,eAAe,CAAC,CAAA;IAChH,CAAC;IACD,MAAM,OAAO,GAAY,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,CAAA;IAC9C,IAAI,GAAG,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;QAClC,IAAI,OAAO,GAAG,CAAC,WAAW,KAAK,QAAQ;YAAE,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,gCAAgC,CAAC,CAAA;;YACtF,OAAO,CAAC,WAAW,GAAG,GAAG,CAAC,WAAW,CAAA;IAC5C,CAAC;IACD,IAAI,GAAG,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAC5B,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;QAC7B,IAAI,GAAG;YAAE,OAAO,CAAC,KAAK,GAAG,GAAG,CAAA;aACvB,IAAI,OAAO,GAAG,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;YACvC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,sFAAsF,CAAC,CAAA;QAC1G,CAAC;;YAAM,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,8CAA8C,CAAC,CAAA;IACzE,CAAC;IACD,IAAI,GAAG,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;QAC9B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC;YAAE,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,6BAA6B,CAAC,CAAA;aACtE,CAAC;YACJ,KAAK,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;gBACtD,MAAM,KAAK,GAAG,GAAG,EAAE,YAAY,MAAM,EAAE,CAAA;gBACvC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC;oBAAE,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,2BAA2B,CAAC,CAAA;gBAC/E,MAAM,GAAG,GAAG,MAAM,CAAC,CAAC,CAAC,CAAA;gBACrB,IAAI,GAAG;oBAAE,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,GAAG,CAAA;qBACjC,IAAI,OAAO,CAAC,KAAK,QAAQ;oBAAE,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,oDAAoD,CAAC,CAAA;qBACpG,IAAI,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;oBAClD,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,wFAAwF,CAAC,CAAA;gBAC/G,CAAC;;oBAAM,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;YACpC,CAAC;QACH,CAAC;IACH,CAAC;IACD,IAAI,GAAG,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;QACjC,MAAM,CAAC,GAAG,GAAG,CAAC,UAAU,CAAA;QACxB,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,IAAI,CAAC,CAAC,IAAI,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,OAAO,KAAK,SAAS,IAAI,OAAO,CAAC,CAAC,OAAO,KAAK,QAAQ,CAAC,EAAE,CAAC;YAC9H,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,6DAA6D,CAAC,CAAA;QACjF,CAAC;;YAAM,OAAO,CAAC,UAAU,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,EAAE,CAAA;IAC5G,CAAC;IACD,OAAO,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAA;AACrD,CAAC;AAED,SAAS,MAAM,CAAC,KAAc;IAC5B,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,SAAS,CAAA;IACtC,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IAC/B,OAAO,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,OAAO,KAAK,CAAC,GAAG,KAAK,QAAQ,IAAI,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,SAAS,CAAA;AACxH,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,GAAe,EAAE,IAAwB;IACnE,OAAO,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,IAAI,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,CAAA;AACnE,CAAC;AASD,mDAAmD;AACnD,MAAM,UAAU,cAAc,CAAC,OAAgB,EAAE,GAAG,GAAuC,OAAO,CAAC,GAAG;IACpG,MAAM,OAAO,GAAa,EAAE,CAAA;IAC5B,MAAM,OAAO,GAAa,EAAE,CAAA;IAC5B,MAAM,IAAI,GAAG,CAAC,GAAW,EAAU,EAAE;QACnC,MAAM,KAAK,GAAG,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAC1B,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;YACxC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC;gBAAE,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;YACrD,OAAO,EAAE,CAAA;QACX,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACnB,OAAO,KAAK,CAAA;IACd,CAAC,CAAA;IACD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IAC7D,MAAM,OAAO,GAA2B,EAAE,CAAA;IAC1C,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC;QAAE,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IAC5H,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;QAAE,MAAM,IAAI,uBAAuB,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;IAChF,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,CAAA;AACpC,CAAC;AAcD,MAAM,UAAU,gBAAgB,CAAC,GAAe,EAAE,GAAG,GAAuC,OAAO,CAAC,GAAG;IACrG,MAAM,KAAK,GAAG,CAAC,IAAY,EAAW,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,SAAS,IAAI,GAAG,CAAC,IAAI,CAAC,KAAK,EAAE,CAAA;IACpF,OAAO,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QAC5B,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,EAAE,CAC9D,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAC9F,CAAA;QACD,MAAM,IAAI,GAAgB;YACxB,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,OAAO,EAAE,CAAC,CAAC,IAAI,KAAK,GAAG,CAAC,OAAO;YAC/B,OAAO;YACP,SAAS,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,KAAK,CAAC;SAClG,CAAA;QACD,IAAI,CAAC,CAAC,WAAW,KAAK,SAAS;YAAE,IAAI,CAAC,WAAW,GAAG,CAAC,CAAC,WAAW,CAAA;QACjE,IAAI,CAAC,CAAC,KAAK;YAAE,IAAI,CAAC,KAAK,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAA;QACvE,IAAI,CAAC,CAAC,UAAU;YAAE,IAAI,CAAC,UAAU,GAAG,CAAC,CAAC,UAAU,CAAA;QAChD,OAAO,IAAI,CAAA;IACb,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,qEAAqE;AACrE,MAAM,UAAU,MAAM,CAAC,IAAY,EAAE,OAA0B;IAC7D,IAAI,GAAG,GAAG,IAAI,CAAA;IACd,KAAK,MAAM,MAAM,IAAI,CAAC,GAAG,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC;QACtE,IAAI,MAAM,KAAK,EAAE;YAAE,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;IAC3D,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;AAC7E,CAAC","sourcesContent":["import { existsSync, readFileSync } from \"node:fs\"\nimport { join, resolve } from \"node:path\"\nimport { isSegment } from \"../ids.ts\"\n\n/**\n * 호출자 프로필 (specs/inspector.md §2): 토큰 · 헤더 묶음에 이름을 붙인 것.\n * 파일에는 환경변수 이름만 적는다 (토큰 평문 금지). 값은 호출할 때마다 환경변수에서 읽고 저장 · 출력하지 않는다\n */\n\n/** <프로젝트>/inspect */\nexport const INSPECT_DIR = \"inspect\"\nexport const PROFILES_FILE = \"profiles.json\"\n/** 프로필 파일이 없을 때의 내장 프로필 이름 · 토큰 환경변수 */\nexport const DEFAULT_PROFILE = \"default\"\nexport const DEFAULT_TOKEN_ENV = \"MEXT_TOKEN\"\n/** 가린 값의 표시 */\nexport const REDACTED = \"[redacted]\"\n\n/** 환경변수 참조 */\nexport interface EnvRef {\n env: string\n}\n\nexport interface Profile {\n name: string\n description?: string\n token?: EnvRef\n /** 이름 → 문자열 (비밀 아님) 또는 환경변수 참조 */\n headers: Record<string, string | EnvRef>\n clientInfo?: { name: string; version: string }\n}\n\nexport interface ProfileSet {\n /** 읽은 파일 (절대 경로). 없음 = 내장 프로필 */\n file: string | undefined\n /** 프로필을 지정하지 않은 호출이 쓰는 이름 */\n default: string\n /** 파일에 적힌 순서 */\n profiles: Profile[]\n}\n\n/** 프로필 파일 형식 오류. issues = \"<경로>: <문제>\" */\nexport class ProfileFileError extends Error {\n readonly file: string\n readonly issues: readonly string[]\n constructor(file: string, issues: readonly string[]) {\n super(`${file}: invalid profiles file\\n${issues.map((i) => ` ${i}`).join(\"\\n\")}`)\n this.name = \"ProfileFileError\"\n this.file = file\n this.issues = issues\n }\n}\n\n/** 환경변수가 없어 그 프로필로 호출할 수 없다. 호출을 보내지 않는다 */\nexport class ProfileUnavailableError extends Error {\n readonly code = \"PROFILE_UNAVAILABLE\"\n readonly profile: string\n /** 없는 환경변수 이름 */\n readonly missing: readonly string[]\n constructor(profile: string, missing: readonly string[]) {\n super(`Profile \"${profile}\" needs environment variable${missing.length === 1 ? \"\" : \"s\"} ${missing.join(\", \")} (not set).`)\n this.name = \"ProfileUnavailableError\"\n this.profile = profile\n this.missing = missing\n }\n}\n\n/** 문자열 값을 허용하지 않는 헤더 (비밀): 환경변수 참조만 */\nconst SECRET_HEADERS = new Set([\"authorization\", \"cookie\", \"proxy-authorization\"])\nconst ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/\nconst HEADER_NAME = /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/\n\n/** <cwd>/inspect/profiles.json 또는 지정한 경로 */\nexport function profilesPath(cwd: string, override?: string): string {\n return override === undefined ? join(resolve(cwd), INSPECT_DIR, PROFILES_FILE) : resolve(cwd, override)\n}\n\n/**\n * 프로필 파일 읽기. 파일 없음 = 내장 프로필 default (MEXT_TOKEN 이 있으면 그 토큰. override 로 지정한 파일이 없으면 오류).\n * 형식 오류 = ProfileFileError (문제를 모두 모아 한 번에)\n */\nexport function loadProfiles(cwd: string, options: { file?: string; env?: Record<string, string | undefined> } = {}): ProfileSet {\n const file = profilesPath(cwd, options.file)\n if (!existsSync(file)) {\n if (options.file !== undefined) throw new ProfileFileError(file, [\"file not found\"])\n const env = options.env ?? process.env\n const hasToken = env[DEFAULT_TOKEN_ENV] !== undefined && env[DEFAULT_TOKEN_ENV] !== \"\"\n return {\n file: undefined,\n default: DEFAULT_PROFILE,\n profiles: [\n {\n name: DEFAULT_PROFILE,\n description: hasToken ? `Token from ${DEFAULT_TOKEN_ENV}` : `No token (set ${DEFAULT_TOKEN_ENV}, or add ${INSPECT_DIR}/${PROFILES_FILE})`,\n ...(hasToken ? { token: { env: DEFAULT_TOKEN_ENV } } : {}),\n headers: {},\n },\n ],\n }\n }\n let value: unknown\n try {\n value = JSON.parse(readFileSync(file, \"utf8\"))\n } catch (error) {\n throw new ProfileFileError(file, [`not valid JSON (${error instanceof Error ? error.message : String(error)})`])\n }\n return parseProfiles(value, file)\n}\n\n/** 프로필 파일 내용 검증 */\nexport function parseProfiles(value: unknown, file: string): ProfileSet {\n const issues: string[] = []\n if (!isObject(value)) throw new ProfileFileError(file, ['must be an object ({ \"profiles\": { … } })'])\n for (const key of Object.keys(value)) if (key !== \"profiles\" && key !== \"default\") issues.push(`${key}: unknown key`)\n const profiles: Profile[] = []\n if (!isObject(value.profiles) || Object.keys(value.profiles).length === 0) {\n issues.push(\"profiles: must be an object with at least one profile\")\n } else {\n for (const [name, raw] of Object.entries(value.profiles)) {\n const profile = parseProfile(name, raw, issues)\n if (profile) profiles.push(profile)\n }\n }\n let fallback = profiles[0]?.name ?? DEFAULT_PROFILE\n if (value.default !== undefined) {\n if (typeof value.default !== \"string\" || !profiles.some((p) => p.name === value.default)) {\n issues.push(`default: must be one of the profile names (${profiles.map((p) => p.name).join(\", \")})`)\n } else fallback = value.default\n }\n if (issues.length > 0) throw new ProfileFileError(file, issues)\n return { file, default: fallback, profiles }\n}\n\nfunction parseProfile(name: string, raw: unknown, issues: string[]): Profile | undefined {\n const at = `profiles.${name}`\n const before = issues.length\n if (!isSegment(name)) issues.push(`${at}: profile name must be kebab-case`)\n if (!isObject(raw)) {\n issues.push(`${at}: must be an object`)\n return undefined\n }\n for (const key of Object.keys(raw)) {\n if (![\"description\", \"token\", \"headers\", \"clientInfo\"].includes(key)) issues.push(`${at}.${key}: unknown key`)\n }\n const profile: Profile = { name, headers: {} }\n if (raw.description !== undefined) {\n if (typeof raw.description !== \"string\") issues.push(`${at}.description: must be a string`)\n else profile.description = raw.description\n }\n if (raw.token !== undefined) {\n const ref = envRef(raw.token)\n if (ref) profile.token = ref\n else if (typeof raw.token === \"string\") {\n issues.push(`${at}.token: must be { \"env\": \"<variable name>\" }. Do not write token values in this file`)\n } else issues.push(`${at}.token: must be { \"env\": \"<variable name>\" }`)\n }\n if (raw.headers !== undefined) {\n if (!isObject(raw.headers)) issues.push(`${at}.headers: must be an object`)\n else {\n for (const [header, v] of Object.entries(raw.headers)) {\n const where = `${at}.headers.${header}`\n if (!HEADER_NAME.test(header)) issues.push(`${where}: not a valid header name`)\n const ref = envRef(v)\n if (ref) profile.headers[header] = ref\n else if (typeof v !== \"string\") issues.push(`${where}: must be a string or { \"env\": \"<variable name>\" }`)\n else if (SECRET_HEADERS.has(header.toLowerCase())) {\n issues.push(`${where}: must be { \"env\": \"<variable name>\" }. Do not write secret header values in this file`)\n } else profile.headers[header] = v\n }\n }\n }\n if (raw.clientInfo !== undefined) {\n const c = raw.clientInfo\n if (!isObject(c) || typeof c.name !== \"string\" || c.name === \"\" || (c.version !== undefined && typeof c.version !== \"string\")) {\n issues.push(`${at}.clientInfo: must be { \"name\": string, \"version\"?: string }`)\n } else profile.clientInfo = { name: c.name, version: typeof c.version === \"string\" ? c.version : \"0.0.0\" }\n }\n return issues.length > before ? undefined : profile\n}\n\nfunction envRef(value: unknown): EnvRef | undefined {\n if (!isObject(value)) return undefined\n const keys = Object.keys(value)\n return keys.length === 1 && typeof value.env === \"string\" && ENV_NAME.test(value.env) ? { env: value.env } : undefined\n}\n\nexport function findProfile(set: ProfileSet, name: string | undefined): Profile | undefined {\n return set.profiles.find((p) => p.name === (name ?? set.default))\n}\n\n/** 호출 직전에 읽은 값. secrets = 가려야 하는 값 (환경변수에서 읽은 것 전부) */\nexport interface ResolvedProfile {\n token: string | undefined\n headers: Record<string, string>\n secrets: string[]\n}\n\n/** 환경변수에서 값 읽기. 없는 변수 = ProfileUnavailableError */\nexport function resolveProfile(profile: Profile, env: Record<string, string | undefined> = process.env): ResolvedProfile {\n const missing: string[] = []\n const secrets: string[] = []\n const read = (ref: EnvRef): string => {\n const value = env[ref.env]\n if (value === undefined || value === \"\") {\n if (!missing.includes(ref.env)) missing.push(ref.env)\n return \"\"\n }\n secrets.push(value)\n return value\n }\n const token = profile.token ? read(profile.token) : undefined\n const headers: Record<string, string> = {}\n for (const [name, value] of Object.entries(profile.headers)) headers[name] = typeof value === \"string\" ? value : read(value)\n if (missing.length > 0) throw new ProfileUnavailableError(profile.name, missing)\n return { token, headers, secrets }\n}\n\n/** 표시용 프로필 (값 없음): 토큰 환경변수 이름 · 설정 여부 · 헤더 (비밀 = 환경변수 이름) */\nexport interface ProfileView {\n name: string\n description?: string\n default: boolean\n token?: { env: string; set: boolean }\n headers: { name: string; value?: string; env?: string; set?: boolean }[]\n clientInfo?: { name: string; version: string }\n /** 필요한 환경변수가 모두 있다 */\n available: boolean\n}\n\nexport function describeProfiles(set: ProfileSet, env: Record<string, string | undefined> = process.env): ProfileView[] {\n const isSet = (name: string): boolean => env[name] !== undefined && env[name] !== \"\"\n return set.profiles.map((p) => {\n const headers = Object.entries(p.headers).map(([name, value]) =>\n typeof value === \"string\" ? { name, value } : { name, env: value.env, set: isSet(value.env) },\n )\n const view: ProfileView = {\n name: p.name,\n default: p.name === set.default,\n headers,\n available: (p.token === undefined || isSet(p.token.env)) && headers.every((h) => h.set !== false),\n }\n if (p.description !== undefined) view.description = p.description\n if (p.token) view.token = { env: p.token.env, set: isSet(p.token.env) }\n if (p.clientInfo) view.clientInfo = p.clientInfo\n return view\n })\n}\n\n/** 문자열 안의 비밀 값을 [redacted] 로 (긴 값부터: 한 값이 다른 값의 일부일 때 긴 쪽이 남지 않게) */\nexport function redact(text: string, secrets: readonly string[]): string {\n let out = text\n for (const secret of [...secrets].sort((a, b) => b.length - a.length)) {\n if (secret !== \"\") out = out.split(secret).join(REDACTED)\n }\n return out\n}\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value)\n}\n"]}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { type AgentClient, type AgentExpect, type AgentObservation } from "./agent.ts";
|
|
2
|
+
import { type Failure } from "./compare.ts";
|
|
3
|
+
import { type Answers, type CallOutcome, type CallRecord, type ErrorBody, type Inspector } from "./inspector.ts";
|
|
4
|
+
import { type ProfileSet } from "./profiles.ts";
|
|
5
|
+
/**
|
|
6
|
+
* 시나리오 (specs/inspector.md §4): 호출 순서 + 기대 결과를 적은 JSON 파일. 사람 · CLI · 웹 UI · 에이전트가 같은 파일을 쓴다.
|
|
7
|
+
* 코드 실행 없음 (값 비교만)
|
|
8
|
+
*/
|
|
9
|
+
/** <프로젝트>/inspect/scenarios */
|
|
10
|
+
export declare const SCENARIOS_DIR = "scenarios";
|
|
11
|
+
export interface Expect {
|
|
12
|
+
/** true = 성공해야 한다 · false = 오류여야 한다 */
|
|
13
|
+
ok?: boolean;
|
|
14
|
+
/** 성공 값의 일부 */
|
|
15
|
+
value?: unknown;
|
|
16
|
+
/** 오류 code, 또는 오류 본문의 일부 */
|
|
17
|
+
error?: string | Record<string, unknown>;
|
|
18
|
+
/** 이 호출의 감사 이벤트 목록 (trace ID 가 같은 것) 의 일부 비교 */
|
|
19
|
+
audit?: Record<string, unknown>[];
|
|
20
|
+
}
|
|
21
|
+
export interface CallStep {
|
|
22
|
+
name?: string;
|
|
23
|
+
as?: string;
|
|
24
|
+
call: string;
|
|
25
|
+
input?: Record<string, unknown>;
|
|
26
|
+
confirmed?: boolean;
|
|
27
|
+
answers?: Answers;
|
|
28
|
+
decline?: boolean;
|
|
29
|
+
expect?: Expect;
|
|
30
|
+
/** 변수 → 성공 값의 점 구분 경로 */
|
|
31
|
+
save?: Record<string, string>;
|
|
32
|
+
}
|
|
33
|
+
export interface CommandsStep {
|
|
34
|
+
name?: string;
|
|
35
|
+
as?: string;
|
|
36
|
+
commands: {
|
|
37
|
+
visible?: string[];
|
|
38
|
+
hidden?: string[];
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** 에이전트에게 지시하고 서버의 감사 이벤트를 비교 (§7) */
|
|
42
|
+
export interface AgentStep {
|
|
43
|
+
name?: string;
|
|
44
|
+
/** 지시문 ({{변수}} 치환) */
|
|
45
|
+
agent: string;
|
|
46
|
+
/** 이 에이전트의 호출을 가려내는 조건 (이벤트의 일부) */
|
|
47
|
+
from?: Record<string, unknown>;
|
|
48
|
+
expect?: AgentExpect;
|
|
49
|
+
timeoutMs?: number;
|
|
50
|
+
}
|
|
51
|
+
export type ScenarioStep = CallStep | CommandsStep | AgentStep;
|
|
52
|
+
/** agent 단계 결과의 as */
|
|
53
|
+
export declare const AGENT_AS = "agent";
|
|
54
|
+
export interface Scenario {
|
|
55
|
+
/** 읽은 파일 (절대 경로) */
|
|
56
|
+
file?: string;
|
|
57
|
+
name: string;
|
|
58
|
+
description?: string;
|
|
59
|
+
as?: string;
|
|
60
|
+
steps: ScenarioStep[];
|
|
61
|
+
}
|
|
62
|
+
export type { Failure };
|
|
63
|
+
export interface StepResult {
|
|
64
|
+
/** 1부터 */
|
|
65
|
+
step: number;
|
|
66
|
+
name: string;
|
|
67
|
+
as: string;
|
|
68
|
+
pass: boolean;
|
|
69
|
+
durationMs: number;
|
|
70
|
+
/** call = 성공 값 또는 오류 본문 · commands = 보인 명령어 ID · agent = 관찰 (감사 이벤트 · 에이전트의 말) */
|
|
71
|
+
outcome: CallOutcome | {
|
|
72
|
+
ok: true;
|
|
73
|
+
commands: string[];
|
|
74
|
+
} | AgentObservation | undefined;
|
|
75
|
+
failures: Failure[];
|
|
76
|
+
/** call 단계의 호출 기록 ID (Inspector.history) */
|
|
77
|
+
record?: number;
|
|
78
|
+
}
|
|
79
|
+
export interface ScenarioResult {
|
|
80
|
+
name: string;
|
|
81
|
+
file?: string;
|
|
82
|
+
pass: boolean;
|
|
83
|
+
durationMs: number;
|
|
84
|
+
steps: StepResult[];
|
|
85
|
+
}
|
|
86
|
+
/** 시나리오 파일 형식 오류. issues = "<위치>: <문제>" */
|
|
87
|
+
export declare class ScenarioFileError extends Error {
|
|
88
|
+
readonly file: string;
|
|
89
|
+
readonly issues: readonly string[];
|
|
90
|
+
constructor(file: string, issues: readonly string[]);
|
|
91
|
+
}
|
|
92
|
+
/** 경로 · 폴더 → 시나리오 파일 목록 (폴더 = 그 안의 *.json, 이름 순). 경로 없음 = <cwd>/inspect/scenarios */
|
|
93
|
+
export declare function scenarioFiles(cwd: string, paths: readonly string[]): string[];
|
|
94
|
+
/** 파일 읽기 + 검증. profiles 를 주면 없는 프로필 이름도 오류 */
|
|
95
|
+
export declare function loadScenario(file: string, profiles?: ProfileSet): Scenario;
|
|
96
|
+
/** 시나리오 내용 검증 (문제를 모두 모아 한 번에) */
|
|
97
|
+
export declare function parseScenario(value: unknown, file: string, profiles?: ProfileSet): Scenario;
|
|
98
|
+
export interface RunScenarioOptions {
|
|
99
|
+
/** 첫 실패에서 멈춤 */
|
|
100
|
+
bail?: boolean;
|
|
101
|
+
signal?: AbortSignal;
|
|
102
|
+
onStep?: (result: StepResult) => void;
|
|
103
|
+
/** 단계를 시작할 때 (오래 걸리는 agent 단계의 진행 표시) */
|
|
104
|
+
onStepStart?: (info: {
|
|
105
|
+
step: number;
|
|
106
|
+
name: string;
|
|
107
|
+
as: string;
|
|
108
|
+
}) => void;
|
|
109
|
+
/** agent 단계가 지시할 에이전트 (§7). 없음 = agent 단계 실패 (AGENT_UNAVAILABLE). close 는 만든 쪽이 한다 */
|
|
110
|
+
agent?: AgentClient;
|
|
111
|
+
/** agent 단계: idle 뒤 감사 이벤트를 기다리는 시간 (기본 500ms) */
|
|
112
|
+
agentSettleMs?: number;
|
|
113
|
+
}
|
|
114
|
+
/** 단계 순서대로 실행. 실패한 단계가 있어도 끝까지 (bail 제외) */
|
|
115
|
+
export declare function runScenario(inspector: Inspector, scenario: Scenario, options?: RunScenarioOptions): Promise<ScenarioResult>;
|
|
116
|
+
/** 목록 조회 실패 → 오류 본문 */
|
|
117
|
+
export declare function listErrorBody(error: unknown): ErrorBody;
|
|
118
|
+
/** 기대 결과 비교 (expect 없음 = 통과) */
|
|
119
|
+
export declare function compare(expect: Expect | undefined, outcome: CallOutcome, failures: Failure[]): void;
|
|
120
|
+
/** 호출 기록 → 시나리오 단계 (관찰한 결과를 기대 결과로). 오류 = code · reason 만 */
|
|
121
|
+
export declare function stepFromRecord(record: CallRecord, options?: {
|
|
122
|
+
answers?: Answers;
|
|
123
|
+
decline?: boolean;
|
|
124
|
+
}): CallStep;
|
|
125
|
+
/** 시나리오 파일 끝에 단계를 덧붙인다 (파일이 없으면 만든다). 반환 = 단계 수 */
|
|
126
|
+
export declare function appendStep(file: string, step: ScenarioStep): number;
|