cuelo 0.10.113 → 0.10.115

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 (60) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +32 -32
  3. package/.next/build-manifest.json +2 -2
  4. package/.next/prerender-manifest.json +3 -3
  5. package/.next/required-server-files.js +1 -1
  6. package/.next/required-server-files.json +1 -1
  7. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/.next/server/app/_global-error.html +1 -1
  9. package/.next/server/app/_global-error.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  13. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  14. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  15. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  16. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  17. package/.next/server/app/_not-found.html +1 -1
  18. package/.next/server/app/_not-found.rsc +2 -2
  19. package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  20. package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  21. package/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  22. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  23. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  24. package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  25. package/.next/server/app/api/subagents/eta/route.js +1 -1
  26. package/.next/server/app/api/updates/apply/route.js +2 -2
  27. package/.next/server/app/api/updates/route.js +1 -1
  28. package/.next/server/app/index.html +1 -1
  29. package/.next/server/app/index.rsc +4 -4
  30. package/.next/server/app/index.segments/__PAGE__.segment.rsc +3 -3
  31. package/.next/server/app/index.segments/_full.segment.rsc +4 -4
  32. package/.next/server/app/index.segments/_head.segment.rsc +1 -1
  33. package/.next/server/app/index.segments/_index.segment.rsc +2 -2
  34. package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  35. package/.next/server/app/page.js +4 -4
  36. package/.next/server/app/page_client-reference-manifest.js +1 -1
  37. package/.next/server/app/recover/page_client-reference-manifest.js +1 -1
  38. package/.next/server/app/recover.html +1 -1
  39. package/.next/server/app/recover.rsc +2 -2
  40. package/.next/server/app/recover.segments/_full.segment.rsc +2 -2
  41. package/.next/server/app/recover.segments/_head.segment.rsc +1 -1
  42. package/.next/server/app/recover.segments/_index.segment.rsc +2 -2
  43. package/.next/server/app/recover.segments/_tree.segment.rsc +1 -1
  44. package/.next/server/app/recover.segments/recover/__PAGE__.segment.rsc +1 -1
  45. package/.next/server/app/recover.segments/recover.segment.rsc +1 -1
  46. package/.next/server/app-paths-manifest.json +32 -32
  47. package/.next/server/middleware-build-manifest.js +1 -1
  48. package/.next/server/pages/404.html +1 -1
  49. package/.next/server/pages/500.html +1 -1
  50. package/.next/server/server-reference-manifest.json +1 -1
  51. package/.next/static/chunks/app/{layout-f65c6505ac7e4f7b.js → layout-9375d2e49df3080f.js} +1 -1
  52. package/.next/static/chunks/app/{page-20cef200caf67724.js → page-678e3146de6728f5.js} +2 -2
  53. package/.next/trace +108 -108
  54. package/.next/trace-build +1 -1
  55. package/Tools/CUELO_Setup/files/native-runtime-patch.js +1 -1
  56. package/Tools/OMP_Global_Config/patches/apply-core-patch.mjs +763 -0
  57. package/install.mjs +4 -2
  58. package/package.json +1 -1
  59. /package/.next/static/{-U-IFRIKLIBm-XAbrl-sF → YagVtkf6TjgJwPzqkwhHB}/_buildManifest.js +0 -0
  60. /package/.next/static/{-U-IFRIKLIBm-XAbrl-sF → YagVtkf6TjgJwPzqkwhHB}/_ssgManifest.js +0 -0
@@ -16964,6 +16964,769 @@ function probeLegacyBank(dbPath: string, cwd: string): "empty" | "safe" | "unsaf
16964
16964
  return "unsafe";
16965
16965
  `,
16966
16966
  },
16967
+ {
16968
+ // MCP 코드 실행(2026-10-30 사용자 요청 "이미있는부분은 제외하고 추가해보자" / "MCP 발주가 의미있는거야"): eval 코드가
16969
+ // 모델 도구 목록에 없는 MCP 서버의 도구를 찾아 부른다. 같은 차단·승인 경로를 지나도록 registry MCP 도구와 똑같이 감싼다.
16970
+ file: "src/session/session-tools.ts",
16971
+ marker: " wrapMCPToolForEval(tool: CustomTool): AgentTool {",
16972
+ anchor: ` /** Canonical and unambiguous wire names accepted by the eval bridge. */
16973
+ getEvalBridgeToolNames(): string[] {`,
16974
+ patched: ` /**
16975
+ * Wraps a manager MCP tool with the gates a direct call passes (approval policy, extension
16976
+ * tool_call hooks, ACP permission) for eval's mcp prelude. The tool is neither registered nor
16977
+ * activated, so the model's tool list does not change.
16978
+ */
16979
+ wrapMCPToolForEval(tool: CustomTool): AgentTool {
16980
+ const wrapped = wrapToolWithMetaNotice(CustomToolAdapter.wrap(tool, this.#getCustomToolContext) as AgentTool);
16981
+ const extensionRunner = this.#host.extensionRunner();
16982
+ return this.#wrapToolForAcpPermission(extensionRunner ? new ExtensionToolWrapper(wrapped, extensionRunner) : wrapped);
16983
+ }
16984
+
16985
+ /** Servers whose tools an MCP refresh leaves as they were instead of activating them (connected from eval). */
16986
+ #mcpRefreshHold: (server: string) => boolean = () => false;
16987
+
16988
+ /** Sets which servers' tools {@link refreshMCPTools} must not activate; their tools keep their previous state. */
16989
+ setMCPRefreshHold(hold: (server: string) => boolean): void {
16990
+ this.#mcpRefreshHold = hold;
16991
+ }
16992
+
16993
+ /** Canonical and unambiguous wire names accepted by the eval bridge. */
16994
+ getEvalBridgeToolNames(): string[] {`,
16995
+ },
16996
+ {
16997
+ // MCP 코드 실행: eval이 연결한 서버는 연결 직후의 refresh(소유 세션 tools-changed 포함)가 켜지 않는다. 보류는 연결 시작 전에
16998
+ // 걸리므로 refresh가 언제 끝나든 경합이 없다. 이전에 켜져 있던 도구는 그대로 둔다(선택이 노출한 경우).
16999
+ file: "src/session/session-tools.ts",
17000
+ marker: " // Held servers (connected from eval) keep each tool's previous state instead of activating.",
17001
+ anchor: ` const nextActive = [
17002
+ ...new Set([
17003
+ ...this.#getActiveNonMCPToolNames(),
17004
+ ...(vibeActive ? [] : this.#mcpManagerToolNames),
17005
+ ...retainedActiveMcpToolNames,
17006
+ ]),
17007
+ ];`,
17008
+ patched: ` // Held servers (connected from eval) keep each tool's previous state instead of activating.
17009
+ const heldInactive = new Set(
17010
+ mcpTools
17011
+ .filter(tool => tool.mcpServerName !== undefined && this.#mcpRefreshHold(tool.mcpServerName))
17012
+ .map(tool => tool.name)
17013
+ .filter(name => !previousActiveMcpToolNames.includes(name)),
17014
+ );
17015
+ const nextActive = [
17016
+ ...new Set([
17017
+ ...this.#getActiveNonMCPToolNames(),
17018
+ ...(vibeActive ? [] : [...this.#mcpManagerToolNames].filter(name => !heldInactive.has(name))),
17019
+ ...retainedActiveMcpToolNames,
17020
+ ]),
17021
+ ];`,
17022
+ },
17023
+ {
17024
+ // MCP 코드 실행: 요청별 선택을 가진 세션만 deferred 서버를 eval에서 연결한다. 연결 전에 보류를 걸어 모델 도구 목록에 넣지 않는다.
17025
+ file: "src/session/agent-session.ts",
17026
+ marker: " async connectMCPServerForEval(name: string, signal?: AbortSignal): Promise<void> {",
17027
+ anchor: ` /** Replaces host-owned RPC tools before the next model call. */
17028
+ refreshRpcHostTools(rpcTools: AgentTool[]): Promise<void> {`,
17029
+ patched: ` /** Wraps a manager MCP tool with the gates a direct call passes, for eval's mcp prelude; never registers or activates it. */
17030
+ wrapMCPToolForEval(tool: CustomTool): AgentTool {
17031
+ return this.#tools.wrapMCPToolForEval(tool);
17032
+ }
17033
+
17034
+ /** Deferred servers eval connected; their tools stay out of the model's tool list until a selection exposes them. */
17035
+ #evalHeldMCPServers = new Set<string>();
17036
+
17037
+ /**
17038
+ * Connects one deferred per-request MCP server for eval's mcp prelude. The hold is set before the
17039
+ * connect starts, so no MCP refresh (the connect's own included) activates its tools: only a later
17040
+ * per-request selection exposes them, and a manual reconnect (which clears selection ownership)
17041
+ * releases the hold. Only the session owning per-request selection connects.
17042
+ */
17043
+ async connectMCPServerForEval(name: string, signal?: AbortSignal): Promise<void> {
17044
+ const manager = this.#mcpSelectionManager;
17045
+ if (!manager) {
17046
+ throw new Error(
17047
+ 'MCP server "' + name + '" is not connected, and only the session that owns per-request MCP selection can connect it.',
17048
+ );
17049
+ }
17050
+ this.#evalHeldMCPServers.add(name);
17051
+ this.#tools.setMCPRefreshHold(
17052
+ server => this.#evalHeldMCPServers.has(server) && manager.isSelectionConnected(server),
17053
+ );
17054
+ const result = await manager.connectDeferred([name], signal);
17055
+ signal?.throwIfAborted();
17056
+ if (!manager.getConnectedServers().includes(name)) {
17057
+ throw new Error('MCP server "' + name + '" failed to connect: ' + (result.errors.get(name) ?? "not connected"));
17058
+ }
17059
+ }
17060
+
17061
+ /** Replaces host-owned RPC tools before the next model call. */
17062
+ refreshRpcHostTools(rpcTools: AgentTool[]): Promise<void> {`,
17063
+ },
17064
+ {
17065
+ // MCP 코드 실행: ToolSession 계약(eval prelude가 쓰는 두 진입점).
17066
+ file: "src/tools/index.ts",
17067
+ marker: " connectMCPServerForEval?: (name: string, signal?: AbortSignal) => Promise<void>;",
17068
+ anchor: ` /** Look up an enabled tool through the eval bridge's normal permission pipeline. */
17069
+ getToolForEvalBridge?: (name: string) => AgentTool | undefined;`,
17070
+ patched: ` /** Look up an enabled tool through the eval bridge's normal permission pipeline. */
17071
+ getToolForEvalBridge?: (name: string) => AgentTool | undefined;
17072
+ /** Wrap a manager MCP tool with the direct-call gates for eval's mcp prelude; never registers or activates it. */
17073
+ wrapMCPToolForEval?: (tool: import("../extensibility/custom-tools/types").CustomTool) => AgentTool | undefined;
17074
+ /** Connect a deferred per-request MCP server for eval without exposing its tools to the model. */
17075
+ connectMCPServerForEval?: (name: string, signal?: AbortSignal) => Promise<void>;`,
17076
+ },
17077
+ {
17078
+ file: "src/sdk.ts",
17079
+ marker: " wrapMCPToolForEval: tool => session?.wrapMCPToolForEval(tool),",
17080
+ anchor: ` getToolForEvalBridge: name => session?.getToolForEvalBridge(name),`,
17081
+ patched: ` getToolForEvalBridge: name => session?.getToolForEvalBridge(name),
17082
+ wrapMCPToolForEval: tool => session?.wrapMCPToolForEval(tool),
17083
+ connectMCPServerForEval: async (name, signal) => {
17084
+ if (!session) throw new Error("The session is still starting; try again.");
17085
+ await session.connectMCPServerForEval(name, signal);
17086
+ },`,
17087
+ },
17088
+ {
17089
+ // MCP 코드 실행·스킬 스크립트: 두 eval prelude를 내장 prelude 목록에 더한다. mcp는 세션에 MCP manager가 있을 때만(신뢰하지
17090
+ // 않은 프로젝트는 enableMCP:false라 manager가 없다).
17091
+ file: "src/sdk.ts",
17092
+ marker: "import { createMcpEvalPrelude, createSkillScriptsEvalPrelude, getEnabledEvalPreludes, type EvalPreludeDefinition } from \"./eval/preludes\";",
17093
+ anchor: `import { getEnabledEvalPreludes, type EvalPreludeDefinition } from "./eval/preludes";`,
17094
+ patched: `import { createMcpEvalPrelude, createSkillScriptsEvalPrelude, getEnabledEvalPreludes, type EvalPreludeDefinition } from "./eval/preludes";`,
17095
+ },
17096
+ {
17097
+ file: "src/sdk.ts",
17098
+ marker: " let mcpPrelude: EvalPreludeDefinition | undefined;",
17099
+ anchor: ` let archivePrelude: EvalPreludeDefinition | undefined;`,
17100
+ patched: ` let archivePrelude: EvalPreludeDefinition | undefined;
17101
+ let mcpPrelude: EvalPreludeDefinition | undefined;
17102
+ let skillScriptsPrelude: EvalPreludeDefinition | undefined;`,
17103
+ },
17104
+ {
17105
+ file: "src/sdk.ts",
17106
+ marker: " mcpPrelude ??= createMcpEvalPrelude(toolSession);",
17107
+ anchor: ` archivePrelude ??= createArchivePrelude(toolSession);
17108
+ builtins.push(archivePrelude);
17109
+ }
17110
+ return getEnabledEvalPreludes(builtins);`,
17111
+ patched: ` archivePrelude ??= createArchivePrelude(toolSession);
17112
+ builtins.push(archivePrelude);
17113
+ }
17114
+ if (toolSession.mcpManager) {
17115
+ mcpPrelude ??= createMcpEvalPrelude(toolSession);
17116
+ builtins.push(mcpPrelude);
17117
+ }
17118
+ skillScriptsPrelude ??= createSkillScriptsEvalPrelude(toolSession);
17119
+ builtins.push(skillScriptsPrelude);
17120
+ return getEnabledEvalPreludes(builtins);`,
17121
+ },
17122
+ {
17123
+ file: "src/eval/preludes.ts",
17124
+ marker: `import { sanitizeSkillScriptName } from "../autolearn/managed-skills";`,
17125
+ anchor: `import { untilAborted } from "@oh-my-pi/pi-utils";
17126
+ import type { ToolSession } from "../tools";`,
17127
+ patched: `import * as fs from "node:fs/promises";
17128
+ import { pathToFileURL } from "node:url";
17129
+ import { untilAborted } from "@oh-my-pi/pi-utils";
17130
+ import { sanitizeSkillScriptName } from "../autolearn/managed-skills";
17131
+ import type { CustomTool } from "../extensibility/custom-tools/types";
17132
+ import { InternalUrlRouter, sessionResolveContext } from "../internal-urls";
17133
+ import type { ToolSession } from "../tools";`,
17134
+ },
17135
+ {
17136
+ // mcp prelude: servers/tools/call. 결과는 eval 프로세스 안 변수로만 돌아가고 모델 대화에는 cell이 출력한 것만 남는다.
17137
+ // skills prelude: skill://<name>/scripts/<file> 를 skill:// 해석(포함 경로 검사 포함)으로 찾아 경로를 주고 JS/Python이 모듈로 읽는다.
17138
+ file: "src/eval/preludes.ts",
17139
+ marker: "export function createMcpEvalPrelude(session: ToolSession): EvalPreludeDefinition {",
17140
+ anchor: ` if (findEnabledEvalPrelude(context.session, name) !== definition) {
17141
+ throw new Error(\`Eval prelude "\${name}" changed while authorizing the call.\`);
17142
+ }
17143
+ }
17144
+ return definition.invoke(parameters, context);
17145
+ }`,
17146
+ patched: ` if (findEnabledEvalPrelude(context.session, name) !== definition) {
17147
+ throw new Error(\`Eval prelude "\${name}" changed while authorizing the call.\`);
17148
+ }
17149
+ }
17150
+ return definition.invoke(parameters, context);
17151
+ }
17152
+
17153
+ const MCP_PRELUDE_DOC = [
17154
+ "Call MCP servers from code: their tools need not be in your tool list, and results stay in variables until you print them.",
17155
+ "",
17156
+ "- \`await mcp.servers()\` → \`[{ name, status, exposed, transport, tools }]\` for every enabled, allowed server. \`status\` is \`connected\`, \`connecting\` or \`deferred\` (not started yet); \`exposed\` says whether its tools are also in your direct tool list.",
17157
+ "- \`await mcp.tools(server)\` → \`[{ name, description, inputSchema }]\`. Starts a deferred server on demand; its tools still stay out of your direct tool list.",
17158
+ "- \`await mcp.call(server, tool, args)\` → \`{ isError, text, content, structured }\`; \`structured\` is the server's structured result when it sends one. Every call passes the same approval rules as a direct MCP tool call.",
17159
+ "- Python uses the same names: \`await mcp.servers()\`, \`await mcp.tools(server)\`, \`await mcp.call(server, tool, {...})\`.",
17160
+ "",
17161
+ "Filter and join large results in code and print only what the task needs. Once code works, save it with \`manage_skill\` \`write_script\` and reuse it through \`skills.load\`.",
17162
+ ].join("\\n");
17163
+
17164
+ const MCP_PRELUDE_JAVASCRIPT = \`{
17165
+ const call = async params => {
17166
+ const response = await globalThis.__omp_prelude__('mcp', params);
17167
+ return response && typeof response === 'object' ? response.details : undefined;
17168
+ };
17169
+ const named = (value, label) => {
17170
+ if (typeof value !== 'string' || value.length === 0) throw new TypeError('mcp.' + label + ' expects a non-empty string');
17171
+ return value;
17172
+ };
17173
+ globalThis.mcp = Object.freeze({
17174
+ servers: () => call({ action: 'servers' }),
17175
+ tools: server => call({ action: 'tools', server: named(server, 'tools(server)') }),
17176
+ call: (server, tool, args) =>
17177
+ call({ action: 'call', server: named(server, 'call(server)'), tool: named(tool, 'call(server, tool)'), args: args ?? {} }),
17178
+ });
17179
+ }
17180
+ \`;
17181
+
17182
+ const MCP_PRELUDE_PYTHON = \`def _make_mcp():
17183
+ async def _call(params):
17184
+ response = await _omp_prelude('mcp', params)
17185
+ if not isinstance(response, dict):
17186
+ raise RuntimeError('mcp returned an invalid response')
17187
+ return response.get('details')
17188
+
17189
+ class _Mcp:
17190
+ __slots__ = ()
17191
+
17192
+ def __repr__(self):
17193
+ return 'mcp'
17194
+
17195
+ async def servers(self):
17196
+ return await _call({'action': 'servers'})
17197
+
17198
+ async def tools(self, server):
17199
+ return await _call({'action': 'tools', 'server': server})
17200
+
17201
+ async def call(self, server, tool, args=None):
17202
+ return await _call({'action': 'call', 'server': server, 'tool': tool, 'args': args or {}})
17203
+
17204
+ return _Mcp()
17205
+
17206
+
17207
+ mcp = _make_mcp()
17208
+ del _make_mcp
17209
+ \`;
17210
+
17211
+ type McpPreludeParams =
17212
+ | { action: "servers" }
17213
+ | { action: "tools"; server: string }
17214
+ | { action: "call"; server: string; tool: string; args: Record<string, unknown> };
17215
+
17216
+ function parseMcpPreludeParams(parameters: unknown): McpPreludeParams {
17217
+ const p = (parameters && typeof parameters === "object" ? parameters : {}) as Record<string, unknown>;
17218
+ const text = (key: string): string => {
17219
+ const value = p[key];
17220
+ if (typeof value !== "string" || value.length === 0) throw new Error("mcp " + String(p.action) + " needs a non-empty " + key);
17221
+ return value;
17222
+ };
17223
+ if (p.action === "servers") return { action: "servers" };
17224
+ if (p.action === "tools") return { action: "tools", server: text("server") };
17225
+ if (p.action === "call") {
17226
+ const args = p.args ?? {};
17227
+ if (typeof args !== "object" || args === null || Array.isArray(args)) throw new Error("mcp.call args must be an object");
17228
+ return { action: "call", server: text("server"), tool: text("tool"), args: args as Record<string, unknown> };
17229
+ }
17230
+ throw new Error("mcp received an unknown action: " + String(p.action));
17231
+ }
17232
+
17233
+ /**
17234
+ * Create the mcp prelude: list enabled, allowed MCP servers (deferred ones included), read tool
17235
+ * schemas, and call tools from eval code. Calls go through {@link ToolSession.wrapMCPToolForEval},
17236
+ * the same gates as a direct MCP call; a deferred server is connected without exposing its tools.
17237
+ * Disabled, not allowed, and trust-filtered servers never reach the manager, so they stay unknown here.
17238
+ */
17239
+ export function createMcpEvalPrelude(session: ToolSession): EvalPreludeDefinition {
17240
+ const managerOf = () => {
17241
+ const manager = session.mcpManager;
17242
+ if (!manager) throw new Error("MCP is not available in this session.");
17243
+ return manager;
17244
+ };
17245
+ const serverTools = (server: string): CustomTool[] =>
17246
+ managerOf()
17247
+ .getTools()
17248
+ .filter(tool => tool.mcpServerName === server) as CustomTool[];
17249
+ const ensureConnected = async (server: string, signal?: AbortSignal): Promise<void> => {
17250
+ const manager = managerOf();
17251
+ const status = manager.getConnectionStatus(server);
17252
+ if (status === "connected") return;
17253
+ if (status === "connecting") {
17254
+ await manager.waitForConnection(server);
17255
+ return;
17256
+ }
17257
+ const deferred = await manager.getDeferredServers();
17258
+ if (!deferred.some(entry => entry.name === server)) {
17259
+ const known = [...new Set([...manager.getConnectedServers(), ...deferred.map(entry => entry.name)])];
17260
+ throw new Error(
17261
+ 'MCP server "' + server + '" is not available here (disabled, not allowed, or unknown). Available: ' + (known.join(", ") || "none"),
17262
+ );
17263
+ }
17264
+ if (!session.connectMCPServerForEval) throw new Error('MCP server "' + server + '" is not connected in this session.');
17265
+ await session.connectMCPServerForEval(server, signal);
17266
+ };
17267
+ return {
17268
+ name: "mcp",
17269
+ documentation: MCP_PRELUDE_DOC,
17270
+ javascript: MCP_PRELUDE_JAVASCRIPT,
17271
+ python: MCP_PRELUDE_PYTHON,
17272
+ exports: ["mcp"],
17273
+ approval: "read",
17274
+ enabled: () => session.mcpManager !== undefined,
17275
+ invoke: async (parameters, context) => {
17276
+ const params = parseMcpPreludeParams(parameters);
17277
+ if (params.action === "servers") {
17278
+ const manager = managerOf();
17279
+ const enabled = new Set(session.getEvalBridgeToolNames?.() ?? []);
17280
+ const deferred = await manager.getDeferredServers();
17281
+ const live = new Set<string>(manager.getConnectedServers());
17282
+ for (const tool of manager.getTools()) if (tool.mcpServerName) live.add(tool.mcpServerName);
17283
+ const servers: Array<{ name: string; status: string; exposed: boolean; transport: string; tools: string[] }> = [];
17284
+ for (const name of live) {
17285
+ const tools = serverTools(name);
17286
+ servers.push({
17287
+ name,
17288
+ status: manager.getConnectionStatus(name),
17289
+ exposed: tools.some(tool => enabled.has(tool.name)),
17290
+ transport: manager.getServerConfig(name)?.type ?? "stdio",
17291
+ tools: tools.map(tool => tool.mcpToolName ?? tool.name),
17292
+ });
17293
+ }
17294
+ for (const entry of deferred) {
17295
+ if (live.has(entry.name)) continue;
17296
+ servers.push({
17297
+ name: entry.name,
17298
+ status: "deferred",
17299
+ exposed: false,
17300
+ transport: entry.transport,
17301
+ tools: entry.cachedTools.map(tool => tool.name),
17302
+ });
17303
+ }
17304
+ return { content: [{ type: "text", text: servers.length + " MCP servers" }], details: servers };
17305
+ }
17306
+ await ensureConnected(params.server, context.signal);
17307
+ const tools = serverTools(params.server);
17308
+ if (params.action === "tools") {
17309
+ const listed = tools.map(tool => ({
17310
+ name: tool.mcpToolName ?? tool.name,
17311
+ description: tool.description,
17312
+ inputSchema: tool.parameters,
17313
+ }));
17314
+ return { content: [{ type: "text", text: params.server + ": " + listed.length + " tools" }], details: listed };
17315
+ }
17316
+ const target = tools.find(tool => (tool.mcpToolName ?? tool.name) === params.tool) ?? tools.find(tool => tool.name === params.tool);
17317
+ if (!target) {
17318
+ throw new Error(
17319
+ 'MCP server "' + params.server + '" has no tool "' + params.tool + '". Tools: ' + (tools.map(tool => tool.mcpToolName ?? tool.name).join(", ") || "none"),
17320
+ );
17321
+ }
17322
+ const wrapped = session.wrapMCPToolForEval?.(target);
17323
+ if (!wrapped) throw new Error("This session cannot run MCP tools from eval.");
17324
+ const result = await wrapped.execute(context.toolCallId, params.args, context.signal, undefined, context.context);
17325
+ // MCP tool details (MCPToolDetails); other shapes leave these fields undefined.
17326
+ const details: { isError?: boolean; rawContent?: unknown; structuredContent?: unknown } = result.details ?? {};
17327
+ const text = result.content
17328
+ .flatMap(block => (block.type === "text" ? [block.text] : []))
17329
+ .join("\\n");
17330
+ const isError = result.isError === true || details.isError === true;
17331
+ return {
17332
+ content: [{ type: "text", text: "mcp " + params.server + "/" + params.tool + (isError ? " failed" : " ok") }],
17333
+ details: { isError, text, content: details.rawContent ?? result.content, structured: details.structuredContent },
17334
+ };
17335
+ },
17336
+ status: parameters => {
17337
+ try {
17338
+ const params = parseMcpPreludeParams(parameters);
17339
+ if (params.action === "call") return "mcp.call(" + params.server + "/" + params.tool + ")";
17340
+ if (params.action === "tools") return "mcp.tools(" + params.server + ")";
17341
+ return "mcp.servers()";
17342
+ } catch {
17343
+ return undefined;
17344
+ }
17345
+ },
17346
+ };
17347
+ }
17348
+
17349
+ const SKILL_SCRIPTS_PRELUDE_DOC = [
17350
+ "Load code saved in a skill's scripts/ folder (\`manage_skill\` \`write_script\`) instead of writing it again.",
17351
+ "",
17352
+ "- \`await skills.path(\\"skill://<name>/scripts/<file>\\")\` (or \`skills.path(name, file)\`) → absolute path of the saved file.",
17353
+ "- JavaScript: \`const mod = await skills.load(\\"skill://<name>/scripts/<file>.ts\\")\` imports a \`.ts\`, \`.mjs\` or \`.js\` script and returns its module.",
17354
+ "- Python: \`mod = await skills.load(\\"skill://<name>/scripts/<file>.py\\")\` imports a \`.py\` script and returns its module.",
17355
+ "",
17356
+ "Read the script (\`read skill://<name>/scripts/<file>\`) first when you do not know its exports.",
17357
+ ].join("\\n");
17358
+
17359
+ const SKILL_SCRIPTS_PRELUDE_JAVASCRIPT = \`{
17360
+ const resolve = async (ref, script) => {
17361
+ const response = await globalThis.__omp_prelude__('skills', script === undefined ? { ref } : { name: ref, script });
17362
+ const details = response && typeof response === 'object' ? response.details : undefined;
17363
+ if (!details || typeof details.path !== 'string') throw new Error('skills returned an invalid response');
17364
+ return details;
17365
+ };
17366
+ globalThis.skills = Object.freeze({
17367
+ path: async (ref, script) => (await resolve(ref, script)).path,
17368
+ load: async (ref, script) => {
17369
+ const target = await resolve(ref, script);
17370
+ if (target.path.endsWith('.py')) throw new TypeError('skills.load() in JavaScript imports .ts, .mjs or .js scripts; load .py scripts from Python');
17371
+ const load = typeof globalThis.__omp_import__ === 'function' ? globalThis.__omp_import__ : specifier => import(specifier);
17372
+ return await load(target.url);
17373
+ },
17374
+ });
17375
+ }
17376
+ \`;
17377
+
17378
+ const SKILL_SCRIPTS_PRELUDE_PYTHON = \`def _make_skills():
17379
+ async def _resolve(ref, script):
17380
+ params = {'ref': ref} if script is None else {'name': ref, 'script': script}
17381
+ response = await _omp_prelude('skills', params)
17382
+ details = response.get('details') if isinstance(response, dict) else None
17383
+ if not isinstance(details, dict) or not isinstance(details.get('path'), str):
17384
+ raise RuntimeError('skills returned an invalid response')
17385
+ return details
17386
+
17387
+ class _Skills:
17388
+ __slots__ = ()
17389
+
17390
+ def __repr__(self):
17391
+ return 'skills'
17392
+
17393
+ async def path(self, ref, script=None):
17394
+ return (await _resolve(ref, script))['path']
17395
+
17396
+ async def load(self, ref, script=None):
17397
+ import importlib.util
17398
+ import sys
17399
+
17400
+ target = await _resolve(ref, script)
17401
+ if not target['path'].endswith('.py'):
17402
+ raise TypeError('skills.load() in Python imports .py scripts; load .ts, .mjs or .js scripts from JavaScript')
17403
+ spec = importlib.util.spec_from_file_location(target['module'], target['path'])
17404
+ module = importlib.util.module_from_spec(spec)
17405
+ # Registered like a normal import so code that looks itself up (dataclasses, pickle) works.
17406
+ sys.modules[target['module']] = module
17407
+ try:
17408
+ spec.loader.exec_module(module)
17409
+ except BaseException:
17410
+ sys.modules.pop(target['module'], None)
17411
+ raise
17412
+ return module
17413
+
17414
+ return _Skills()
17415
+
17416
+
17417
+ skills = _make_skills()
17418
+ del _make_skills
17419
+ \`;
17420
+
17421
+ /** \`{ ref: "skill://<name>/scripts/<file>" }\` or \`{ name, script }\`; the file name is validated like a saved script. */
17422
+ function parseSkillScriptRef(parameters: unknown): { name: string; script: string } {
17423
+ const p = (parameters && typeof parameters === "object" ? parameters : {}) as Record<string, unknown>;
17424
+ let name: unknown = p.name;
17425
+ let script: unknown = p.script;
17426
+ if (typeof p.ref === "string") {
17427
+ const prefix = "skill://";
17428
+ const rest = p.ref.startsWith(prefix) ? p.ref.slice(prefix.length) : "";
17429
+ const marker = rest.lastIndexOf("/scripts/");
17430
+ if (marker <= 0) {
17431
+ throw new Error('skills expects "skill://<name>/scripts/<file>" or (name, file); got ' + JSON.stringify(p.ref));
17432
+ }
17433
+ name = rest.slice(0, marker);
17434
+ script = rest.slice(marker + "/scripts/".length);
17435
+ }
17436
+ if (typeof name !== "string" || name.length === 0 || typeof script !== "string") {
17437
+ throw new Error('skills expects "skill://<name>/scripts/<file>" or (name, file)');
17438
+ }
17439
+ return { name, script: sanitizeSkillScriptName(script) };
17440
+ }
17441
+
17442
+ /**
17443
+ * Create the skills prelude: resolve a script saved under a loaded skill's scripts/ folder through the
17444
+ * skill:// handler (traversal and symlink containment checked) and hand eval its path for import.
17445
+ */
17446
+ export function createSkillScriptsEvalPrelude(session: ToolSession): EvalPreludeDefinition {
17447
+ return {
17448
+ name: "skills",
17449
+ documentation: SKILL_SCRIPTS_PRELUDE_DOC,
17450
+ javascript: SKILL_SCRIPTS_PRELUDE_JAVASCRIPT,
17451
+ python: SKILL_SCRIPTS_PRELUDE_PYTHON,
17452
+ exports: ["skills"],
17453
+ approval: "read",
17454
+ invoke: async (parameters, context) => {
17455
+ const { name, script } = parseSkillScriptRef(parameters);
17456
+ const url = "skill://" + name + "/scripts/" + script;
17457
+ const located = await InternalUrlRouter.instance().locate(
17458
+ url,
17459
+ sessionResolveContext(session, { signal: context.signal, containSkillPaths: true }),
17460
+ { localOnly: true },
17461
+ );
17462
+ const stats = located ? await fs.stat(located) : undefined;
17463
+ if (!located || !stats?.isFile()) throw new Error("No saved script at " + url + ".");
17464
+ return {
17465
+ content: [{ type: "text", text: url }],
17466
+ details: {
17467
+ path: located,
17468
+ url: pathToFileURL(located).href + "?v=" + Math.trunc(stats.mtimeMs),
17469
+ module: ("skill_script_" + name + "_" + script).replace(/[^A-Za-z0-9_]/g, "_"),
17470
+ },
17471
+ };
17472
+ },
17473
+ status: parameters => {
17474
+ try {
17475
+ const { name, script } = parseSkillScriptRef(parameters);
17476
+ return "skills(" + name + "/scripts/" + script + ")";
17477
+ } catch {
17478
+ return undefined;
17479
+ }
17480
+ },
17481
+ };
17482
+ }`,
17483
+ },
17484
+ {
17485
+ // 스킬 스크립트: 관리 스킬 폴더의 scripts/<file> 하나만 만들고 바꾸고 지운다. 이름은 한 경로 조각·허용 확장자·크기·개수 상한,
17486
+ // symlink·hard link·SKILL.md 없는 폴더는 거절한다. SKILL.md 생성·수정·삭제 경로는 그대로다.
17487
+ file: "src/autolearn/managed-skills.ts",
17488
+ marker: "export async function writeManagedSkillScript(",
17489
+ anchor: ` if (isEnoent(err)) {
17490
+ throw new Error(\`Managed skill "\${safe}" does not exist.\`);
17491
+ }
17492
+ throw err;
17493
+ }
17494
+ });
17495
+ }`,
17496
+ patched: ` if (isEnoent(err)) {
17497
+ throw new Error(\`Managed skill "\${safe}" does not exist.\`);
17498
+ }
17499
+ throw err;
17500
+ }
17501
+ });
17502
+ }
17503
+
17504
+ /** Hard cap on one managed skill script file (UTF-8 bytes). */
17505
+ export const MAX_MANAGED_SKILL_SCRIPT_BYTES = 64_000;
17506
+
17507
+ /** Most script files one managed skill may carry. */
17508
+ export const MAX_MANAGED_SKILL_SCRIPTS = 16;
17509
+
17510
+ /** One file name under scripts/: lowercase letters, digits, "-" or "_", and a runnable extension. */
17511
+ const SKILL_SCRIPT_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}\\.(?:ts|mjs|js|py)$/;
17512
+
17513
+ /**
17514
+ * Validate a skill script name. Only one file name directly under scripts/ is allowed (a leading
17515
+ * "scripts/" is accepted), so the name can carry no path separator, "..", or other extension.
17516
+ */
17517
+ export function sanitizeSkillScriptName(raw: string): string {
17518
+ const trimmed = raw.trim();
17519
+ const script = trimmed.startsWith("scripts/") ? trimmed.slice("scripts/".length) : trimmed;
17520
+ if (!SKILL_SCRIPT_PATTERN.test(script)) {
17521
+ throw new Error(
17522
+ 'Invalid script name "' +
17523
+ raw +
17524
+ '". Use one file name of lowercase letters, digits, "-" or "_" (1-64 chars) ending in .ts, .mjs, .js or .py; it is stored as scripts/<name>.',
17525
+ );
17526
+ }
17527
+ return script;
17528
+ }
17529
+
17530
+ async function lstatIfExists(file: string): Promise<Stats | null> {
17531
+ return fs.lstat(file).catch(err => {
17532
+ if (isEnoent(err)) return null;
17533
+ throw err;
17534
+ });
17535
+ }
17536
+
17537
+ /** The scripts/ folder of an existing managed skill; refuses symlinks and a skill without SKILL.md. */
17538
+ async function managedSkillScriptsDir(name: string): Promise<{ dir: string; exists: boolean }> {
17539
+ await assertManagedRootSafe();
17540
+ const skillDir = path.join(getManagedSkillsDir(), name);
17541
+ const skillDirStat = await lstatIfExists(skillDir);
17542
+ if (skillDirStat?.isSymbolicLink()) {
17543
+ throw new Error('Managed skill "' + name + '" resolves through a symlink; refusing to touch its scripts.');
17544
+ }
17545
+ const skillFileStat = skillDirStat?.isDirectory() ? await lstatIfExists(path.join(skillDir, "SKILL.md")) : null;
17546
+ if (!skillFileStat?.isFile()) {
17547
+ throw new Error('Managed skill "' + name + '" does not exist. Create it with action "create" first.');
17548
+ }
17549
+ const dir = path.join(skillDir, "scripts");
17550
+ const dirStat = await lstatIfExists(dir);
17551
+ if (dirStat && !dirStat.isDirectory()) {
17552
+ throw new Error('Managed skill "' + name + '" scripts/ is not a plain directory; refusing to touch it.');
17553
+ }
17554
+ return { dir, exists: dirStat !== null };
17555
+ }
17556
+
17557
+ export interface WriteManagedSkillScriptInput {
17558
+ name: string;
17559
+ script: string;
17560
+ content: string;
17561
+ }
17562
+
17563
+ /** Create or replace one file in a managed skill's scripts/ folder. Returns its path. */
17564
+ export async function writeManagedSkillScript(
17565
+ input: WriteManagedSkillScriptInput,
17566
+ ): Promise<{ path: string; created: boolean }> {
17567
+ const name = sanitizeSkillName(input.name);
17568
+ const script = sanitizeSkillScriptName(input.script);
17569
+ if (!input.content.trim()) throw new Error('Script "' + script + '" needs non-empty content.');
17570
+ const bytes = Buffer.byteLength(input.content, "utf8");
17571
+ if (bytes > MAX_MANAGED_SKILL_SCRIPT_BYTES) {
17572
+ throw new Error('Script "' + script + '" is ' + bytes + " bytes; the limit is " + MAX_MANAGED_SKILL_SCRIPT_BYTES + ".");
17573
+ }
17574
+ return serializeSkillMutation(name, async () => {
17575
+ const { dir, exists } = await managedSkillScriptsDir(name);
17576
+ if (!exists) await fs.mkdir(dir);
17577
+ const file = path.join(dir, script);
17578
+ const fileStat = await lstatIfExists(file);
17579
+ if (fileStat === null) {
17580
+ if ((await fs.readdir(dir)).length >= MAX_MANAGED_SKILL_SCRIPTS) {
17581
+ throw new Error('Managed skill "' + name + '" already has ' + MAX_MANAGED_SKILL_SCRIPTS + " scripts; delete one first.");
17582
+ }
17583
+ // "wx": atomic create that refuses an existing file or symlink placed after the check.
17584
+ await fs.writeFile(file, input.content, { flag: "wx" });
17585
+ return { path: file, created: true };
17586
+ }
17587
+ if (fileStat.isSymbolicLink()) throw new Error('Script "scripts/' + script + '" is a symlink; refusing to overwrite it.');
17588
+ const assertPlain = (stat: Stats) => {
17589
+ if (!stat.isFile() || stat.nlink > 1) {
17590
+ throw new Error('Script "scripts/' + script + '" is not a plain single-link file; refusing to overwrite it.');
17591
+ }
17592
+ };
17593
+ assertPlain(fileStat);
17594
+ const handle = await fs.open(file, UPDATE_FILE_OPEN_FLAGS).catch(err => {
17595
+ if ((err as { code?: string }).code === "ELOOP") {
17596
+ throw new Error('Script "scripts/' + script + '" is a symlink; refusing to overwrite it.');
17597
+ }
17598
+ throw err;
17599
+ });
17600
+ try {
17601
+ assertPlain(await handle.stat());
17602
+ await handle.truncate(0);
17603
+ await handle.writeFile(input.content);
17604
+ } finally {
17605
+ await handle.close();
17606
+ }
17607
+ return { path: file, created: false };
17608
+ });
17609
+ }
17610
+
17611
+ /** Delete one file from a managed skill's scripts/ folder. Throws when it does not exist. */
17612
+ export async function deleteManagedSkillScript(input: { name: string; script: string }): Promise<void> {
17613
+ const name = sanitizeSkillName(input.name);
17614
+ const script = sanitizeSkillScriptName(input.script);
17615
+ await serializeSkillMutation(name, async () => {
17616
+ const { dir, exists } = await managedSkillScriptsDir(name);
17617
+ const file = path.join(dir, script);
17618
+ const fileStat = exists ? await lstatIfExists(file) : null;
17619
+ if (!fileStat) throw new Error('Script "scripts/' + script + '" does not exist in managed skill "' + name + '".');
17620
+ if (!fileStat.isFile()) throw new Error('Script "scripts/' + script + '" is not a regular file; refusing to delete it.');
17621
+ await fs.unlink(file);
17622
+ });
17623
+ }`,
17624
+ },
17625
+ {
17626
+ // 스킬 스크립트: manage_skill 스키마에 write_script/delete_script와 script/content 두 칸만 더한다.
17627
+ file: "src/tools/manage-skill.ts",
17628
+ marker: " writeManagedSkillScript,",
17629
+ anchor: `import {
17630
+ deleteManagedSkill,
17631
+ getManagedSkillsDir,
17632
+ sanitizeSkillName,
17633
+ writeManagedSkill,
17634
+ } from "../autolearn/managed-skills";`,
17635
+ patched: `import {
17636
+ deleteManagedSkill,
17637
+ deleteManagedSkillScript,
17638
+ getManagedSkillsDir,
17639
+ sanitizeSkillName,
17640
+ sanitizeSkillScriptName,
17641
+ writeManagedSkill,
17642
+ writeManagedSkillScript,
17643
+ } from "../autolearn/managed-skills";`,
17644
+ },
17645
+ {
17646
+ file: "src/tools/manage-skill.ts",
17647
+ marker: ` action: "'create' | 'update' | 'delete' | 'write_script' | 'delete_script'",`,
17648
+ anchor: ` action: "'create' | 'update' | 'delete'",
17649
+ name: type("string").describe("kebab-case skill name"),
17650
+ "description?": type("string").describe(
17651
+ "one-line description of when to use the skill (required for create/update)",
17652
+ ),
17653
+ "body?": type("string").describe("the SKILL.md body in markdown, no frontmatter (required for create/update)"),
17654
+ }).narrow(
17655
+ (p, ctx) =>
17656
+ p.action === "delete" ||`,
17657
+ patched: ` action: "'create' | 'update' | 'delete' | 'write_script' | 'delete_script'",
17658
+ name: type("string").describe("kebab-case skill name"),
17659
+ "description?": type("string").describe(
17660
+ "one-line description of when to use the skill (required for create/update)",
17661
+ ),
17662
+ "body?": type("string").describe("the SKILL.md body in markdown, no frontmatter (required for create/update)"),
17663
+ "script?": type("string").describe(
17664
+ "script file name for write_script/delete_script, e.g. fetch-report.ts (.ts, .mjs, .js or .py); stored as scripts/<script>",
17665
+ ),
17666
+ "content?": type("string").describe("full script source (required for write_script)"),
17667
+ }).narrow(
17668
+ (p, ctx) =>
17669
+ p.action === "write_script"
17670
+ ? (p.script !== undefined && p.content !== undefined) ||
17671
+ ctx.mustBe('used with both "script" and "content" for "write_script"')
17672
+ : p.action === "delete_script"
17673
+ ? p.script !== undefined || ctx.mustBe('used with "script" for "delete_script"')
17674
+ : p.action === "delete" ||`,
17675
+ },
17676
+ {
17677
+ file: "src/tools/manage-skill.ts",
17678
+ marker: ` if (params.action === "write_script" || params.action === "delete_script") {`,
17679
+ anchor: ` async execute(_id: string, params: ManageSkillParams): Promise<AgentToolResult> {
17680
+ if (params.action === "delete") {`,
17681
+ patched: ` async execute(_id: string, params: ManageSkillParams): Promise<AgentToolResult> {
17682
+ if (params.action === "write_script" || params.action === "delete_script") {
17683
+ if (params.script === undefined) throw new Error('"' + params.action + '" requires "script".');
17684
+ const skillName = sanitizeSkillName(params.name);
17685
+ const script = sanitizeSkillScriptName(params.script);
17686
+ const url = "skill://" + skillName + "/scripts/" + script;
17687
+ if (params.action === "delete_script") {
17688
+ await deleteManagedSkillScript({ name: skillName, script });
17689
+ return {
17690
+ content: [{ type: "text", text: "Deleted " + url + "." }],
17691
+ details: { action: params.action, name: skillName, script },
17692
+ };
17693
+ }
17694
+ // skill:// resolves an authored skill first, so a script saved under a shadowed managed skill could never load.
17695
+ if (isNameClaimedByAuthoredSkill(skillName)) {
17696
+ return {
17697
+ content: [
17698
+ {
17699
+ type: "text",
17700
+ text:
17701
+ 'Cannot save scripts for "' +
17702
+ params.name +
17703
+ '": an authored skill of that name exists and managed skills cannot change or shadow it.',
17704
+ },
17705
+ ],
17706
+ isError: true,
17707
+ details: { action: params.action, name: skillName, script, shadowed: true },
17708
+ };
17709
+ }
17710
+ if (params.content === undefined) throw new Error('"write_script" requires "content".');
17711
+ const { created } = await writeManagedSkillScript({ name: skillName, script, content: params.content });
17712
+ return {
17713
+ content: [
17714
+ {
17715
+ type: "text",
17716
+ text: (created ? "Saved " : "Updated ") + url + ". Load it in eval with skills.load(" + JSON.stringify(url) + ").",
17717
+ },
17718
+ ],
17719
+ details: { action: params.action, name: skillName, script },
17720
+ };
17721
+ }
17722
+ if (params.action === "delete") {`,
17723
+ },
17724
+ {
17725
+ file: "src/prompts/tools/manage-skill.md",
17726
+ marker: "- `action: \"write_script\"`",
17727
+ anchor: "- `action: \"delete\"` — fails if skill absent.\n",
17728
+ patched: "- `action: \"delete\"` — fails if skill absent.\n- `action: \"write_script\"` — saves `content` as `scripts/<script>` (`.ts`, `.mjs`, `.js`, `.py`; 64 KB, 16 files) in an existing managed skill; replaces a file of that name. Save code only after it ran correctly; later sessions load it in eval with `skills.load(\"skill://<name>/scripts/<script>\")`.\n- `action: \"delete_script\"` — removes `scripts/<script>`.\n",
17729
+ },
16967
17730
  ];
16968
17731
  // EDITS 문자열의 줄 끝을 LF로 통일한다. 이 파일의 작업 사본이 CRLF여도 core 파일(LF)과
16969
17732
  // 비교·치환이 어긋나지 않는다. core 파일 자체의 줄 끝은 건드리지 않는다.