@wrongstack/core 0.313.0 → 0.313.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.
Files changed (182) hide show
  1. package/dist/chronicle/index.js +84 -6391
  2. package/dist/chronicle/project-server.js +84 -4868
  3. package/dist/chunk-2BUUIMQB.js +225 -0
  4. package/dist/chunk-2FK4WTFF.js +207 -0
  5. package/dist/chunk-2FQL4PDC.js +292 -0
  6. package/dist/chunk-2KKODIQU.js +2256 -0
  7. package/dist/chunk-2YU4WVBW.js +134 -0
  8. package/dist/chunk-3E244UDA.js +356 -0
  9. package/dist/chunk-3XPVB7BA.js +135 -0
  10. package/dist/chunk-4LJJYBAU.js +388 -0
  11. package/dist/chunk-4VWO3ZXL.js +342 -0
  12. package/dist/chunk-4XV6IR37.js +187 -0
  13. package/dist/chunk-4ZHTKJHM.js +183 -0
  14. package/dist/chunk-52RTVH4M.js +361 -0
  15. package/dist/chunk-57GEWK5X.js +26 -0
  16. package/dist/chunk-5DSMEVA4.js +931 -0
  17. package/dist/chunk-5RP3QTRL.js +1223 -0
  18. package/dist/chunk-5S437PRC.js +50 -0
  19. package/dist/chunk-5XDUGTKN.js +511 -0
  20. package/dist/chunk-62V2AFVG.js +422 -0
  21. package/dist/chunk-65RDIOR2.js +2470 -0
  22. package/dist/chunk-6AKOTGLM.js +597 -0
  23. package/dist/chunk-7ZHZE5EO.js +28 -0
  24. package/dist/chunk-A455ATZB.js +140 -0
  25. package/dist/chunk-AD7ZFZCS.js +46 -0
  26. package/dist/chunk-AI4LLQ2U.js +856 -0
  27. package/dist/chunk-ASX6AW36.js +789 -0
  28. package/dist/chunk-AT4V4XGI.js +3522 -0
  29. package/dist/chunk-AWNQ6ED6.js +311 -0
  30. package/dist/chunk-B4AMLZPB.js +9 -0
  31. package/dist/chunk-BAKVJLEV.js +417 -0
  32. package/dist/chunk-BJHVULHO.js +55 -0
  33. package/dist/chunk-CB2HFKTP.js +9 -0
  34. package/dist/chunk-CE4ZOCOI.js +1640 -0
  35. package/dist/chunk-CQQL4ZOQ.js +151 -0
  36. package/dist/chunk-DEIQMBD2.js +65 -0
  37. package/dist/chunk-E6I5RXVK.js +58 -0
  38. package/dist/chunk-EFEHDQGY.js +1589 -0
  39. package/dist/chunk-EODDVJBM.js +82 -0
  40. package/dist/chunk-EPC3GZD4.js +308 -0
  41. package/dist/chunk-FAYRX2AD.js +13 -0
  42. package/dist/chunk-FFSX2QDQ.js +362 -0
  43. package/dist/chunk-FHAMQS7Q.js +5856 -0
  44. package/dist/chunk-FIV4IJL7.js +2263 -0
  45. package/dist/chunk-FNXVT7HF.js +52 -0
  46. package/dist/chunk-FSWCWSTS.js +395 -0
  47. package/dist/chunk-FW5LDDMJ.js +16464 -0
  48. package/dist/chunk-FXQLIX3K.js +211 -0
  49. package/dist/chunk-G5AAKEEZ.js +228 -0
  50. package/dist/chunk-G5S3S5ZN.js +38 -0
  51. package/dist/chunk-GAZEWQEZ.js +231 -0
  52. package/dist/chunk-GFJOMSHG.js +125 -0
  53. package/dist/chunk-GFXZTHNI.js +158 -0
  54. package/dist/chunk-GM54GJBU.js +960 -0
  55. package/dist/chunk-GPQ3NJPR.js +900 -0
  56. package/dist/chunk-GTCU6DZ3.js +1 -0
  57. package/dist/chunk-H5E77KUJ.js +15 -0
  58. package/dist/chunk-H72H4XDU.js +1023 -0
  59. package/dist/chunk-HA4ZYA6T.js +178 -0
  60. package/dist/chunk-I45KAW2N.js +5020 -0
  61. package/dist/chunk-IQ35HMQL.js +1396 -0
  62. package/dist/chunk-IYFBFX34.js +86 -0
  63. package/dist/chunk-JJPMWPB6.js +109 -0
  64. package/dist/chunk-JTHKOYWU.js +663 -0
  65. package/dist/chunk-JVGPR3TS.js +365 -0
  66. package/dist/chunk-JVKB2TBL.js +48 -0
  67. package/dist/chunk-JX7VBZDK.js +31 -0
  68. package/dist/chunk-K3G6JBUC.js +5232 -0
  69. package/dist/chunk-K4YUNKU7.js +589 -0
  70. package/dist/chunk-KBZGE6LN.js +710 -0
  71. package/dist/chunk-KDQJCT2J.js +45 -0
  72. package/dist/chunk-KI4L2DUL.js +560 -0
  73. package/dist/chunk-KJ2ZTQSL.js +47 -0
  74. package/dist/chunk-KSJ5UTP7.js +154 -0
  75. package/dist/chunk-LHOA5DWA.js +28 -0
  76. package/dist/chunk-LX5QWLBL.js +27 -0
  77. package/dist/chunk-LZDDLNCZ.js +279 -0
  78. package/dist/chunk-M2WF6AXG.js +115 -0
  79. package/dist/chunk-M7BS2K6U.js +1 -0
  80. package/dist/chunk-NDBIPFNY.js +102 -0
  81. package/dist/chunk-NEMIWEXC.js +1199 -0
  82. package/dist/chunk-NFAYHPRH.js +323 -0
  83. package/dist/chunk-O37BQ65S.js +21 -0
  84. package/dist/chunk-O74MJHXU.js +187 -0
  85. package/dist/chunk-OJZEX73F.js +112 -0
  86. package/dist/chunk-PAAWLLZX.js +74 -0
  87. package/dist/chunk-PFXFQQRT.js +339 -0
  88. package/dist/chunk-PFZO3C6H.js +14 -0
  89. package/dist/chunk-PHH7LD45.js +113 -0
  90. package/dist/chunk-QRTG4XTI.js +188 -0
  91. package/dist/chunk-QVOVMXJR.js +1351 -0
  92. package/dist/chunk-RCQYT2I4.js +757 -0
  93. package/dist/chunk-RD6DEE36.js +1178 -0
  94. package/dist/chunk-RJIZEJ42.js +30 -0
  95. package/dist/chunk-RPOA7IAE.js +150 -0
  96. package/dist/chunk-SN2DQRL6.js +1172 -0
  97. package/dist/chunk-SQJVFEBV.js +41 -0
  98. package/dist/chunk-SQP64RCS.js +5391 -0
  99. package/dist/chunk-SVT4RNTR.js +49 -0
  100. package/dist/chunk-SWUFN7XM.js +43 -0
  101. package/dist/chunk-SXMVYC5H.js +174 -0
  102. package/dist/chunk-SZ6AYUIZ.js +23 -0
  103. package/dist/chunk-T3KGZBU5.js +3171 -0
  104. package/dist/chunk-TBRL2RCC.js +9 -0
  105. package/dist/chunk-TRT2HRWZ.js +18 -0
  106. package/dist/chunk-TYW2GI5I.js +1077 -0
  107. package/dist/chunk-UIWONWVG.js +4959 -0
  108. package/dist/chunk-VB3H5WWB.js +151 -0
  109. package/dist/chunk-W4A6QA3D.js +258 -0
  110. package/dist/chunk-WBPVKEEE.js +227 -0
  111. package/dist/chunk-WEBMSQP2.js +123 -0
  112. package/dist/chunk-WOEZV3Q4.js +333 -0
  113. package/dist/chunk-WQQON3SV.js +473 -0
  114. package/dist/chunk-WQTOVMGV.js +119 -0
  115. package/dist/chunk-WZO23AWO.js +2779 -0
  116. package/dist/chunk-XBDP4QPW.js +7 -0
  117. package/dist/chunk-XEBHSJLT.js +103 -0
  118. package/dist/chunk-XQ2ECNK3.js +1 -0
  119. package/dist/chunk-XQ2XGIBF.js +2086 -0
  120. package/dist/chunk-Y2GE5TSI.js +355 -0
  121. package/dist/chunk-YBUJCZ57.js +351 -0
  122. package/dist/chunk-YNCVSFGF.js +149 -0
  123. package/dist/chunk-YUPMXOGP.js +282 -0
  124. package/dist/chunk-YY6APYUM.js +49 -0
  125. package/dist/chunk-Z2IJUO45.js +691 -0
  126. package/dist/chunk-ZEHUCJHY.js +129 -0
  127. package/dist/chunk-ZYU2YOJ6.js +1496 -0
  128. package/dist/chunk-ZZ4V3WIZ.js +163 -0
  129. package/dist/coordination/agents/index.js +145 -6260
  130. package/dist/coordination/index.js +365 -34324
  131. package/dist/coordination/mailbox-project-server.js +48 -3276
  132. package/dist/coordination/provider-status-tracker.d.ts +4 -0
  133. package/dist/core/agent.d.ts +14 -1
  134. package/dist/core/index.js +267 -12480
  135. package/dist/core/model-ref.js +5 -23
  136. package/dist/core/provider-runner.d.ts +36 -1
  137. package/dist/design/index.js +50 -1087
  138. package/dist/execution/index.d.ts +1 -1
  139. package/dist/execution/index.js +1599 -21888
  140. package/dist/execution/prompt-enhancer.js +16 -535
  141. package/dist/execution/retry-policy.d.ts +31 -1
  142. package/dist/extension/index.js +7 -311
  143. package/dist/goal/index.js +33 -3435
  144. package/dist/hooks/index.js +14 -1138
  145. package/dist/hq/index.js +112 -5139
  146. package/dist/hq/protocol.js +21 -668
  147. package/dist/index.js +1410 -92656
  148. package/dist/infrastructure/index.js +64 -4811
  149. package/dist/kernel/events/provider-events.d.ts +15 -0
  150. package/dist/kernel/index.js +14 -992
  151. package/dist/models/index.js +40 -1117
  152. package/dist/notifications/index.js +3 -108
  153. package/dist/observability/index.js +17 -279
  154. package/dist/plugin/index.js +584 -14835
  155. package/dist/prompts/index.js +14 -948
  156. package/dist/registry/index.js +9 -822
  157. package/dist/registry-R23UBE7H.js +14 -0
  158. package/dist/replay/index.js +6 -84
  159. package/dist/review-finding-store-DKAV3AS6.js +21 -0
  160. package/dist/review-report-store-OWY3EC3N.js +19 -0
  161. package/dist/security/index.js +114 -2202
  162. package/dist/session-catalog/index.js +36 -2103
  163. package/dist/session-catalog/project-server.js +42 -1435
  164. package/dist/skills/index.js +35 -1578
  165. package/dist/storage/index.js +182 -15600
  166. package/dist/tasking/index.js +10 -676
  167. package/dist/tools/index.js +52 -9590
  168. package/dist/types/config/autonomy.d.ts +5 -0
  169. package/dist/types/config/ui.d.ts +1 -1
  170. package/dist/types/errors.d.ts +18 -0
  171. package/dist/types/index.js +121 -1342
  172. package/dist/types/provider-runner.d.ts +8 -0
  173. package/dist/types/session-markers.js +8 -144
  174. package/dist/utils/error.js +3 -4
  175. package/dist/utils/expect-defined.js +3 -9
  176. package/dist/utils/heap-watchdog.js +6 -706
  177. package/dist/utils/index.js +269 -5685
  178. package/dist/utils/sage-output-block.js +5 -42
  179. package/dist/wiring/proxy-rewrite.d.ts +18 -0
  180. package/dist/wiring/proxy-rewrite.js +18 -121
  181. package/dist/worktree/index.js +6 -887
  182. package/package.json +4 -4
@@ -0,0 +1,2263 @@
1
+ import {
2
+ toolMutates
3
+ } from "./chunk-ZEHUCJHY.js";
4
+ import {
5
+ allServers
6
+ } from "./chunk-FXQLIX3K.js";
7
+ import {
8
+ resolvePluginEnablement
9
+ } from "./chunk-3XPVB7BA.js";
10
+ import {
11
+ CouncilOrchestrator,
12
+ DEFAULT_COUNCIL_PROFILE_REGISTRY,
13
+ OneShotOrchestrator,
14
+ validateCouncilOptions
15
+ } from "./chunk-CE4ZOCOI.js";
16
+ import {
17
+ isValidMatrixKey,
18
+ phaseForRole,
19
+ resolveSubagentModelTarget
20
+ } from "./chunk-2BUUIMQB.js";
21
+ import {
22
+ AGENT_CATALOG
23
+ } from "./chunk-FHAMQS7Q.js";
24
+ import {
25
+ ToolCapabilities,
26
+ getDangerousCapabilities
27
+ } from "./chunk-A455ATZB.js";
28
+ import {
29
+ normalizeModelRef,
30
+ parseModelRef
31
+ } from "./chunk-RJIZEJ42.js";
32
+ import {
33
+ GOVERNED_TOOL_EXECUTOR_META_KEY
34
+ } from "./chunk-XBDP4QPW.js";
35
+ import {
36
+ mcpQualifiedToolName,
37
+ mcpServerToolPrefix,
38
+ readJsonObjectFile,
39
+ setJsonPath,
40
+ updateJsonObjectFile,
41
+ validateAgainstSchema
42
+ } from "./chunk-FFSX2QDQ.js";
43
+ import {
44
+ toErrorMessage
45
+ } from "./chunk-TBRL2RCC.js";
46
+ import {
47
+ expectDefined
48
+ } from "./chunk-PFZO3C6H.js";
49
+
50
+ // src/tools/council-tool.ts
51
+ var COUNCIL_TOOL_NAME = "council";
52
+ var MAX_COUNCIL_TOOL_OPTIONS = 12;
53
+ var MAX_COUNCIL_QUESTION_CHARS = 2e4;
54
+ var MAX_COUNCIL_CONTEXT_CHARS = 8e4;
55
+ var INPUT_SCHEMA = {
56
+ type: "object",
57
+ properties: {
58
+ question: {
59
+ type: "string",
60
+ description: "The decision or open question for the Council.",
61
+ maxLength: MAX_COUNCIL_QUESTION_CHARS
62
+ },
63
+ context: {
64
+ type: "string",
65
+ description: "Optional evidence and constraints. Treated as untrusted quoted data.",
66
+ maxLength: MAX_COUNCIL_CONTEXT_CHARS
67
+ },
68
+ options: {
69
+ type: "array",
70
+ maxItems: MAX_COUNCIL_TOOL_OPTIONS,
71
+ items: {
72
+ type: "object",
73
+ properties: {
74
+ id: { type: "string", description: "Stable option id." },
75
+ label: { type: "string", description: "Human-readable option label." },
76
+ consequence: { type: "string", description: "Optional consequence or trade-off." }
77
+ },
78
+ required: ["id", "label"],
79
+ additionalProperties: false
80
+ },
81
+ description: "Optional bounded list of choices. Omit for an open-ended Council answer."
82
+ },
83
+ profile: {
84
+ type: "string",
85
+ description: 'Registered Council profile id (e.g. "balanced", "fast", "risk-review"). Defaults to the host-configured profile.'
86
+ }
87
+ },
88
+ required: ["question"],
89
+ additionalProperties: false
90
+ };
91
+ function createCouncilTool(opts) {
92
+ const orchestrator = new CouncilOrchestrator({
93
+ ...opts,
94
+ fallbackProfileManager: opts.fallbackProfileManager
95
+ });
96
+ const profiles = opts.profiles ?? DEFAULT_COUNCIL_PROFILE_REGISTRY;
97
+ const profileIds = profiles.list().map((profile) => profile.id);
98
+ return {
99
+ name: COUNCIL_TOOL_NAME,
100
+ description: "Ask an independent, multi-persona Council to evaluate a decision or synthesize an answer. Uses bounded parallel voters, quorum/veto/weighted resolution, optional judging, model routing, fallback chains, and cancellation.",
101
+ usageHint: `Use for consequential or disputed decisions that benefit from independent lenses. Provide \`options\` for a vote or omit them for an open answer. Keep context evidence-focused; the Council treats it as untrusted data. Available profiles: ${profileIds.join(", ")}.`,
102
+ category: "meta",
103
+ inputSchema: INPUT_SCHEMA,
104
+ permission: "auto",
105
+ mutating: false,
106
+ riskTier: "safe",
107
+ managesOwnTimeout: true,
108
+ maxOutputBytes: 256e3,
109
+ async execute(input, _ctx, { signal }) {
110
+ const question = {
111
+ question: input.question,
112
+ ...input.context ? { context: input.context } : {},
113
+ ...input.options ? { options: input.options } : {},
114
+ ...input.profile ? { profile: input.profile } : {},
115
+ signal
116
+ };
117
+ return orchestrator.ask(question);
118
+ },
119
+ validate: (input) => validateCouncilToolInput(input, profileIds)
120
+ };
121
+ }
122
+ function validateCouncilToolInput(input, profileIds) {
123
+ const errors = [];
124
+ if (typeof input.profile === "string" && input.profile.trim()) {
125
+ const id = input.profile.trim();
126
+ if (!profileIds.includes(id)) {
127
+ errors.push(`Unknown \`profile\` "${id}". Available: ${profileIds.join(", ")}.`);
128
+ }
129
+ }
130
+ const question = input.question?.trim() ?? "";
131
+ if (!question) errors.push("`question` must not be empty.");
132
+ if (question.length > MAX_COUNCIL_QUESTION_CHARS) {
133
+ errors.push(`\`question\` must not exceed ${MAX_COUNCIL_QUESTION_CHARS} characters.`);
134
+ }
135
+ if ((input.context?.length ?? 0) > MAX_COUNCIL_CONTEXT_CHARS) {
136
+ errors.push(`\`context\` must not exceed ${MAX_COUNCIL_CONTEXT_CHARS} characters.`);
137
+ }
138
+ if ((input.options?.length ?? 0) > MAX_COUNCIL_TOOL_OPTIONS) {
139
+ errors.push(`\`options\` must not contain more than ${MAX_COUNCIL_TOOL_OPTIONS} items.`);
140
+ }
141
+ errors.push(...validateCouncilOptions(input.options));
142
+ return errors;
143
+ }
144
+
145
+ // src/tools/fallback-manage-helpers.ts
146
+ function normalizeRef(ref) {
147
+ return ref.trim().replace(/\s*\/\s*/g, "/").replace(/\s+/g, " ");
148
+ }
149
+ function isFavoriteRef(ref, config) {
150
+ const favorites = config.favoriteModels ?? [];
151
+ if (favorites.length === 0) return true;
152
+ const canonical = normalizeModelRef(ref, config.provider);
153
+ return favorites.some((f) => normalizeModelRef(f, config.provider) === canonical);
154
+ }
155
+ function notFavoriteError(ref, config) {
156
+ const favorites = config.favoriteModels ?? [];
157
+ return `"${ref}" is not in your favorites list. ` + (favorites.length === 0 ? 'Add some favorites first with favorite_manage({ action: "add", model: "<provider/model>" }).' : `Current favorites: ${favorites.join(", ") || "(none)"}. Use favorite_manage to add this model first.`);
158
+ }
159
+ function modelList(config) {
160
+ return config.favoriteModels ?? [];
161
+ }
162
+ function profileList(config) {
163
+ return config.fallbackProfiles ?? {};
164
+ }
165
+ function chainList(config) {
166
+ return config.fallbackModels ?? [];
167
+ }
168
+
169
+ // src/tools/fallback-chain-manage-tool.ts
170
+ var FALLBACK_CHAIN_MANAGE_TOOL_NAME = "fallback_chain_manage";
171
+ var FALLBACK_CHAIN_SCHEMA = {
172
+ type: "object",
173
+ properties: {
174
+ action: {
175
+ type: "string",
176
+ enum: ["list", "add", "insert", "remove", "clear"],
177
+ description: "Operation: list (show chain), add (append), insert (insert at position), remove (by index or ref), clear (empty the chain)."
178
+ },
179
+ model: {
180
+ type: "string",
181
+ description: 'Model reference for add/insert/remove (e.g. "anthropic/claude-haiku-3"). Must be in your favorites list for add/insert. Required for add, insert, and remove (when removing by ref).'
182
+ },
183
+ index: {
184
+ type: "number",
185
+ description: '1-based insertion position (for action="insert") or removal index (for action="remove"). For insert: the new entry is placed before this position. Omit to append. For remove: alternative to model. Omit to remove by model ref.'
186
+ }
187
+ },
188
+ required: ["action"],
189
+ additionalProperties: false
190
+ };
191
+ function createFallbackChainManageTool(opts) {
192
+ return {
193
+ name: FALLBACK_CHAIN_MANAGE_TOOL_NAME,
194
+ description: "View or change the active rate-limit fallback chain. When the primary model is overloaded (429/5xx), the agent rotates through this chain in order. Every new entry must be a FAVORITE model \u2014 add it via favorite_manage first. Use insert to place a fallback at a specific position; use remove to delete an entry.",
195
+ usageHint: '"list" to see the current chain. "add" with a favorite model to append. "insert" with an index (1-based) to place before that position. "remove" with index or model ref. "clear" to empty the chain (auto fallback takes over).',
196
+ category: "config",
197
+ inputSchema: FALLBACK_CHAIN_SCHEMA,
198
+ permission: "auto",
199
+ mutating: true,
200
+ riskTier: "standard",
201
+ icon: "settings",
202
+ async execute(input) {
203
+ const config = opts.getConfig();
204
+ const chain = [...chainList(config)];
205
+ if (input.action === "list") {
206
+ if (chain.length === 0) {
207
+ return {
208
+ status: "ok",
209
+ message: 'Fallback chain is empty. Add entries with "add" or enable auto fallback.',
210
+ chain: []
211
+ };
212
+ }
213
+ const msg = chain.map((ref, i) => ` ${i + 1}. ${ref}`).join("\n");
214
+ return {
215
+ status: "ok",
216
+ message: `Fallback chain (${chain.length}):
217
+ ${msg}`,
218
+ chain: [...chain]
219
+ };
220
+ }
221
+ if (input.action === "add") {
222
+ if (!input.model) {
223
+ return {
224
+ status: "error",
225
+ message: 'Provide "model" (e.g. "anthropic/claude-haiku-3") to add to the chain.'
226
+ };
227
+ }
228
+ const ref = normalizeRef(input.model);
229
+ if (!isFavoriteRef(ref, config)) {
230
+ return { status: "error", message: notFavoriteError(ref, config) };
231
+ }
232
+ if (chain.some((e) => normalizeRef(e) === ref)) {
233
+ return { status: "error", message: `"${ref}" is already in the chain.` };
234
+ }
235
+ chain.push(ref);
236
+ await opts.updateConfig((cfg) => {
237
+ cfg.fallbackModels = chain;
238
+ });
239
+ return {
240
+ status: "ok",
241
+ message: `\u2713 Added to chain: ${ref} (position ${chain.length})`,
242
+ chain: [...chain]
243
+ };
244
+ }
245
+ if (input.action === "insert") {
246
+ if (!input.model) {
247
+ return { status: "error", message: 'Provide "model" to insert into the chain.' };
248
+ }
249
+ const ref = normalizeRef(input.model);
250
+ if (!isFavoriteRef(ref, config)) {
251
+ return { status: "error", message: notFavoriteError(ref, config) };
252
+ }
253
+ if (chain.some((e) => normalizeRef(e) === ref)) {
254
+ return { status: "error", message: `"${ref}" is already in the chain.` };
255
+ }
256
+ let pos = chain.length;
257
+ if (input.index !== void 0) {
258
+ pos = Math.max(0, Math.min(chain.length, input.index - 1));
259
+ }
260
+ chain.splice(pos, 0, ref);
261
+ await opts.updateConfig((cfg) => {
262
+ cfg.fallbackModels = chain;
263
+ });
264
+ return {
265
+ status: "ok",
266
+ message: `\u2713 Inserted at position ${pos + 1}: ${ref}`,
267
+ chain: [...chain]
268
+ };
269
+ }
270
+ if (input.action === "remove") {
271
+ if (chain.length === 0) {
272
+ return { status: "error", message: "Chain is empty \u2014 nothing to remove." };
273
+ }
274
+ if (input.index !== void 0) {
275
+ const idx = input.index - 1;
276
+ if (idx < 0 || idx >= chain.length) {
277
+ return {
278
+ status: "error",
279
+ message: `Index ${input.index} is out of range (1\u2013${chain.length}).`
280
+ };
281
+ }
282
+ const [removed] = chain.splice(idx, 1);
283
+ await opts.updateConfig((cfg) => {
284
+ cfg.fallbackModels = chain;
285
+ });
286
+ return { status: "ok", message: `\u2713 Removed: ${removed}`, chain: [...chain] };
287
+ }
288
+ if (input.model) {
289
+ const ref = normalizeRef(input.model);
290
+ const idx = chain.findIndex((e) => normalizeRef(e) === ref);
291
+ if (idx === -1) {
292
+ return { status: "error", message: `"${ref}" not found in chain.` };
293
+ }
294
+ const [removed] = chain.splice(idx, 1);
295
+ await opts.updateConfig((cfg) => {
296
+ cfg.fallbackModels = chain;
297
+ });
298
+ return { status: "ok", message: `\u2713 Removed: ${removed}`, chain: [...chain] };
299
+ }
300
+ return { status: "error", message: 'Provide "index" or "model" to remove from the chain.' };
301
+ }
302
+ if (input.action === "clear") {
303
+ if (chain.length === 0) {
304
+ return { status: "ok", message: "Chain is already empty." };
305
+ }
306
+ await opts.updateConfig((cfg) => {
307
+ cfg.fallbackModels = [];
308
+ });
309
+ return {
310
+ status: "ok",
311
+ message: "\u2713 Cleared the fallback chain. Auto fallback will take over when enabled."
312
+ };
313
+ }
314
+ return { status: "error", message: `Unknown action: "${input.action}".` };
315
+ }
316
+ };
317
+ }
318
+
319
+ // src/tools/fallback-favorite-manage-tool.ts
320
+ var FAVORITE_MANAGE_TOOL_NAME = "favorite_manage";
321
+ var FAVORITE_MANAGE_SCHEMA = {
322
+ type: "object",
323
+ properties: {
324
+ action: {
325
+ type: "string",
326
+ enum: ["list", "add", "remove"],
327
+ description: "Operation to perform: list (show all), add (add a favorite), remove (remove by index or ref)."
328
+ },
329
+ model: {
330
+ type: "string",
331
+ description: 'Model reference to add/remove (e.g. "anthropic/claude-haiku-3", "openai/gpt-4o-mini"). Required for "add" and "remove" (when removing by ref).'
332
+ },
333
+ index: {
334
+ type: "number",
335
+ description: '1-based index for removal. Alternative to `model`. Used when action="remove".'
336
+ }
337
+ },
338
+ required: ["action"],
339
+ additionalProperties: false
340
+ };
341
+ function createFavoriteManageTool(opts) {
342
+ return {
343
+ name: FAVORITE_MANAGE_TOOL_NAME,
344
+ description: "Manage your favorite provider/model list. Favorites are the only models that can be added to fallback chains and profiles. The LLM uses this tool to curate which models are available for fallback and role assignment.",
345
+ usageHint: 'Start with "list" to see current favorites. Use "add <provider/model>" to add. Use "remove <index|ref>" to remove.',
346
+ category: "config",
347
+ inputSchema: FAVORITE_MANAGE_SCHEMA,
348
+ permission: "auto",
349
+ mutating: true,
350
+ riskTier: "standard",
351
+ icon: "settings",
352
+ async execute(input) {
353
+ const config = opts.getConfig();
354
+ const favorites = [...modelList(config)];
355
+ if (input.action === "list") {
356
+ const msg = favorites.length === 0 ? 'No favorites set. Add one with favorite_manage({ action: "add", model: "<provider/model>" }).' : `Favorites (${favorites.length}):
357
+ ` + favorites.map((f, i) => ` ${i + 1}. ${f}`).join("\n");
358
+ return { status: "ok", message: msg, favorites: [...favorites] };
359
+ }
360
+ if (input.action === "add") {
361
+ if (!input.model) {
362
+ return { status: "error", message: 'Provide "model" (e.g. "anthropic/claude-haiku-3") to add a favorite.' };
363
+ }
364
+ const ref = normalizeRef(input.model);
365
+ const canonical = normalizeModelRef(ref, config.provider);
366
+ if (favorites.some((f) => normalizeModelRef(f, config.provider) === canonical)) {
367
+ return { status: "error", message: `"${ref}" is already a favorite.` };
368
+ }
369
+ favorites.push(ref);
370
+ await opts.updateConfig((cfg) => {
371
+ cfg.favoriteModels = favorites;
372
+ });
373
+ return {
374
+ status: "ok",
375
+ message: `\u2713 Added favorite: ${ref} (${favorites.length} total)`,
376
+ favorites: [...favorites]
377
+ };
378
+ }
379
+ if (input.action === "remove") {
380
+ if (input.index !== void 0) {
381
+ const idx = input.index - 1;
382
+ if (idx < 0 || idx >= favorites.length) {
383
+ return { status: "error", message: `Index ${input.index} is out of range (1\u2013${favorites.length}).` };
384
+ }
385
+ const [removed] = favorites.splice(idx, 1);
386
+ await opts.updateConfig((cfg) => {
387
+ cfg.favoriteModels = favorites;
388
+ });
389
+ return { status: "ok", message: `\u2713 Removed favorite: ${removed}`, favorites: [...favorites] };
390
+ }
391
+ if (input.model) {
392
+ const ref = normalizeRef(input.model);
393
+ const canonical = normalizeModelRef(ref, config.provider);
394
+ const idx = favorites.findIndex((f) => normalizeModelRef(f, config.provider) === canonical);
395
+ if (idx === -1) {
396
+ return { status: "error", message: `Favorite "${ref}" not found. Use "list" to see all favorites.` };
397
+ }
398
+ const [removed] = favorites.splice(idx, 1);
399
+ await opts.updateConfig((cfg) => {
400
+ cfg.favoriteModels = favorites;
401
+ });
402
+ return { status: "ok", message: `\u2713 Removed favorite: ${removed}`, favorites: [...favorites] };
403
+ }
404
+ return { status: "error", message: 'Provide either "model" or "index" to remove a favorite.' };
405
+ }
406
+ return { status: "error", message: `Unknown action: "${input.action}". Use "list", "add", or "remove".` };
407
+ }
408
+ };
409
+ }
410
+
411
+ // src/tools/fallback-profile-manage-tool.ts
412
+ var FALLBACK_PROFILE_MANAGE_TOOL_NAME = "fallback_profile_manage";
413
+ var FALLBACK_PROFILE_SCHEMA = {
414
+ type: "object",
415
+ properties: {
416
+ action: {
417
+ type: "string",
418
+ enum: ["list", "set", "delete"],
419
+ description: "Operation: list (show all profiles), set (create/update), delete (remove a profile)."
420
+ },
421
+ name: {
422
+ type: "string",
423
+ description: 'Profile name (e.g. "fast", "economy", "reliable"). Required for "set" and "delete".'
424
+ },
425
+ chain: {
426
+ type: "array",
427
+ items: { type: "string" },
428
+ description: 'Ordered list of model references for the profile. Each entry must be a favorite model. Required for "set". Example: ["anthropic/claude-haiku-3", "openai/gpt-4o-mini"].'
429
+ }
430
+ },
431
+ required: ["action"],
432
+ additionalProperties: false
433
+ };
434
+ function createFallbackProfileManageTool(opts) {
435
+ return {
436
+ name: FALLBACK_PROFILE_MANAGE_TOOL_NAME,
437
+ description: "Manage named fallback profiles. A profile is a reusable, ordered list of model references that can be assigned to agent roles. Every entry in a profile must be a FAVORITE model \u2014 add it via favorite_manage first. Use /setmodel or agent_model_assign to assign a profile to a role.",
438
+ usageHint: '"list" to see all profiles. "set" with name and chain (array of model refs) to create or replace a profile. "delete" with name to remove a profile.',
439
+ category: "config",
440
+ inputSchema: FALLBACK_PROFILE_SCHEMA,
441
+ permission: "auto",
442
+ mutating: true,
443
+ riskTier: "standard",
444
+ icon: "settings",
445
+ async execute(input) {
446
+ const config = opts.getConfig();
447
+ const profiles = { ...profileList(config) };
448
+ if (input.action === "list") {
449
+ const names = Object.keys(profiles);
450
+ if (names.length === 0) {
451
+ return {
452
+ status: "ok",
453
+ message: 'No fallback profiles. Create one with "set".',
454
+ profiles: {}
455
+ };
456
+ }
457
+ const msg = names.sort().map((name) => ` ${name} \u2192 ${profiles[name]?.join(" \u2192 ") || "(empty)"}`).join("\n");
458
+ return { status: "ok", message: `Fallback profiles:
459
+ ${msg}`, profiles: { ...profiles } };
460
+ }
461
+ if (input.action === "set") {
462
+ if (!input.name) {
463
+ return { status: "error", message: 'Provide "name" for the profile (e.g. "fast").' };
464
+ }
465
+ if (!input.chain || input.chain.length === 0) {
466
+ return { status: "error", message: 'Provide "chain" \u2014 a non-empty array of model references.' };
467
+ }
468
+ const invalid = [];
469
+ for (const ref of input.chain) {
470
+ if (!isFavoriteRef(ref, config)) {
471
+ invalid.push(ref);
472
+ }
473
+ }
474
+ if (invalid.length > 0) {
475
+ return {
476
+ status: "error",
477
+ message: `The following entries are not in your favorites list:
478
+ ${invalid.join("\n ")}
479
+
480
+ Add them first with favorite_manage({ action: "add", model: "<ref>" }).`
481
+ };
482
+ }
483
+ profiles[input.name] = [...input.chain];
484
+ await opts.updateConfig((cfg) => {
485
+ cfg.fallbackProfiles = profiles;
486
+ });
487
+ return {
488
+ status: "ok",
489
+ message: `\u2713 Profile "${input.name}" \u2192 ${input.chain.join(" \u2192 ")}`,
490
+ profiles: { ...profiles }
491
+ };
492
+ }
493
+ if (input.action === "delete") {
494
+ if (!input.name) {
495
+ return { status: "error", message: 'Provide "name" of the profile to delete.' };
496
+ }
497
+ if (!(input.name in profiles)) {
498
+ return { status: "error", message: `Profile "${input.name}" not found.` };
499
+ }
500
+ delete profiles[input.name];
501
+ await opts.updateConfig((cfg) => {
502
+ cfg.fallbackProfiles = profiles;
503
+ });
504
+ return {
505
+ status: "ok",
506
+ message: `\u2713 Deleted profile: ${input.name}`,
507
+ profiles: { ...profiles }
508
+ };
509
+ }
510
+ return { status: "error", message: `Unknown action: "${input.action}".` };
511
+ }
512
+ };
513
+ }
514
+
515
+ // src/tools/fallback-agent-model-assign-tool.ts
516
+ var AGENT_MODEL_ASSIGN_TOOL_NAME = "agent_model_assign";
517
+ var AGENT_MODEL_ASSIGN_SCHEMA = {
518
+ type: "object",
519
+ properties: {
520
+ role: {
521
+ type: "string",
522
+ description: 'Matrix key: a catalog role (e.g. "security-scanner", "bug-hunter"), a phase name (e.g. "review", "implementation"), or "*" for the fleet-wide default.'
523
+ },
524
+ provider: {
525
+ type: "string",
526
+ description: 'Provider id (e.g. "anthropic", "openai"). When omitted, the leader provider is used. The provider.model combination must be in your favorites list.'
527
+ },
528
+ model: {
529
+ type: "string",
530
+ description: 'Model id (e.g. "claude-haiku-3", "gpt-4o-mini"). When omitted together with provider, the role falls back to the leader model. Must be in your favorites list.'
531
+ },
532
+ profile: {
533
+ type: "string",
534
+ description: 'Named fallback profile to assign (e.g. "fast", "economy"). Alternative to provider+model. When set, the first profile entry becomes the primary model and the rest are the fallback chain.'
535
+ },
536
+ clear: {
537
+ type: "boolean",
538
+ description: "Set to true to remove the matrix entry for this role (it will fall through to phase/*/leader)."
539
+ }
540
+ },
541
+ required: ["role"],
542
+ additionalProperties: false
543
+ };
544
+ function createAgentModelAssignTool(opts) {
545
+ return {
546
+ name: AGENT_MODEL_ASSIGN_TOOL_NAME,
547
+ description: "Assign a provider/model or a fallback profile to a specific agent role, phase, or the fleet-wide default. This is the LLM-accessible equivalent of /setmodel set. The provider+model combination must be in your favorites list (unless only clearing). Resolution precedence: exact role \u2192 phase \u2192 * \u2192 leader model.",
548
+ usageHint: 'Use "list" as role to see current assignments. Set with role + model, or role + provider + model, or role + profile. Set role + clear=true to remove a matrix entry. The provider/model must be a favorite.',
549
+ category: "config",
550
+ inputSchema: AGENT_MODEL_ASSIGN_SCHEMA,
551
+ permission: "auto",
552
+ mutating: true,
553
+ riskTier: "standard",
554
+ icon: "settings",
555
+ async execute(input) {
556
+ const config = opts.getConfig();
557
+ const modes = [input.clear ? "clear" : null, input.profile ? "profile" : null, input.model ? "model" : null].filter(Boolean);
558
+ if (modes.length > 1) {
559
+ return {
560
+ status: "error",
561
+ message: `Conflicting assignment modes: ${modes.join(" + ")}. Use exactly one: clear=true, profile="name", or model="name" (optionally with provider).`
562
+ };
563
+ }
564
+ if (input.role === "list") {
565
+ const matrix = config.modelMatrix ?? {};
566
+ const keys = Object.keys(matrix);
567
+ if (keys.length === 0) {
568
+ return { status: "ok", message: "No matrix assignments. All roles use the leader model." };
569
+ }
570
+ const msg = keys.sort().map((k) => ` ${k} \u2192 ${JSON.stringify(matrix[k])}`).join("\n");
571
+ return { status: "ok", message: `Model matrix (${keys.length} entries):
572
+ ${msg}` };
573
+ }
574
+ if (!isValidMatrixKey(input.role)) {
575
+ return {
576
+ status: "error",
577
+ message: `"${input.role}" is not a valid matrix key. Use a catalog role (e.g. "security-scanner"), a phase (e.g. "review"), or "*" for the fleet-wide default.`
578
+ };
579
+ }
580
+ if (input.clear) {
581
+ const matrix = { ...config.modelMatrix ?? {} };
582
+ if (!(input.role in matrix)) {
583
+ return { status: "ok", message: `No matrix entry for "${input.role}" to clear.` };
584
+ }
585
+ delete matrix[input.role];
586
+ await opts.updateConfig((cfg) => {
587
+ cfg.modelMatrix = matrix;
588
+ });
589
+ return { status: "ok", message: `\u2713 Cleared matrix entry for "${input.role}".` };
590
+ }
591
+ if (!input.model && !input.profile && !input.provider) {
592
+ const matrix = config.modelMatrix ?? {};
593
+ const entry = matrix[input.role];
594
+ if (!entry) {
595
+ return { status: "ok", message: `No specific assignment for "${input.role}". It uses the leader model or phase/* fallback.` };
596
+ }
597
+ return { status: "ok", message: `"${input.role}" \u2192 ${JSON.stringify(entry)}` };
598
+ }
599
+ if (input.profile && !input.model) {
600
+ const profiles = profileList(config);
601
+ if (!profiles[input.profile]) {
602
+ return { status: "error", message: `Profile "${input.profile}" not found. Create it with fallback_profile_manage first.` };
603
+ }
604
+ const matrix = { ...config.modelMatrix ?? {} };
605
+ matrix[input.role] = { fallbackProfile: input.profile };
606
+ await opts.updateConfig((cfg) => {
607
+ cfg.modelMatrix = matrix;
608
+ });
609
+ return { status: "ok", message: `\u2713 "${input.role}" \u2192 profile: ${input.profile}` };
610
+ }
611
+ if (input.model) {
612
+ const effectiveProvider = input.provider ?? config.provider;
613
+ const ref = `${effectiveProvider}/${input.model}`;
614
+ if (!isFavoriteRef(ref, config)) {
615
+ return { status: "error", message: notFavoriteError(ref, config) };
616
+ }
617
+ const matrix = { ...config.modelMatrix ?? {} };
618
+ const previousRuntime = matrix[input.role]?.modelRuntime;
619
+ matrix[input.role] = input.provider ? { provider: input.provider, model: input.model, ...previousRuntime ? { modelRuntime: previousRuntime } : {} } : { model: input.model, ...previousRuntime ? { modelRuntime: previousRuntime } : {} };
620
+ await opts.updateConfig((cfg) => {
621
+ cfg.modelMatrix = matrix;
622
+ });
623
+ const display = input.provider ? `${input.provider}/${input.model}` : `${input.model} (leader provider)`;
624
+ return { status: "ok", message: `\u2713 "${input.role}" \u2192 ${display}` };
625
+ }
626
+ return { status: "error", message: "Provide model, profile, or clear=true for the role assignment." };
627
+ }
628
+ };
629
+ }
630
+
631
+ // src/tools/fallback-provider-manage-tool.ts
632
+ var PROVIDER_MANAGE_TOOL_NAME = "provider_manage";
633
+ var PROVIDER_MANAGE_SCHEMA = {
634
+ type: "object",
635
+ properties: {
636
+ action: {
637
+ type: "string",
638
+ enum: ["list", "add", "configure", "remove"],
639
+ description: "Operation: list (show all providers), add (add a new provider config), configure (update fields of an existing provider), remove (delete a provider config)."
640
+ },
641
+ provider: {
642
+ type: "string",
643
+ description: 'Provider id (e.g. "openai", "anthropic"). Required for all actions except list.'
644
+ },
645
+ type: {
646
+ type: "string",
647
+ description: 'Provider type (e.g. "openai", "anthropic"). Required for "add".'
648
+ },
649
+ models: {
650
+ type: "array",
651
+ items: { type: "string" },
652
+ description: "Model list to restrict visibility for this provider. Optional for add/configure."
653
+ },
654
+ baseUrl: {
655
+ type: "string",
656
+ description: "Custom base URL (e.g. for self-hosted endpoints). Optional."
657
+ },
658
+ family: {
659
+ type: "string",
660
+ description: 'Wire-family override (e.g. "openai", "openai-compatible", "anthropic"). When set, the provider can be constructed without a catalog entry.'
661
+ },
662
+ envVars: {
663
+ type: "array",
664
+ items: { type: "string" },
665
+ description: "Custom env var names to probe when apiKey is missing. Optional."
666
+ },
667
+ autoDiscoverModels: {
668
+ type: "boolean",
669
+ description: "Auto-fetch model list from {baseUrl}/models. Optional."
670
+ },
671
+ apiKey: {
672
+ type: "string",
673
+ description: "**NOT RECOMMENDED** \u2014 use provider_key_set instead. The LLM output may contain this value; use env var references for safety."
674
+ }
675
+ },
676
+ required: ["action"],
677
+ additionalProperties: false
678
+ };
679
+ var CREDENTIAL_SELECTOR_FIELDS = ["apiKey", "apiKeys", "activeKey", "envVars"];
680
+ function envVarsClaimedByOtherProviders(providers, exceptProvider) {
681
+ const claimed = /* @__PURE__ */ new Map();
682
+ for (const [id, entry] of Object.entries(providers)) {
683
+ if (id === exceptProvider) continue;
684
+ const names = entry?.["envVars"];
685
+ if (!Array.isArray(names)) continue;
686
+ for (const name of names) {
687
+ if (typeof name === "string" && !claimed.has(name)) claimed.set(name, id);
688
+ }
689
+ }
690
+ return claimed;
691
+ }
692
+ function rejectBorrowedEnvVars(providers, provider, requested) {
693
+ if (!requested) return null;
694
+ const claimed = envVarsClaimedByOtherProviders(providers, provider);
695
+ for (const name of requested) {
696
+ const owner = typeof name === "string" ? claimed.get(name) : void 0;
697
+ if (owner !== void 0) {
698
+ return `Environment variable "${String(name)}" already supplies the key for provider "${owner}". Reading another provider's credential from "${provider}" is not allowed; use provider_key_set to give "${provider}" its own key.`;
699
+ }
700
+ }
701
+ return null;
702
+ }
703
+ function validateProviderBaseUrl(raw) {
704
+ let url;
705
+ try {
706
+ url = new URL(raw);
707
+ } catch {
708
+ return `Invalid baseUrl: ${raw} is not a valid URL.`;
709
+ }
710
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
711
+ return `Invalid baseUrl: ${url.protocol} is not supported \u2014 use http: or https:.`;
712
+ }
713
+ if (url.username || url.password) {
714
+ return "Invalid baseUrl: credentials embedded in the URL are not accepted.";
715
+ }
716
+ return null;
717
+ }
718
+ function createProviderManageTool(opts) {
719
+ return {
720
+ name: PROVIDER_MANAGE_TOOL_NAME,
721
+ description: "View or configure provider entries. List all configured providers with their type, model lists, base URL, and key status. Add new providers, update their settings, or remove unused ones. API keys should be set via provider_key_set instead of passing them here \u2014 they are visible in the LLM output.",
722
+ usageHint: '"list" to see all providers. "add" with provider id and type to create. "configure" to update models, baseUrl, family, or envVars. "remove" to delete a provider. Use provider_key_set for API key management.',
723
+ category: "config",
724
+ inputSchema: PROVIDER_MANAGE_SCHEMA,
725
+ permission: "auto",
726
+ mutating: true,
727
+ subjectKey: "baseUrl",
728
+ riskTier: "standard",
729
+ icon: "settings",
730
+ async execute(input) {
731
+ const config = opts.getConfig();
732
+ const providers = {
733
+ ...config.providers ?? {}
734
+ };
735
+ const leaderProvider = config.provider ?? "";
736
+ if (input.action === "list") {
737
+ const ids = Object.keys(providers);
738
+ if (ids.length === 0) {
739
+ return { status: "ok", message: "No providers configured.", providers: [] };
740
+ }
741
+ const msg = ids.sort().map((id) => {
742
+ const entry = providers[id] ?? {};
743
+ const type = entry.type ?? "(unknown)";
744
+ const models = Array.isArray(entry.models) ? entry.models.join(", ") : "(all)";
745
+ const hasKey = entry.apiKey ? "\u2713" : entry.apiKeys ? "\u2713" : "\u2717";
746
+ const prefix = id === leaderProvider ? "\u2605 " : " ";
747
+ const baseUrl = entry.baseUrl ? ` url:${entry.baseUrl}` : "";
748
+ const family = entry.family ? ` family:${entry.family}` : "";
749
+ return ` ${prefix}${id} (${type}) key:${hasKey} models:[${models}]${baseUrl}${family}`;
750
+ }).join("\n");
751
+ return {
752
+ status: "ok",
753
+ message: `Providers (leader: ${leaderProvider}):
754
+ ${msg}`,
755
+ providers: ids
756
+ };
757
+ }
758
+ if (input.action === "add") {
759
+ if (!input.provider || !input.type) {
760
+ return { status: "error", message: 'Provide "provider" (id) and "type" to add a provider.' };
761
+ }
762
+ if (providers[input.provider]) {
763
+ return { status: "error", message: `Provider "${input.provider}" already exists. Use "configure" to update.` };
764
+ }
765
+ if (input.baseUrl) {
766
+ const invalid = validateProviderBaseUrl(input.baseUrl);
767
+ if (invalid) return { status: "error", message: invalid };
768
+ }
769
+ const borrowed = rejectBorrowedEnvVars(providers, input.provider, input.envVars);
770
+ if (borrowed) return { status: "error", message: borrowed };
771
+ const entry = { type: input.type };
772
+ if (input.models) entry.models = input.models;
773
+ if (input.baseUrl) entry.baseUrl = input.baseUrl;
774
+ if (input.family) entry.family = input.family;
775
+ if (input.envVars) entry.envVars = input.envVars;
776
+ if (input.autoDiscoverModels !== void 0) entry.autoDiscoverModels = input.autoDiscoverModels;
777
+ if (input.apiKey) entry.apiKey = input.apiKey;
778
+ providers[input.provider] = entry;
779
+ await opts.updateConfig((cfg) => {
780
+ cfg.providers = providers;
781
+ });
782
+ return { status: "ok", message: `\u2713 Added provider: ${input.provider} (type: ${input.type})` };
783
+ }
784
+ if (input.action === "configure") {
785
+ if (!input.provider) {
786
+ return { status: "error", message: 'Provide "provider" id to configure.' };
787
+ }
788
+ if (!providers[input.provider]) {
789
+ return { status: "error", message: `Provider "${input.provider}" not found. Use "add" first or check "list".` };
790
+ }
791
+ if (input.baseUrl) {
792
+ const invalid = validateProviderBaseUrl(input.baseUrl);
793
+ if (invalid) return { status: "error", message: invalid };
794
+ }
795
+ const previous = { ...providers[input.provider] };
796
+ const entry = { ...previous };
797
+ if (input.models !== void 0) entry.models = input.models;
798
+ if (input.baseUrl !== void 0) entry.baseUrl = input.baseUrl || void 0;
799
+ if (input.family !== void 0) entry.family = input.family || void 0;
800
+ if (input.envVars !== void 0) entry.envVars = input.envVars;
801
+ if (input.autoDiscoverModels !== void 0) entry.autoDiscoverModels = input.autoDiscoverModels;
802
+ if (input.apiKey !== void 0) entry.apiKey = input.apiKey || void 0;
803
+ const endpointChanged = input.baseUrl !== void 0 && (entry.baseUrl ?? void 0) !== (previous.baseUrl ?? void 0);
804
+ const explicitlySupplied = /* @__PURE__ */ new Set([
805
+ ...input.apiKey !== void 0 ? ["apiKey"] : [],
806
+ ...input.envVars !== void 0 ? ["envVars"] : []
807
+ ]);
808
+ const droppedFields = endpointChanged ? CREDENTIAL_SELECTOR_FIELDS.filter(
809
+ (field) => !explicitlySupplied.has(field) && previous[field] !== void 0
810
+ ) : [];
811
+ for (const field of droppedFields) entry[field] = void 0;
812
+ const borrowed = rejectBorrowedEnvVars(providers, input.provider, input.envVars);
813
+ if (borrowed) return { status: "error", message: borrowed };
814
+ providers[input.provider] = entry;
815
+ await opts.updateConfig((cfg) => {
816
+ cfg.providers = providers;
817
+ });
818
+ const updated = Object.keys({ ...entry }).filter((k) => k !== "apiKey").join(", ");
819
+ const keyNote = droppedFields.length > 0 ? ` \u2014 cleared ${droppedFields.join(", ")} because the base URL changed; set the key again with provider_key_set` : "";
820
+ return { status: "ok", message: `\u2713 Updated ${input.provider}: ${updated}${keyNote}` };
821
+ }
822
+ if (input.action === "remove") {
823
+ if (!input.provider) {
824
+ return { status: "error", message: 'Provide "provider" id to remove.' };
825
+ }
826
+ if (!providers[input.provider]) {
827
+ return { status: "error", message: `Provider "${input.provider}" not found.` };
828
+ }
829
+ if (input.provider === leaderProvider) {
830
+ return { status: "error", message: `Cannot remove the active leader provider "${input.provider}". Switch the leader first.` };
831
+ }
832
+ delete providers[input.provider];
833
+ await opts.updateConfig((cfg) => {
834
+ cfg.providers = providers;
835
+ });
836
+ return { status: "ok", message: `\u2713 Removed provider: ${input.provider}` };
837
+ }
838
+ return { status: "error", message: `Unknown action: "${input.action}".` };
839
+ }
840
+ };
841
+ }
842
+
843
+ // src/tools/fallback-provider-key-store.ts
844
+ async function storeProviderKey(providers, input, keyValue, opts) {
845
+ const providerId = input.provider;
846
+ if (!providers[providerId]) {
847
+ providers[providerId] = { type: providerId };
848
+ }
849
+ const entry = providers[providerId];
850
+ const existingKeys = Array.isArray(entry.apiKeys) ? [...entry.apiKeys] : [];
851
+ const label = input.label ?? "default";
852
+ existingKeys.push({
853
+ label,
854
+ apiKey: keyValue,
855
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
856
+ });
857
+ entry.apiKeys = existingKeys;
858
+ delete entry.apiKey;
859
+ if (input.setActive !== false) {
860
+ entry.activeKey = label;
861
+ }
862
+ providers[providerId] = entry;
863
+ await opts.updateConfig((cfg) => {
864
+ cfg.providers = providers;
865
+ });
866
+ const sourceName = input.envVar ? `env:${input.envVar}` : "direct key";
867
+ return {
868
+ status: "ok",
869
+ message: `\u2713 API key stored for "${providerId}" from ${sourceName}. Now add models to favorites with favorite_manage to use them in fallback chains.`
870
+ };
871
+ }
872
+
873
+ // src/tools/fallback-provider-key-set-tool.ts
874
+ var PROVIDER_KEY_SET_TOOL_NAME = "provider_key_set";
875
+ var PROVIDER_KEY_SET_SCHEMA = {
876
+ type: "object",
877
+ properties: {
878
+ provider: {
879
+ type: "string",
880
+ description: 'Provider id (e.g. "openai", "anthropic"). Required.'
881
+ },
882
+ key: {
883
+ type: "string",
884
+ description: "The API key value. When provided directly, it is stored as the provider's primary key. \u26A0\uFE0F This value is visible to the LLM \u2014 for secrets, omit this field and use envVar instead, or provide the key through the interactive prompt."
885
+ },
886
+ envVar: {
887
+ type: "string",
888
+ description: 'Environment variable name that contains the key (e.g. "OPENAI_API_KEY"). The key is read from the environment at tool execution time \u2014 the value is never visible to the LLM. Preferred over passing the key directly.'
889
+ },
890
+ label: {
891
+ type: "string",
892
+ description: 'Optional label for the key entry (e.g. "work", "personal"). Useful when managing multiple keys.'
893
+ },
894
+ setActive: {
895
+ type: "boolean",
896
+ description: "Whether to make this the active key. Default: true."
897
+ }
898
+ },
899
+ required: ["provider"],
900
+ additionalProperties: false
901
+ };
902
+ function createProviderKeySetTool(opts) {
903
+ return {
904
+ name: PROVIDER_KEY_SET_TOOL_NAME,
905
+ description: "Set the API key for a provider. For security, prefer using envVar (reads from environment variable, value never visible to the LLM) over passing the key directly. When neither key nor envVar is provided, the tool returns a prompt for interactive key entry \u2014 the UI will present an input field and the key is stored without LLM visibility.\n\nAfter setting a key, the provider becomes usable for model assignments and fallback chains. Add its models to favorites with favorite_manage to unlock them for fallback/profile use.",
906
+ usageHint: 'Preferred: provider_key_set({ provider: "openai", envVar: "OPENAI_API_KEY" }). For interactive input: provider_key_set({ provider: "openai" }) \u2014 the UI will prompt. Direct key: provider_key_set({ provider: "openai", key: "sk-..." }) \u2014 visible to LLM.',
907
+ category: "config",
908
+ inputSchema: PROVIDER_KEY_SET_SCHEMA,
909
+ permission: "confirm",
910
+ mutating: true,
911
+ riskTier: "standard",
912
+ icon: "settings",
913
+ async execute(input) {
914
+ const config = opts.getConfig();
915
+ const providers = {
916
+ ...config.providers ?? {}
917
+ };
918
+ if (input.key && input.envVar) {
919
+ return {
920
+ status: "error",
921
+ message: "Provide either key (direct, visible to LLM) OR envVar (reads from environment, never visible to LLM), not both. Use envVar for security."
922
+ };
923
+ }
924
+ if (!input.key && !input.envVar) {
925
+ if (opts.requestInput) {
926
+ try {
927
+ const value = await opts.requestInput(
928
+ `Enter API key for "${input.provider}" (will be stored securely, LLM will not see it):`
929
+ );
930
+ if (!value || value.trim().length === 0) {
931
+ return { status: "error", message: "No key was entered. Operation cancelled." };
932
+ }
933
+ return storeProviderKey(providers, input, value.trim(), opts);
934
+ } catch (err) {
935
+ return {
936
+ status: "error",
937
+ message: `Interactive input failed or was cancelled: ${err instanceof Error ? err.message : String(err)}`
938
+ };
939
+ }
940
+ }
941
+ return {
942
+ status: "needs_key",
943
+ message: `To set the API key for "${input.provider}", use provider_key_set with either:
944
+ 1. envVar: "${input.provider.toUpperCase()}_API_KEY" (reads from env, LLM never sees it)
945
+ 2. key: "sk-..." (pass directly, visible to LLM)
946
+
947
+ Interactive key entry is handled by the UI \u2014 enter your key through the prompt surface.`
948
+ };
949
+ }
950
+ if (input.envVar) {
951
+ const envValue = process.env[input.envVar];
952
+ if (!envValue) {
953
+ return {
954
+ status: "error",
955
+ message: `Environment variable "${input.envVar}" is not set or empty. Set it first or use a different envVar.`
956
+ };
957
+ }
958
+ return storeProviderKey(providers, input, envValue, opts);
959
+ }
960
+ if (input.key) {
961
+ return storeProviderKey(providers, input, input.key, opts);
962
+ }
963
+ return { status: "error", message: "Unexpected \u2014 no key source available." };
964
+ }
965
+ };
966
+ }
967
+
968
+ // src/tools/fallback-leader-model-set-tool.ts
969
+ var LEADER_MODEL_SET_TOOL_NAME = "leader_model_set";
970
+ var LEADER_MODEL_SET_SCHEMA = {
971
+ type: "object",
972
+ properties: {
973
+ action: {
974
+ type: "string",
975
+ enum: ["show", "set", "profile", "toggle"],
976
+ description: "Operation: show (current leader + toggles), set (change provider/model), profile (set from a fallback profile), toggle (change fallbackAuto/favoriteModelsOnly)."
977
+ },
978
+ provider: {
979
+ type: "string",
980
+ description: 'Provider id for the leader (e.g. "anthropic", "openai"). Required for "set".'
981
+ },
982
+ model: {
983
+ type: "string",
984
+ description: 'Model id for the leader (e.g. "claude-sonnet-4-20250514"). Required for "set".'
985
+ },
986
+ profile: {
987
+ type: "string",
988
+ description: 'Fallback profile name to derive the leader + chain from. Required for "profile".'
989
+ },
990
+ toggle: {
991
+ type: "string",
992
+ enum: ["fallbackAuto", "favoriteModelsOnly"],
993
+ description: 'Which toggle to change. Required for "toggle".'
994
+ },
995
+ value: {
996
+ type: "boolean",
997
+ description: 'New value for the toggle. Required for "toggle".'
998
+ }
999
+ },
1000
+ additionalProperties: false
1001
+ };
1002
+ function createLeaderModelSetTool(opts) {
1003
+ return {
1004
+ name: LEADER_MODEL_SET_TOOL_NAME,
1005
+ description: 'View or change the leader provider/model and system toggles. The leader is the primary model used for the main agent interactions. "set" changes it directly. "profile" derives it from a named fallback profile (first entry becomes leader, rest become the fallback chain). "toggle" controls fallbackAuto (smart default fallback) and favoriteModelsOnly (restrict auto-fallback to favorites only).',
1006
+ usageHint: '"show" to see current state. "set" with provider+model to change. "profile" with name to derive from a profile. "toggle" with toggle name and value to change a boolean setting.',
1007
+ category: "config",
1008
+ inputSchema: LEADER_MODEL_SET_SCHEMA,
1009
+ permission: "auto",
1010
+ mutating: true,
1011
+ riskTier: "standard",
1012
+ icon: "settings",
1013
+ async execute(input) {
1014
+ const config = opts.getConfig();
1015
+ if (input.action === "show") {
1016
+ const lines = [
1017
+ ` ${"leader"}: ${config.provider}/${config.model}`,
1018
+ ` ${"fallbackAuto"}: ${config.fallbackAuto !== false ? "on" : "off"}`,
1019
+ ` ${"favoriteModelsOnly"}: ${config.favoriteModelsOnly ? "on" : "off"}`,
1020
+ "",
1021
+ ` ${"fallback models"}: ${(config.fallbackModels ?? []).length > 0 ? (config.fallbackModels ?? []).join(" \u2192 ") : "empty (auto fallback)"}`,
1022
+ ` ${"favorites"}: ${(config.favoriteModels ?? []).length > 0 ? `${(config.favoriteModels ?? []).length} models` : "(none)"}`,
1023
+ ` ${"refiner"}: ${config.autonomy?.refinerProvider ? `${config.autonomy.refinerProvider}/${config.autonomy.refinerModel ?? "(default model)"}` : "(same as leader)"}`
1024
+ ];
1025
+ return { status: "ok", message: lines.join("\n") };
1026
+ }
1027
+ if (input.action === "set") {
1028
+ if (!input.provider || !input.model) {
1029
+ return { status: "error", message: 'Provide "provider" and "model" for the leader.' };
1030
+ }
1031
+ if (opts.switchProviderAndModel) {
1032
+ const switchError = await opts.switchProviderAndModel(input.provider, input.model);
1033
+ if (switchError) {
1034
+ return {
1035
+ status: "error",
1036
+ message: `Could not switch to ${input.provider}/${input.model}: ${switchError}. Config was not changed.`
1037
+ };
1038
+ }
1039
+ }
1040
+ await opts.updateConfig((cfg) => {
1041
+ cfg.provider = input.provider;
1042
+ cfg.model = input.model;
1043
+ });
1044
+ const liveNote = opts.switchProviderAndModel ? "" : " (config updated \u2014 the live session keeps its current model until restart or /setmodel)";
1045
+ return { status: "ok", message: `\u2713 Leader \u2192 ${input.provider}/${input.model}${liveNote}` };
1046
+ }
1047
+ if (input.action === "profile") {
1048
+ if (!input.profile) {
1049
+ return { status: "error", message: 'Provide "profile" name to derive the leader from.' };
1050
+ }
1051
+ const profiles = config.fallbackProfiles ?? {};
1052
+ const chain = profiles[input.profile];
1053
+ if (!chain || chain.length === 0) {
1054
+ return { status: "error", message: `Profile "${input.profile}" not found or empty.` };
1055
+ }
1056
+ const first = chain[0];
1057
+ const p = parseModelRef(first);
1058
+ const provider = p.provider ?? config.provider;
1059
+ const model = p.model;
1060
+ if (!model) {
1061
+ return { status: "error", message: `Cannot parse "${first}" as a valid model reference.` };
1062
+ }
1063
+ const rest = chain.slice(1);
1064
+ if (opts.switchProviderAndModel) {
1065
+ const switchError = await opts.switchProviderAndModel(provider, model);
1066
+ if (switchError) {
1067
+ return {
1068
+ status: "error",
1069
+ message: `Could not switch to ${provider}/${model}: ${switchError}. Config was not changed.`
1070
+ };
1071
+ }
1072
+ }
1073
+ await opts.updateConfig((cfg) => {
1074
+ cfg.provider = provider;
1075
+ cfg.model = model;
1076
+ cfg.fallbackModels = rest;
1077
+ });
1078
+ const profileLiveNote = opts.switchProviderAndModel ? "" : "\n (config updated \u2014 the live session keeps its current model until restart or /setmodel)";
1079
+ return {
1080
+ status: "ok",
1081
+ message: `\u2713 Leader \u2192 ${provider}/${model} (profile: ${input.profile})` + (rest.length > 0 ? `
1082
+ Fallback chain: ${rest.join(" \u2192 ")}` : "") + profileLiveNote
1083
+ };
1084
+ }
1085
+ if (input.action === "toggle") {
1086
+ if (!input.toggle || input.value === void 0) {
1087
+ return { status: "error", message: 'Provide "toggle" (fallbackAuto | favoriteModelsOnly) and "value" (boolean).' };
1088
+ }
1089
+ await opts.updateConfig((cfg) => {
1090
+ if (input.toggle === "fallbackAuto") {
1091
+ cfg.fallbackAuto = input.value;
1092
+ } else if (input.toggle === "favoriteModelsOnly") {
1093
+ cfg.favoriteModelsOnly = input.value;
1094
+ }
1095
+ });
1096
+ return {
1097
+ status: "ok",
1098
+ message: `\u2713 ${input.toggle} \u2192 ${input.value ? "on" : "off"}`
1099
+ };
1100
+ }
1101
+ return { status: "error", message: `Unknown action: "${input.action}".` };
1102
+ }
1103
+ };
1104
+ }
1105
+
1106
+ // src/tools/fallback-system-config-view-tool.ts
1107
+ var SYSTEM_CONFIG_VIEW_TOOL_NAME = "system_config_view";
1108
+ var SYSTEM_CONFIG_VIEW_SCHEMA = {
1109
+ type: "object",
1110
+ properties: {
1111
+ section: {
1112
+ type: "string",
1113
+ enum: [
1114
+ "all",
1115
+ "providers",
1116
+ "models",
1117
+ "fallbacks",
1118
+ "matrix",
1119
+ "agents",
1120
+ "fleet",
1121
+ "refiner",
1122
+ "doctor"
1123
+ ],
1124
+ description: "Which section to show: all (everything), providers (configured providers + keys), models (favorites + leader), fallbacks (chain + profiles + toggles), matrix (per-role assignments), agents (every catalog agent with resolved model), fleet (concurrency + lifetime spawn/token/cost budgets), refiner (goal refinement config), doctor (validate config and show issues/warnings). Default: all."
1125
+ }
1126
+ },
1127
+ additionalProperties: false
1128
+ };
1129
+ function createSystemConfigViewTool(opts) {
1130
+ return {
1131
+ name: SYSTEM_CONFIG_VIEW_TOOL_NAME,
1132
+ description: "Get a comprehensive view of all provider, model, fallback, and matrix configuration. Shows the complete state across all configurable areas so you can see what is available and make informed decisions when assigning models, creating fallback profiles, or managing providers. Use the section parameter to focus on specific areas.",
1133
+ usageHint: '"section: all" for everything. "section: providers" for configured providers and key status. "section: models" for leader model and favorites. "section: fallbacks" for chains, profiles, and toggles. "section: matrix" for per-role assignments. "section: refiner" for goal refinement config.',
1134
+ category: "config",
1135
+ inputSchema: SYSTEM_CONFIG_VIEW_SCHEMA,
1136
+ permission: "auto",
1137
+ mutating: false,
1138
+ riskTier: "safe",
1139
+ icon: "settings",
1140
+ async execute(input) {
1141
+ const config = opts.getConfig();
1142
+ const section = input.section ?? "all";
1143
+ const sections = [];
1144
+ const addSection = (title, content) => {
1145
+ sections.push(`\u2500\u2500 ${title} \u2500\u2500
1146
+ ${content}`);
1147
+ };
1148
+ addSection("Leader", ` ${config.provider}/${config.model}`);
1149
+ if (section === "all" || section === "providers") {
1150
+ const providers = config.providers ?? {};
1151
+ const ids = Object.keys(providers);
1152
+ if (ids.length === 0) {
1153
+ addSection("Providers", " (none configured)");
1154
+ } else {
1155
+ const lines = ids.sort().map((id) => {
1156
+ const e = providers[id] ?? {};
1157
+ const type = e.type ?? "?";
1158
+ const models = Array.isArray(e.models) ? `[${e.models.join(", ")}]` : "(all)";
1159
+ const hasKey = e.apiKey || Array.isArray(e.apiKeys) && e.apiKeys.length > 0 ? "\u2713" : "\u2717";
1160
+ const baseUrl = e.baseUrl ? ` url:${e.baseUrl}` : "";
1161
+ const family = e.family ? ` family:${e.family}` : "";
1162
+ const envVars = Array.isArray(e.envVars) ? ` env:[${e.envVars.join(", ")}]` : "";
1163
+ return ` ${id === config.provider ? "\u2605" : " "} ${id} (${type}) key:${hasKey} models:${models}${baseUrl}${family}${envVars}`;
1164
+ });
1165
+ addSection("Providers", lines.join("\n"));
1166
+ }
1167
+ }
1168
+ if (section === "all" || section === "models") {
1169
+ const favorites = config.favoriteModels ?? [];
1170
+ addSection(
1171
+ "Favorites",
1172
+ favorites.length > 0 ? favorites.map((f, i) => ` ${i + 1}. ${f}`).join("\n") : " (none \u2014 use favorite_manage to add)"
1173
+ );
1174
+ addSection(
1175
+ "Settings",
1176
+ ` fallbackAuto: ${config.fallbackAuto !== false ? "on" : "off"}
1177
+ favoriteModelsOnly: ${config.favoriteModelsOnly ? "on" : "off"}`
1178
+ );
1179
+ }
1180
+ if (section === "all" || section === "fallbacks") {
1181
+ const fallbackModels = config.fallbackModels ?? [];
1182
+ const profiles = config.fallbackProfiles ?? {};
1183
+ addSection("Continuity Bridge", ` ${config.fallbackBridge?.trim() || "(disabled)"}`);
1184
+ addSection(
1185
+ "Fallback Chain",
1186
+ fallbackModels.length > 0 ? fallbackModels.map((f, i) => ` ${i + 1}. ${f}`).join("\n") : " (empty \u2014 auto fallback applies when fallbackAuto is on)"
1187
+ );
1188
+ const profileNames = Object.keys(profiles);
1189
+ addSection(
1190
+ "Fallback Profiles",
1191
+ profileNames.length > 0 ? profileNames.sort().map((n) => ` ${n} \u2192 ${profiles[n]?.join(" \u2192 ") ?? "(empty)"}`).join("\n") : " (none)"
1192
+ );
1193
+ }
1194
+ if (section === "all" || section === "matrix") {
1195
+ const matrix = config.modelMatrix ?? {};
1196
+ const keys = Object.keys(matrix);
1197
+ addSection(
1198
+ "Model Matrix (role assignments)",
1199
+ keys.length > 0 ? keys.sort().map((k) => ` ${k} \u2192 ${JSON.stringify(matrix[k])}`).join("\n") : " (empty \u2014 all roles use the leader model)"
1200
+ );
1201
+ }
1202
+ if (section === "all" || section === "agents") {
1203
+ const roleNames = Object.keys(AGENT_CATALOG).sort();
1204
+ const lines = roleNames.map((role) => {
1205
+ const phase = phaseForRole(role) ?? "?";
1206
+ const target = resolveSubagentModelTarget(config, role);
1207
+ const model = target?.provider ? `${target.provider}/${target.model ?? "(default)"}` : `${config.provider}/${config.model ?? "(leader)"}`;
1208
+ const src = target?.diversified ? " (diversified)" : target?.source === "matrix" ? ` (matrix:${target.matrixSource ?? "?"})` : "";
1209
+ return ` ${role.padEnd(24)} ${phase.padEnd(14)} ${model}${src}`;
1210
+ });
1211
+ addSection(
1212
+ `Agent Models (${roleNames.length} roles)`,
1213
+ lines.length > 0 ? ` ${"ROLE".padEnd(24)} ${"PHASE".padEnd(14)} RESOLVED MODEL
1214
+ ` + lines.join("\n") : " (no agents in catalog)"
1215
+ );
1216
+ }
1217
+ if (section === "all" || section === "doctor") {
1218
+ const issues = [];
1219
+ const warnings = [];
1220
+ const ok = [];
1221
+ const providers = config.providers ?? {};
1222
+ const favorites = config.favoriteModels ?? [];
1223
+ const profiles = config.fallbackProfiles ?? {};
1224
+ const chain = config.fallbackModels ?? [];
1225
+ const matrix = config.modelMatrix ?? {};
1226
+ const fleetBudget = config.fleet?.budget;
1227
+ const bridge = config.fallbackBridge?.trim();
1228
+ if (bridge) {
1229
+ const parsed = parseModelRef(bridge);
1230
+ if (!parsed.provider || !parsed.model) {
1231
+ issues.push(`Continuity bridge "${bridge}" must be a full provider/model reference`);
1232
+ } else if (!providers[parsed.provider] && parsed.provider !== config.provider) {
1233
+ issues.push(
1234
+ `Continuity bridge "${bridge}" references unknown provider "${parsed.provider}"`
1235
+ );
1236
+ } else {
1237
+ ok.push(`Continuity bridge "${bridge}" \u2014 provider OK`);
1238
+ }
1239
+ }
1240
+ for (const fav of favorites) {
1241
+ const p = parseModelRef(fav);
1242
+ if (!p.model) {
1243
+ issues.push(`Favorite "${fav}" has no model (empty or whitespace-only reference)`);
1244
+ continue;
1245
+ }
1246
+ const provId = p.provider ?? config.provider;
1247
+ const model = p.model;
1248
+ const prov = providers[provId];
1249
+ if (!prov) {
1250
+ warnings.push(`Favorite "${fav}" references unknown provider "${provId}"`);
1251
+ continue;
1252
+ }
1253
+ const provModels = prov.models;
1254
+ if (provModels && provModels.length > 0 && !provModels.includes(model)) {
1255
+ warnings.push(
1256
+ `Favorite "${fav}" \u2014 model "${model}" not in ${provId} model list (${provModels.join(", ")})`
1257
+ );
1258
+ } else {
1259
+ ok.push(`Favorite "${fav}" \u2014 provider ${provId} is configured`);
1260
+ }
1261
+ }
1262
+ for (const entry of chain) {
1263
+ const p = parseModelRef(entry);
1264
+ if (!p.model) {
1265
+ issues.push(`Chain entry "${entry}" has no model (empty or whitespace-only reference)`);
1266
+ continue;
1267
+ }
1268
+ const provId = p.provider ?? config.provider;
1269
+ if (!providers[provId] && provId !== config.provider) {
1270
+ issues.push(`Chain entry "${entry}" references unknown provider "${provId}"`);
1271
+ } else {
1272
+ ok.push(`Chain entry "${entry}" \u2014 provider OK`);
1273
+ }
1274
+ }
1275
+ for (const [pname, pchain] of Object.entries(profiles)) {
1276
+ if (!pchain || pchain.length === 0) {
1277
+ warnings.push(`Profile "${pname}" is empty`);
1278
+ continue;
1279
+ }
1280
+ for (const entry of pchain) {
1281
+ const p = parseModelRef(entry);
1282
+ if (!p.model) {
1283
+ issues.push(
1284
+ `Profile "${pname}" entry "${entry}" has no model (empty or whitespace-only reference)`
1285
+ );
1286
+ continue;
1287
+ }
1288
+ const provId = p.provider ?? config.provider;
1289
+ if (!providers[provId] && provId !== config.provider) {
1290
+ issues.push(
1291
+ `Profile "${pname}" entry "${entry}" references unknown provider "${provId}"`
1292
+ );
1293
+ }
1294
+ }
1295
+ }
1296
+ if (typeof config.maxConcurrent === "number") {
1297
+ if (!Number.isFinite(config.maxConcurrent) || config.maxConcurrent < 0) {
1298
+ issues.push(
1299
+ `maxConcurrent must be a non-negative number (got ${config.maxConcurrent})`
1300
+ );
1301
+ } else if (config.maxConcurrent === 0) {
1302
+ warnings.push("maxConcurrent is 0 \u2014 subagent concurrency effectively disabled");
1303
+ } else {
1304
+ ok.push(`maxConcurrent ${config.maxConcurrent}`);
1305
+ }
1306
+ }
1307
+ if (fleetBudget && typeof fleetBudget === "object") {
1308
+ for (const key of ["maxSpawns", "maxTokens", "maxCostUsd"]) {
1309
+ const v = fleetBudget[key];
1310
+ if (v === void 0) continue;
1311
+ if (typeof v !== "number" || !Number.isFinite(v) || v < 0) {
1312
+ issues.push(
1313
+ `fleet.budget.${key} must be a non-negative number (got ${JSON.stringify(v)})`
1314
+ );
1315
+ } else {
1316
+ ok.push(`fleet.budget.${key} ${v}`);
1317
+ }
1318
+ }
1319
+ }
1320
+ const lastResortCap = config.fallbackMaxLastResortCandidates;
1321
+ if (lastResortCap !== void 0) {
1322
+ if (typeof lastResortCap !== "number" || !Number.isFinite(lastResortCap) || lastResortCap < 0) {
1323
+ issues.push(
1324
+ `fallbackMaxLastResortCandidates must be a non-negative number (got ${String(lastResortCap)})`
1325
+ );
1326
+ } else if (Math.floor(lastResortCap) === 0) {
1327
+ warnings.push(
1328
+ `fallbackMaxLastResortCandidates is ${lastResortCap} \u2014 floors to 0, last-resort auto-discovery append is disabled`
1329
+ );
1330
+ } else {
1331
+ ok.push(`fallbackMaxLastResortCandidates ${Math.floor(lastResortCap)}`);
1332
+ }
1333
+ }
1334
+ for (const [key, entry] of Object.entries(matrix)) {
1335
+ const eProvider = entry.provider ?? config.provider;
1336
+ const eModel = entry.model;
1337
+ if (eModel) {
1338
+ const provData = providers[eProvider];
1339
+ if (!provData && eProvider !== config.provider) {
1340
+ issues.push(`Matrix "${key}" references unknown provider "${eProvider}"`);
1341
+ }
1342
+ const provModels = provData?.models;
1343
+ if (provModels && provModels.length > 0 && !provModels.includes(eModel)) {
1344
+ warnings.push(`Matrix "${key}" \u2014 model "${eModel}" not in ${eProvider} model list`);
1345
+ }
1346
+ }
1347
+ const eProfile = entry.fallbackProfile;
1348
+ if (eProfile && !profiles[eProfile]) {
1349
+ issues.push(`Matrix "${key}" references unknown fallback profile "${eProfile}"`);
1350
+ }
1351
+ }
1352
+ if (!providers[config.provider] && Object.keys(providers).length > 0) {
1353
+ warnings.push(`Leader provider "${config.provider}" has no explicit config entry`);
1354
+ }
1355
+ const summary = ` \u2713 ${ok.length} checks passed
1356
+ \u26A0 ${warnings.length} warnings
1357
+ \u2717 ${issues.length} issues`;
1358
+ const lines = [summary, ""];
1359
+ if (warnings.length > 0) {
1360
+ lines.push("\u2500\u2500 Warnings \u2500\u2500");
1361
+ lines.push(...warnings.map((w) => ` \u26A0 ${w}`));
1362
+ lines.push("");
1363
+ }
1364
+ if (issues.length > 0) {
1365
+ lines.push("\u2500\u2500 Issues \u2500\u2500");
1366
+ lines.push(...issues.map((i) => ` \u2717 ${i}`));
1367
+ lines.push("");
1368
+ }
1369
+ if (warnings.length === 0 && issues.length === 0 && ok.length > 0) {
1370
+ lines.push(" All checks passed \u2014 configuration is healthy.");
1371
+ }
1372
+ addSection("Configuration Doctor", lines.join("\n"));
1373
+ }
1374
+ if (section === "all" || section === "fleet") {
1375
+ const fleet = config.fleet;
1376
+ const budget = fleet?.budget;
1377
+ const lifecycle = fleet?.lifecycle;
1378
+ const fmt = (n, unit = "") => typeof n === "number" && Number.isFinite(n) ? `${n}${unit}` : "(default)";
1379
+ addSection(
1380
+ "Fleet Budgets (configured ceilings)",
1381
+ [
1382
+ ` maxConcurrent: ${typeof config.maxConcurrent === "number" ? config.maxConcurrent : "(default 4)"}`,
1383
+ ` fleet.budget.maxSpawns: ${fmt(budget?.maxSpawns)} ${typeof budget?.maxSpawns !== "number" ? "\u2192 default 64" : ""}`,
1384
+ ` fleet.budget.maxTokens: ${fmt(budget?.maxTokens)}`,
1385
+ ` fleet.budget.maxCostUsd: ${fmt(budget?.maxCostUsd)}`,
1386
+ ` fleet.lifecycle.idleTimeoutMs: ${fmt(lifecycle?.idleTimeoutMs, "ms")}`,
1387
+ ` fleet.lifecycle.retireOnTaskComplete: ${lifecycle?.retireOnTaskComplete === void 0 ? "(default true)" : String(lifecycle.retireOnTaskComplete)}`,
1388
+ "",
1389
+ " Live used/remaining spawns are on /fleet status (not static config).",
1390
+ " Override ceilings: --max-concurrent / WRONGSTACK_MAX_CONCURRENT,",
1391
+ " --max-spawns / WRONGSTACK_MAX_SPAWNS, or fleet.budget.maxSpawns in profile."
1392
+ ].join("\n")
1393
+ );
1394
+ }
1395
+ if (section === "all" || section === "refiner") {
1396
+ const ref = config.autonomy;
1397
+ addSection(
1398
+ "Goal Refinement",
1399
+ ` refinerProvider: ${ref?.refinerProvider ?? "(same as leader)"}
1400
+ refinerModel: ${ref?.refinerModel ?? "(default for provider)"}
1401
+ refinerFallbackProfile: ${ref?.refinerFallbackProfile ?? "(none)"}`
1402
+ );
1403
+ }
1404
+ return { status: "ok", message: sections.join("\n\n") };
1405
+ }
1406
+ };
1407
+ }
1408
+
1409
+ // src/tools/fallback-manage-tools.ts
1410
+ function createFallbackManageTools(opts) {
1411
+ return [
1412
+ createFavoriteManageTool(opts),
1413
+ createFallbackChainManageTool(opts),
1414
+ createFallbackProfileManageTool(opts),
1415
+ createAgentModelAssignTool(opts),
1416
+ createProviderManageTool(opts),
1417
+ createProviderKeySetTool(opts),
1418
+ createLeaderModelSetTool(opts),
1419
+ createSystemConfigViewTool(opts)
1420
+ ];
1421
+ }
1422
+
1423
+ // src/tools/mcp-control.ts
1424
+ function createMcpControlTool(opts) {
1425
+ const { getConfig, configPath, registry } = opts;
1426
+ const inputSchema = {
1427
+ type: "object",
1428
+ properties: {
1429
+ action: {
1430
+ type: "string",
1431
+ enum: ["list", "search", "enable", "disable", "restart", "activate", "deactivate"],
1432
+ description: "The management action to perform. activate/deactivate toggle tool registration ephemerally without disconnecting."
1433
+ },
1434
+ /** Filter for `search`. Matches server name or description case-insensitively. */
1435
+ query: {
1436
+ type: "string",
1437
+ description: "Search term for `search` action. Matches server name or description."
1438
+ },
1439
+ /** Target server name for `enable`, `disable`, `restart`, `activate`, `deactivate`. */
1440
+ server: {
1441
+ type: "string",
1442
+ description: 'Server name (e.g. "github", "filesystem", "brave-search").'
1443
+ }
1444
+ },
1445
+ required: ["action"]
1446
+ };
1447
+ return {
1448
+ name: "mcp_control",
1449
+ description: "Manage MCP server lifecycle: list available servers, search by name or capability, enable or disable servers at runtime, restart running servers. Use activate/deactivate to ephemerally toggle tool registration without disconnecting \u2014 ideal for token-saving mode where MCP tools are lazy-loaded on demand. NOTE: `enable`/`restart` start a server process, which for the built-in stdio presets runs `npx -y <package>` \u2014 i.e. it fetches and executes an npm package from the network. Treat it as code execution.",
1450
+ category: "mcp",
1451
+ permission: "confirm",
1452
+ mutating: true,
1453
+ // `enable`/`restart` spawn a server process that, for the stdio presets,
1454
+ // fetches and runs an npm package (`npx -y <pkg>`) — effectively remote
1455
+ // code execution. Marking the tool destructive preserves risk metadata for
1456
+ // UI/audit surfaces; YOLO itself still auto-approves unless an explicit
1457
+ // deny rule blocks the call. Read-only actions (list/search) ride the same
1458
+ // tool but are cheap to confirm/trust once when YOLO is off.
1459
+ riskTier: "destructive",
1460
+ capabilities: [ToolCapabilities.CONFIG_MUTATE],
1461
+ inputSchema,
1462
+ async execute(raw) {
1463
+ const input = raw;
1464
+ return mcpControlDispatch(input, { getConfig, configPath, registry });
1465
+ }
1466
+ };
1467
+ }
1468
+ async function mcpControlDispatch(input, deps) {
1469
+ const { action, query, server } = input;
1470
+ switch (action) {
1471
+ case "list":
1472
+ return renderList(deps);
1473
+ case "search":
1474
+ return renderSearch(query ?? "", deps);
1475
+ case "enable":
1476
+ return server ? runEnable(server, deps) : "`server` is required for enable.";
1477
+ case "disable":
1478
+ return server ? runDisable(server, deps) : "`server` is required for disable.";
1479
+ case "restart":
1480
+ return server ? runRestart(server, deps) : "`server` is required for restart.";
1481
+ case "activate":
1482
+ return server ? runActivate(server, deps) : "`server` is required for activate.";
1483
+ case "deactivate":
1484
+ return server ? runDeactivate(server, deps) : "`server` is required for deactivate.";
1485
+ default:
1486
+ return `Unknown action "${action}". Use one of: list, search, enable, disable, restart, activate, deactivate.`;
1487
+ }
1488
+ }
1489
+ async function renderList(deps) {
1490
+ const configured = await getConfiguredMcpServers(deps);
1491
+ const live = deps.registry.describe();
1492
+ if (Object.keys(configured).length === 0) {
1493
+ return [
1494
+ "No MCP servers configured.",
1495
+ ' Use `mcp_control({ action: "search" })` to see available presets,',
1496
+ ' then `mcp_control({ action: "enable", server: "<name>" })` to add one.'
1497
+ ].join("\n");
1498
+ }
1499
+ const lines = [];
1500
+ const liveMap = new Map(live.map((s) => [s.name, s]));
1501
+ for (const [name, cfg] of Object.entries(configured)) {
1502
+ const liveInfo = liveMap.get(name);
1503
+ const toolCount = liveInfo ? ` (${liveInfo.toolCount} tools)` : "";
1504
+ const stateStr = liveInfo ? badge(liveInfo.state) : dim("\u25CB not loaded");
1505
+ const enabled = cfg.enabled === false ? `${dim("disabled")} ` : `${green("\u25CF enabled")} `;
1506
+ lines.push(` ${bold(name)} ${enabled}${stateStr}${toolCount}`);
1507
+ if (cfg.description) lines.push(` ${dim(cfg.description)}`);
1508
+ }
1509
+ lines.push("");
1510
+ lines.push(dim(' Use `mcp_control({ action: "search", query: "<keyword>" })` to find servers.'));
1511
+ lines.push(dim(' Use `mcp_control({ action: "enable", server: "<name>" })` to start a server.'));
1512
+ return lines.join("\n");
1513
+ }
1514
+ async function renderSearch(query, deps) {
1515
+ const configured = await getConfiguredMcpServers(deps);
1516
+ const all = allServers();
1517
+ const q = query.toLowerCase();
1518
+ const configuredNames = new Set(Object.keys(configured));
1519
+ const configuredEntries = Object.entries(configured).filter(
1520
+ ([name, cfg]) => name.toLowerCase().includes(q) || (cfg.description ?? "").toLowerCase().includes(q)
1521
+ );
1522
+ const unconfiguredEntries = Object.entries(all).filter(([name]) => !configuredNames.has(name)).filter(
1523
+ ([name, cfg]) => name.toLowerCase().includes(q) || (cfg.description ?? "").toLowerCase().includes(q)
1524
+ );
1525
+ const lines = [];
1526
+ if (configuredEntries.length > 0) {
1527
+ lines.push(bold('Configured servers matching "') + query + '":');
1528
+ for (const [name, cfg] of configuredEntries) {
1529
+ lines.push(` ${bold(name)} ${cfg.description ?? cfg.transport}`);
1530
+ }
1531
+ lines.push("");
1532
+ }
1533
+ if (unconfiguredEntries.length > 0) {
1534
+ lines.push(bold('Available presets matching "') + query + '":');
1535
+ for (const [name, cfg] of unconfiguredEntries) {
1536
+ const warn = cfg.permission === "deny" ? red(" \u26A0 confirm required") : "";
1537
+ lines.push(` ${bold(name)} ${cfg.description ?? cfg.transport}${warn}`);
1538
+ }
1539
+ lines.push("");
1540
+ }
1541
+ if (configuredEntries.length === 0 && unconfiguredEntries.length === 0) {
1542
+ return `No servers match "${query}". Try a shorter keyword or \`mcp_control({ action: "list" })\`.`;
1543
+ }
1544
+ const total = configuredEntries.length + unconfiguredEntries.length;
1545
+ lines.push(dim(` ${total} server${total !== 1 ? "s" : ""} shown. Run \`enable\` on one to activate it.`));
1546
+ return lines.join("\n");
1547
+ }
1548
+ async function runEnable(name, deps) {
1549
+ if (!name) return '`server` is required for enable. Example: { action: "enable", server: "github" }';
1550
+ const all = allServers();
1551
+ const configured = deps.getConfig().mcpServers ?? {};
1552
+ const cfg = configured[name] ?? all[name];
1553
+ if (!cfg) {
1554
+ const known = Object.keys(all).join(", ");
1555
+ return `Unknown server "${name}". Available presets: ${known}`;
1556
+ }
1557
+ const persistEnabled = () => updateJsonObjectFile(deps.configPath, (full) => {
1558
+ const current = isMcpServerRecord(full.mcpServers) ? full.mcpServers : {};
1559
+ setJsonPath(full, ["mcpServers", name], { ...current[name], ...cfg, enabled: true });
1560
+ });
1561
+ try {
1562
+ const live = deps.registry.describe().find((s) => s.name === name);
1563
+ if (live && live.state === "connected") {
1564
+ await persistEnabled();
1565
+ return `Server "${name}" is already running (${live.toolCount} tools registered).`;
1566
+ }
1567
+ await deps.registry.start({ ...cfg, enabled: true });
1568
+ await persistEnabled();
1569
+ const updated = deps.registry.describe().find((s) => s.name === name);
1570
+ return `Enabled and started "${name}"${updated ? ` (${updated.toolCount} tools registered).` : "."}`;
1571
+ } catch (err) {
1572
+ return `Failed to start "${name}": ${toErrorMessage(err)}. Config was left unchanged (server stays disabled).`;
1573
+ }
1574
+ }
1575
+ async function runDisable(name, deps) {
1576
+ if (!name) return '`server` is required for disable. Example: { action: "disable", server: "github" }';
1577
+ const configured = deps.getConfig().mcpServers ?? {};
1578
+ if (!configured[name]) {
1579
+ return `Server "${name}" is not in config. Add it with \`mcp_control({ action: "enable", server: "${name}" })\`.`;
1580
+ }
1581
+ await updateJsonObjectFile(deps.configPath, (full) => {
1582
+ const current = isMcpServerRecord(full.mcpServers) ? full.mcpServers : {};
1583
+ const existing = expectDefined(current[name]);
1584
+ setJsonPath(full, ["mcpServers", name], { ...existing, enabled: false });
1585
+ });
1586
+ try {
1587
+ await deps.registry.stop(name);
1588
+ return `${yellow("\u25CB Disabled")} "${name}". It will not be started on next boot.`;
1589
+ } catch {
1590
+ return `${yellow("\u25CB Disabled")} "${name}" (it was not running). Config updated.`;
1591
+ }
1592
+ }
1593
+ async function runRestart(name, deps) {
1594
+ if (!name) return '`server` is required for restart. Example: { action: "restart", server: "github" }';
1595
+ const configured = deps.getConfig().mcpServers ?? {};
1596
+ if (!configured[name]) {
1597
+ return `Server "${name}" is not configured. Use \`mcp_control({ action: "enable", server: "${name}" })\` first.`;
1598
+ }
1599
+ try {
1600
+ await deps.registry.restart(name);
1601
+ const updated = deps.registry.describe().find((s) => s.name === name);
1602
+ return `${green("\u2713 Restarted")} "${name}"${updated ? ` (${updated.toolCount} tools registered).` : "."}`;
1603
+ } catch (err) {
1604
+ return `${red("\u2717 Restart failed")} for "${name}": ${toErrorMessage(err)}`;
1605
+ }
1606
+ }
1607
+ async function runActivate(name, deps) {
1608
+ if (!name) return "`server` is required for activate.";
1609
+ if (!deps.registry.activateServer) {
1610
+ return `Registry does not support ephemeral activation. Use \`enable\` to start "${name}" instead.`;
1611
+ }
1612
+ const live = deps.registry.describe().find((s) => s.name === name);
1613
+ if (!live) {
1614
+ return `Server "${name}" is not registered. Use \`mcp_control({ action: "enable", server: "${name}" })\` first.`;
1615
+ }
1616
+ if (live.state !== "connected") {
1617
+ return `Server "${name}" is not connected (state: ${live.state}). Use \`enable\` to start it first.`;
1618
+ }
1619
+ if (deps.registry.isActivated?.(name)) {
1620
+ return `${green("\u25CF")} Server "${name}" tools are already active. Use \`deactivate\` to hide them.`;
1621
+ }
1622
+ deps.registry.activateServer(name);
1623
+ const updated = deps.registry.describe().find((s) => s.name === name);
1624
+ return `${green("\u2713 Activated")} "${name}" \u2014 ${updated?.toolCount ?? 0} tool(s) now registered. Use \`mcp_control({ action: "deactivate", server: "${name}" })\` to hide them when done.`;
1625
+ }
1626
+ async function runDeactivate(name, deps) {
1627
+ if (!name) return "`server` is required for deactivate.";
1628
+ if (!deps.registry.deactivateServer) {
1629
+ return `Registry does not support ephemeral deactivation. Use \`disable\` to stop "${name}" instead.`;
1630
+ }
1631
+ if (!deps.registry.isActivated?.(name)) {
1632
+ return `Server "${name}" tools are not currently active.`;
1633
+ }
1634
+ const count = deps.registry.deactivateServer(name);
1635
+ return `${yellow("\u25CB Deactivated")} "${name}" \u2014 ${count} tool(s) unregistered. Server stays connected.`;
1636
+ }
1637
+ async function getConfiguredMcpServers(deps) {
1638
+ const diskConfig = await readJsonObjectFile(deps.configPath);
1639
+ if (isMcpServerRecord(diskConfig.mcpServers)) return diskConfig.mcpServers;
1640
+ return deps.getConfig().mcpServers ?? {};
1641
+ }
1642
+ function isMcpServerRecord(value) {
1643
+ return !!value && typeof value === "object" && !Array.isArray(value);
1644
+ }
1645
+ function bold(s) {
1646
+ return s;
1647
+ }
1648
+ function dim(s) {
1649
+ return s;
1650
+ }
1651
+ function green(s) {
1652
+ return s;
1653
+ }
1654
+ function yellow(s) {
1655
+ return s;
1656
+ }
1657
+ function red(s) {
1658
+ return s;
1659
+ }
1660
+ function badge(state) {
1661
+ switch (state) {
1662
+ case "connected":
1663
+ return "\u25CF connected";
1664
+ case "connecting":
1665
+ return "\u25D0 connecting";
1666
+ case "reconnecting":
1667
+ return "\u25D1 reconnecting";
1668
+ case "disconnected":
1669
+ return "\u25CB disconnected";
1670
+ case "failed":
1671
+ return "\u2717 failed";
1672
+ default:
1673
+ return state;
1674
+ }
1675
+ }
1676
+
1677
+ // src/tools/mcp-use.ts
1678
+ function createMcpUseTool(opts) {
1679
+ const { registry, toolRegistry } = opts;
1680
+ const inputSchema = {
1681
+ type: "object",
1682
+ properties: {
1683
+ server: {
1684
+ type: "string",
1685
+ description: 'MCP server name (e.g. "github", "filesystem", "brave-search"). Use mcp_control list or search first to discover available servers.'
1686
+ },
1687
+ tool: {
1688
+ type: "string",
1689
+ description: "Tool name on the MCP server to call (without the mcp__server__ prefix \u2014 just the bare tool name)."
1690
+ },
1691
+ input: {
1692
+ type: "object",
1693
+ description: "JSON input to pass to the tool. Use the tool's own input schema \u2014 check with mcp_control describe or the server's documentation.",
1694
+ properties: {},
1695
+ additionalProperties: true
1696
+ }
1697
+ },
1698
+ required: ["server", "tool", "input"]
1699
+ };
1700
+ return {
1701
+ name: "mcp_use",
1702
+ description: "Call an MCP tool on a lazy-loaded server. Activates the server temporarily, calls the tool, returns the result, and deactivates. Use this instead of the manual activate\u2192use\u2192deactivate cycle. First call mcp_control list/search to find the right server and tool name.",
1703
+ category: "mcp",
1704
+ permission: "confirm",
1705
+ mutating: true,
1706
+ riskTier: "standard",
1707
+ capabilities: [ToolCapabilities.MCP_PROXY],
1708
+ inputSchema,
1709
+ async execute(raw, ctx) {
1710
+ const input = raw;
1711
+ const { server: serverName, tool: toolName, input: toolInput } = input;
1712
+ const servers = registry.describe();
1713
+ const serverInfo = servers.find((s) => s.name === serverName);
1714
+ if (!serverInfo) {
1715
+ throw new Error(
1716
+ `Server "${serverName}" not found. Available: ${servers.map((s) => s.name).join(", ") || "none"}.`
1717
+ );
1718
+ }
1719
+ if (serverInfo.state !== "connected") {
1720
+ throw new Error(
1721
+ `Server "${serverName}" is not connected (state: ${serverInfo.state}). Use \`mcp_control({ action: "enable", server: "${serverName}" })\` first.`
1722
+ );
1723
+ }
1724
+ const alreadyActive = registry.isActivated?.(serverName) === true;
1725
+ const didActivate = !alreadyActive && Boolean(registry.activateServer);
1726
+ if (didActivate) {
1727
+ registry.activateServer?.(serverName);
1728
+ }
1729
+ try {
1730
+ const qualifiedName = mcpQualifiedToolName(serverName, toolName);
1731
+ const mcpTool = toolRegistry.get(qualifiedName);
1732
+ if (!mcpTool) {
1733
+ const allTools = toolRegistry.list().filter((t) => t.name.startsWith(mcpServerToolPrefix(serverName))).map((t) => t.name.replace(mcpServerToolPrefix(serverName), ""));
1734
+ const hint = allTools.length > 0 ? `Available tools on "${serverName}": ${allTools.join(", ")}.` : `No tools found on "${serverName}". The server may not have published any tools.`;
1735
+ throw new Error(`Tool "${toolName}" not found on server "${serverName}". ${hint}`);
1736
+ }
1737
+ const governedExecute = ctx.meta[GOVERNED_TOOL_EXECUTOR_META_KEY];
1738
+ if (typeof governedExecute !== "function") {
1739
+ throw new Error("mcp_use: governed nested execution is unavailable");
1740
+ }
1741
+ const result = await governedExecute(qualifiedName, toolInput ?? {});
1742
+ if (!result.success) throw new Error(result.error ?? "MCP tool execution failed");
1743
+ return result.result;
1744
+ } finally {
1745
+ if (didActivate && registry.deactivateServer) {
1746
+ registry.deactivateServer(serverName);
1747
+ }
1748
+ }
1749
+ }
1750
+ };
1751
+ }
1752
+
1753
+ // src/tools/one-shot-llm-tool.ts
1754
+ var ONE_SHOT_LLM_TOOL_NAME = "llm";
1755
+ var MAX_TIMEOUT_MS = 12e4;
1756
+ var INPUT_SCHEMA2 = {
1757
+ type: "object",
1758
+ properties: {
1759
+ system: {
1760
+ type: "string",
1761
+ description: "System prompt guiding the LLM behaviour."
1762
+ },
1763
+ userPrompt: {
1764
+ type: "string",
1765
+ description: "Single user turn \u2014 appended as a user-role message."
1766
+ },
1767
+ messages: {
1768
+ type: "array",
1769
+ items: {
1770
+ type: "object",
1771
+ properties: {
1772
+ role: {
1773
+ type: "string",
1774
+ enum: ["system", "user", "assistant", "tool"],
1775
+ description: "Message author role."
1776
+ },
1777
+ content: {
1778
+ type: "string",
1779
+ description: "Message text content."
1780
+ }
1781
+ },
1782
+ required: ["role", "content"],
1783
+ additionalProperties: true
1784
+ },
1785
+ description: "Conversation messages for few-shot examples or multi-turn context. Appended before userPrompt when both are set."
1786
+ },
1787
+ model: {
1788
+ type: "string",
1789
+ description: 'Model id (e.g. "deepseek-chat", "gpt-4o-mini"). Defaults to session model.'
1790
+ },
1791
+ providerId: {
1792
+ type: "string",
1793
+ description: 'Provider id (e.g. "anthropic", "openai"). Defaults to session provider.'
1794
+ },
1795
+ role: {
1796
+ type: "string",
1797
+ description: "Roster role for model-matrix routing. Overrides model/providerId."
1798
+ },
1799
+ fallbackModels: {
1800
+ type: "array",
1801
+ items: { type: "string" },
1802
+ description: 'Explicit fallback model chain (e.g. ["anthropic/claude-haiku", "openai/gpt-4o-mini"]). Resolved named profiles from FallbackProfileManager are passed here.'
1803
+ },
1804
+ maxTokens: {
1805
+ type: "number",
1806
+ description: "Maximum output tokens (default 1024)."
1807
+ },
1808
+ responseFormat: {
1809
+ oneOf: [
1810
+ { type: "string", enum: ["text", "json_object"], description: "Simple response format." },
1811
+ {
1812
+ type: "object",
1813
+ properties: {
1814
+ type: { type: "string", enum: ["json_schema"], description: "Structured JSON output." },
1815
+ json_schema: {
1816
+ type: "object",
1817
+ description: "JSON Schema definition for the structured output."
1818
+ }
1819
+ },
1820
+ required: ["type"],
1821
+ additionalProperties: false
1822
+ }
1823
+ ],
1824
+ description: 'Response format: "text" (default), "json_object", or { type: "json_schema", json_schema: {...} }.'
1825
+ },
1826
+ temperature: {
1827
+ type: "number",
1828
+ description: "Sampling temperature."
1829
+ },
1830
+ timeoutMs: {
1831
+ type: "number",
1832
+ description: `Hard timeout in ms (default 30s, clamped to a maximum of ${MAX_TIMEOUT_MS}).`
1833
+ }
1834
+ },
1835
+ additionalProperties: false
1836
+ };
1837
+ function createOneShotLLMTool(opts) {
1838
+ const orchestrator = new OneShotOrchestrator({
1839
+ buildProvider: opts.buildProvider,
1840
+ getConfig: opts.getConfig,
1841
+ fallbackProfileManager: opts.fallbackProfileManager,
1842
+ modelRouter: opts.modelRouter,
1843
+ statusTracker: opts.statusTracker,
1844
+ logger: opts.logger,
1845
+ wrapProviderCall: opts.wrapProviderCall
1846
+ });
1847
+ return {
1848
+ name: ONE_SHOT_LLM_TOOL_NAME,
1849
+ description: "Make a one-shot LLM call with a system prompt and user input. Supports provider selection, model routing by role, fallback chains, and timeout. Returns the response text, model info, token usage, and whether a fallback was used. Use this for summarization, classification, extraction, and any single-turn LLM task.",
1850
+ usageHint: "Provide `system` for the instruction and `userPrompt` for the input. Either set `model`+`providerId`, or have defaults configured on the tool. Set `fallbackModels` for resilience. Check `error` on the result for failure details.",
1851
+ inputSchema: INPUT_SCHEMA2,
1852
+ // Metadata mirrors council-tool.ts — both are read-only meta tools that
1853
+ // spend tokens but never touch the workspace.
1854
+ category: "meta",
1855
+ permission: "auto",
1856
+ mutating: false,
1857
+ riskTier: "safe",
1858
+ maxOutputBytes: 262144,
1859
+ validate(input) {
1860
+ const hasPrompt = typeof input.userPrompt === "string" && input.userPrompt.trim().length > 0;
1861
+ const hasMessages = Array.isArray(input.messages) && input.messages.length > 0;
1862
+ if (!hasPrompt && !hasMessages) {
1863
+ return [
1864
+ "Provide `userPrompt` (a single user turn) or `messages` (a conversation array) \u2014 without either the llm tool has nothing to send to the model."
1865
+ ];
1866
+ }
1867
+ return [];
1868
+ },
1869
+ async execute(input, _ctx, { signal }) {
1870
+ if (!input.model && !input.providerId && !opts.defaultModel && !opts.defaultProvider) {
1871
+ return {
1872
+ text: "",
1873
+ model: "",
1874
+ provider: "",
1875
+ tokens: { input: 0, output: 0, total: 0 },
1876
+ durationMs: 0,
1877
+ fromFallback: false,
1878
+ error: "Either provide `model` and `providerId` in the call, or configure defaultProvider/defaultModel when creating the tool. The `llm` tool does not infer provider/model from the session by default."
1879
+ };
1880
+ }
1881
+ const effectiveInput = {
1882
+ ...input,
1883
+ signal: input.signal ? AbortSignal.any([input.signal, signal]) : signal,
1884
+ model: input.model ?? opts.defaultModel,
1885
+ providerId: input.providerId ?? opts.defaultProvider,
1886
+ // Clamp runaway timeouts (documented on the schema). Non-positive
1887
+ // values fall back to the orchestrator default rather than making the
1888
+ // call instantly un-completable.
1889
+ ...typeof input.timeoutMs === "number" && input.timeoutMs > 0 ? { timeoutMs: Math.min(input.timeoutMs, MAX_TIMEOUT_MS) } : { timeoutMs: void 0 }
1890
+ };
1891
+ return orchestrator.call(effectiveInput);
1892
+ }
1893
+ };
1894
+ }
1895
+
1896
+ // src/tools/plugin-manager.ts
1897
+ var PLUGIN_MANAGER_TOOL_NAME = "plugin_manager";
1898
+ var PLUGIN_VIEW_STATE_SOURCE = {
1899
+ "feature-flag": "feature_flag",
1900
+ "plugin-entry": "config",
1901
+ extension: "extension",
1902
+ default: "default"
1903
+ };
1904
+ var INPUT_SCHEMA3 = {
1905
+ type: "object",
1906
+ properties: {
1907
+ action: {
1908
+ type: "string",
1909
+ enum: ["list", "search", "describe", "enable", "disable", "use"],
1910
+ description: "Operation. list: inventory with optional state filter. search: find plugins by need/query. describe: inspect one plugin, managerControl, callableNow, and full tool schemas. enable/disable: persist boot state (often restartRequired). use: inspect or invoke a tool from a plugin already loaded in this session."
1911
+ },
1912
+ plugin: {
1913
+ type: "string",
1914
+ description: "Plugin name or unambiguous alias. Required for describe, enable, disable, and use."
1915
+ },
1916
+ query: {
1917
+ type: "string",
1918
+ description: "Free-text query over plugin names, aliases, descriptions, and registered tool metadata. Required for search."
1919
+ },
1920
+ state: {
1921
+ type: "string",
1922
+ enum: ["all", "enabled", "disabled"],
1923
+ description: "Optional list/search filter. enabled/disabled means effective boot state, not whether plugin code can be hot-loaded in the current session. Defaults to all."
1924
+ },
1925
+ tool: {
1926
+ type: "string",
1927
+ description: 'Registered plugin tool to invoke when action="use". Omit to receive the available tool schemas first.'
1928
+ },
1929
+ input: {
1930
+ type: "object",
1931
+ description: 'Exact JSON input for the selected plugin tool when action="use". Read the schema returned by describe or use-without-tool first; do not guess fields.',
1932
+ properties: {},
1933
+ additionalProperties: true
1934
+ }
1935
+ },
1936
+ required: ["action"],
1937
+ additionalProperties: false
1938
+ };
1939
+ var USAGE_HINT = [
1940
+ "Always discover before mutating: never enable a guessed plugin name.",
1941
+ "Workflow: (1) search with a capability/need query, or list with an optional state filter;",
1942
+ "(2) describe the chosen plugin and inspect enabled, managerControl, callableNow, and tools[].inputSchema;",
1943
+ '(3) if disabled, enable only when managerControl="allowed"\u2014never work around "locked"; when restartRequired=true, tell the user the plugin cannot be used until WrongStack restarts;',
1944
+ "(4) call use with only plugin to refresh/inspect callable tool schemas;",
1945
+ "(5) call use again with the exact tool name and schema-valid input.",
1946
+ 'Examples: search={"action":"search","query":"release notes"}; describe={"action":"describe","plugin":"release-notes-generator"}; inspect-use={"action":"use","plugin":"release-notes-generator"}; invoke-use={"action":"use","plugin":"release-notes-generator","tool":"generate_release_notes","input":{"to":"HEAD"}}.',
1947
+ "use works only for tools loaded now. If the result is needs_direct_call, call the returned tool directly so its confirm/deny/destructive policy runs.",
1948
+ "disable also normally affects the next boot, not already-loaded code."
1949
+ ].join(" ");
1950
+ function createPluginManagerTool(opts) {
1951
+ return {
1952
+ name: PLUGIN_MANAGER_TOOL_NAME,
1953
+ description: "Manage WrongStack plugins through a guarded discovery-first workflow. Search the host catalog, inspect effective state and live plugin-owned tool schemas, persist allowed enable/disable changes, and invoke tools from plugins already loaded in this session. This does not install arbitrary packages or bypass plugin/tool permissions.",
1954
+ usageHint: USAGE_HINT,
1955
+ selection: {
1956
+ doNotUseWhen: "installing a new npm package, editing plugin-specific options, changing per-plugin LLM routing, or bypassing a managerControl lock/tool permission"
1957
+ },
1958
+ category: "Config",
1959
+ inputSchema: INPUT_SCHEMA3,
1960
+ permission: "auto",
1961
+ mutating: true,
1962
+ riskTier: "standard",
1963
+ subjectKey: "plugin",
1964
+ capabilities: [
1965
+ ToolCapabilities.TOOL_META,
1966
+ ToolCapabilities.TOOL_MUTATE_ANY,
1967
+ ToolCapabilities.CONFIG_MUTATE
1968
+ ],
1969
+ icon: "settings",
1970
+ validate(input) {
1971
+ const value = input;
1972
+ const errors = [];
1973
+ if (value.action === "search" && !value.query?.trim()) {
1974
+ errors.push('query is required when action="search"');
1975
+ }
1976
+ if (["describe", "enable", "disable", "use"].includes(value.action) && !value.plugin?.trim()) {
1977
+ errors.push(`plugin is required when action="${value.action}"`);
1978
+ }
1979
+ if (value.action === "use" && value.tool && value.input === void 0) {
1980
+ errors.push('input is required when action="use" includes a tool');
1981
+ }
1982
+ return errors;
1983
+ },
1984
+ async execute(rawInput, ctx, executeOpts) {
1985
+ const input = rawInput;
1986
+ const views = buildPluginViews(opts);
1987
+ if (input.action === "list" || input.action === "search") {
1988
+ const needle = input.action === "search" ? input.query.trim().toLowerCase() : "";
1989
+ const state = input.state ?? "all";
1990
+ const matches = views.filter((view) => {
1991
+ if (state !== "all" && view.enabled !== (state === "enabled")) return false;
1992
+ if (!needle) return true;
1993
+ const haystack = [
1994
+ view.name,
1995
+ ...view.aliases,
1996
+ view.description,
1997
+ ...view.tools.flatMap((tool2) => [tool2.name, tool2.description])
1998
+ ].join("\n").toLowerCase();
1999
+ return haystack.includes(needle);
2000
+ });
2001
+ return {
2002
+ status: "ok",
2003
+ action: input.action,
2004
+ count: matches.length,
2005
+ plugins: matches.map(compactView)
2006
+ };
2007
+ }
2008
+ const resolved = resolvePlugin(input.plugin, views);
2009
+ if ("error" in resolved) return { status: "error", message: resolved.error };
2010
+ const plugin = resolved.plugin;
2011
+ if (input.action === "describe") {
2012
+ return { status: "ok", action: "describe", plugin };
2013
+ }
2014
+ if (input.action === "enable" || input.action === "disable") {
2015
+ const enabled = input.action === "enable";
2016
+ if (plugin.managerControl === "locked") {
2017
+ return {
2018
+ status: "error",
2019
+ code: "plugin_manager_locked",
2020
+ message: `Plugin "${plugin.name}" is locked against LLM enable/disable changes. Only the user can change it with /plugin or update pluginManager.locked.`
2021
+ };
2022
+ }
2023
+ if (!enabled && !plugin.canDisable) {
2024
+ return {
2025
+ status: "error",
2026
+ message: `Plugin "${plugin.name}" is locked and cannot be disabled.`
2027
+ };
2028
+ }
2029
+ if (plugin.enabled === enabled && plugin.stateSource !== "feature_flag") {
2030
+ return {
2031
+ status: "ok",
2032
+ action: input.action,
2033
+ changed: false,
2034
+ restartRequired: false,
2035
+ message: `Plugin "${plugin.name}" is already ${enabled ? "enabled" : "disabled"}.`
2036
+ };
2037
+ }
2038
+ const result2 = await opts.setEnabled(plugin.name, enabled);
2039
+ return {
2040
+ status: result2.ok ? "ok" : "error",
2041
+ action: input.action,
2042
+ changed: result2.ok,
2043
+ restartRequired: result2.restartRequired ?? result2.ok,
2044
+ message: result2.message
2045
+ };
2046
+ }
2047
+ if (!plugin.enabled) {
2048
+ return {
2049
+ status: "error",
2050
+ message: `Plugin "${plugin.name}" is disabled. Enable it first; newly enabled plugins become callable after restart.`
2051
+ };
2052
+ }
2053
+ if (!input.tool) {
2054
+ return {
2055
+ status: "ok",
2056
+ action: "use",
2057
+ ready: plugin.callableNow,
2058
+ message: plugin.callableNow ? `Choose one of the loaded tools registered by "${plugin.name}".` : `Plugin "${plugin.name}" has no callable tool loaded in this session. It may be hook/command-only or require a restart/configuration.`,
2059
+ plugin
2060
+ };
2061
+ }
2062
+ const selected = plugin.tools.find((tool2) => tool2.name === input.tool);
2063
+ if (!selected) {
2064
+ return {
2065
+ status: "error",
2066
+ message: `Tool "${input.tool}" is not registered by plugin "${plugin.name}" in this session.`,
2067
+ availableTools: plugin.tools.map((tool2) => tool2.name)
2068
+ };
2069
+ }
2070
+ if (!selected.enabled) {
2071
+ return {
2072
+ status: "error",
2073
+ message: `Tool "${selected.name}" is registered but disabled by the tool configuration.`
2074
+ };
2075
+ }
2076
+ const tool = opts.toolRegistry.get(selected.name);
2077
+ if (!tool || !ownerMatches(opts.toolRegistry.ownerOf(selected.name), plugin)) {
2078
+ return {
2079
+ status: "error",
2080
+ message: `Tool "${selected.name}" is no longer available from "${plugin.name}".`
2081
+ };
2082
+ }
2083
+ if (tool.permission !== "auto" || tool.riskTier === "destructive") {
2084
+ return {
2085
+ status: "needs_direct_call",
2086
+ message: `Tool "${tool.name}" requires the normal permission path (${tool.permission}, risk=${tool.riskTier ?? "standard"}). Call it directly so WrongStack can apply its own approval and policy checks.`,
2087
+ directCall: { tool: tool.name, input: input.input ?? {}, inputSchema: tool.inputSchema }
2088
+ };
2089
+ }
2090
+ const dangerousCaps = getDangerousCapabilities(tool);
2091
+ if (dangerousCaps.length > 0) {
2092
+ return {
2093
+ status: "needs_direct_call",
2094
+ message: `Tool "${tool.name}" declares dangerous capabilities (${dangerousCaps.join(", ")}); the permission pipeline must rule on it. Call it directly.`,
2095
+ directCall: { tool: tool.name, input: input.input ?? {}, inputSchema: tool.inputSchema }
2096
+ };
2097
+ }
2098
+ if (ctx.meta["readOnly"] === true && toolMutates(tool)) {
2099
+ return {
2100
+ status: "needs_direct_call",
2101
+ message: `Session is in read-only mode and "${tool.name}" mutates. Call it directly so read-only mode can rule on it.`,
2102
+ directCall: { tool: tool.name, input: input.input ?? {}, inputSchema: tool.inputSchema }
2103
+ };
2104
+ }
2105
+ let nestedInput = input.input ?? {};
2106
+ const hooks = opts.getHookRunner?.();
2107
+ if (hooks) {
2108
+ const pre = await hooks.preToolUse(tool.name, nestedInput, ctx, {
2109
+ capabilities: tool.capabilities,
2110
+ mutating: tool.mutating
2111
+ });
2112
+ if (pre.block) {
2113
+ return {
2114
+ status: "error",
2115
+ message: `A policy hook blocked "${tool.name}"` + (pre.reason ? `: ${pre.reason}` : ".")
2116
+ };
2117
+ }
2118
+ if (pre.input) nestedInput = pre.input;
2119
+ }
2120
+ const validation = validateAgainstSchema(nestedInput, tool.inputSchema);
2121
+ if (!validation.ok) {
2122
+ return {
2123
+ status: "error",
2124
+ message: `Invalid input for plugin tool "${tool.name}".`,
2125
+ errors: validation.errors,
2126
+ inputSchema: tool.inputSchema
2127
+ };
2128
+ }
2129
+ const crossFieldErrors = tool.validate?.(nestedInput) ?? [];
2130
+ if (crossFieldErrors.length > 0) {
2131
+ return {
2132
+ status: "error",
2133
+ message: `Invalid input for plugin tool "${tool.name}".`,
2134
+ errors: crossFieldErrors,
2135
+ inputSchema: tool.inputSchema
2136
+ };
2137
+ }
2138
+ const result = await tool.execute(nestedInput, ctx, executeOpts);
2139
+ return { status: "ok", action: "use", plugin: plugin.name, tool: tool.name, result };
2140
+ }
2141
+ };
2142
+ }
2143
+ function buildPluginViews(opts) {
2144
+ const config = opts.getConfig();
2145
+ const configured = config.plugins ?? [];
2146
+ const catalog = [...opts.catalog];
2147
+ const knownNames = new Set(catalog.flatMap((entry) => [entry.name, ...entry.aliases ?? []]));
2148
+ for (const item of configured) {
2149
+ const name = pluginConfigName(item);
2150
+ if (!knownNames.has(name)) {
2151
+ catalog.push({
2152
+ name,
2153
+ description: "User-configured plugin (not present in the built-in discovery catalog).",
2154
+ risk: "medium",
2155
+ defaultState: "inactive",
2156
+ canDisable: true
2157
+ });
2158
+ knownNames.add(name);
2159
+ }
2160
+ }
2161
+ return catalog.map((entry) => {
2162
+ const aliases = [...entry.aliases ?? []];
2163
+ const names = /* @__PURE__ */ new Set([entry.name, ...aliases]);
2164
+ const { enabled, source } = resolvePluginEnablement({
2165
+ name: entry.name,
2166
+ aliases,
2167
+ defaultState: entry.defaultState,
2168
+ config,
2169
+ matches: (spec) => names.has(spec)
2170
+ });
2171
+ const stateSource = PLUGIN_VIEW_STATE_SOURCE[source];
2172
+ const tools = pluginTools(opts.toolRegistry, entry.name, aliases);
2173
+ const managerControl = isManagerLocked(config, entry.name, aliases) ? "locked" : "allowed";
2174
+ return {
2175
+ name: entry.name,
2176
+ description: entry.description,
2177
+ risk: entry.risk,
2178
+ enabled,
2179
+ stateSource,
2180
+ canDisable: entry.canDisable,
2181
+ managerControl,
2182
+ aliases,
2183
+ callableNow: enabled && tools.some((tool) => tool.enabled),
2184
+ tools
2185
+ };
2186
+ }).sort((a, b) => a.name.localeCompare(b.name));
2187
+ }
2188
+ function isManagerLocked(config, name, aliases) {
2189
+ const locked = new Set(
2190
+ (config.pluginManager?.locked ?? []).map((entry) => entry.trim().toLowerCase())
2191
+ );
2192
+ if (locked.has("*")) return true;
2193
+ return [name, ...aliases].some((entry) => locked.has(entry.toLowerCase()));
2194
+ }
2195
+ function pluginTools(registry, name, aliases) {
2196
+ const plugin = { name, aliases };
2197
+ const active = registry.listWithOwner().filter(({ owner }) => ownerMatches(owner, plugin)).map(({ tool }) => toolView(tool, true));
2198
+ const disabled = registry.listDisabled().filter(({ owner }) => ownerMatches(owner, plugin)).map(({ tool }) => toolView(tool, false));
2199
+ return [...active, ...disabled].sort((a, b) => a.name.localeCompare(b.name));
2200
+ }
2201
+ function toolView(tool, enabled) {
2202
+ return {
2203
+ name: tool.name,
2204
+ description: tool.description,
2205
+ inputSchema: tool.inputSchema,
2206
+ permission: tool.permission,
2207
+ enabled
2208
+ };
2209
+ }
2210
+ function ownerMatches(owner, plugin) {
2211
+ if (!owner) return false;
2212
+ const names = /* @__PURE__ */ new Set([plugin.name, ...plugin.aliases]);
2213
+ const segments = owner.split("+");
2214
+ return segments.length > 0 && segments.every((segment) => names.has(segment));
2215
+ }
2216
+ function resolvePlugin(input, views) {
2217
+ const needle = input.trim().toLowerCase();
2218
+ const exact = views.find(
2219
+ (view) => view.name.toLowerCase() === needle || view.aliases.some((alias) => alias.toLowerCase() === needle)
2220
+ );
2221
+ if (exact) return { plugin: exact };
2222
+ const partial = views.filter(
2223
+ (view) => view.name.toLowerCase().includes(needle) || view.aliases.some((alias) => alias.toLowerCase().includes(needle))
2224
+ );
2225
+ if (partial.length === 1) return { plugin: partial[0] };
2226
+ if (partial.length > 1) {
2227
+ return {
2228
+ error: `Plugin name "${input}" is ambiguous. Matches: ${partial.map((view) => view.name).join(", ")}.`
2229
+ };
2230
+ }
2231
+ return { error: `Plugin "${input}" was not found. Use plugin_manager search first.` };
2232
+ }
2233
+ function compactView(view) {
2234
+ return { ...view, tools: view.tools.map((tool) => tool.name) };
2235
+ }
2236
+ function pluginConfigName(item) {
2237
+ return typeof item === "string" ? item : item.name;
2238
+ }
2239
+
2240
+ export {
2241
+ COUNCIL_TOOL_NAME,
2242
+ MAX_COUNCIL_TOOL_OPTIONS,
2243
+ MAX_COUNCIL_QUESTION_CHARS,
2244
+ MAX_COUNCIL_CONTEXT_CHARS,
2245
+ createCouncilTool,
2246
+ FALLBACK_CHAIN_MANAGE_TOOL_NAME,
2247
+ FAVORITE_MANAGE_TOOL_NAME,
2248
+ FALLBACK_PROFILE_MANAGE_TOOL_NAME,
2249
+ AGENT_MODEL_ASSIGN_TOOL_NAME,
2250
+ PROVIDER_MANAGE_TOOL_NAME,
2251
+ validateProviderBaseUrl,
2252
+ PROVIDER_KEY_SET_TOOL_NAME,
2253
+ LEADER_MODEL_SET_TOOL_NAME,
2254
+ SYSTEM_CONFIG_VIEW_TOOL_NAME,
2255
+ createFallbackManageTools,
2256
+ createMcpControlTool,
2257
+ createMcpUseTool,
2258
+ ONE_SHOT_LLM_TOOL_NAME,
2259
+ createOneShotLLMTool,
2260
+ PLUGIN_MANAGER_TOOL_NAME,
2261
+ createPluginManagerTool
2262
+ };
2263
+ //# sourceMappingURL=chunk-FIV4IJL7.js.map