@elisym/sdk 0.22.0 → 0.24.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.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { A as Asset } from './assets-C-nzSYD4.cjs';
2
- import { S as SkillRateLimit } from './types-COvV499T.cjs';
2
+ import { S as SkillRateLimit } from './types-Cdscy9kY.cjs';
3
3
 
4
4
  /**
5
5
  * Shared SKILL.md runtime types. A skill is a markdown document whose
@@ -12,10 +12,29 @@ interface SkillInput {
12
12
  inputType: string;
13
13
  tags: string[];
14
14
  jobId: string;
15
+ /**
16
+ * Local path to a file input fetched out-of-band (P2P via iroh), when the job
17
+ * carried a file attachment. The runtime fetches it after payment and removes
18
+ * it after execution; the skill reads from disk rather than from `data`.
19
+ */
20
+ filePath?: string;
15
21
  }
16
22
  interface SkillOutput {
17
23
  data: string;
18
24
  outputMime?: string;
25
+ /**
26
+ * Local path to a file result. When set, the runtime seeds the file via iroh
27
+ * and the customer fetches it out-of-band; `data` carries any text note (or '').
28
+ * `outputMime` is reused as the attachment's mime.
29
+ */
30
+ filePath?: string;
31
+ /**
32
+ * Releases the resources backing `filePath` (e.g. a temp dir). The runtime
33
+ * calls it once it has seeded the file - or failed to - since seeding happens
34
+ * after `execute()` returns and the producer cannot release the file itself.
35
+ * Mirrors the input-side cleanup callback in the runtime's `resolveInputFile`.
36
+ */
37
+ cleanup?: () => Promise<void>;
19
38
  }
20
39
  /**
21
40
  * Optional per-skill LLM override declared in SKILL.md frontmatter.
@@ -342,6 +361,12 @@ interface DynamicScriptSkillParams {
342
361
  * itself reads the key from its environment.
343
362
  */
344
363
  llmOverride?: SkillLlmOverride;
364
+ /**
365
+ * MIME type the script declares for a file result (SKILL.md `output_mime`).
366
+ * Used only when the script writes a file to `ELISYM_OUTPUT_FILE`; becomes the
367
+ * iroh attachment's mime. Defaults to `application/octet-stream`.
368
+ */
369
+ outputMime?: string;
345
370
  }
346
371
  /**
347
372
  * Pipes the user's job input to the script's stdin and returns its
@@ -363,6 +388,7 @@ declare class DynamicScriptSkill implements Skill {
363
388
  private scriptArgs;
364
389
  private scriptTimeoutMs?;
365
390
  private scriptEnv?;
391
+ private outputMime?;
366
392
  constructor(params: DynamicScriptSkillParams);
367
393
  execute(input: SkillInput, ctx: SkillContext): Promise<SkillOutput>;
368
394
  }
@@ -411,6 +437,20 @@ interface SkillFrontmatter {
411
437
  script_args?: unknown;
412
438
  /** Optional override of `DEFAULT_SCRIPT_TIMEOUT_MS`. */
413
439
  script_timeout_ms?: unknown;
440
+ /**
441
+ * MIME type for a file result. Only valid in mode 'dynamic-script' (the only
442
+ * mode that can emit a file via `ELISYM_OUTPUT_FILE`). Becomes the iroh
443
+ * attachment's mime; defaults to `application/octet-stream` when omitted.
444
+ */
445
+ output_mime?: unknown;
446
+ /**
447
+ * MIME the skill expects as a file input. Only valid in mode 'dynamic-script'
448
+ * (the only mode that receives a file via `ELISYM_INPUT_FILE`). Discovery hint
449
+ * only - the runtime still content-sniffs the actual file and does not enforce
450
+ * this. Convention: `*` = any file, `image/*` = any image, `image/png` =
451
+ * exact. Its presence signals "this capability needs a file input".
452
+ */
453
+ input_mime?: unknown;
414
454
  /**
415
455
  * Optional per-skill rate limit. Applies to any skill mode. Snake-case
416
456
  * keys here match the YAML frontmatter convention; parsed into camelCase
@@ -455,6 +495,14 @@ interface ParsedSkill {
455
495
  scriptArgs: string[];
456
496
  /** Undefined => caller uses `DEFAULT_SCRIPT_TIMEOUT_MS`. */
457
497
  scriptTimeoutMs?: number;
498
+ /** MIME for a file result (mode 'dynamic-script' only). */
499
+ outputMime?: string;
500
+ /**
501
+ * MIME the skill expects as a file input (mode 'dynamic-script' only).
502
+ * Discovery hint only; not enforced at runtime. Presence signals the
503
+ * capability needs a file input (clients gate file-only flows on it).
504
+ */
505
+ inputMime?: string;
458
506
  /** Optional per-skill rate limit (any mode). */
459
507
  rateLimit?: SkillRateLimit;
460
508
  /**
package/dist/skills.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { A as Asset } from './assets-C-nzSYD4.js';
2
- import { S as SkillRateLimit } from './types-COvV499T.js';
2
+ import { S as SkillRateLimit } from './types-Cdscy9kY.js';
3
3
 
4
4
  /**
5
5
  * Shared SKILL.md runtime types. A skill is a markdown document whose
@@ -12,10 +12,29 @@ interface SkillInput {
12
12
  inputType: string;
13
13
  tags: string[];
14
14
  jobId: string;
15
+ /**
16
+ * Local path to a file input fetched out-of-band (P2P via iroh), when the job
17
+ * carried a file attachment. The runtime fetches it after payment and removes
18
+ * it after execution; the skill reads from disk rather than from `data`.
19
+ */
20
+ filePath?: string;
15
21
  }
16
22
  interface SkillOutput {
17
23
  data: string;
18
24
  outputMime?: string;
25
+ /**
26
+ * Local path to a file result. When set, the runtime seeds the file via iroh
27
+ * and the customer fetches it out-of-band; `data` carries any text note (or '').
28
+ * `outputMime` is reused as the attachment's mime.
29
+ */
30
+ filePath?: string;
31
+ /**
32
+ * Releases the resources backing `filePath` (e.g. a temp dir). The runtime
33
+ * calls it once it has seeded the file - or failed to - since seeding happens
34
+ * after `execute()` returns and the producer cannot release the file itself.
35
+ * Mirrors the input-side cleanup callback in the runtime's `resolveInputFile`.
36
+ */
37
+ cleanup?: () => Promise<void>;
19
38
  }
20
39
  /**
21
40
  * Optional per-skill LLM override declared in SKILL.md frontmatter.
@@ -342,6 +361,12 @@ interface DynamicScriptSkillParams {
342
361
  * itself reads the key from its environment.
343
362
  */
344
363
  llmOverride?: SkillLlmOverride;
364
+ /**
365
+ * MIME type the script declares for a file result (SKILL.md `output_mime`).
366
+ * Used only when the script writes a file to `ELISYM_OUTPUT_FILE`; becomes the
367
+ * iroh attachment's mime. Defaults to `application/octet-stream`.
368
+ */
369
+ outputMime?: string;
345
370
  }
346
371
  /**
347
372
  * Pipes the user's job input to the script's stdin and returns its
@@ -363,6 +388,7 @@ declare class DynamicScriptSkill implements Skill {
363
388
  private scriptArgs;
364
389
  private scriptTimeoutMs?;
365
390
  private scriptEnv?;
391
+ private outputMime?;
366
392
  constructor(params: DynamicScriptSkillParams);
367
393
  execute(input: SkillInput, ctx: SkillContext): Promise<SkillOutput>;
368
394
  }
@@ -411,6 +437,20 @@ interface SkillFrontmatter {
411
437
  script_args?: unknown;
412
438
  /** Optional override of `DEFAULT_SCRIPT_TIMEOUT_MS`. */
413
439
  script_timeout_ms?: unknown;
440
+ /**
441
+ * MIME type for a file result. Only valid in mode 'dynamic-script' (the only
442
+ * mode that can emit a file via `ELISYM_OUTPUT_FILE`). Becomes the iroh
443
+ * attachment's mime; defaults to `application/octet-stream` when omitted.
444
+ */
445
+ output_mime?: unknown;
446
+ /**
447
+ * MIME the skill expects as a file input. Only valid in mode 'dynamic-script'
448
+ * (the only mode that receives a file via `ELISYM_INPUT_FILE`). Discovery hint
449
+ * only - the runtime still content-sniffs the actual file and does not enforce
450
+ * this. Convention: `*` = any file, `image/*` = any image, `image/png` =
451
+ * exact. Its presence signals "this capability needs a file input".
452
+ */
453
+ input_mime?: unknown;
414
454
  /**
415
455
  * Optional per-skill rate limit. Applies to any skill mode. Snake-case
416
456
  * keys here match the YAML frontmatter convention; parsed into camelCase
@@ -455,6 +495,14 @@ interface ParsedSkill {
455
495
  scriptArgs: string[];
456
496
  /** Undefined => caller uses `DEFAULT_SCRIPT_TIMEOUT_MS`. */
457
497
  scriptTimeoutMs?: number;
498
+ /** MIME for a file result (mode 'dynamic-script' only). */
499
+ outputMime?: string;
500
+ /**
501
+ * MIME the skill expects as a file input (mode 'dynamic-script' only).
502
+ * Discovery hint only; not enforced at runtime. Presence signals the
503
+ * capability needs a file input (clients gate file-only flows on it).
504
+ */
505
+ inputMime?: string;
458
506
  /** Optional per-skill rate limit (any mode). */
459
507
  rateLimit?: SkillRateLimit;
460
508
  /**
package/dist/skills.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { StringDecoder } from 'node:string_decoder';
3
- import { readFile } from 'node:fs/promises';
4
- import { dirname, resolve, relative, sep, join } from 'node:path';
3
+ import { readFile, mkdtemp, stat, rm } from 'node:fs/promises';
4
+ import { dirname, join, resolve, relative, sep } from 'node:path';
5
+ import { tmpdir } from 'node:os';
5
6
  import { readdirSync, statSync, readFileSync } from 'node:fs';
6
7
  import YAML from 'yaml';
7
8
  import Decimal from 'decimal.js-light';
@@ -56,6 +57,14 @@ function runScript(cmd, args, opts) {
56
57
  }
57
58
  });
58
59
  }
60
+ var SECRET_ENV_VARS = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY"];
61
+ function scopedToolEnv() {
62
+ const env = { ...process.env };
63
+ for (const key of SECRET_ENV_VARS) {
64
+ delete env[key];
65
+ }
66
+ return env;
67
+ }
59
68
  var ScriptSkill = class {
60
69
  name;
61
70
  description;
@@ -177,13 +186,21 @@ var ScriptSkill = class {
177
186
  if (value === void 0) {
178
187
  continue;
179
188
  }
189
+ const stringValue = String(value);
190
+ if (stringValue.startsWith("-")) {
191
+ return `Error: tool "${toolDef.name}" argument "${param.name}" must not begin with "-".`;
192
+ }
180
193
  if (param.required && index === 0) {
181
- args.push(String(value));
194
+ args.push(stringValue);
182
195
  } else {
183
- args.push(`--${param.name}`, String(value));
196
+ args.push(`--${param.name}`, stringValue);
184
197
  }
185
198
  }
186
- const result = await runScript(cmd, args, { cwd: this.skillDir, signal });
199
+ const result = await runScript(cmd, args, {
200
+ cwd: this.skillDir,
201
+ signal,
202
+ env: scopedToolEnv()
203
+ });
187
204
  if (result.spawnError) {
188
205
  return `Error: ${result.spawnError.message}`;
189
206
  }
@@ -246,6 +263,16 @@ var ScriptBillingExhaustedError = class extends Error {
246
263
  this.stderr = stderr;
247
264
  }
248
265
  };
266
+ var ScriptExecutionError = class extends Error {
267
+ exitCode;
268
+ detail;
269
+ constructor(exitCode, detail, summary) {
270
+ super(summary ?? `script failed (exit ${exitCode ?? "unknown"})`);
271
+ this.name = "ScriptExecutionError";
272
+ this.exitCode = exitCode;
273
+ this.detail = detail;
274
+ }
275
+ };
249
276
 
250
277
  // src/skills/staticScriptSkill.ts
251
278
  var StaticScriptSkill = class {
@@ -284,19 +311,23 @@ var StaticScriptSkill = class {
284
311
  env: this.scriptEnv
285
312
  });
286
313
  if (result.spawnError) {
287
- throw new Error(`script spawn failed: ${result.spawnError.message}`);
314
+ throw new ScriptExecutionError(
315
+ null,
316
+ result.spawnError.message,
317
+ "script could not be started"
318
+ );
288
319
  }
289
320
  if (result.code === SCRIPT_EXIT_BILLING_EXHAUSTED) {
290
321
  throw new ScriptBillingExhaustedError(result.code, result.stdout, result.stderr);
291
322
  }
292
323
  if (result.code !== 0) {
293
324
  const detail = result.stderr.trim() || result.stdout.trim() || "(no output)";
294
- throw new Error(`script failed (exit ${result.code}): ${detail}`);
325
+ throw new ScriptExecutionError(result.code, detail);
295
326
  }
296
327
  const output = result.stdout.trim();
297
328
  if (output === "") {
298
329
  const detail = result.stderr.trim() || "(no stderr)";
299
- throw new Error(`script exited 0 but produced empty output: ${detail}`);
330
+ throw new ScriptExecutionError(result.code, detail, "script produced empty output");
300
331
  }
301
332
  return { data: output };
302
333
  }
@@ -315,6 +346,7 @@ var DynamicScriptSkill = class {
315
346
  scriptArgs;
316
347
  scriptTimeoutMs;
317
348
  scriptEnv;
349
+ outputMime;
318
350
  constructor(params) {
319
351
  this.name = params.name;
320
352
  this.description = params.description;
@@ -328,31 +360,65 @@ var DynamicScriptSkill = class {
328
360
  this.scriptArgs = params.scriptArgs;
329
361
  this.scriptTimeoutMs = params.scriptTimeoutMs;
330
362
  this.scriptEnv = params.scriptEnv;
363
+ this.outputMime = params.outputMime;
331
364
  }
332
365
  async execute(input, ctx) {
333
- const result = await runScript(this.scriptPath, this.scriptArgs, {
334
- cwd: dirname(this.scriptPath),
335
- stdin: input.data,
336
- signal: ctx.signal,
337
- timeoutMs: this.scriptTimeoutMs,
338
- env: this.scriptEnv
339
- });
340
- if (result.spawnError) {
341
- throw new Error(`script spawn failed: ${result.spawnError.message}`);
366
+ const outDir = await mkdtemp(join(tmpdir(), "elisym-skill-out-"));
367
+ const outputFile = join(outDir, "output");
368
+ const env = {
369
+ ...this.scriptEnv ?? process.env,
370
+ ELISYM_OUTPUT_FILE: outputFile
371
+ };
372
+ if (input.filePath !== void 0) {
373
+ env.ELISYM_INPUT_FILE = input.filePath;
342
374
  }
343
- if (result.code === SCRIPT_EXIT_BILLING_EXHAUSTED) {
344
- throw new ScriptBillingExhaustedError(result.code, result.stdout, result.stderr);
345
- }
346
- if (result.code !== 0) {
347
- const detail = result.stderr.trim() || result.stdout.trim() || "(no output)";
348
- throw new Error(`script failed (exit ${result.code}): ${detail}`);
349
- }
350
- const output = result.stdout.trim();
351
- if (output === "") {
352
- const detail = result.stderr.trim() || "(no stderr)";
353
- throw new Error(`script exited 0 but produced empty output: ${detail}`);
375
+ let keepOutDir = false;
376
+ try {
377
+ const result = await runScript(this.scriptPath, this.scriptArgs, {
378
+ cwd: dirname(this.scriptPath),
379
+ stdin: input.data,
380
+ signal: ctx.signal,
381
+ timeoutMs: this.scriptTimeoutMs,
382
+ env
383
+ });
384
+ if (result.spawnError) {
385
+ throw new ScriptExecutionError(
386
+ null,
387
+ result.spawnError.message,
388
+ "script could not be started"
389
+ );
390
+ }
391
+ if (result.code === SCRIPT_EXIT_BILLING_EXHAUSTED) {
392
+ throw new ScriptBillingExhaustedError(result.code, result.stdout, result.stderr);
393
+ }
394
+ if (result.code !== 0) {
395
+ const detail = result.stderr.trim() || result.stdout.trim() || "(no output)";
396
+ throw new ScriptExecutionError(result.code, detail);
397
+ }
398
+ const outputStat = await stat(outputFile).catch(() => null);
399
+ if (outputStat !== null && outputStat.isFile() && outputStat.size > 0) {
400
+ keepOutDir = true;
401
+ return {
402
+ data: result.stdout.trim(),
403
+ filePath: outputFile,
404
+ outputMime: this.outputMime ?? "application/octet-stream",
405
+ cleanup: async () => {
406
+ await rm(outDir, { recursive: true, force: true });
407
+ }
408
+ };
409
+ }
410
+ const output = result.stdout.trim();
411
+ if (output === "") {
412
+ const detail = result.stderr.trim() || "(no stderr)";
413
+ throw new ScriptExecutionError(result.code, detail, "script produced empty output");
414
+ }
415
+ return { data: output };
416
+ } finally {
417
+ if (!keepOutDir) {
418
+ await rm(outDir, { recursive: true, force: true }).catch(() => {
419
+ });
420
+ }
354
421
  }
355
- return { data: output };
356
422
  }
357
423
  };
358
424
  function resolveInsidePath(rootDir, value) {
@@ -364,13 +430,13 @@ function resolveInsidePath(rootDir, value) {
364
430
  }
365
431
  return candidate;
366
432
  }
367
- var LAMPORTS_PER_SOL = 1e9;
368
433
  var LIMITS = {
369
434
  // Upper bound for execution budgets (`max_execution_secs` / `execution_timeout_secs`).
370
435
  // Distinct from MAX_TIMEOUT_SECS (the result-wait cap): execution budgets may be
371
436
  // hours, so this exists only to keep `secs * 1000` within Node's setTimeout limit
372
437
  // (2_147_483_647 ms) - a larger value overflows and fires the timer immediately.
373
438
  MAX_EXECUTION_SECS: 2147483};
439
+ new TextEncoder();
374
440
  var NATIVE_SOL = {
375
441
  chain: "solana",
376
442
  token: "sol",
@@ -447,11 +513,15 @@ var VALID_MODES = [
447
513
  "dynamic-script"
448
514
  ];
449
515
  function solToLamports(sol) {
450
- const asNumber = typeof sol === "string" ? Number(sol) : sol;
451
- if (!Number.isFinite(asNumber) || asNumber < 0) {
516
+ const asString = (typeof sol === "string" ? sol : String(sol)).trim();
517
+ if (/^0+(?:\.0+)?$/.test(asString)) {
518
+ return 0n;
519
+ }
520
+ try {
521
+ return parseAssetAmount(NATIVE_SOL, asString);
522
+ } catch {
452
523
  throw new Error(`Invalid SOL amount: ${sol}`);
453
524
  }
454
- return BigInt(Math.round(asNumber * LAMPORTS_PER_SOL));
455
525
  }
456
526
  function resolveSkillAsset(skillName, token, mint) {
457
527
  if (token === void 0 || token === null) {
@@ -664,6 +734,34 @@ function validateScriptTimeoutMs(skillName, raw) {
664
734
  }
665
735
  return raw;
666
736
  }
737
+ function validateOutputMime(skillName, raw) {
738
+ if (raw === void 0 || raw === null) {
739
+ return void 0;
740
+ }
741
+ if (typeof raw !== "string" || raw.length === 0) {
742
+ throw new Error(
743
+ `SKILL.md "${skillName}": "output_mime" must be a non-empty string (e.g. "image/png")`
744
+ );
745
+ }
746
+ if (raw.length > 255) {
747
+ throw new Error(`SKILL.md "${skillName}": "output_mime" too long (max 255 chars)`);
748
+ }
749
+ return raw;
750
+ }
751
+ function validateInputMime(skillName, raw) {
752
+ if (raw === void 0 || raw === null) {
753
+ return void 0;
754
+ }
755
+ if (typeof raw !== "string" || raw.length === 0) {
756
+ throw new Error(
757
+ `SKILL.md "${skillName}": "input_mime" must be a non-empty string (e.g. "image/png" or "*")`
758
+ );
759
+ }
760
+ if (raw.length > 255) {
761
+ throw new Error(`SKILL.md "${skillName}": "input_mime" too long (max 255 chars)`);
762
+ }
763
+ return raw;
764
+ }
667
765
  function validateMaxExecutionSecs(skillName, raw) {
668
766
  if (raw === void 0 || raw === null) {
669
767
  return void 0;
@@ -715,7 +813,7 @@ function validateSkillFrontmatter(frontmatter, systemPrompt, options = {}) {
715
813
  }
716
814
  const priceString = typeof priceRaw === "number" ? String(priceRaw) : priceRaw;
717
815
  if (asset === NATIVE_SOL) {
718
- priceSubunits = solToLamports(priceRaw);
816
+ priceSubunits = solToLamports(priceString);
719
817
  } else {
720
818
  try {
721
819
  priceSubunits = parseAssetAmount(asset, priceString);
@@ -764,6 +862,8 @@ function validateSkillFrontmatter(frontmatter, systemPrompt, options = {}) {
764
862
  let script;
765
863
  let scriptArgs = [];
766
864
  let scriptTimeoutMs;
865
+ let outputMime;
866
+ let inputMime;
767
867
  if (mode === "static-file") {
768
868
  if (typeof frontmatter.output_file !== "string" || frontmatter.output_file.length === 0) {
769
869
  throw new Error(
@@ -775,6 +875,16 @@ function validateSkillFrontmatter(frontmatter, systemPrompt, options = {}) {
775
875
  `SKILL.md "${frontmatter.name}": "script" is not valid in mode 'static-file'`
776
876
  );
777
877
  }
878
+ if (frontmatter.output_mime !== void 0) {
879
+ throw new Error(
880
+ `SKILL.md "${frontmatter.name}": "output_mime" is only valid in mode 'dynamic-script'`
881
+ );
882
+ }
883
+ if (frontmatter.input_mime !== void 0) {
884
+ throw new Error(
885
+ `SKILL.md "${frontmatter.name}": "input_mime" is only valid in mode 'dynamic-script'`
886
+ );
887
+ }
778
888
  outputFile = frontmatter.output_file;
779
889
  } else if (mode === "static-script" || mode === "dynamic-script") {
780
890
  if (typeof frontmatter.script !== "string" || frontmatter.script.length === 0) {
@@ -788,6 +898,21 @@ function validateSkillFrontmatter(frontmatter, systemPrompt, options = {}) {
788
898
  script = frontmatter.script;
789
899
  scriptArgs = validateScriptArgs(frontmatter.name, frontmatter.script_args);
790
900
  scriptTimeoutMs = validateScriptTimeoutMs(frontmatter.name, frontmatter.script_timeout_ms);
901
+ if (mode === "dynamic-script") {
902
+ outputMime = validateOutputMime(frontmatter.name, frontmatter.output_mime);
903
+ inputMime = validateInputMime(frontmatter.name, frontmatter.input_mime);
904
+ } else {
905
+ if (frontmatter.output_mime !== void 0) {
906
+ throw new Error(
907
+ `SKILL.md "${frontmatter.name}": "output_mime" is only valid in mode 'dynamic-script'`
908
+ );
909
+ }
910
+ if (frontmatter.input_mime !== void 0) {
911
+ throw new Error(
912
+ `SKILL.md "${frontmatter.name}": "input_mime" is only valid in mode 'dynamic-script'`
913
+ );
914
+ }
915
+ }
791
916
  } else {
792
917
  if (frontmatter.output_file !== void 0) {
793
918
  throw new Error(
@@ -809,6 +934,16 @@ function validateSkillFrontmatter(frontmatter, systemPrompt, options = {}) {
809
934
  `SKILL.md "${frontmatter.name}": "script_timeout_ms" is only valid in script modes`
810
935
  );
811
936
  }
937
+ if (frontmatter.output_mime !== void 0) {
938
+ throw new Error(
939
+ `SKILL.md "${frontmatter.name}": "output_mime" is only valid in mode 'dynamic-script'`
940
+ );
941
+ }
942
+ if (frontmatter.input_mime !== void 0) {
943
+ throw new Error(
944
+ `SKILL.md "${frontmatter.name}": "input_mime" is only valid in mode 'dynamic-script'`
945
+ );
946
+ }
812
947
  }
813
948
  const image = typeof frontmatter.image === "string" ? frontmatter.image : void 0;
814
949
  const imageFile = typeof frontmatter.image_file === "string" ? frontmatter.image_file : void 0;
@@ -835,11 +970,21 @@ function validateSkillFrontmatter(frontmatter, systemPrompt, options = {}) {
835
970
  script,
836
971
  scriptArgs,
837
972
  scriptTimeoutMs,
973
+ outputMime,
974
+ inputMime,
838
975
  rateLimit,
839
976
  executionTimeoutSecs
840
977
  };
841
978
  }
842
979
  function buildSkillFromParsed(parsed, skillDir, logger) {
980
+ let imageFile = parsed.imageFile;
981
+ if (imageFile !== void 0 && resolveInsidePath(skillDir, imageFile) === null) {
982
+ logger.warn?.(
983
+ { skill: parsed.name, imageFile },
984
+ 'SKILL.md "image_file" escapes the skill directory; ignoring it'
985
+ );
986
+ imageFile = void 0;
987
+ }
843
988
  switch (parsed.mode) {
844
989
  case "llm":
845
990
  return new ScriptSkill({
@@ -854,7 +999,7 @@ function buildSkillFromParsed(parsed, skillDir, logger) {
854
999
  maxToolRounds: parsed.maxToolRounds,
855
1000
  llmOverride: parsed.llmOverride,
856
1001
  image: parsed.image,
857
- imageFile: parsed.imageFile,
1002
+ imageFile,
858
1003
  logger
859
1004
  });
860
1005
  case "static-file": {
@@ -877,7 +1022,7 @@ function buildSkillFromParsed(parsed, skillDir, logger) {
877
1022
  asset: parsed.asset,
878
1023
  outputFilePath,
879
1024
  image: parsed.image,
880
- imageFile: parsed.imageFile,
1025
+ imageFile,
881
1026
  llmOverride: parsed.llmOverride
882
1027
  });
883
1028
  }
@@ -892,8 +1037,7 @@ function buildSkillFromParsed(parsed, skillDir, logger) {
892
1037
  if (!scriptPath) {
893
1038
  throw new Error(`SKILL.md "${parsed.name}": "script" must stay inside the skill directory`);
894
1039
  }
895
- const Ctor = parsed.mode === "static-script" ? StaticScriptSkill : DynamicScriptSkill;
896
- return new Ctor({
1040
+ const scriptParams = {
897
1041
  name: parsed.name,
898
1042
  description: parsed.description,
899
1043
  capabilities: parsed.capabilities,
@@ -903,9 +1047,10 @@ function buildSkillFromParsed(parsed, skillDir, logger) {
903
1047
  scriptArgs: parsed.scriptArgs,
904
1048
  scriptTimeoutMs: parsed.scriptTimeoutMs ?? DEFAULT_SCRIPT_TIMEOUT_MS,
905
1049
  image: parsed.image,
906
- imageFile: parsed.imageFile,
1050
+ imageFile,
907
1051
  llmOverride: parsed.llmOverride
908
- });
1052
+ };
1053
+ return parsed.mode === "dynamic-script" ? new DynamicScriptSkill({ ...scriptParams, outputMime: parsed.outputMime }) : new StaticScriptSkill(scriptParams);
909
1054
  }
910
1055
  }
911
1056
  }