@moikapy/lich 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +84 -4
  3. package/dist/chunk-JYURFAGB.js +92 -0
  4. package/dist/chunk-JYURFAGB.js.map +1 -0
  5. package/dist/{chunk-CV2YH3FH.js → chunk-QVJCIZIF.js} +748 -13
  6. package/dist/chunk-QVJCIZIF.js.map +1 -0
  7. package/dist/chunk-SAEB3QL3.js +58 -0
  8. package/dist/chunk-SAEB3QL3.js.map +1 -0
  9. package/dist/cli.js +263 -22
  10. package/dist/cli.js.map +1 -1
  11. package/dist/{gateway-W6S43ETE.js → gateway-5BG3YCZF.js} +2 -2
  12. package/dist/index.d.ts +229 -118
  13. package/dist/index.js +5 -3
  14. package/dist/{tui-2VO6LAFM.js → tui-L6RABP2J.js} +43 -41
  15. package/dist/tui-L6RABP2J.js.map +1 -0
  16. package/docs/.vitepress/config.mts +6 -1
  17. package/docs/architecture/agent-loop.md +5 -3
  18. package/docs/architecture/overview.md +36 -22
  19. package/docs/architecture/plugins.md +1 -1
  20. package/docs/architecture/tools.md +7 -2
  21. package/docs/getting-started.md +9 -7
  22. package/docs/index.md +6 -4
  23. package/docs/user-guide/cli.md +24 -9
  24. package/docs/user-guide/games.md +93 -0
  25. package/docs/user-guide/godot.md +3 -1
  26. package/docs/user-guide/library.md +8 -2
  27. package/docs/user-guide/plugins.md +2 -2
  28. package/docs/user-guide/redot.md +93 -0
  29. package/docs/user-guide/tui.md +11 -11
  30. package/examples/game_bridge/README.md +2 -0
  31. package/optional-mcps/godot/manifest.json +6 -0
  32. package/optional-mcps/redot/manifest.json +18 -0
  33. package/package.json +2 -1
  34. package/dist/chunk-6M6OAQGN.js +0 -17
  35. package/dist/chunk-6M6OAQGN.js.map +0 -1
  36. package/dist/chunk-CV2YH3FH.js.map +0 -1
  37. package/dist/tui-2VO6LAFM.js.map +0 -1
  38. /package/dist/{gateway-W6S43ETE.js.map → gateway-5BG3YCZF.js.map} +0 -0
@@ -1912,12 +1912,178 @@ function read_platform_token(config, platform) {
1912
1912
  return value;
1913
1913
  }
1914
1914
 
1915
+ // src/mcp/mcp_pin.ts
1916
+ import path11 from "path";
1917
+
1918
+ // src/mcp/mcp_catalog.ts
1919
+ import { existsSync as existsSync2, readFileSync as readFileSync4, readdirSync as readdirSync3 } from "fs";
1920
+ import path9 from "path";
1921
+ import { fileURLToPath as fileURLToPath2 } from "url";
1922
+ var cached;
1923
+ function catalog_dir() {
1924
+ let dir = path9.dirname(fileURLToPath2(import.meta.url));
1925
+ for (let hop = 0; hop < 6; hop += 1) {
1926
+ const candidate = path9.join(dir, "optional-mcps");
1927
+ if (existsSync2(path9.join(candidate, "redot", "manifest.json")) === true) {
1928
+ return candidate;
1929
+ }
1930
+ dir = path9.dirname(dir);
1931
+ }
1932
+ throw new Error("mcp catalog not found");
1933
+ }
1934
+ function read_manifest(file) {
1935
+ const parsed = safe_json_parse(readFileSync4(file, "utf8"));
1936
+ if (parsed === void 0 || typeof parsed.name !== "string") {
1937
+ throw new Error("mcp catalog manifest rejected");
1938
+ }
1939
+ return parsed;
1940
+ }
1941
+ function load_catalog() {
1942
+ if (cached !== void 0) {
1943
+ return cached;
1944
+ }
1945
+ const manifests = [];
1946
+ for (const name of readdirSync3(catalog_dir())) {
1947
+ const file = path9.join(catalog_dir(), name, "manifest.json");
1948
+ if (existsSync2(file) === true) {
1949
+ manifests.push(read_manifest(file));
1950
+ }
1951
+ }
1952
+ cached = manifests;
1953
+ return cached;
1954
+ }
1955
+ function catalog_by_name(name) {
1956
+ return load_catalog().find((entry) => entry.name === name);
1957
+ }
1958
+
1959
+ // src/mcp/mcp_refuse.ts
1960
+ import path10 from "path";
1961
+ var SHELL = /[;&|`$<>]/;
1962
+ var DOWNLOADERS = /* @__PURE__ */ new Set(["npx", "npm", "bunx", "uvx", "curl", "wget"]);
1963
+ function refuse_stdio_command(command) {
1964
+ if (command.includes("://") === true) {
1965
+ return "refused url; only a local binary is allowed";
1966
+ }
1967
+ if (SHELL.test(command) === true) {
1968
+ return "refused shell metacharacters in mcp command";
1969
+ }
1970
+ if (command.length === 0 || command.trim() !== command || /\s/.test(command) === true) {
1971
+ return "refused mcp command";
1972
+ }
1973
+ const base = path10.basename(command);
1974
+ if (DOWNLOADERS.has(base) === true) {
1975
+ return `refused download command '${base}'`;
1976
+ }
1977
+ return void 0;
1978
+ }
1979
+ function refuse_stdio_arg(arg) {
1980
+ if (arg.includes("://") === true) {
1981
+ return "refused url in mcp args";
1982
+ }
1983
+ if (SHELL.test(arg) === true) {
1984
+ return "refused shell metacharacters in mcp args";
1985
+ }
1986
+ return void 0;
1987
+ }
1988
+
1989
+ // src/mcp/mcp_url.ts
1990
+ function refuse_http_url(url) {
1991
+ let parsed;
1992
+ try {
1993
+ parsed = new URL(url);
1994
+ } catch {
1995
+ return "refused mcp url";
1996
+ }
1997
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
1998
+ return "refused mcp url";
1999
+ }
2000
+ if (parsed.username.length > 0 || parsed.password.length > 0) {
2001
+ return "refused mcp url credentials";
2002
+ }
2003
+ const host = parsed.hostname.toLowerCase();
2004
+ if (host !== "127.0.0.1" && host !== "localhost") {
2005
+ return "refused mcp url; loopback only";
2006
+ }
2007
+ return void 0;
2008
+ }
2009
+
2010
+ // src/mcp/mcp_pin.ts
2011
+ function refuse_catalog_stdio(name, command, args) {
2012
+ const pin = catalog_by_name(name);
2013
+ if (pin?.command_basename === void 0) {
2014
+ return void 0;
2015
+ }
2016
+ if (path11.basename(command) !== pin.command_basename) {
2017
+ return `refused command basename '${path11.basename(command)}'; only '${pin.command_basename}' is allowed`;
2018
+ }
2019
+ const prefix = pin.args_prefix ?? [];
2020
+ if (args.length !== prefix.length + 1) {
2021
+ return `refused ${name} args; expected ${prefix.join(" ")} <project>`;
2022
+ }
2023
+ for (let index = 0; index < prefix.length; index += 1) {
2024
+ if (args[index] !== prefix[index]) {
2025
+ return `refused ${name} args; expected ${prefix.join(" ")} <project>`;
2026
+ }
2027
+ }
2028
+ const project = args[prefix.length];
2029
+ if (project === void 0 || project.length === 0 || project.includes("://") === true) {
2030
+ return "refused project path; pass a local project directory";
2031
+ }
2032
+ return void 0;
2033
+ }
2034
+ function refuse_arg_list(args) {
2035
+ for (const arg of args) {
2036
+ const refused = refuse_stdio_arg(arg);
2037
+ if (refused !== void 0) {
2038
+ return refused;
2039
+ }
2040
+ }
2041
+ return void 0;
2042
+ }
2043
+ function refuse_mcp_entry(name, entry) {
2044
+ if (typeof entry.url === "string") {
2045
+ return refuse_http_url(entry.url);
2046
+ }
2047
+ if (typeof entry.command !== "string") {
2048
+ return "refused mcp entry";
2049
+ }
2050
+ const args = entry.args ?? [];
2051
+ return refuse_stdio_command(entry.command) ?? refuse_catalog_stdio(name, entry.command, args) ?? refuse_arg_list(args);
2052
+ }
2053
+
1915
2054
  // src/agent/config.ts
1916
2055
  var gateway_schema = z.object({
1917
2056
  platforms: z.array(z.enum(["webhook", "telegram", "discord", "twitch"])).default([]),
1918
2057
  /** Env-var names that hold tokens. Never store the secrets themselves. */
1919
2058
  token_envs: z.record(z.string(), z.string().regex(ENV_VAR_NAME, "invalid env var name")).default({})
1920
2059
  }).optional();
2060
+ var stdio_mcp_schema = z.object({
2061
+ enabled: z.boolean().default(false),
2062
+ command: z.string().min(1),
2063
+ args: z.array(z.string()).default([]),
2064
+ env: z.record(z.string().regex(ENV_VAR_NAME, "invalid env var name"), z.string()).optional()
2065
+ }).strict();
2066
+ var http_mcp_schema = z.object({
2067
+ enabled: z.boolean().default(false),
2068
+ url: z.string().min(1)
2069
+ }).strict();
2070
+ var mcp_server_schema = z.union([stdio_mcp_schema, http_mcp_schema]);
2071
+ var SERVER_NAME = /^[a-z][a-z0-9_]*$/;
2072
+ var mcp_servers_schema = z.record(z.string(), mcp_server_schema).superRefine((servers, ctx) => {
2073
+ for (const [name, entry] of Object.entries(servers)) {
2074
+ if (SERVER_NAME.test(name) === false) {
2075
+ ctx.addIssue({
2076
+ code: z.ZodIssueCode.custom,
2077
+ path: [name],
2078
+ message: "mcp server name must be snake_case"
2079
+ });
2080
+ }
2081
+ const refused = refuse_mcp_entry(name, entry);
2082
+ if (refused !== void 0) {
2083
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: [name], message: refused });
2084
+ }
2085
+ }
2086
+ }).optional();
1921
2087
  var provider_schema = z.object({
1922
2088
  kind: z.enum(["openai_compat", "anthropic", "ollama"]),
1923
2089
  name: z.string().min(1),
@@ -1930,7 +2096,7 @@ var provider_schema = z.object({
1930
2096
  fetch_fn: z.custom(() => true).optional()
1931
2097
  }).passthrough();
1932
2098
  var agent_config_schema = z.object({
1933
- /** Display name used by the TUI banner. */
2099
+ /** Wizard label. The TUI banner uses the active theme welcome string. */
1934
2100
  agent_name: z.string().min(1).default("lich"),
1935
2101
  system_prompt: z.string().optional(),
1936
2102
  max_turns: z.number().int().min(1).default(25),
@@ -1946,7 +2112,10 @@ var agent_config_schema = z.object({
1946
2112
  /** Plugin entry module specifiers, relative to work_dir or absolute. */
1947
2113
  plugins: z.array(z.string()).default([]),
1948
2114
  gateway: gateway_schema,
1949
- log_level: z.enum(["debug", "info", "warn", "error"]).default("info")
2115
+ log_level: z.enum(["debug", "info", "warn", "error"]).default("info"),
2116
+ theme: z.string().min(1).default("lich"),
2117
+ /** Named MCP servers. Each entry is stdio or loopback http. Default off. */
2118
+ mcp_servers: mcp_servers_schema
1950
2119
  }).transform((config) => {
1951
2120
  const work_dir = config.work_dir ?? process.cwd();
1952
2121
  return {
@@ -1967,6 +2136,18 @@ function freeze_config(config) {
1967
2136
  Object.freeze(config.gateway.token_envs);
1968
2137
  Object.freeze(config.gateway);
1969
2138
  }
2139
+ if (config.mcp_servers !== void 0) {
2140
+ for (const entry of Object.values(config.mcp_servers)) {
2141
+ if ("args" in entry) {
2142
+ Object.freeze(entry.args);
2143
+ }
2144
+ if ("env" in entry && entry.env !== void 0) {
2145
+ Object.freeze(entry.env);
2146
+ }
2147
+ Object.freeze(entry);
2148
+ }
2149
+ Object.freeze(config.mcp_servers);
2150
+ }
1970
2151
  return config;
1971
2152
  }
1972
2153
  function parse_agent_config(raw) {
@@ -1999,15 +2180,543 @@ var AgentEmitter = class {
1999
2180
  }
2000
2181
  };
2001
2182
 
2002
- // src/plugins/builtin/gatekeeper.plugin.ts
2183
+ // src/mcp/mcp_http.ts
2184
+ async function post_rpc(url, fetch_fn, body) {
2185
+ const response = await fetch_fn(url, {
2186
+ method: "POST",
2187
+ redirect: "error",
2188
+ headers: { "content-type": "application/json", accept: "application/json" },
2189
+ body: JSON.stringify(body)
2190
+ });
2191
+ const parsed = await response.json();
2192
+ if (parsed.error !== void 0) {
2193
+ const detail = parsed.error.message;
2194
+ throw new Error(typeof detail === "string" && detail.length > 0 ? detail : "mcp error");
2195
+ }
2196
+ return parsed.result;
2197
+ }
2198
+ function http_pipe(url, fetch_fn) {
2199
+ let next_id = 1;
2200
+ return {
2201
+ request(method, params) {
2202
+ const id = next_id;
2203
+ next_id += 1;
2204
+ return post_rpc(url, fetch_fn, { jsonrpc: "2.0", id, method, params });
2205
+ },
2206
+ notify(method) {
2207
+ void fetch_fn(url, {
2208
+ method: "POST",
2209
+ redirect: "error",
2210
+ headers: { "content-type": "application/json" },
2211
+ body: JSON.stringify({ jsonrpc: "2.0", method })
2212
+ }).catch(() => void 0);
2213
+ },
2214
+ close() {
2215
+ return void 0;
2216
+ }
2217
+ };
2218
+ }
2219
+
2220
+ // src/mcp/mcp_plan.ts
2221
+ import { existsSync as existsSync3 } from "fs";
2222
+ import path12 from "path";
2223
+ function find_on_path(command, env_path) {
2224
+ if (env_path === void 0 || env_path.length === 0) {
2225
+ return void 0;
2226
+ }
2227
+ for (const dir of env_path.split(path12.delimiter)) {
2228
+ if (dir.length === 0) {
2229
+ continue;
2230
+ }
2231
+ const candidate = path12.join(dir, command);
2232
+ if (existsSync3(candidate) === true) {
2233
+ return candidate;
2234
+ }
2235
+ }
2236
+ return void 0;
2237
+ }
2238
+ function locate(command, env_path) {
2239
+ if (command.includes("/") === true || command.includes("\\") === true) {
2240
+ return existsSync3(command) === true ? command : void 0;
2241
+ }
2242
+ return find_on_path(command, env_path);
2243
+ }
2244
+ function plan_stdio(name, command, args, env_path) {
2245
+ const refused = refuse_mcp_entry(name, { command, args });
2246
+ if (refused !== void 0) {
2247
+ return refused;
2248
+ }
2249
+ const binary = locate(command, env_path);
2250
+ if (binary === void 0) {
2251
+ return catalog_by_name(name)?.missing_hint ?? "mcp command not found";
2252
+ }
2253
+ return { command: binary, args };
2254
+ }
2255
+
2256
+ // src/mcp/mcp_pipe.ts
2257
+ var SKIP_LIMIT = 32;
2258
+ function parse_rpc_line(line) {
2259
+ const parsed = safe_json_parse(line);
2260
+ if (typeof parsed !== "object" || parsed === null) {
2261
+ return void 0;
2262
+ }
2263
+ return parsed;
2264
+ }
2265
+ async function read_id(child, id) {
2266
+ for (let skipped = 0; skipped < SKIP_LIMIT; skipped += 1) {
2267
+ const line = await child.read_line();
2268
+ const failure = child.failed();
2269
+ if (failure !== void 0) {
2270
+ throw new Error(failure);
2271
+ }
2272
+ if (line === void 0) {
2273
+ throw new Error("mcp closed the pipe");
2274
+ }
2275
+ const parsed = parse_rpc_line(line);
2276
+ if (parsed === void 0 || parsed.id !== id) {
2277
+ continue;
2278
+ }
2279
+ if (parsed.error !== void 0) {
2280
+ const detail = parsed.error.message;
2281
+ throw new Error(typeof detail === "string" && detail.length > 0 ? detail : "mcp error");
2282
+ }
2283
+ return parsed.result;
2284
+ }
2285
+ throw new Error("mcp sent no matching response");
2286
+ }
2287
+ function stdio_pipe(child) {
2288
+ let next_id = 1;
2289
+ return {
2290
+ request(method, params) {
2291
+ const id = next_id;
2292
+ next_id += 1;
2293
+ child.write_line(JSON.stringify({ jsonrpc: "2.0", id, method, params }));
2294
+ return read_id(child, id);
2295
+ },
2296
+ notify(method) {
2297
+ child.write_line(JSON.stringify({ jsonrpc: "2.0", method }));
2298
+ },
2299
+ close() {
2300
+ child.stop();
2301
+ }
2302
+ };
2303
+ }
2304
+
2305
+ // src/mcp/mcp_names.ts
2306
+ function mcp_tool_name(server, tool) {
2307
+ return `mcp_${sanitize(server)}_${sanitize(tool)}`;
2308
+ }
2309
+ function sanitize(value) {
2310
+ return value.replace(/[^a-zA-Z0-9_]/g, "_");
2311
+ }
2312
+
2313
+ // src/mcp/mcp_register.ts
2314
+ function name_allowed(enabled, name) {
2315
+ if (enabled === "all") {
2316
+ return true;
2317
+ }
2318
+ return enabled.includes(name);
2319
+ }
2320
+ function run_call(session, wire_name, args, context) {
2321
+ return capture_errors(async () => {
2322
+ if (context.signal?.aborted === true) {
2323
+ throw new Error("cancelled");
2324
+ }
2325
+ return { ok: true, output: await session.call_tool(wire_name, args) };
2326
+ });
2327
+ }
2328
+ function tool_for(registered, wire_name, spec, session) {
2329
+ return {
2330
+ name: registered,
2331
+ description: spec.description,
2332
+ parameters: spec.parameters,
2333
+ timeout_ms: 12e4,
2334
+ execute: (args, context) => run_call(session, wire_name, args, context)
2335
+ };
2336
+ }
2337
+ function register_listed(registry, server, listed, enabled, session) {
2338
+ const excluded = new Set(catalog_by_name(server)?.exclude_tools ?? []);
2339
+ for (const spec of listed) {
2340
+ if (excluded.has(spec.name) === true) {
2341
+ continue;
2342
+ }
2343
+ const registered = mcp_tool_name(server, spec.name);
2344
+ if (name_allowed(enabled, registered) === false || registry.has(registered) === true) {
2345
+ continue;
2346
+ }
2347
+ registry.register(tool_for(registered, spec.name, spec, session));
2348
+ }
2349
+ }
2350
+
2351
+ // src/mcp/mcp_handshake.ts
2352
+ var PROTOCOL_VERSION = "2024-11-05";
2353
+ function init_params() {
2354
+ return {
2355
+ protocolVersion: PROTOCOL_VERSION,
2356
+ capabilities: {},
2357
+ clientInfo: { name: "lich", version: "1" }
2358
+ };
2359
+ }
2360
+ function assert_handshake(result) {
2361
+ if (typeof result !== "object" || result === null) {
2362
+ throw new Error("mcp handshake rejected");
2363
+ }
2364
+ const body = result;
2365
+ if (typeof body.protocolVersion !== "string" || body.protocolVersion.length === 0) {
2366
+ throw new Error("mcp handshake rejected");
2367
+ }
2368
+ if (typeof body.serverInfo?.name !== "string" || body.serverInfo.name.length === 0) {
2369
+ throw new Error("mcp handshake rejected");
2370
+ }
2371
+ }
2372
+
2373
+ // src/mcp/mcp_content.ts
2374
+ function content_text(result) {
2375
+ if (typeof result !== "object" || result === null) {
2376
+ return "";
2377
+ }
2378
+ const body = result;
2379
+ const parts = [];
2380
+ if (Array.isArray(body.content) === true) {
2381
+ for (const item of body.content) {
2382
+ if (typeof item !== "object" || item === null) {
2383
+ continue;
2384
+ }
2385
+ const chunk = item;
2386
+ if (chunk.type === "text" && typeof chunk.text === "string") {
2387
+ parts.push(chunk.text);
2388
+ }
2389
+ if (chunk.type === "image") {
2390
+ parts.push("[image omitted]");
2391
+ }
2392
+ }
2393
+ }
2394
+ const text = parts.join("\n");
2395
+ if (body.isError === true) {
2396
+ throw new Error(text.length > 0 ? text : "mcp tool failed");
2397
+ }
2398
+ return text;
2399
+ }
2400
+
2401
+ // src/mcp/mcp_result.ts
2402
+ function tool_schema(raw) {
2403
+ if (typeof raw !== "object" || raw === null) {
2404
+ return { type: "object" };
2405
+ }
2406
+ const body = raw;
2407
+ const schema = { type: "object" };
2408
+ if (typeof body.properties === "object" && body.properties !== null) {
2409
+ schema.properties = body.properties;
2410
+ }
2411
+ if (Array.isArray(body.required) === true) {
2412
+ schema.required = body.required.filter((item) => typeof item === "string");
2413
+ }
2414
+ if (typeof body.additionalProperties === "boolean") {
2415
+ schema.additionalProperties = body.additionalProperties;
2416
+ }
2417
+ return schema;
2418
+ }
2419
+ function parse_tools(result) {
2420
+ if (typeof result !== "object" || result === null || Array.isArray(result.tools) === false) {
2421
+ throw new Error("mcp tools/list rejected");
2422
+ }
2423
+ const tools = [];
2424
+ for (const item of result.tools) {
2425
+ if (typeof item !== "object" || item === null) {
2426
+ continue;
2427
+ }
2428
+ const tool = item;
2429
+ if (typeof tool.name !== "string" || tool.name.length === 0) {
2430
+ continue;
2431
+ }
2432
+ tools.push({
2433
+ name: tool.name,
2434
+ description: typeof tool.description === "string" ? tool.description : tool.name,
2435
+ parameters: tool_schema(tool.inputSchema)
2436
+ });
2437
+ }
2438
+ return tools;
2439
+ }
2440
+
2441
+ // src/mcp/mcp_session.ts
2442
+ var McpSession = class {
2443
+ constructor(pipe) {
2444
+ this.pipe = pipe;
2445
+ }
2446
+ pipe;
2447
+ ready_done = false;
2448
+ closed = false;
2449
+ close() {
2450
+ if (this.closed === true) {
2451
+ return;
2452
+ }
2453
+ this.closed = true;
2454
+ this.pipe.close();
2455
+ }
2456
+ async list_tools() {
2457
+ await this.ensure_ready();
2458
+ return parse_tools(await this.pipe.request("tools/list", {}));
2459
+ }
2460
+ async call_tool(name, args) {
2461
+ await this.ensure_ready();
2462
+ return content_text(await this.pipe.request("tools/call", { name, arguments: args }));
2463
+ }
2464
+ async ensure_ready() {
2465
+ if (this.ready_done === true) {
2466
+ return;
2467
+ }
2468
+ try {
2469
+ assert_handshake(await this.pipe.request("initialize", init_params()));
2470
+ this.pipe.notify("notifications/initialized");
2471
+ this.ready_done = true;
2472
+ } catch (error) {
2473
+ this.close();
2474
+ throw error;
2475
+ }
2476
+ }
2477
+ };
2478
+
2479
+ // src/mcp/mcp_lines.ts
2480
+ function create_line_queue() {
2481
+ const pending = [];
2482
+ const waiters = [];
2483
+ let closed = false;
2484
+ return {
2485
+ push(line) {
2486
+ const waiter = waiters.shift();
2487
+ if (waiter !== void 0) {
2488
+ waiter(line);
2489
+ return;
2490
+ }
2491
+ pending.push(line);
2492
+ },
2493
+ close() {
2494
+ closed = true;
2495
+ for (const waiter of waiters.splice(0)) {
2496
+ waiter(void 0);
2497
+ }
2498
+ },
2499
+ read() {
2500
+ const next = pending.shift();
2501
+ if (next !== void 0) {
2502
+ return Promise.resolve(next);
2503
+ }
2504
+ if (closed === true) {
2505
+ return Promise.resolve(void 0);
2506
+ }
2507
+ return new Promise((resolve) => {
2508
+ waiters.push(resolve);
2509
+ });
2510
+ }
2511
+ };
2512
+ }
2513
+
2514
+ // src/mcp/mcp_child.ts
2515
+ function spawn_failure(code) {
2516
+ if (code === "ENOENT") {
2517
+ return "mcp command not found";
2518
+ }
2519
+ return "mcp spawn failed";
2520
+ }
2521
+ function failed_child(message) {
2522
+ return {
2523
+ write_line() {
2524
+ return void 0;
2525
+ },
2526
+ read_line() {
2527
+ return Promise.resolve(void 0);
2528
+ },
2529
+ stop() {
2530
+ return void 0;
2531
+ },
2532
+ failed() {
2533
+ return message;
2534
+ }
2535
+ };
2536
+ }
2537
+
2538
+ // src/mcp/mcp_stdio_bun.ts
2539
+ async function pump_stdout(stream, queue) {
2540
+ const reader = stream.getReader();
2541
+ const decoder = new TextDecoder();
2542
+ let buffer = "";
2543
+ for (; ; ) {
2544
+ const next = await reader.read();
2545
+ if (next.done === true) {
2546
+ if (buffer.length > 0) {
2547
+ queue.push(buffer);
2548
+ }
2549
+ queue.close();
2550
+ return;
2551
+ }
2552
+ buffer += decoder.decode(next.value, { stream: true });
2553
+ const parts = buffer.split("\n");
2554
+ buffer = parts.pop() ?? "";
2555
+ for (const part of parts) {
2556
+ queue.push(part);
2557
+ }
2558
+ }
2559
+ }
2560
+ async function drain_stderr(stream) {
2561
+ const reader = stream.getReader();
2562
+ while ((await reader.read()).done === false) {
2563
+ continue;
2564
+ }
2565
+ }
2566
+ function bun_line_child(command, args, env) {
2567
+ const queue = create_line_queue();
2568
+ try {
2569
+ const options = { stdin: "pipe", stdout: "pipe", stderr: "pipe" };
2570
+ const child = env === void 0 ? Bun.spawn([command, ...args], options) : Bun.spawn([command, ...args], { ...options, env });
2571
+ void pump_stdout(child.stdout, queue);
2572
+ void drain_stderr(child.stderr);
2573
+ return {
2574
+ write_line(line) {
2575
+ child.stdin.write(`${line}
2576
+ `);
2577
+ void child.stdin.flush();
2578
+ },
2579
+ read_line() {
2580
+ return queue.read();
2581
+ },
2582
+ stop() {
2583
+ child.kill();
2584
+ },
2585
+ failed() {
2586
+ return void 0;
2587
+ }
2588
+ };
2589
+ } catch (error) {
2590
+ const coded = error;
2591
+ return failed_child(spawn_failure(coded.code));
2592
+ }
2593
+ }
2594
+
2595
+ // src/mcp/mcp_stdio_node.ts
2003
2596
  import { spawn as spawn3 } from "child_process";
2597
+ import { createInterface } from "readline";
2598
+ function child_env(extra) {
2599
+ if (extra === void 0) {
2600
+ return void 0;
2601
+ }
2602
+ const merged = {};
2603
+ for (const [key, value] of Object.entries(process.env)) {
2604
+ if (typeof value === "string") {
2605
+ merged[key] = value;
2606
+ }
2607
+ }
2608
+ for (const [key, value] of Object.entries(extra)) {
2609
+ merged[key] = value;
2610
+ }
2611
+ return merged;
2612
+ }
2613
+ function node_line_child(command, args, env) {
2614
+ const queue = create_line_queue();
2615
+ let failure;
2616
+ const child = spawn3(command, [...args], { stdio: ["pipe", "pipe", "pipe"], env: child_env(env) });
2617
+ const reader = createInterface({ input: child.stdout });
2618
+ reader.on("line", (line) => {
2619
+ queue.push(line);
2620
+ });
2621
+ reader.on("close", () => {
2622
+ queue.close();
2623
+ });
2624
+ child.stderr?.resume();
2625
+ child.on("error", (error) => {
2626
+ failure = spawn_failure(error.code);
2627
+ queue.close();
2628
+ });
2629
+ return {
2630
+ write_line(line) {
2631
+ child.stdin?.write(`${line}
2632
+ `);
2633
+ },
2634
+ read_line() {
2635
+ return queue.read();
2636
+ },
2637
+ stop() {
2638
+ child.kill();
2639
+ },
2640
+ failed() {
2641
+ return failure;
2642
+ }
2643
+ };
2644
+ }
2645
+
2646
+ // src/mcp/mcp_stdio.ts
2647
+ function running_under_bun() {
2648
+ return process.versions.bun !== void 0;
2649
+ }
2650
+ function default_line_spawner(command, args, env) {
2651
+ if (running_under_bun() === true) {
2652
+ return bun_line_child(command, args, env);
2653
+ }
2654
+ return node_line_child(command, args, env);
2655
+ }
2656
+
2657
+ // src/mcp/mcp_attach.ts
2658
+ async function open_and_register(registry, name, enabled, session) {
2659
+ try {
2660
+ register_listed(registry, name, await session.list_tools(), enabled, session);
2661
+ } catch (error) {
2662
+ session.close();
2663
+ const message = error instanceof Error ? error.message : "mcp skipped";
2664
+ logger.warn(`mcp ${name} skipped: ${message}`);
2665
+ }
2666
+ }
2667
+ async function attach_stdio(registry, name, entry, config, runtime) {
2668
+ const planned = plan_stdio(name, entry.command, entry.args, runtime?.env_path ?? process.env.PATH);
2669
+ if (typeof planned === "string") {
2670
+ logger.warn(`mcp ${name} skipped: ${planned}`);
2671
+ return;
2672
+ }
2673
+ const spawn5 = runtime?.spawn ?? default_line_spawner;
2674
+ const session = new McpSession(stdio_pipe(spawn5(planned.command, planned.args, entry.env)));
2675
+ await open_and_register(registry, name, config.tools_enabled, session);
2676
+ }
2677
+ async function attach_http(registry, name, url, config, runtime) {
2678
+ const session = new McpSession(http_pipe(url, runtime?.fetch_fn ?? fetch));
2679
+ await open_and_register(registry, name, config.tools_enabled, session);
2680
+ }
2681
+
2682
+ // src/mcp/mcp_tools.ts
2683
+ function allowlist_wants_mcp(enabled) {
2684
+ if (enabled === "all") {
2685
+ return true;
2686
+ }
2687
+ return enabled.some((name) => name.startsWith("mcp_") === true);
2688
+ }
2689
+ async function attach_enabled_mcp_tools(registry, config, runtime) {
2690
+ const servers = config.mcp_servers;
2691
+ if (servers === void 0 || allowlist_wants_mcp(config.tools_enabled) === false) {
2692
+ return;
2693
+ }
2694
+ for (const [name, entry] of Object.entries(servers)) {
2695
+ if (entry.enabled !== true) {
2696
+ continue;
2697
+ }
2698
+ try {
2699
+ if ("url" in entry) {
2700
+ await attach_http(registry, name, entry.url, config, runtime);
2701
+ continue;
2702
+ }
2703
+ await attach_stdio(registry, name, entry, config, runtime);
2704
+ } catch (error) {
2705
+ const message = error instanceof Error ? error.message : "mcp skipped";
2706
+ logger.warn(`mcp ${name} skipped: ${message}`);
2707
+ }
2708
+ }
2709
+ }
2710
+
2711
+ // src/plugins/builtin/gatekeeper.plugin.ts
2712
+ import { spawn as spawn4 } from "child_process";
2004
2713
  import { statSync as statSync2 } from "fs";
2005
- import path9 from "path";
2714
+ import path13 from "path";
2006
2715
  var HOOKS_OFF = ["-c", "core.hooksPath=/dev/null"];
2007
2716
  var PATHSPEC_MAGIC = /[:*?[]/;
2008
2717
  var SECRET_BASENAMES = [".env", ".env.local", "id_rsa"];
2009
2718
  function is_secret_path(file_path) {
2010
- const base = path9.basename(file_path);
2719
+ const base = path13.basename(file_path);
2011
2720
  if (SECRET_BASENAMES.includes(base) === true) {
2012
2721
  return true;
2013
2722
  }
@@ -2034,7 +2743,7 @@ function matches_git_denylist(command) {
2034
2743
  }
2035
2744
  function run_git(args, work_dir, signal) {
2036
2745
  return new Promise((resolve) => {
2037
- const child = spawn3("git", args, { cwd: work_dir, env: process.env });
2746
+ const child = spawn4("git", args, { cwd: work_dir, env: process.env });
2038
2747
  let settled = false;
2039
2748
  let out = "";
2040
2749
  const on_abort = () => {
@@ -2085,7 +2794,7 @@ function invalid_commit_path(raw, work_dir) {
2085
2794
  return `invalid_path: ${raw}`;
2086
2795
  }
2087
2796
  const resolved = resolve_safe_path(work_dir, raw);
2088
- if (resolved === path9.resolve(work_dir)) {
2797
+ if (resolved === path13.resolve(work_dir)) {
2089
2798
  return `invalid_path: ${raw} resolves to work_dir`;
2090
2799
  }
2091
2800
  if (is_secret_path(raw) === true) {
@@ -3387,7 +4096,7 @@ function to_provider_error(error, fallback_name) {
3387
4096
 
3388
4097
  // src/session/store.ts
3389
4098
  import { appendFile, mkdir as mkdir2, readFile as readFile4 } from "fs/promises";
3390
- import path10 from "path";
4099
+ import path14 from "path";
3391
4100
  var counter_state = { value: 0 };
3392
4101
  function slugify_label(label) {
3393
4102
  const slug = label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
@@ -3398,7 +4107,7 @@ async function open_session(dir, label) {
3398
4107
  counter_state.value += 1;
3399
4108
  const label_part = label === void 0 ? "" : slugify_label(label);
3400
4109
  const id = `${Date.now().toString(36)}-${counter_state.value}${label_part}`;
3401
- const file_path = path10.join(dir, `${id}.jsonl`);
4110
+ const file_path = path14.join(dir, `${id}.jsonl`);
3402
4111
  return {
3403
4112
  id,
3404
4113
  path: file_path,
@@ -3640,6 +4349,17 @@ function collect_usage(total) {
3640
4349
  function append_meta(handle, meta) {
3641
4350
  return handle.append({ ts: (/* @__PURE__ */ new Date()).toISOString(), kind: "meta", meta });
3642
4351
  }
4352
+ function append_run_end(handle, stopped_reason, usage_total) {
4353
+ return append_meta(handle, {
4354
+ event: "run_end",
4355
+ stopped_reason,
4356
+ usage: {
4357
+ prompt_tokens: usage_total.prompt_tokens,
4358
+ completion_tokens: usage_total.completion_tokens,
4359
+ total_tokens: usage_total.total_tokens
4360
+ }
4361
+ });
4362
+ }
3643
4363
  function register_plugin_tools(registry, plugins) {
3644
4364
  for (const loaded of plugins) {
3645
4365
  for (const tool of loaded.plugin.tools ?? []) {
@@ -3668,8 +4388,11 @@ var Agent = class {
3668
4388
  registry;
3669
4389
  executor;
3670
4390
  hook_runner;
3671
- constructor(config, plugins = []) {
4391
+ mcp_runtime;
4392
+ mcp_attached = false;
4393
+ constructor(config, plugins = [], runtime) {
3672
4394
  this.config = config;
4395
+ this.mcp_runtime = runtime?.mcp;
3673
4396
  this.events = new AgentEmitter();
3674
4397
  this.router = new ProviderRouter(config.providers);
3675
4398
  const base_registry = new ToolRegistry();
@@ -3693,6 +4416,7 @@ var Agent = class {
3693
4416
  }
3694
4417
  }
3695
4418
  async run(options) {
4419
+ await this.attach_mcp_once();
3696
4420
  const usage_total = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
3697
4421
  const stop_collecting = this.events.on(collect_usage(usage_total));
3698
4422
  await this.call_plugin_run_start(options.input);
@@ -3716,10 +4440,18 @@ var Agent = class {
3716
4440
  await this.call_plugin_run_end(outcome);
3717
4441
  }
3718
4442
  }
3719
- const session_path = await this.persist_session(outcome, options);
4443
+ const session_path = await this.persist_session(outcome, options, usage_total);
3720
4444
  const full_messages = [...options.history ?? [], ...outcome.messages];
3721
4445
  return { outcome, messages: full_messages, usage_total, session_path };
3722
4446
  }
4447
+ /** tools/list once, before the model sees definitions. Empty allowlists never connect. */
4448
+ async attach_mcp_once() {
4449
+ if (this.mcp_attached === true) {
4450
+ return;
4451
+ }
4452
+ this.mcp_attached = true;
4453
+ await attach_enabled_mcp_tools(this.registry, this.config, this.mcp_runtime);
4454
+ }
3723
4455
  /** Per-run deps: the built-once ToolContext threads through every tool execution. */
3724
4456
  loop_deps(tool_context) {
3725
4457
  return {
@@ -3750,7 +4482,7 @@ var Agent = class {
3750
4482
  );
3751
4483
  }
3752
4484
  /** Best-effort JSONL transcript: never fails the run, returns undefined path on error. */
3753
- async persist_session(outcome, options) {
4485
+ async persist_session(outcome, options, usage_total) {
3754
4486
  try {
3755
4487
  const handle = await open_session(this.config.session_dir, options.label);
3756
4488
  await append_meta(handle, { event: "run_start", input_chars: options.input.length, history_size: outcome.messages.length });
@@ -3760,6 +4492,7 @@ var Agent = class {
3760
4492
  if (outcome.stopped_reason === "budget") {
3761
4493
  await append_meta(handle, { event: "budget_exhausted" });
3762
4494
  }
4495
+ await append_run_end(handle, outcome.stopped_reason, usage_total);
3763
4496
  return handle.path;
3764
4497
  } catch (error) {
3765
4498
  logger.warn("session persistence failed; continuing without transcript", error);
@@ -3788,6 +4521,8 @@ export {
3788
4521
  truncate_text,
3789
4522
  logger,
3790
4523
  register_builtin_tools,
4524
+ catalog_by_name,
4525
+ refuse_mcp_entry,
3791
4526
  ToolRegistry,
3792
4527
  ToolExecutor,
3793
4528
  HookedToolRunner,
@@ -3806,4 +4541,4 @@ export {
3806
4541
  create_agent_with_plugins,
3807
4542
  run_agent
3808
4543
  };
3809
- //# sourceMappingURL=chunk-CV2YH3FH.js.map
4544
+ //# sourceMappingURL=chunk-QVJCIZIF.js.map