zidane 1.8.0 → 2.0.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 (36) hide show
  1. package/dist/{agent-CWQ5XOw6.d.ts → agent-D-ZFMbSd.d.ts} +371 -21
  2. package/dist/{chunk-FFFDQHMA.js → chunk-7JTBBZ2U.js} +21 -0
  3. package/dist/chunk-BCXXXJ3G.js +779 -0
  4. package/dist/{chunk-WQBKOZVI.js → chunk-FRNFVKWW.js} +50 -12
  5. package/dist/{chunk-EC7IRWVS.js → chunk-LN4LLLHA.js} +101 -7
  6. package/dist/chunk-LVC7NQUZ.js +22 -0
  7. package/dist/chunk-MYWDHD7C.js +14 -0
  8. package/dist/{chunk-3RJOWJOJ.js → chunk-OVQ4N64O.js} +1 -1
  9. package/dist/{chunk-4N5ADW7A.js → chunk-PASFWG7S.js} +343 -32
  10. package/dist/{chunk-TBC6MSVK.js → chunk-PJUUYBKF.js} +53 -4
  11. package/dist/{chunk-2IB4XBQE.js → chunk-VG2E6YK3.js} +0 -97
  12. package/dist/harnesses.d.ts +1 -2
  13. package/dist/harnesses.js +5 -5
  14. package/dist/index.d.ts +5 -6
  15. package/dist/index.js +42 -12
  16. package/dist/mcp.d.ts +1 -2
  17. package/dist/mcp.js +3 -1
  18. package/dist/providers.d.ts +1 -2
  19. package/dist/providers.js +3 -3
  20. package/dist/session/sqlite.d.ts +16 -0
  21. package/dist/session/sqlite.js +98 -0
  22. package/dist/session.d.ts +1 -2
  23. package/dist/session.js +3 -5
  24. package/dist/skills-use-C4KFVla0.d.ts +66 -0
  25. package/dist/skills.d.ts +215 -30
  26. package/dist/skills.js +21 -2
  27. package/dist/{spawn-EEv1Johs.d.ts → spawn-RoqpjYLZ.d.ts} +1 -1
  28. package/dist/tools.d.ts +3 -4
  29. package/dist/tools.js +10 -4
  30. package/dist/types.d.ts +2 -3
  31. package/dist/types.js +8 -2
  32. package/package.json +12 -3
  33. package/dist/chunk-4C6Y56CC.js +0 -390
  34. package/dist/chunk-CFLC2I7D.js +0 -8
  35. package/dist/glob-j9gbk6xm.d.ts +0 -5
  36. package/dist/types-CDI8Kmve.d.ts +0 -64
@@ -1,20 +1,23 @@
1
1
  import {
2
2
  buildCatalog,
3
+ createSkillActivationState,
4
+ installAllowedToolsGate,
3
5
  interpolateShellCommands,
4
6
  mergeSkillsConfig,
5
- resolveSkills
6
- } from "./chunk-4C6Y56CC.js";
7
+ resolveSkills,
8
+ validateResourcePath
9
+ } from "./chunk-BCXXXJ3G.js";
7
10
  import {
8
11
  createProcessContext
9
12
  } from "./chunk-SZA4FKW5.js";
10
13
  import {
11
14
  connectMcpServers
12
- } from "./chunk-TBC6MSVK.js";
15
+ } from "./chunk-PJUUYBKF.js";
13
16
  import {
14
17
  AgentAbortedError,
15
18
  AgentProviderError,
16
19
  toTypedError
17
- } from "./chunk-FFFDQHMA.js";
20
+ } from "./chunk-7JTBBZ2U.js";
18
21
 
19
22
  // src/tools/glob.ts
20
23
  var DEFAULT_LIMIT = 1e3;
@@ -157,6 +160,227 @@ ${result.stderr}`.trim();
157
160
  }
158
161
  };
159
162
 
163
+ // src/tools/skills-read.ts
164
+ var SNIFF_BYTES = 8192;
165
+ function looksBinary(text) {
166
+ const len = Math.min(text.length, SNIFF_BYTES);
167
+ for (let i = 0; i < len; i++) {
168
+ if (text.charCodeAt(i) === 0)
169
+ return true;
170
+ }
171
+ return false;
172
+ }
173
+ function createSkillsReadTool(options) {
174
+ const byName = new Map(options.catalog.map((s) => [s.name, s]));
175
+ return {
176
+ spec: {
177
+ name: "skills_read",
178
+ description: `Read a bundled resource file from an active skill. The skill must have been activated via skills_use first. Path is relative to the skill's directory (e.g. "references/REFERENCE.md", "assets/template.txt").`,
179
+ inputSchema: {
180
+ type: "object",
181
+ properties: {
182
+ name: {
183
+ type: "string",
184
+ enum: options.catalog.map((s) => s.name),
185
+ description: "The name of the active skill."
186
+ },
187
+ path: {
188
+ type: "string",
189
+ description: "Path to the resource, relative to the skill root. Cannot escape the skill directory."
190
+ }
191
+ },
192
+ required: ["name", "path"],
193
+ additionalProperties: false
194
+ }
195
+ },
196
+ async execute(input, ctx) {
197
+ const skillName = input.name;
198
+ const relPath = input.path;
199
+ const skill = byName.get(skillName);
200
+ if (!skill)
201
+ return `Error: unknown skill "${skillName}".`;
202
+ if (!options.state.isActive(skillName))
203
+ return `Error: skill "${skillName}" is not active. Call skills_use with name: "${skillName}" first.`;
204
+ if (!skill.baseDir) {
205
+ return `Error: skill "${skillName}" has no base directory (likely an inline skill without bundled resources); cannot read files.`;
206
+ }
207
+ const validated = validateResourcePath(relPath, skill.baseDir);
208
+ if (!validated.valid)
209
+ return `Error: ${validated.error}`;
210
+ let content;
211
+ try {
212
+ content = await ctx.execution.readFile(ctx.handle, validated.absolutePath);
213
+ } catch (err) {
214
+ return `Error reading "${relPath}" in skill "${skillName}": ${err.message}`;
215
+ }
216
+ if (looksBinary(content)) {
217
+ return JSON.stringify({
218
+ kind: "binary-unsupported",
219
+ path: validated.absolutePath,
220
+ note: "This file appears to be binary. The skills_read tool returns text only; binary files are not delivered through the execution context's text-based readFile API."
221
+ });
222
+ }
223
+ return content;
224
+ }
225
+ };
226
+ }
227
+
228
+ // src/tools/skills-run-script.ts
229
+ var SINGLE_QUOTE_RE = /'/g;
230
+ var ABS_WINDOWS_RE = /^[a-z]:[\\/]/i;
231
+ var COLLAPSE_SLASHES_RE = /\/+/g;
232
+ function quoteShellArg(arg) {
233
+ return `'${arg.replace(SINGLE_QUOTE_RE, `'\\''`)}'`;
234
+ }
235
+ function createSkillsRunScriptTool(options) {
236
+ const byName = new Map(options.catalog.map((s) => [s.name, s]));
237
+ const timeoutMs = options.scriptTimeoutMs ?? 6e4;
238
+ return {
239
+ spec: {
240
+ name: "skills_run_script",
241
+ description: "Execute a script bundled with an active skill (from its scripts/ directory). The skill must have been activated via skills_use first. Returns stdout, stderr, and the exit code. Honors the script's shebang.",
242
+ inputSchema: {
243
+ type: "object",
244
+ properties: {
245
+ name: {
246
+ type: "string",
247
+ enum: options.catalog.map((s) => s.name),
248
+ description: "The name of the active skill."
249
+ },
250
+ script: {
251
+ type: "string",
252
+ description: `Path to the script relative to the skill's scripts/ directory (e.g. "extract.py", "merge.sh").`
253
+ },
254
+ args: {
255
+ type: "array",
256
+ items: { type: "string" },
257
+ description: "Optional argv array passed to the script."
258
+ }
259
+ },
260
+ required: ["name", "script"],
261
+ additionalProperties: false
262
+ }
263
+ },
264
+ async execute(input, ctx) {
265
+ const skillName = input.name;
266
+ const scriptRel = input.script;
267
+ const args = input.args ?? [];
268
+ const skill = byName.get(skillName);
269
+ if (!skill)
270
+ return `Error: unknown skill "${skillName}".`;
271
+ if (!options.state.isActive(skillName))
272
+ return `Error: skill "${skillName}" is not active. Call skills_use with name: "${skillName}" first.`;
273
+ if (!skill.baseDir)
274
+ return `Error: skill "${skillName}" has no base directory (likely an inline skill); cannot run scripts.`;
275
+ if (scriptRel.startsWith("/") || ABS_WINDOWS_RE.test(scriptRel))
276
+ return `Error: Absolute paths are not allowed ("${scriptRel}").`;
277
+ const joinedPath = `scripts/${scriptRel}`.replace(COLLAPSE_SLASHES_RE, "/");
278
+ const validated = validateResourcePath(joinedPath, skill.baseDir);
279
+ if (!validated.valid)
280
+ return `Error: ${validated.error}`;
281
+ const cmd = [validated.absolutePath, ...args].map(quoteShellArg).join(" ");
282
+ try {
283
+ const result = await ctx.execution.exec(ctx.handle, cmd, {
284
+ timeout: Math.max(1, Math.round(timeoutMs / 1e3))
285
+ });
286
+ return JSON.stringify({
287
+ exitCode: result.exitCode,
288
+ stdout: result.stdout,
289
+ stderr: result.stderr
290
+ });
291
+ } catch (err) {
292
+ return `Error running script "${scriptRel}" for skill "${skillName}": ${err.message}`;
293
+ }
294
+ }
295
+ };
296
+ }
297
+
298
+ // src/tools/skills-use.ts
299
+ var MAX_RESOURCE_LIST = 50;
300
+ var AMP_RE = /&/g;
301
+ var LT_RE = /</g;
302
+ var GT_RE = />/g;
303
+ var QUOT_RE = /"/g;
304
+ function escapeXml(str) {
305
+ return str.replace(AMP_RE, "&amp;").replace(LT_RE, "&lt;").replace(GT_RE, "&gt;").replace(QUOT_RE, "&quot;");
306
+ }
307
+ function buildSkillContentWrapper(skill, body) {
308
+ const parts = [];
309
+ parts.push(`<skill_content name="${escapeXml(skill.name)}" spec_version="0.1">`);
310
+ parts.push(body);
311
+ if (skill.baseDir) {
312
+ parts.push("");
313
+ parts.push(`Skill directory: ${skill.baseDir}`);
314
+ parts.push("Relative paths resolve against this directory.");
315
+ }
316
+ if (skill.resources?.length) {
317
+ parts.push("");
318
+ parts.push("<skill_resources>");
319
+ const shown = skill.resources.slice(0, MAX_RESOURCE_LIST);
320
+ for (const res of shown) {
321
+ parts.push(` <file type="${res.type}">${escapeXml(res.path)}</file>`);
322
+ }
323
+ if (skill.resources.length > MAX_RESOURCE_LIST) {
324
+ parts.push(` <!-- \u2026(${skill.resources.length - MAX_RESOURCE_LIST} more) -->`);
325
+ }
326
+ parts.push("</skill_resources>");
327
+ }
328
+ if (skill.compatibility) {
329
+ parts.push("");
330
+ parts.push(`Compatibility: ${skill.compatibility}`);
331
+ }
332
+ if (skill.allowedTools?.length) {
333
+ parts.push(`Allowed tools: ${skill.allowedTools.join(" ")}`);
334
+ }
335
+ parts.push("</skill_content>");
336
+ return parts.join("\n");
337
+ }
338
+ function createSkillsUseTool(options) {
339
+ const byName = new Map(options.catalog.map((s) => [s.name, s]));
340
+ const interpolatedBodyCache = /* @__PURE__ */ new Map();
341
+ return {
342
+ spec: {
343
+ name: "skills_use",
344
+ description: "Activate a specialized skill and load its full instructions. Call this when a task matches a skill's description from the catalog. After calling, follow the returned instructions; use skills_read to load referenced files and skills_run_script to execute bundled scripts.",
345
+ inputSchema: {
346
+ type: "object",
347
+ properties: {
348
+ name: {
349
+ type: "string",
350
+ enum: options.catalog.map((s) => s.name),
351
+ description: "The name of the skill to activate (must be in the available skills catalog)."
352
+ }
353
+ },
354
+ required: ["name"],
355
+ additionalProperties: false
356
+ }
357
+ },
358
+ async execute(input, ctx) {
359
+ const skillName = input.name;
360
+ const skill = byName.get(skillName);
361
+ if (!skill) {
362
+ const available = [...byName.keys()].join(", ") || "<none>";
363
+ return `Error: unknown skill "${skillName}". Available skills: ${available}.`;
364
+ }
365
+ const wasActive = options.state.isActive(skillName);
366
+ if (!wasActive) {
367
+ const outcome = options.state.activate(skill, "model");
368
+ if (outcome === "cap-reached") {
369
+ const activeNames = options.state.active().map((a) => a.skill.name).join(", ");
370
+ return `Error: cannot activate "${skillName}" \u2014 the maxActive skill cap has been reached. Currently active: ${activeNames}. Deactivate an existing skill first.`;
371
+ }
372
+ await options.hooks.callHook("skills:activate", { skill, via: "model" });
373
+ }
374
+ let body = interpolatedBodyCache.get(skillName);
375
+ if (body === void 0) {
376
+ body = skill.instructions.includes("!`") ? await interpolateShellCommands(skill.instructions, ctx.execution, ctx.handle) : skill.instructions;
377
+ interpolatedBodyCache.set(skillName, body);
378
+ }
379
+ return buildSkillContentWrapper(skill, body);
380
+ }
381
+ };
382
+ }
383
+
160
384
  // src/agent.ts
161
385
  import { createHooks } from "hookable";
162
386
 
@@ -241,9 +465,25 @@ function validateToolArgs(input, schema) {
241
465
  }
242
466
 
243
467
  // src/loop.ts
468
+ var IMAGE_OMITTED_MARKER = "[image omitted \u2014 model does not support vision]";
244
469
  function turnsToMessages(turns) {
245
470
  return turns.filter((t) => t.role !== "system").map((t) => ({ role: t.role, content: t.content }));
246
471
  }
472
+ function sanitizeStoredToolResults(provider, messages) {
473
+ if (provider.meta.capabilities?.vision !== false)
474
+ return messages;
475
+ return messages.map((msg) => {
476
+ let changed = false;
477
+ const newContent = msg.content.map((block) => {
478
+ if (block.type !== "tool_result" || typeof block.output === "string")
479
+ return block;
480
+ changed = true;
481
+ const flattened = block.output.map((b) => b.type === "image" ? IMAGE_OMITTED_MARKER : b.text).join("\n");
482
+ return { ...block, output: flattened };
483
+ });
484
+ return changed ? { ...msg, content: newContent } : msg;
485
+ });
486
+ }
247
487
  async function runLoop(ctx) {
248
488
  let totalIn = 0;
249
489
  let totalOut = 0;
@@ -338,11 +578,12 @@ async function executeTurn(ctx, turn) {
338
578
  const turnId = await ctx.generateTurnId();
339
579
  const canonicalMessages = turnsToMessages(ctx.turns);
340
580
  const wireMessages = rewriteMessagesToWire(canonicalMessages, ctx.aliasMaps);
581
+ const sanitizedMessages = sanitizeStoredToolResults(ctx.provider, wireMessages);
341
582
  const streamOptions = {
342
583
  model: ctx.model,
343
584
  system: ctx.system,
344
585
  tools: ctx.formattedTools,
345
- messages: wireMessages,
586
+ messages: sanitizedMessages,
346
587
  maxTokens: ctx.maxTokens ?? 16384,
347
588
  thinking: ctx.thinking,
348
589
  thinkingBudget: ctx.thinkingBudget,
@@ -474,6 +715,13 @@ async function executeTurn(ctx, turn) {
474
715
  });
475
716
  return { ended: false, turnId, usage: result.usage };
476
717
  }
718
+ function stripImagesForNonVision(provider, output) {
719
+ if (typeof output === "string")
720
+ return output;
721
+ if (provider.meta.capabilities?.vision !== false)
722
+ return output;
723
+ return output.map((b) => b.type === "image" ? IMAGE_OMITTED_MARKER : b.text).join("\n");
724
+ }
477
725
  async function executeSingleTool(ctx, call, turnId) {
478
726
  const toolDef = ctx.tools[call.name];
479
727
  const callId = call.id;
@@ -553,6 +801,7 @@ async function executeSingleTool(ctx, call, turnId) {
553
801
  await ctx.hooks.callHook("tool:transform", transformCtx);
554
802
  output = transformCtx.result;
555
803
  isError = transformCtx.isError;
804
+ output = stripImagesForNonVision(ctx.provider, output);
556
805
  await ctx.hooks.callHook("tool:after", {
557
806
  turnId,
558
807
  callId,
@@ -701,6 +950,9 @@ function createAgent({ harness: harnessOption, provider, behavior: agentBehavior
701
950
  const skillsDisabled = skillsEnabledValue === false || Array.isArray(skillsEnabledValue) && skillsEnabledValue.length === 0;
702
951
  let resolvedSkills = null;
703
952
  let skillsCatalog = null;
953
+ const skillActivationState = createSkillActivationState({
954
+ maxActive: mergedSkillsConfig?.maxActive
955
+ });
704
956
  async function run(options) {
705
957
  if (running) {
706
958
  throw new Error("Agent is already running. Use steer() or followUp() to queue messages, or waitForIdle().");
@@ -763,34 +1015,32 @@ function createAgent({ harness: harnessOption, provider, behavior: agentBehavior
763
1015
  if (!skillsDisabled && mergedSkillsConfig && !resolvedSkills) {
764
1016
  resolvedSkills = await resolveSkills(mergedSkillsConfig);
765
1017
  await hooks.callHook("skills:resolve", { skills: resolvedSkills });
766
- if (executionHandle) {
767
- for (const skill of resolvedSkills) {
768
- if (skill.instructions.includes("!`")) {
769
- skill.instructions = await interpolateShellCommands(
770
- skill.instructions,
771
- executionContext,
772
- executionHandle
773
- );
774
- }
775
- }
776
- }
777
- const readToolName = mergedSkillsConfig.readToolName ?? "read_file";
778
- const catalogCtx = { catalog: buildCatalog(resolvedSkills, readToolName), skills: resolvedSkills };
1018
+ const skillsToolRegistered = mergedSkillsConfig?.tool !== false && resolvedSkills.length > 0;
1019
+ const catalogCtx = {
1020
+ catalog: buildCatalog(resolvedSkills, { skillsToolRegistered }),
1021
+ skills: resolvedSkills
1022
+ };
779
1023
  await hooks.callHook("skills:catalog", catalogCtx);
780
1024
  skillsCatalog = catalogCtx.catalog;
781
- const fsSkills = resolvedSkills.filter((s) => s.location);
782
- if (fsSkills.length > 0) {
783
- const skillByLocation = new Map(fsSkills.map((s) => [s.location, s]));
784
- hooks.hook("tool:after", (ctx) => {
785
- if (ctx.name !== readToolName)
786
- return;
787
- const path = ctx.input.path;
788
- if (!path)
789
- return;
790
- const matched = skillByLocation.get(path) ?? fsSkills.find((s) => s.location.endsWith(path));
791
- if (matched)
792
- hooks.callHook("skills:activate", { skill: matched });
793
- });
1025
+ }
1026
+ if (resolvedSkills && session && session.turns.length > 0 && skillActivationState.active().length === 0) {
1027
+ const skillsByName = new Map(resolvedSkills.map((s) => [s.name, s]));
1028
+ for (const turn of session.turns) {
1029
+ if (turn.role !== "assistant")
1030
+ continue;
1031
+ for (const block of turn.content) {
1032
+ if (block.type !== "tool_call" || block.name !== "skills_use")
1033
+ continue;
1034
+ const skillName = block.input?.name;
1035
+ if (!skillName)
1036
+ continue;
1037
+ const skill = skillsByName.get(skillName);
1038
+ if (!skill)
1039
+ continue;
1040
+ if (skillActivationState.activate(skill, "resume") === "ok") {
1041
+ await hooks.callHook("skills:activate", { skill, via: "resume" });
1042
+ }
1043
+ }
794
1044
  }
795
1045
  }
796
1046
  const thinking = options.thinking ?? "off";
@@ -803,8 +1053,27 @@ function createAgent({ harness: harnessOption, provider, behavior: agentBehavior
803
1053
  ${skillsCatalog}`;
804
1054
  }
805
1055
  const baseTools = options.tools !== void 0 ? options.tools : mcpConnection ? { ...harness.tools, ...mcpConnection.tools } : harness.tools;
1056
+ const shouldInjectSkillTools = options.tools === void 0 && !!resolvedSkills && resolvedSkills.length > 0 && mergedSkillsConfig?.tool !== false;
1057
+ const mergedWithSkills = shouldInjectSkillTools ? {
1058
+ // Auto-injected first so harness + MCP tools can override by name.
1059
+ skills_use: createSkillsUseTool({
1060
+ catalog: resolvedSkills,
1061
+ state: skillActivationState,
1062
+ hooks
1063
+ }),
1064
+ skills_read: createSkillsReadTool({
1065
+ catalog: resolvedSkills,
1066
+ state: skillActivationState
1067
+ }),
1068
+ skills_run_script: createSkillsRunScriptTool({
1069
+ catalog: resolvedSkills,
1070
+ state: skillActivationState,
1071
+ scriptTimeoutMs: mergedSkillsConfig?.scriptTimeoutMs
1072
+ }),
1073
+ ...baseTools
1074
+ } : baseTools;
806
1075
  const tools = {};
807
- for (const tool of Object.values(baseTools)) {
1076
+ for (const tool of Object.values(mergedWithSkills)) {
808
1077
  tools[tool.spec.name] = tool;
809
1078
  }
810
1079
  const aliasMaps = buildAliasMaps(harness.toolAliases, Object.keys(tools));
@@ -856,6 +1125,10 @@ ${skillsCatalog}`;
856
1125
  await hooks.callHook("session:turns", { sessionId: session.id, turns: remaining, count: turns.length });
857
1126
  }
858
1127
  }
1128
+ async function deactivateAllSkills() {
1129
+ for (const record of skillActivationState.clear())
1130
+ await hooks.callHook("skills:deactivate", { skill: record.skill, reason: "run-end" });
1131
+ }
859
1132
  async function finalizeSession(status) {
860
1133
  if (!session)
861
1134
  return;
@@ -865,6 +1138,7 @@ ${skillsCatalog}`;
865
1138
  await session.updateStatus(status === "aborted" ? "idle" : status);
866
1139
  await hooks.callHook("session:end", { sessionId: session.id, runId, status, turnRange: [runTurnStart, turns.length - 1] });
867
1140
  }
1141
+ const uninstallAllowedToolsGate = installAllowedToolsGate(hooks, skillActivationState);
868
1142
  const runStartMs = Date.now();
869
1143
  try {
870
1144
  const stats = await runLoop({
@@ -929,6 +1203,8 @@ ${skillsCatalog}`;
929
1203
  await finalizeSession("error");
930
1204
  throw err;
931
1205
  } finally {
1206
+ await deactivateAllSkills();
1207
+ uninstallAllowedToolsGate();
932
1208
  unregisterSpawnHook();
933
1209
  unregisterSessionSync?.();
934
1210
  for (const unregister of perRunUnregisters)
@@ -958,6 +1234,33 @@ ${skillsCatalog}`;
958
1234
  conversationTurns = [];
959
1235
  steeringQueue.length = 0;
960
1236
  followUpQueue.length = 0;
1237
+ for (const record of skillActivationState.clear())
1238
+ void hooks.callHook("skills:deactivate", { skill: record.skill, reason: "reset" });
1239
+ }
1240
+ async function activateSkill(name) {
1241
+ if (!resolvedSkills) {
1242
+ throw new Error(
1243
+ `Cannot activate skill "${name}" \u2014 skills have not been resolved yet. Call activateSkill after the first \`run()\`, or pass a skills config that resolves at agent-creation time.`
1244
+ );
1245
+ }
1246
+ const skill = resolvedSkills.find((s) => s.name === name);
1247
+ if (!skill) {
1248
+ const available = resolvedSkills.map((s) => s.name).join(", ") || "<none>";
1249
+ throw new Error(`Unknown skill "${name}". Available skills: ${available}.`);
1250
+ }
1251
+ const outcome = skillActivationState.activate(skill, "explicit");
1252
+ if (outcome === "cap-reached") {
1253
+ throw new Error(
1254
+ `Cannot activate skill "${name}" \u2014 the maxActive cap of ${mergedSkillsConfig?.maxActive} has been reached.`
1255
+ );
1256
+ }
1257
+ if (outcome === "ok")
1258
+ await hooks.callHook("skills:activate", { skill, via: "explicit" });
1259
+ }
1260
+ async function deactivateSkill(name) {
1261
+ const removed = skillActivationState.deactivate(name);
1262
+ if (removed)
1263
+ await hooks.callHook("skills:deactivate", { skill: removed.skill, reason: "explicit" });
961
1264
  }
962
1265
  if (session) {
963
1266
  const originalSave = session.save.bind(session);
@@ -990,6 +1293,8 @@ ${skillsCatalog}`;
990
1293
  waitForIdle,
991
1294
  reset,
992
1295
  destroy,
1296
+ activateSkill,
1297
+ deactivateSkill,
993
1298
  get isRunning() {
994
1299
  return running;
995
1300
  },
@@ -1005,6 +1310,9 @@ ${skillsCatalog}`;
1005
1310
  get session() {
1006
1311
  return session ?? null;
1007
1312
  },
1313
+ get activeSkills() {
1314
+ return skillActivationState.active();
1315
+ },
1008
1316
  meta: provider.meta
1009
1317
  };
1010
1318
  }
@@ -1134,6 +1442,9 @@ var writeFile = {
1134
1442
 
1135
1443
  export {
1136
1444
  validateToolArgs,
1445
+ createSkillsReadTool,
1446
+ createSkillsRunScriptTool,
1447
+ createSkillsUseTool,
1137
1448
  createAgent,
1138
1449
  glob,
1139
1450
  createInteractionTool,
@@ -1,7 +1,7 @@
1
1
  // src/mcp/index.ts
2
2
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
3
  import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
4
- import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
4
+ import { getDefaultEnvironment, StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
5
5
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
6
6
  function inferTransport(raw) {
7
7
  if (raw.transport === "stdio" || raw.transport === "sse" || raw.transport === "streamable-http")
@@ -28,6 +28,8 @@ function normalizeOne(name, raw) {
28
28
  config.args = raw.args;
29
29
  if (raw.env)
30
30
  config.env = raw.env;
31
+ if (raw.strictEnv === true)
32
+ config.strictEnv = true;
31
33
  if (url)
32
34
  config.url = url;
33
35
  if (raw.headers)
@@ -72,14 +74,60 @@ function resultToString(content) {
72
74
  return JSON.stringify(block);
73
75
  }).join("\n");
74
76
  }
77
+ function normalizeMcpBlocks(content) {
78
+ if (!Array.isArray(content))
79
+ return null;
80
+ const out = [];
81
+ for (const raw of content) {
82
+ if (!raw || typeof raw !== "object")
83
+ continue;
84
+ const block = raw;
85
+ if (block.type === "text" && typeof block.text === "string") {
86
+ out.push({ type: "text", text: block.text });
87
+ continue;
88
+ }
89
+ if (block.type === "image" && typeof block.data === "string") {
90
+ const mediaType = typeof block.mimeType === "string" ? block.mimeType : typeof block.mediaType === "string" ? block.mediaType : "image/png";
91
+ out.push({ type: "image", mediaType, data: block.data });
92
+ continue;
93
+ }
94
+ if (block.type === "resource" && block.resource && typeof block.resource === "object") {
95
+ const res = block.resource;
96
+ if (typeof res.text === "string") {
97
+ out.push({ type: "text", text: res.text });
98
+ continue;
99
+ }
100
+ if (typeof res.blob === "string" && typeof res.mimeType === "string" && res.mimeType.startsWith("image/")) {
101
+ out.push({ type: "image", mediaType: res.mimeType, data: res.blob });
102
+ continue;
103
+ }
104
+ }
105
+ out.push({ type: "text", text: JSON.stringify(block) });
106
+ }
107
+ return out;
108
+ }
109
+ function packMcpResult(content) {
110
+ const normalized = normalizeMcpBlocks(content);
111
+ if (!normalized || normalized.length === 0)
112
+ return "";
113
+ const parts = [];
114
+ for (const block of normalized) {
115
+ if (block.type !== "text")
116
+ return normalized;
117
+ parts.push(block.text);
118
+ }
119
+ return parts.join("\n");
120
+ }
75
121
  function createTransport(config) {
76
122
  switch (config.transport) {
77
- case "stdio":
123
+ case "stdio": {
124
+ const mergedEnv = config.env && !config.strictEnv ? { ...getDefaultEnvironment(), ...config.env } : config.env;
78
125
  return new StdioClientTransport({
79
126
  command: config.command,
80
127
  args: config.args,
81
- env: config.env
128
+ env: mergedEnv
82
129
  });
130
+ }
83
131
  case "sse":
84
132
  return new SSEClientTransport(new URL(config.url), {
85
133
  requestInit: config.headers ? { headers: config.headers } : void 0
@@ -147,7 +195,7 @@ async function connectMcpServers(configs, _clientFactory, hooks) {
147
195
  timer = setTimeout(() => reject(new Error(`MCP tool "${tool.name}" on server "${config.name}" timed out after ${timeout}ms`)), timeout);
148
196
  })
149
197
  ]).finally(() => clearTimeout(timer));
150
- let output = resultToString(result.content);
198
+ let output = packMcpResult(result.content);
151
199
  const transformCtx = {
152
200
  turnId,
153
201
  callId,
@@ -223,5 +271,6 @@ async function connectMcpServers(configs, _clientFactory, hooks) {
223
271
  export {
224
272
  normalizeMcpServers,
225
273
  resultToString,
274
+ normalizeMcpBlocks,
226
275
  connectMcpServers
227
276
  };