@elisym/sdk 0.12.4 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/skills.cjs CHANGED
@@ -2,8 +2,9 @@
2
2
 
3
3
  var child_process = require('child_process');
4
4
  var string_decoder = require('string_decoder');
5
- var fs = require('fs');
5
+ var promises = require('fs/promises');
6
6
  var path = require('path');
7
+ var fs = require('fs');
7
8
  var YAML = require('yaml');
8
9
  var Decimal = require('decimal.js-light');
9
10
 
@@ -29,9 +30,9 @@ function sleepWithSignal(ms, signal) {
29
30
  return Promise.reject(createAbortError());
30
31
  }
31
32
  if (!signal) {
32
- return new Promise((resolve) => setTimeout(resolve, ms));
33
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
33
34
  }
34
- return new Promise((resolve, reject) => {
35
+ return new Promise((resolve2, reject) => {
35
36
  const cleanup = () => {
36
37
  clearTimeout(timer);
37
38
  signal.removeEventListener("abort", onAbort);
@@ -42,7 +43,7 @@ function sleepWithSignal(ms, signal) {
42
43
  };
43
44
  const timer = setTimeout(() => {
44
45
  cleanup();
45
- resolve();
46
+ resolve2();
46
47
  }, ms);
47
48
  signal.addEventListener("abort", onAbort, { once: true });
48
49
  });
@@ -309,14 +310,62 @@ function createLlmClient(config) {
309
310
  }
310
311
  return createAnthropicClient(config);
311
312
  }
312
- var MAX_TOOL_OUTPUT = 1e6;
313
- var TOOL_TIMEOUT_MS = 6e4;
313
+ var MAX_SCRIPT_OUTPUT = 1e6;
314
+ var DEFAULT_SCRIPT_TIMEOUT_MS = 6e4;
315
+ function runScript(cmd, args, opts) {
316
+ return new Promise((resolveResult) => {
317
+ const maxOutput = opts.maxOutput ?? MAX_SCRIPT_OUTPUT;
318
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_SCRIPT_TIMEOUT_MS;
319
+ const child = child_process.spawn(cmd, args, {
320
+ cwd: opts.cwd,
321
+ stdio: ["pipe", "pipe", "pipe"],
322
+ timeout: timeoutMs,
323
+ killSignal: "SIGKILL",
324
+ signal: opts.signal
325
+ });
326
+ let stdout = "";
327
+ let stderr = "";
328
+ const stdoutDecoder = new string_decoder.StringDecoder("utf8");
329
+ const stderrDecoder = new string_decoder.StringDecoder("utf8");
330
+ child.stdout?.on("data", (data) => {
331
+ if (stdout.length < maxOutput) {
332
+ stdout += stdoutDecoder.write(data);
333
+ if (stdout.length > maxOutput) {
334
+ stdout = stdout.slice(0, maxOutput);
335
+ }
336
+ }
337
+ });
338
+ child.stderr?.on("data", (data) => {
339
+ if (stderr.length < maxOutput) {
340
+ stderr += stderrDecoder.write(data);
341
+ if (stderr.length > maxOutput) {
342
+ stderr = stderr.slice(0, maxOutput);
343
+ }
344
+ }
345
+ });
346
+ child.on("close", (code) => {
347
+ stdout += stdoutDecoder.end();
348
+ stderr += stderrDecoder.end();
349
+ resolveResult({ stdout, stderr, code });
350
+ });
351
+ child.on("error", (err) => {
352
+ resolveResult({ stdout, stderr, code: null, spawnError: err });
353
+ });
354
+ if (child.stdin) {
355
+ child.stdin.on("error", () => {
356
+ });
357
+ child.stdin.end(opts.stdin ?? "");
358
+ }
359
+ });
360
+ }
314
361
  var ScriptSkill = class {
315
362
  name;
316
363
  description;
317
364
  capabilities;
318
365
  priceSubunits;
319
366
  asset;
367
+ mode = "llm";
368
+ llmOverride;
320
369
  image;
321
370
  imageFile;
322
371
  skillDir;
@@ -330,6 +379,7 @@ var ScriptSkill = class {
330
379
  this.capabilities = params.capabilities;
331
380
  this.priceSubunits = params.priceSubunits;
332
381
  this.asset = params.asset;
382
+ this.llmOverride = params.llmOverride;
333
383
  this.image = params.image;
334
384
  this.imageFile = params.imageFile;
335
385
  this.skillDir = params.skillDir;
@@ -339,11 +389,9 @@ var ScriptSkill = class {
339
389
  this.logger = params.logger ?? {};
340
390
  }
341
391
  async execute(input, ctx) {
342
- if (!ctx.llm) {
343
- throw new Error("LLM client not configured for skill runtime");
344
- }
392
+ const llm = this.resolveLlmClient(ctx);
345
393
  if (this.tools.length === 0) {
346
- const result = await ctx.llm.complete(this.systemPrompt, input.data, ctx.signal);
394
+ const result = await llm.complete(this.systemPrompt, input.data, ctx.signal);
347
395
  return { data: result };
348
396
  }
349
397
  const toolDefs = this.tools.map((tool) => ({
@@ -356,7 +404,6 @@ var ScriptSkill = class {
356
404
  }))
357
405
  }));
358
406
  const messages = [{ role: "user", content: input.data }];
359
- const llm = ctx.llm;
360
407
  for (let round = 0; round < this.maxToolRounds; round++) {
361
408
  if (ctx.signal?.aborted) {
362
409
  throw new Error("Job aborted");
@@ -388,76 +435,191 @@ var ScriptSkill = class {
388
435
  }
389
436
  throw new Error(`Max tool rounds (${this.maxToolRounds}) exceeded`);
390
437
  }
391
- runTool(toolDef, call, signal) {
392
- return new Promise((resolve) => {
393
- const args = [...toolDef.command];
394
- const cmd = args.shift();
395
- if (!cmd) {
396
- resolve(`Error: tool "${toolDef.name}" has an empty command`);
397
- return;
438
+ /**
439
+ * Resolve the LLM client for this skill from the runtime context.
440
+ *
441
+ * Contract:
442
+ * - When `llmOverride` is set, `ctx.getLlm` MUST be wired. Falling back to
443
+ * `ctx.llm` (the agent default) would silently use the wrong configuration
444
+ * for max-tokens-only overrides.
445
+ * - When no override is set, prefer `ctx.getLlm()` (returns the agent
446
+ * default), then fall back to `ctx.llm` for legacy callers that wire only
447
+ * a single client.
448
+ */
449
+ resolveLlmClient(ctx) {
450
+ let client;
451
+ if (this.llmOverride) {
452
+ client = ctx.getLlm?.(this.llmOverride);
453
+ if (!client) {
454
+ throw new Error(
455
+ `Skill "${this.name}" requires ctx.getLlm to be configured (llmOverride is set)`
456
+ );
398
457
  }
399
- const params = toolDef.parameters ?? [];
400
- for (let index = 0; index < params.length; index++) {
401
- const param = params[index];
402
- if (!param) {
403
- continue;
404
- }
405
- const value = call.arguments[param.name];
406
- if (value === void 0) {
407
- continue;
408
- }
409
- if (param.required && index === 0) {
410
- args.push(String(value));
411
- } else {
412
- args.push(`--${param.name}`, String(value));
413
- }
458
+ return client;
459
+ }
460
+ client = ctx.getLlm?.() ?? ctx.llm;
461
+ if (!client) {
462
+ throw new Error("LLM client not configured for skill runtime");
463
+ }
464
+ return client;
465
+ }
466
+ async runTool(toolDef, call, signal) {
467
+ const args = [...toolDef.command];
468
+ const cmd = args.shift();
469
+ if (!cmd) {
470
+ return `Error: tool "${toolDef.name}" has an empty command`;
471
+ }
472
+ const params = toolDef.parameters ?? [];
473
+ for (let index = 0; index < params.length; index++) {
474
+ const param = params[index];
475
+ if (!param) {
476
+ continue;
414
477
  }
415
- const child = child_process.spawn(cmd, args, {
416
- cwd: this.skillDir,
417
- stdio: ["pipe", "pipe", "pipe"],
418
- timeout: TOOL_TIMEOUT_MS,
419
- killSignal: "SIGKILL",
420
- signal
421
- });
422
- let stdout = "";
423
- let stderr = "";
424
- const stdoutDecoder = new string_decoder.StringDecoder("utf8");
425
- const stderrDecoder = new string_decoder.StringDecoder("utf8");
426
- child.stdout?.on("data", (data) => {
427
- if (stdout.length < MAX_TOOL_OUTPUT) {
428
- stdout += stdoutDecoder.write(data);
429
- if (stdout.length > MAX_TOOL_OUTPUT) {
430
- stdout = stdout.slice(0, MAX_TOOL_OUTPUT);
431
- }
432
- }
433
- });
434
- child.stderr?.on("data", (data) => {
435
- if (stderr.length < MAX_TOOL_OUTPUT) {
436
- stderr += stderrDecoder.write(data);
437
- if (stderr.length > MAX_TOOL_OUTPUT) {
438
- stderr = stderr.slice(0, MAX_TOOL_OUTPUT);
439
- }
440
- }
441
- });
442
- child.on("close", (code) => {
443
- stdout += stdoutDecoder.end();
444
- stderr += stderrDecoder.end();
445
- if (code === 0) {
446
- resolve(stdout.trim());
447
- return;
448
- }
449
- this.logger.debug?.(
450
- { tool: toolDef.name, code, stderrLen: stderr.length },
451
- "skill tool exited non-zero"
452
- );
453
- resolve(`Error (exit ${code}): ${stderr.trim() || stdout.trim()}`);
454
- });
455
- child.on("error", (err) => {
456
- resolve(`Error: ${err.message}`);
457
- });
478
+ const value = call.arguments[param.name];
479
+ if (value === void 0) {
480
+ continue;
481
+ }
482
+ if (param.required && index === 0) {
483
+ args.push(String(value));
484
+ } else {
485
+ args.push(`--${param.name}`, String(value));
486
+ }
487
+ }
488
+ const result = await runScript(cmd, args, { cwd: this.skillDir, signal });
489
+ if (result.spawnError) {
490
+ return `Error: ${result.spawnError.message}`;
491
+ }
492
+ if (result.code === 0) {
493
+ return result.stdout.trim();
494
+ }
495
+ this.logger.debug?.(
496
+ { tool: toolDef.name, code: result.code, stderrLen: result.stderr.length },
497
+ "skill tool exited non-zero"
498
+ );
499
+ return `Error (exit ${result.code}): ${result.stderr.trim() || result.stdout.trim()}`;
500
+ }
501
+ };
502
+ var MAX_STATIC_FILE_SIZE = 256 * 1024;
503
+ var StaticFileSkill = class {
504
+ name;
505
+ description;
506
+ capabilities;
507
+ priceSubunits;
508
+ asset;
509
+ mode = "static-file";
510
+ image;
511
+ imageFile;
512
+ outputFilePath;
513
+ constructor(params) {
514
+ this.name = params.name;
515
+ this.description = params.description;
516
+ this.capabilities = params.capabilities;
517
+ this.priceSubunits = params.priceSubunits;
518
+ this.asset = params.asset;
519
+ this.image = params.image;
520
+ this.imageFile = params.imageFile;
521
+ this.outputFilePath = params.outputFilePath;
522
+ }
523
+ async execute(_input, _ctx) {
524
+ const buffer = await promises.readFile(this.outputFilePath);
525
+ if (buffer.length > MAX_STATIC_FILE_SIZE) {
526
+ throw new Error(
527
+ `static-file output exceeds ${MAX_STATIC_FILE_SIZE} bytes (got ${buffer.length})`
528
+ );
529
+ }
530
+ return { data: buffer.toString("utf-8") };
531
+ }
532
+ };
533
+ var StaticScriptSkill = class {
534
+ name;
535
+ description;
536
+ capabilities;
537
+ priceSubunits;
538
+ asset;
539
+ mode = "static-script";
540
+ image;
541
+ imageFile;
542
+ scriptPath;
543
+ scriptArgs;
544
+ scriptTimeoutMs;
545
+ constructor(params) {
546
+ this.name = params.name;
547
+ this.description = params.description;
548
+ this.capabilities = params.capabilities;
549
+ this.priceSubunits = params.priceSubunits;
550
+ this.asset = params.asset;
551
+ this.image = params.image;
552
+ this.imageFile = params.imageFile;
553
+ this.scriptPath = params.scriptPath;
554
+ this.scriptArgs = params.scriptArgs;
555
+ this.scriptTimeoutMs = params.scriptTimeoutMs;
556
+ }
557
+ async execute(_input, ctx) {
558
+ const result = await runScript(this.scriptPath, this.scriptArgs, {
559
+ cwd: path.dirname(this.scriptPath),
560
+ signal: ctx.signal,
561
+ timeoutMs: this.scriptTimeoutMs
458
562
  });
563
+ if (result.spawnError) {
564
+ throw new Error(`script spawn failed: ${result.spawnError.message}`);
565
+ }
566
+ if (result.code !== 0) {
567
+ const detail = result.stderr.trim() || result.stdout.trim() || "(no output)";
568
+ throw new Error(`script failed (exit ${result.code}): ${detail}`);
569
+ }
570
+ return { data: result.stdout.trim() };
459
571
  }
460
572
  };
573
+ var DynamicScriptSkill = class {
574
+ name;
575
+ description;
576
+ capabilities;
577
+ priceSubunits;
578
+ asset;
579
+ mode = "dynamic-script";
580
+ image;
581
+ imageFile;
582
+ scriptPath;
583
+ scriptArgs;
584
+ scriptTimeoutMs;
585
+ constructor(params) {
586
+ this.name = params.name;
587
+ this.description = params.description;
588
+ this.capabilities = params.capabilities;
589
+ this.priceSubunits = params.priceSubunits;
590
+ this.asset = params.asset;
591
+ this.image = params.image;
592
+ this.imageFile = params.imageFile;
593
+ this.scriptPath = params.scriptPath;
594
+ this.scriptArgs = params.scriptArgs;
595
+ this.scriptTimeoutMs = params.scriptTimeoutMs;
596
+ }
597
+ async execute(input, ctx) {
598
+ const result = await runScript(this.scriptPath, this.scriptArgs, {
599
+ cwd: path.dirname(this.scriptPath),
600
+ stdin: input.data,
601
+ signal: ctx.signal,
602
+ timeoutMs: this.scriptTimeoutMs
603
+ });
604
+ if (result.spawnError) {
605
+ throw new Error(`script spawn failed: ${result.spawnError.message}`);
606
+ }
607
+ if (result.code !== 0) {
608
+ const detail = result.stderr.trim() || result.stdout.trim() || "(no output)";
609
+ throw new Error(`script failed (exit ${result.code}): ${detail}`);
610
+ }
611
+ return { data: result.stdout.trim() };
612
+ }
613
+ };
614
+ function resolveInsidePath(rootDir, value) {
615
+ const root = path.resolve(rootDir);
616
+ const candidate = path.resolve(root, value);
617
+ const rel = path.relative(root, candidate);
618
+ if (rel === "" || rel.startsWith("..") || rel.includes(`..${path.sep}`)) {
619
+ return null;
620
+ }
621
+ return candidate;
622
+ }
461
623
  var LAMPORTS_PER_SOL = 1e9;
462
624
  var NATIVE_SOL = {
463
625
  chain: "solana",
@@ -526,7 +688,15 @@ function parseAssetAmount(asset, human) {
526
688
  Decimal__default.default.clone({ toExpNeg: -100, toExpPos: 100, precision: 50 });
527
689
 
528
690
  // src/skills/loader.ts
691
+ var VALID_PROVIDERS = ["anthropic", "openai"];
692
+ var MAX_TOKENS_LIMIT = 2e5;
529
693
  var DEFAULT_MAX_TOOL_ROUNDS = 10;
694
+ var VALID_MODES = [
695
+ "llm",
696
+ "static-file",
697
+ "static-script",
698
+ "dynamic-script"
699
+ ];
530
700
  function solToLamports(sol) {
531
701
  const asNumber = typeof sol === "string" ? Number(sol) : sol;
532
702
  if (!Number.isFinite(asNumber) || asNumber < 0) {
@@ -643,6 +813,86 @@ function validateTool(raw, skillName, index) {
643
813
  parameters
644
814
  };
645
815
  }
816
+ function validateMode(skillName, raw) {
817
+ if (raw === void 0 || raw === null) {
818
+ return "llm";
819
+ }
820
+ if (typeof raw !== "string") {
821
+ throw new Error(`SKILL.md "${skillName}": "mode" must be a string`);
822
+ }
823
+ if (!VALID_MODES.includes(raw)) {
824
+ throw new Error(
825
+ `SKILL.md "${skillName}": invalid mode "${raw}". Allowed: ${VALID_MODES.join(", ")}`
826
+ );
827
+ }
828
+ return raw;
829
+ }
830
+ function validateScriptArgs(skillName, raw) {
831
+ if (raw === void 0 || raw === null) {
832
+ return [];
833
+ }
834
+ if (!Array.isArray(raw)) {
835
+ throw new Error(`SKILL.md "${skillName}": "script_args" must be an array of strings`);
836
+ }
837
+ for (const part of raw) {
838
+ if (typeof part !== "string") {
839
+ throw new Error(`SKILL.md "${skillName}": "script_args" entries must be strings`);
840
+ }
841
+ }
842
+ return raw;
843
+ }
844
+ function validateLlmOverride(skillName, frontmatter, mode) {
845
+ const hasProvider = frontmatter.provider !== void 0 && frontmatter.provider !== null;
846
+ const hasModel = frontmatter.model !== void 0 && frontmatter.model !== null;
847
+ const hasMaxTokens = frontmatter.max_tokens !== void 0 && frontmatter.max_tokens !== null;
848
+ if (!hasProvider && !hasModel && !hasMaxTokens) {
849
+ return void 0;
850
+ }
851
+ if (mode !== "llm") {
852
+ throw new Error(
853
+ `SKILL.md "${skillName}": "provider"/"model"/"max_tokens" are only valid in mode 'llm' (got '${mode}')`
854
+ );
855
+ }
856
+ if (hasProvider !== hasModel) {
857
+ throw new Error(
858
+ `SKILL.md "${skillName}": "provider" and "model" must be set together (declare both, or neither)`
859
+ );
860
+ }
861
+ const override = {};
862
+ if (hasProvider && hasModel) {
863
+ if (typeof frontmatter.provider !== "string") {
864
+ throw new Error(`SKILL.md "${skillName}": "provider" must be a string`);
865
+ }
866
+ if (!VALID_PROVIDERS.includes(frontmatter.provider)) {
867
+ throw new Error(
868
+ `SKILL.md "${skillName}": invalid provider "${frontmatter.provider}". Allowed: ${VALID_PROVIDERS.join(", ")}`
869
+ );
870
+ }
871
+ if (typeof frontmatter.model !== "string" || frontmatter.model.length === 0) {
872
+ throw new Error(`SKILL.md "${skillName}": "model" must be a non-empty string`);
873
+ }
874
+ override.provider = frontmatter.provider;
875
+ override.model = frontmatter.model;
876
+ }
877
+ if (hasMaxTokens) {
878
+ if (typeof frontmatter.max_tokens !== "number" || !Number.isInteger(frontmatter.max_tokens) || frontmatter.max_tokens <= 0 || frontmatter.max_tokens > MAX_TOKENS_LIMIT) {
879
+ throw new Error(
880
+ `SKILL.md "${skillName}": "max_tokens" must be a positive integer <= ${MAX_TOKENS_LIMIT}`
881
+ );
882
+ }
883
+ override.maxTokens = frontmatter.max_tokens;
884
+ }
885
+ return override;
886
+ }
887
+ function validateScriptTimeoutMs(skillName, raw) {
888
+ if (raw === void 0 || raw === null) {
889
+ return void 0;
890
+ }
891
+ if (typeof raw !== "number" || !Number.isInteger(raw) || raw <= 0) {
892
+ throw new Error(`SKILL.md "${skillName}": "script_timeout_ms" must be a positive integer`);
893
+ }
894
+ return raw;
895
+ }
646
896
  function validateSkillFrontmatter(frontmatter, systemPrompt, options = {}) {
647
897
  if (typeof frontmatter.name !== "string" || frontmatter.name.length === 0) {
648
898
  throw new Error('SKILL.md: missing or invalid "name" field');
@@ -694,8 +944,14 @@ function validateSkillFrontmatter(frontmatter, systemPrompt, options = {}) {
694
944
  );
695
945
  }
696
946
  }
947
+ const mode = validateMode(frontmatter.name, frontmatter.mode);
697
948
  const tools = [];
698
949
  if (frontmatter.tools !== void 0) {
950
+ if (mode !== "llm") {
951
+ throw new Error(
952
+ `SKILL.md "${frontmatter.name}": "tools" is only valid in mode 'llm' (got '${mode}')`
953
+ );
954
+ }
699
955
  if (!Array.isArray(frontmatter.tools)) {
700
956
  throw new Error(`SKILL.md "${frontmatter.name}": "tools" must be an array`);
701
957
  }
@@ -705,6 +961,11 @@ function validateSkillFrontmatter(frontmatter, systemPrompt, options = {}) {
705
961
  }
706
962
  let maxToolRounds = DEFAULT_MAX_TOOL_ROUNDS;
707
963
  if (frontmatter.max_tool_rounds !== void 0) {
964
+ if (mode !== "llm") {
965
+ throw new Error(
966
+ `SKILL.md "${frontmatter.name}": "max_tool_rounds" is only valid in mode 'llm' (got '${mode}')`
967
+ );
968
+ }
708
969
  if (typeof frontmatter.max_tool_rounds !== "number" || !Number.isInteger(frontmatter.max_tool_rounds) || frontmatter.max_tool_rounds <= 0) {
709
970
  throw new Error(
710
971
  `SKILL.md "${frontmatter.name}": "max_tool_rounds" must be a positive integer`
@@ -712,21 +973,146 @@ function validateSkillFrontmatter(frontmatter, systemPrompt, options = {}) {
712
973
  }
713
974
  maxToolRounds = frontmatter.max_tool_rounds;
714
975
  }
976
+ let outputFile;
977
+ let script;
978
+ let scriptArgs = [];
979
+ let scriptTimeoutMs;
980
+ if (mode === "static-file") {
981
+ if (typeof frontmatter.output_file !== "string" || frontmatter.output_file.length === 0) {
982
+ throw new Error(
983
+ `SKILL.md "${frontmatter.name}": mode 'static-file' requires "output_file" (string)`
984
+ );
985
+ }
986
+ if (frontmatter.script !== void 0) {
987
+ throw new Error(
988
+ `SKILL.md "${frontmatter.name}": "script" is not valid in mode 'static-file'`
989
+ );
990
+ }
991
+ outputFile = frontmatter.output_file;
992
+ } else if (mode === "static-script" || mode === "dynamic-script") {
993
+ if (typeof frontmatter.script !== "string" || frontmatter.script.length === 0) {
994
+ throw new Error(`SKILL.md "${frontmatter.name}": mode '${mode}' requires "script" (string)`);
995
+ }
996
+ if (frontmatter.output_file !== void 0) {
997
+ throw new Error(
998
+ `SKILL.md "${frontmatter.name}": "output_file" is only valid in mode 'static-file'`
999
+ );
1000
+ }
1001
+ script = frontmatter.script;
1002
+ scriptArgs = validateScriptArgs(frontmatter.name, frontmatter.script_args);
1003
+ scriptTimeoutMs = validateScriptTimeoutMs(frontmatter.name, frontmatter.script_timeout_ms);
1004
+ } else {
1005
+ if (frontmatter.output_file !== void 0) {
1006
+ throw new Error(
1007
+ `SKILL.md "${frontmatter.name}": "output_file" is only valid in mode 'static-file'`
1008
+ );
1009
+ }
1010
+ if (frontmatter.script !== void 0) {
1011
+ throw new Error(
1012
+ `SKILL.md "${frontmatter.name}": "script" is only valid in script modes (static-script, dynamic-script)`
1013
+ );
1014
+ }
1015
+ if (frontmatter.script_args !== void 0) {
1016
+ throw new Error(
1017
+ `SKILL.md "${frontmatter.name}": "script_args" is only valid in script modes`
1018
+ );
1019
+ }
1020
+ if (frontmatter.script_timeout_ms !== void 0) {
1021
+ throw new Error(
1022
+ `SKILL.md "${frontmatter.name}": "script_timeout_ms" is only valid in script modes`
1023
+ );
1024
+ }
1025
+ }
715
1026
  const image = typeof frontmatter.image === "string" ? frontmatter.image : void 0;
716
1027
  const imageFile = typeof frontmatter.image_file === "string" ? frontmatter.image_file : void 0;
1028
+ const llmOverride = validateLlmOverride(frontmatter.name, frontmatter, mode);
717
1029
  return {
718
1030
  name: frontmatter.name,
719
1031
  description: frontmatter.description,
720
1032
  capabilities,
721
1033
  priceSubunits,
722
1034
  asset,
1035
+ mode,
723
1036
  systemPrompt,
724
1037
  tools,
725
1038
  maxToolRounds,
1039
+ llmOverride,
726
1040
  image,
727
- imageFile
1041
+ imageFile,
1042
+ outputFile,
1043
+ script,
1044
+ scriptArgs,
1045
+ scriptTimeoutMs
728
1046
  };
729
1047
  }
1048
+ function buildSkillFromParsed(parsed, skillDir, logger) {
1049
+ switch (parsed.mode) {
1050
+ case "llm":
1051
+ return new ScriptSkill({
1052
+ name: parsed.name,
1053
+ description: parsed.description,
1054
+ capabilities: parsed.capabilities,
1055
+ priceSubunits: parsed.priceSubunits,
1056
+ asset: parsed.asset,
1057
+ skillDir,
1058
+ systemPrompt: parsed.systemPrompt,
1059
+ tools: parsed.tools,
1060
+ maxToolRounds: parsed.maxToolRounds,
1061
+ llmOverride: parsed.llmOverride,
1062
+ image: parsed.image,
1063
+ imageFile: parsed.imageFile,
1064
+ logger
1065
+ });
1066
+ case "static-file": {
1067
+ if (parsed.outputFile === void 0) {
1068
+ throw new Error(
1069
+ `SKILL.md "${parsed.name}": internal error - outputFile missing for mode 'static-file'`
1070
+ );
1071
+ }
1072
+ const outputFilePath = resolveInsidePath(skillDir, parsed.outputFile);
1073
+ if (!outputFilePath) {
1074
+ throw new Error(
1075
+ `SKILL.md "${parsed.name}": "output_file" must stay inside the skill directory`
1076
+ );
1077
+ }
1078
+ return new StaticFileSkill({
1079
+ name: parsed.name,
1080
+ description: parsed.description,
1081
+ capabilities: parsed.capabilities,
1082
+ priceSubunits: parsed.priceSubunits,
1083
+ asset: parsed.asset,
1084
+ outputFilePath,
1085
+ image: parsed.image,
1086
+ imageFile: parsed.imageFile
1087
+ });
1088
+ }
1089
+ case "static-script":
1090
+ case "dynamic-script": {
1091
+ if (parsed.script === void 0) {
1092
+ throw new Error(
1093
+ `SKILL.md "${parsed.name}": internal error - script missing for mode '${parsed.mode}'`
1094
+ );
1095
+ }
1096
+ const scriptPath = resolveInsidePath(skillDir, parsed.script);
1097
+ if (!scriptPath) {
1098
+ throw new Error(`SKILL.md "${parsed.name}": "script" must stay inside the skill directory`);
1099
+ }
1100
+ const Ctor = parsed.mode === "static-script" ? StaticScriptSkill : DynamicScriptSkill;
1101
+ return new Ctor({
1102
+ name: parsed.name,
1103
+ description: parsed.description,
1104
+ capabilities: parsed.capabilities,
1105
+ priceSubunits: parsed.priceSubunits,
1106
+ asset: parsed.asset,
1107
+ scriptPath,
1108
+ scriptArgs: parsed.scriptArgs,
1109
+ scriptTimeoutMs: parsed.scriptTimeoutMs ?? DEFAULT_SCRIPT_TIMEOUT_MS,
1110
+ image: parsed.image,
1111
+ imageFile: parsed.imageFile
1112
+ });
1113
+ }
1114
+ }
1115
+ }
730
1116
  function loadSkillsFromDir(skillsDir, options = {}) {
731
1117
  const logger = options.logger ?? {};
732
1118
  const skills = [];
@@ -751,22 +1137,7 @@ function loadSkillsFromDir(skillsDir, options = {}) {
751
1137
  const content = fs.readFileSync(skillMdPath, "utf-8");
752
1138
  const { frontmatter, systemPrompt } = parseSkillMd(content);
753
1139
  const parsed = validateSkillFrontmatter(frontmatter, systemPrompt, options);
754
- skills.push(
755
- new ScriptSkill({
756
- name: parsed.name,
757
- description: parsed.description,
758
- capabilities: parsed.capabilities,
759
- priceSubunits: parsed.priceSubunits,
760
- asset: parsed.asset,
761
- skillDir: entryPath,
762
- systemPrompt: parsed.systemPrompt,
763
- tools: parsed.tools,
764
- maxToolRounds: parsed.maxToolRounds,
765
- image: parsed.image,
766
- imageFile: parsed.imageFile,
767
- logger
768
- })
769
- );
1140
+ skills.push(buildSkillFromParsed(parsed, entryPath, logger));
770
1141
  } catch (error) {
771
1142
  const message = error instanceof Error ? error.message : String(error);
772
1143
  logger.warn?.({ dir: entry, err: message }, "skipping malformed skill directory");
@@ -776,12 +1147,20 @@ function loadSkillsFromDir(skillsDir, options = {}) {
776
1147
  }
777
1148
 
778
1149
  exports.DEFAULT_MAX_TOOL_ROUNDS = DEFAULT_MAX_TOOL_ROUNDS;
1150
+ exports.DEFAULT_SCRIPT_TIMEOUT_MS = DEFAULT_SCRIPT_TIMEOUT_MS;
1151
+ exports.DynamicScriptSkill = DynamicScriptSkill;
1152
+ exports.MAX_SCRIPT_OUTPUT = MAX_SCRIPT_OUTPUT;
1153
+ exports.MAX_STATIC_FILE_SIZE = MAX_STATIC_FILE_SIZE;
779
1154
  exports.ScriptSkill = ScriptSkill;
1155
+ exports.StaticFileSkill = StaticFileSkill;
1156
+ exports.StaticScriptSkill = StaticScriptSkill;
780
1157
  exports.createAnthropicClient = createAnthropicClient;
781
1158
  exports.createLlmClient = createLlmClient;
782
1159
  exports.createOpenAIClient = createOpenAIClient;
783
1160
  exports.loadSkillsFromDir = loadSkillsFromDir;
784
1161
  exports.parseSkillMd = parseSkillMd;
1162
+ exports.resolveInsidePath = resolveInsidePath;
1163
+ exports.runScript = runScript;
785
1164
  exports.validateSkillFrontmatter = validateSkillFrontmatter;
786
1165
  //# sourceMappingURL=skills.cjs.map
787
1166
  //# sourceMappingURL=skills.cjs.map