@dudamel/mcp-agents 2026.918.1 → 2026.923.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.
- package/dist/index.js +263 -25
- package/dist/skill-bundle.d.ts +57 -0
- package/dist/skill-bundle.d.ts.map +1 -0
- package/dist/stdio-server.js +263 -25
- package/dist/tools/skills.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -462,33 +462,239 @@ function registerAgentTools(server, client, ctx) {
|
|
|
462
462
|
|
|
463
463
|
// src/tools/skills.ts
|
|
464
464
|
import { z as z2 } from "zod";
|
|
465
|
+
|
|
466
|
+
// src/skill-bundle.ts
|
|
467
|
+
import { createHash } from "node:crypto";
|
|
468
|
+
import { mkdir, readdir, readFile, realpath, writeFile } from "node:fs/promises";
|
|
469
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
470
|
+
var EXCLUDED = /* @__PURE__ */ new Set([".git", "node_modules", "__pycache__", ".venv", ".DS_Store"]);
|
|
471
|
+
var MAX_PATH_LENGTH = 512;
|
|
472
|
+
var MAX_FILES = 200;
|
|
473
|
+
var MAX_BUNDLE_BYTES = 2 * 1024 * 1024;
|
|
474
|
+
var SKILL_MD = "SKILL.md";
|
|
475
|
+
function digest(content) {
|
|
476
|
+
return {
|
|
477
|
+
bytes: Buffer.byteLength(content, "utf8"),
|
|
478
|
+
sha256: createHash("sha256").update(content, "utf8").digest("hex")
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
function digestFiles(files) {
|
|
482
|
+
return files.map((f) => ({ path: f.path, ...digest(f.content) }));
|
|
483
|
+
}
|
|
484
|
+
function decodeUtf8(raw, relPath, notText) {
|
|
485
|
+
if (raw.includes(0)) {
|
|
486
|
+
notText.push(relPath);
|
|
487
|
+
return void 0;
|
|
488
|
+
}
|
|
489
|
+
try {
|
|
490
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(raw);
|
|
491
|
+
} catch {
|
|
492
|
+
notText.push(relPath);
|
|
493
|
+
return void 0;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
async function readSkillDir(skillDir) {
|
|
497
|
+
const root = resolve(skillDir);
|
|
498
|
+
let realRoot;
|
|
499
|
+
try {
|
|
500
|
+
realRoot = await realpath(root);
|
|
501
|
+
} catch {
|
|
502
|
+
throw new Error(`skillDir not found or not readable: ${root}`);
|
|
503
|
+
}
|
|
504
|
+
let content;
|
|
505
|
+
try {
|
|
506
|
+
content = await readFile(join(realRoot, SKILL_MD), "utf8");
|
|
507
|
+
} catch {
|
|
508
|
+
throw new Error(
|
|
509
|
+
`${SKILL_MD} not found at the root of ${root}. A skill directory must contain ${SKILL_MD}; use download-skill with outDir to get the expected layout.`
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
const files = [];
|
|
513
|
+
const notText = [];
|
|
514
|
+
const escaping = [];
|
|
515
|
+
const tooLongPath = [];
|
|
516
|
+
async function walk(dir) {
|
|
517
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
518
|
+
if (EXCLUDED.has(entry.name)) continue;
|
|
519
|
+
const full = join(dir, entry.name);
|
|
520
|
+
const relPath = relative(realRoot, full).split(sep).join("/");
|
|
521
|
+
if (relPath === SKILL_MD) continue;
|
|
522
|
+
let resolved = full;
|
|
523
|
+
if (entry.isSymbolicLink()) {
|
|
524
|
+
try {
|
|
525
|
+
resolved = await realpath(full);
|
|
526
|
+
} catch {
|
|
527
|
+
escaping.push(relPath);
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
if (resolved !== realRoot && !resolved.startsWith(realRoot + sep)) {
|
|
531
|
+
escaping.push(relPath);
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
const isDir = entry.isDirectory() || entry.isSymbolicLink() && await isDirectory(resolved);
|
|
536
|
+
if (isDir) {
|
|
537
|
+
await walk(resolved);
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
if (!entry.isFile() && !entry.isSymbolicLink()) continue;
|
|
541
|
+
if (relPath.length > MAX_PATH_LENGTH) {
|
|
542
|
+
tooLongPath.push(relPath);
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
const text = decodeUtf8(await readFile(resolved), relPath, notText);
|
|
546
|
+
if (text !== void 0) files.push({ path: relPath, content: text });
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
await walk(realRoot);
|
|
550
|
+
if (notText.length > 0) {
|
|
551
|
+
throw new Error(
|
|
552
|
+
`A skill can only hold text files (the column that stores them is text, not bytes). Not text: ${notText.sort().join(", ")}. Remove them from the directory, or keep them out of the skill entirely.`
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
if (escaping.length > 0) {
|
|
556
|
+
throw new Error(
|
|
557
|
+
`These symlinks point outside the skill directory and were refused: ${escaping.sort().join(", ")}. Replace them with real files if they belong to the skill.`
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
if (tooLongPath.length > 0) {
|
|
561
|
+
throw new Error(
|
|
562
|
+
`These paths exceed the ${MAX_PATH_LENGTH}-character limit: ${tooLongPath.sort().join(", ")}.`
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
if (files.length > MAX_FILES) {
|
|
566
|
+
throw new Error(
|
|
567
|
+
`A skill bundle is capped at ${MAX_FILES} files; ${skillDir} has ${files.length}.`
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
const total = Buffer.byteLength(content, "utf8") + files.reduce((n, f) => n + digest(f.content).bytes, 0);
|
|
571
|
+
if (total > MAX_BUNDLE_BYTES) {
|
|
572
|
+
const biggest = digestFiles(files).sort((a, b) => b.bytes - a.bytes).slice(0, 5).map((f) => `${f.path} (${Math.round(f.bytes / 1024)} KB)`);
|
|
573
|
+
throw new Error(
|
|
574
|
+
`The bundle is ${Math.round(total / 1024)} KB, over the ${MAX_BUNDLE_BYTES / 1024 / 1024} MB cap. Largest files: ${biggest.join(", ")}.`
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
files.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
578
|
+
return { content, files };
|
|
579
|
+
}
|
|
580
|
+
async function isDirectory(path) {
|
|
581
|
+
try {
|
|
582
|
+
await readdir(path);
|
|
583
|
+
return true;
|
|
584
|
+
} catch {
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
async function writeSkillFiles(outDir, files, opts) {
|
|
589
|
+
const root = resolve(outDir);
|
|
590
|
+
await mkdir(root, { recursive: true });
|
|
591
|
+
const realRoot = await realpath(root);
|
|
592
|
+
const targets = files.map((file) => {
|
|
593
|
+
const target = resolve(realRoot, file.path);
|
|
594
|
+
if (target !== realRoot && !target.startsWith(realRoot + sep)) {
|
|
595
|
+
throw new Error(`Refusing to write outside outDir: ${file.path}`);
|
|
596
|
+
}
|
|
597
|
+
return { file, target };
|
|
598
|
+
});
|
|
599
|
+
if (!opts.overwrite) {
|
|
600
|
+
const conflicts = [];
|
|
601
|
+
for (const { file, target } of targets) {
|
|
602
|
+
const existing = await readFile(target, "utf8").catch(() => void 0);
|
|
603
|
+
if (existing !== void 0 && existing !== file.content) conflicts.push(file.path);
|
|
604
|
+
}
|
|
605
|
+
if (conflicts.length > 0) {
|
|
606
|
+
throw new Error(
|
|
607
|
+
`These files already exist in ${root} with different content: ${conflicts.sort().join(", ")}. Nothing was written. Pass overwrite: true to replace them.`
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
for (const { file, target } of targets) {
|
|
612
|
+
await mkdir(dirname(target), { recursive: true });
|
|
613
|
+
await writeFile(target, file.content, "utf8");
|
|
614
|
+
}
|
|
615
|
+
return digestFiles(files);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// src/tools/skills.ts
|
|
619
|
+
function skillMetadata(skill) {
|
|
620
|
+
return {
|
|
621
|
+
id: skill.id,
|
|
622
|
+
name: skill.name,
|
|
623
|
+
description: skill.description ?? null,
|
|
624
|
+
status: skill.status,
|
|
625
|
+
origin: skill.origin,
|
|
626
|
+
currentVersion: skill.currentVersion ?? null,
|
|
627
|
+
updatedAt: skill.updatedAt,
|
|
628
|
+
canEdit: skill.canEdit,
|
|
629
|
+
fileCount: skill.files?.length ?? 0,
|
|
630
|
+
usedBy: skill.usedBy ?? []
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
function writeReceipt(skill) {
|
|
634
|
+
return {
|
|
635
|
+
id: skill.id,
|
|
636
|
+
name: skill.name,
|
|
637
|
+
currentVersion: skill.currentVersion ?? null,
|
|
638
|
+
updatedAt: skill.updatedAt,
|
|
639
|
+
content: digest(skill.content ?? ""),
|
|
640
|
+
files: digestFiles(skill.files ?? [])
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
async function resolveBundle(args) {
|
|
644
|
+
if (args.skillDir === void 0) return { content: args.content, files: args.files };
|
|
645
|
+
if (args.content !== void 0 || args.files !== void 0) {
|
|
646
|
+
throw new Error(
|
|
647
|
+
"skillDir cannot be combined with content or files: pass the directory alone (its SKILL.md becomes the content and everything else becomes the files), or pass content/files inline."
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
return readSkillDir(args.skillDir);
|
|
651
|
+
}
|
|
652
|
+
var FILES_SEMANTICS = "Omitting `files` keeps the existing ones; sending it REPLACES the whole set, so a file missing from the list is deleted (`files: []` deletes all of them). To delete a file, leave it out of the directory (or the array) and upload.";
|
|
653
|
+
var SKILL_DIR_HELP = "Path to a skill directory on disk \u2014 `SKILL.md` at its root becomes the content (frontmatter included) and every other file becomes an embedded file, path relative to the directory. This is the layout download-skill writes with outDir, so the loop is: download to a folder, edit, upload the folder. Prefer this over inline content for anything non-trivial \u2014 it costs the same whether the skill is 1 KB or 1 MB. Text files only; .git/node_modules/__pycache__/.venv are skipped.";
|
|
465
654
|
function registerSkillTools(server, client, ctx) {
|
|
466
655
|
server.registerTool(
|
|
467
656
|
"list-skills",
|
|
468
657
|
{
|
|
469
|
-
description: "List skill definitions (active by default).
|
|
658
|
+
description: "List skill definitions (active by default). Metadata only \u2014 id, name, description, status, origin, version, file count and timestamps. Use get-skill or download-skill to read a specific skill.",
|
|
470
659
|
inputSchema: {
|
|
471
|
-
status: z2.enum(["active", "archived"]).optional().describe("Lifecycle filter \u2014 defaults to 'active'; pass 'archived' for archived skills")
|
|
660
|
+
status: z2.enum(["active", "archived"]).optional().describe("Lifecycle filter \u2014 defaults to 'active'; pass 'archived' for archived skills"),
|
|
661
|
+
query: z2.string().optional().describe("Case-insensitive substring match against the name and description")
|
|
472
662
|
},
|
|
473
663
|
annotations: { readOnlyHint: true }
|
|
474
664
|
},
|
|
475
|
-
async ({ status }) => {
|
|
476
|
-
const
|
|
477
|
-
const page = await client.get(`/api/skills${
|
|
478
|
-
|
|
665
|
+
async ({ status, query }) => {
|
|
666
|
+
const qs = status ? `?status=${status}` : "";
|
|
667
|
+
const page = await client.get(`/api/skills${qs}`);
|
|
668
|
+
const needle = query?.trim().toLowerCase();
|
|
669
|
+
const items = page.items.filter(
|
|
670
|
+
(s) => !needle || s.name.toLowerCase().includes(needle) || (s.description ?? "").toLowerCase().includes(needle)
|
|
671
|
+
).map(skillMetadata);
|
|
672
|
+
return jsonResult({ items, total: items.length, totalBeforeFilter: page.total });
|
|
479
673
|
}
|
|
480
674
|
);
|
|
481
675
|
server.registerTool(
|
|
482
676
|
"get-skill",
|
|
483
677
|
{
|
|
484
|
-
description: "Get detailed information about a specific skill, including content, allowed tools, files, and version info.",
|
|
678
|
+
description: "Get detailed information about a specific skill, including content, allowed tools, files, and version info. For a large skill prefer download-skill with outDir, which puts the files on disk instead of in the conversation.",
|
|
485
679
|
inputSchema: {
|
|
486
|
-
id: z2.string().describe("Skill ID (UUID)")
|
|
680
|
+
id: z2.string().describe("Skill ID (UUID)"),
|
|
681
|
+
includeContent: z2.boolean().optional().describe(
|
|
682
|
+
"Defaults to true. With false, returns metadata plus a size and sha256 per file \u2014 enough to check what is stored without pulling the content in."
|
|
683
|
+
)
|
|
487
684
|
},
|
|
488
685
|
annotations: { readOnlyHint: true }
|
|
489
686
|
},
|
|
490
|
-
async ({ id }) => {
|
|
687
|
+
async ({ id, includeContent }) => {
|
|
491
688
|
const skill = await client.get(`/api/skills/${id}`);
|
|
689
|
+
if (includeContent === false) {
|
|
690
|
+
return jsonResult({
|
|
691
|
+
...skillMetadata(skill),
|
|
692
|
+
model: skill.model ?? null,
|
|
693
|
+
allowedTools: skill.allowedTools ?? [],
|
|
694
|
+
content: digest(skill.content ?? ""),
|
|
695
|
+
files: digestFiles(skill.files ?? [])
|
|
696
|
+
});
|
|
697
|
+
}
|
|
492
698
|
return jsonResult(skill);
|
|
493
699
|
}
|
|
494
700
|
);
|
|
@@ -509,18 +715,30 @@ function registerSkillTools(server, client, ctx) {
|
|
|
509
715
|
server.registerTool(
|
|
510
716
|
"download-skill",
|
|
511
717
|
{
|
|
512
|
-
description: "Download a skill as files in the target tool's on-disk layout (e.g. .claude/skills/<name>/SKILL.md for Claude Code).
|
|
718
|
+
description: "Download a skill as files in the target tool's on-disk layout (e.g. .claude/skills/<name>/SKILL.md for Claude Code). With `outDir` the files are written to disk and only their paths, sizes and hashes come back \u2014 the way to handle a skill of any size. Without it, the content is returned inline. Then edit the folder and push it back with update-skill + skillDir.",
|
|
513
719
|
inputSchema: {
|
|
514
720
|
id: z2.string().describe("Skill ID (UUID)"),
|
|
515
721
|
format: z2.enum(["claude-code", "codex", "kimi-code", "opencode", "raw"]).optional().describe(
|
|
516
722
|
"Target tool layout \u2014 defaults to 'claude-code'; 'raw' is the dudamel-native <name>/SKILL.md layout"
|
|
723
|
+
),
|
|
724
|
+
outDir: z2.string().optional().describe(
|
|
725
|
+
"Directory to write the files into (created if missing). Paths from the chosen format are kept, so outDir is the root the layout hangs from."
|
|
726
|
+
),
|
|
727
|
+
overwrite: z2.boolean().optional().describe(
|
|
728
|
+
"Defaults to false: if a file already exists with different content the call is rejected and nothing is written, so a re-download cannot erase edits in flight."
|
|
517
729
|
)
|
|
518
730
|
},
|
|
519
731
|
annotations: { readOnlyHint: true }
|
|
520
732
|
},
|
|
521
|
-
async ({ id, format }) => {
|
|
522
|
-
const result = await client.get(
|
|
523
|
-
|
|
733
|
+
async ({ id, format, outDir, overwrite }) => {
|
|
734
|
+
const result = await client.get(
|
|
735
|
+
`/api/skills/${id}/export?format=${format ?? "claude-code"}`
|
|
736
|
+
);
|
|
737
|
+
if (outDir === void 0) return jsonResult(result);
|
|
738
|
+
const written = await writeSkillFiles(outDir, result.files, {
|
|
739
|
+
overwrite: overwrite ?? false
|
|
740
|
+
});
|
|
741
|
+
return jsonResult({ skillName: result.skillName, outDir, files: written });
|
|
524
742
|
}
|
|
525
743
|
);
|
|
526
744
|
server.registerTool(
|
|
@@ -541,10 +759,13 @@ function registerSkillTools(server, client, ctx) {
|
|
|
541
759
|
server.registerTool(
|
|
542
760
|
"create-skill",
|
|
543
761
|
{
|
|
544
|
-
description: "Create a new skill definition. The creator becomes the owner and can edit it and share edition with business roles via set-skill-roles.",
|
|
762
|
+
description: "Create a new skill definition. The creator becomes the owner and can edit it and share edition with business roles via set-skill-roles. Pass `skillDir` to upload a directory from disk instead of inlining every file. Returns the new version plus a size and sha256 per stored file. " + FILES_SEMANTICS,
|
|
545
763
|
inputSchema: {
|
|
546
764
|
name: z2.string().describe("Skill name (unique, kebab-case)"),
|
|
547
|
-
content: z2.string().describe(
|
|
765
|
+
content: z2.string().optional().describe(
|
|
766
|
+
"Skill content \u2014 the full SKILL.md is fine: its frontmatter (description, version, allowed-tools, model) is honored, and an argument passed explicitly here wins over it. Required unless skillDir is given."
|
|
767
|
+
),
|
|
768
|
+
skillDir: z2.string().optional().describe(SKILL_DIR_HELP),
|
|
548
769
|
description: z2.string().optional().describe("Skill description"),
|
|
549
770
|
disableModelInvocation: z2.boolean().optional().describe("Disable model-triggered invocation"),
|
|
550
771
|
userInvocable: z2.boolean().optional().describe("Allow user to invoke via slash command"),
|
|
@@ -556,13 +777,14 @@ function registerSkillTools(server, client, ctx) {
|
|
|
556
777
|
path: z2.string().describe("Relative file path within the skill"),
|
|
557
778
|
content: z2.string().describe("File content")
|
|
558
779
|
})
|
|
559
|
-
).optional().describe(
|
|
780
|
+
).optional().describe(`Embedded skill files. ${FILES_SEMANTICS}`)
|
|
560
781
|
},
|
|
561
782
|
annotations: { readOnlyHint: false, destructiveHint: false }
|
|
562
783
|
},
|
|
563
784
|
async ({
|
|
564
785
|
name,
|
|
565
786
|
content,
|
|
787
|
+
skillDir,
|
|
566
788
|
description,
|
|
567
789
|
disableModelInvocation,
|
|
568
790
|
userInvocable,
|
|
@@ -571,30 +793,37 @@ function registerSkillTools(server, client, ctx) {
|
|
|
571
793
|
currentVersion,
|
|
572
794
|
files
|
|
573
795
|
}) => {
|
|
796
|
+
const bundle = await resolveBundle({ content, files, skillDir });
|
|
797
|
+
if (bundle.content === void 0) {
|
|
798
|
+
throw new Error("A new skill needs either content or skillDir.");
|
|
799
|
+
}
|
|
574
800
|
const result = await client.post("/api/skills", {
|
|
575
801
|
name,
|
|
576
|
-
content,
|
|
802
|
+
content: bundle.content,
|
|
577
803
|
description,
|
|
578
804
|
disableModelInvocation,
|
|
579
805
|
userInvocable,
|
|
580
806
|
model,
|
|
581
807
|
allowedTools,
|
|
582
808
|
currentVersion,
|
|
583
|
-
files
|
|
809
|
+
files: bundle.files
|
|
584
810
|
});
|
|
585
|
-
return jsonResult(result);
|
|
811
|
+
return jsonResult(writeReceipt(result));
|
|
586
812
|
}
|
|
587
813
|
);
|
|
588
814
|
server.registerTool(
|
|
589
815
|
"update-skill",
|
|
590
816
|
{
|
|
591
|
-
description: "Update a skill definition. Creates a new version entry.",
|
|
817
|
+
description: "Update a skill definition. Creates a new version entry. Pass `skillDir` to upload an edited directory from disk in one call instead of retyping every file. Returns the new version plus a size and sha256 per stored file, so the upload can be verified without downloading it back. " + FILES_SEMANTICS,
|
|
592
818
|
inputSchema: {
|
|
593
819
|
id: z2.string().describe("Skill ID (UUID)"),
|
|
594
820
|
name: z2.string().optional().describe(
|
|
595
821
|
"New skill name (unique, kebab-case) \u2014 rename the skill; agent assignments are by id and survive the rename"
|
|
596
822
|
),
|
|
597
|
-
content: z2.string().optional().describe(
|
|
823
|
+
content: z2.string().optional().describe(
|
|
824
|
+
"New skill content \u2014 the full SKILL.md is fine: its frontmatter (description, version, allowed-tools, model) is honored, and an argument passed explicitly here wins over it."
|
|
825
|
+
),
|
|
826
|
+
skillDir: z2.string().optional().describe(SKILL_DIR_HELP),
|
|
598
827
|
description: z2.string().optional().describe("New description"),
|
|
599
828
|
disableModelInvocation: z2.boolean().optional().describe("Disable model-triggered invocation"),
|
|
600
829
|
userInvocable: z2.boolean().optional().describe("Allow user to invoke via slash command"),
|
|
@@ -607,13 +836,22 @@ function registerSkillTools(server, client, ctx) {
|
|
|
607
836
|
path: z2.string().describe("Relative file path within the skill"),
|
|
608
837
|
content: z2.string().describe("File content")
|
|
609
838
|
})
|
|
610
|
-
).optional().describe(
|
|
839
|
+
).optional().describe(`Embedded skill files. ${FILES_SEMANTICS}`)
|
|
611
840
|
},
|
|
612
841
|
annotations: { readOnlyHint: false, destructiveHint: false }
|
|
613
842
|
},
|
|
614
|
-
async ({ id, ...updates }) => {
|
|
615
|
-
const
|
|
616
|
-
|
|
843
|
+
async ({ id, skillDir, ...updates }) => {
|
|
844
|
+
const bundle = await resolveBundle({
|
|
845
|
+
content: updates.content,
|
|
846
|
+
files: updates.files,
|
|
847
|
+
skillDir
|
|
848
|
+
});
|
|
849
|
+
const result = await client.put(`/api/skills/${id}`, {
|
|
850
|
+
...updates,
|
|
851
|
+
content: bundle.content,
|
|
852
|
+
files: bundle.files
|
|
853
|
+
});
|
|
854
|
+
return jsonResult(writeReceipt(result));
|
|
617
855
|
}
|
|
618
856
|
);
|
|
619
857
|
server.registerTool(
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Disk I/O for skill bundles (Plan 53).
|
|
3
|
+
*
|
|
4
|
+
* This MCP server runs as a stdio child of the agent, on the same filesystem, so it can
|
|
5
|
+
* read and write the skill directory itself instead of making the model retype every file
|
|
6
|
+
* inside the tool call. That is the whole point: the cost that hurts is the model's
|
|
7
|
+
* context, not the HTTP request. A 110 KB skill now uploads for the same price as a 1 KB
|
|
8
|
+
* one.
|
|
9
|
+
*
|
|
10
|
+
* Under the coding-agent sandbox the Landlock ruleset is inherited and cannot be relaxed,
|
|
11
|
+
* so a directory inside the workspace works and one outside fails closed — which is the
|
|
12
|
+
* behaviour we want, not a limitation to work around.
|
|
13
|
+
*
|
|
14
|
+
* Everything here fails loudly. A silently skipped file would recreate exactly the failure
|
|
15
|
+
* mode this replaces: believing you uploaded something you did not.
|
|
16
|
+
*/
|
|
17
|
+
export interface SkillFile {
|
|
18
|
+
path: string;
|
|
19
|
+
content: string;
|
|
20
|
+
}
|
|
21
|
+
export interface FileDigest {
|
|
22
|
+
path: string;
|
|
23
|
+
bytes: number;
|
|
24
|
+
sha256: string;
|
|
25
|
+
}
|
|
26
|
+
export declare function digest(content: string): {
|
|
27
|
+
bytes: number;
|
|
28
|
+
sha256: string;
|
|
29
|
+
};
|
|
30
|
+
export declare function digestFiles(files: SkillFile[]): FileDigest[];
|
|
31
|
+
/**
|
|
32
|
+
* Read a skill directory into the shape the API expects: `SKILL.md` at the root becomes
|
|
33
|
+
* `content` (frontmatter included — the API parses and honors it), everything else becomes
|
|
34
|
+
* `files` with paths relative to the directory.
|
|
35
|
+
*
|
|
36
|
+
* This is exactly the layout `download-skill` produces, so the cycle closes:
|
|
37
|
+
* download to a folder → edit → upload the folder.
|
|
38
|
+
*/
|
|
39
|
+
export declare function readSkillDir(skillDir: string): Promise<{
|
|
40
|
+
content: string;
|
|
41
|
+
files: SkillFile[];
|
|
42
|
+
}>;
|
|
43
|
+
/**
|
|
44
|
+
* Write a downloaded skill to disk and report digests instead of content.
|
|
45
|
+
*
|
|
46
|
+
* Refuses by default to clobber a file whose content differs: re-downloading on top of a
|
|
47
|
+
* folder with edits in flight would erase them with no trace — the same silent loss this
|
|
48
|
+
* plan removes on the upload side. Conflicts are all collected before anything is written,
|
|
49
|
+
* so a rejected call leaves the directory untouched.
|
|
50
|
+
*
|
|
51
|
+
* Files present in `outDir` but absent from the skill are left alone (same "omitting
|
|
52
|
+
* conserves" semantics as uploading).
|
|
53
|
+
*/
|
|
54
|
+
export declare function writeSkillFiles(outDir: string, files: SkillFile[], opts: {
|
|
55
|
+
overwrite: boolean;
|
|
56
|
+
}): Promise<FileDigest[]>;
|
|
57
|
+
//# sourceMappingURL=skill-bundle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skill-bundle.d.ts","sourceRoot":"","sources":["../src/skill-bundle.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAgBD,wBAAgB,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAKzE;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,UAAU,EAAE,CAE5D;AAgBD;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAChC,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,SAAS,EAAE,CAAA;CAAE,CAAC,CA4GlD;AAWD;;;;;;;;;;GAUG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,SAAS,EAAE,EAClB,IAAI,EAAE;IAAE,SAAS,EAAE,OAAO,CAAA;CAAE,GAC3B,OAAO,CAAC,UAAU,EAAE,CAAC,CAiCvB"}
|
package/dist/stdio-server.js
CHANGED
|
@@ -520,33 +520,239 @@ function registerAgentTools(server2, client, ctx2) {
|
|
|
520
520
|
|
|
521
521
|
// src/tools/skills.ts
|
|
522
522
|
import { z as z2 } from "zod";
|
|
523
|
+
|
|
524
|
+
// src/skill-bundle.ts
|
|
525
|
+
import { createHash } from "node:crypto";
|
|
526
|
+
import { mkdir, readdir, readFile, realpath, writeFile } from "node:fs/promises";
|
|
527
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
528
|
+
var EXCLUDED = /* @__PURE__ */ new Set([".git", "node_modules", "__pycache__", ".venv", ".DS_Store"]);
|
|
529
|
+
var MAX_PATH_LENGTH = 512;
|
|
530
|
+
var MAX_FILES = 200;
|
|
531
|
+
var MAX_BUNDLE_BYTES = 2 * 1024 * 1024;
|
|
532
|
+
var SKILL_MD = "SKILL.md";
|
|
533
|
+
function digest(content) {
|
|
534
|
+
return {
|
|
535
|
+
bytes: Buffer.byteLength(content, "utf8"),
|
|
536
|
+
sha256: createHash("sha256").update(content, "utf8").digest("hex")
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
function digestFiles(files) {
|
|
540
|
+
return files.map((f) => ({ path: f.path, ...digest(f.content) }));
|
|
541
|
+
}
|
|
542
|
+
function decodeUtf8(raw, relPath, notText) {
|
|
543
|
+
if (raw.includes(0)) {
|
|
544
|
+
notText.push(relPath);
|
|
545
|
+
return void 0;
|
|
546
|
+
}
|
|
547
|
+
try {
|
|
548
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(raw);
|
|
549
|
+
} catch {
|
|
550
|
+
notText.push(relPath);
|
|
551
|
+
return void 0;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
async function readSkillDir(skillDir) {
|
|
555
|
+
const root = resolve(skillDir);
|
|
556
|
+
let realRoot;
|
|
557
|
+
try {
|
|
558
|
+
realRoot = await realpath(root);
|
|
559
|
+
} catch {
|
|
560
|
+
throw new Error(`skillDir not found or not readable: ${root}`);
|
|
561
|
+
}
|
|
562
|
+
let content;
|
|
563
|
+
try {
|
|
564
|
+
content = await readFile(join(realRoot, SKILL_MD), "utf8");
|
|
565
|
+
} catch {
|
|
566
|
+
throw new Error(
|
|
567
|
+
`${SKILL_MD} not found at the root of ${root}. A skill directory must contain ${SKILL_MD}; use download-skill with outDir to get the expected layout.`
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
const files = [];
|
|
571
|
+
const notText = [];
|
|
572
|
+
const escaping = [];
|
|
573
|
+
const tooLongPath = [];
|
|
574
|
+
async function walk(dir) {
|
|
575
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
576
|
+
if (EXCLUDED.has(entry.name)) continue;
|
|
577
|
+
const full = join(dir, entry.name);
|
|
578
|
+
const relPath = relative(realRoot, full).split(sep).join("/");
|
|
579
|
+
if (relPath === SKILL_MD) continue;
|
|
580
|
+
let resolved = full;
|
|
581
|
+
if (entry.isSymbolicLink()) {
|
|
582
|
+
try {
|
|
583
|
+
resolved = await realpath(full);
|
|
584
|
+
} catch {
|
|
585
|
+
escaping.push(relPath);
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
if (resolved !== realRoot && !resolved.startsWith(realRoot + sep)) {
|
|
589
|
+
escaping.push(relPath);
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
const isDir = entry.isDirectory() || entry.isSymbolicLink() && await isDirectory(resolved);
|
|
594
|
+
if (isDir) {
|
|
595
|
+
await walk(resolved);
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
if (!entry.isFile() && !entry.isSymbolicLink()) continue;
|
|
599
|
+
if (relPath.length > MAX_PATH_LENGTH) {
|
|
600
|
+
tooLongPath.push(relPath);
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
const text = decodeUtf8(await readFile(resolved), relPath, notText);
|
|
604
|
+
if (text !== void 0) files.push({ path: relPath, content: text });
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
await walk(realRoot);
|
|
608
|
+
if (notText.length > 0) {
|
|
609
|
+
throw new Error(
|
|
610
|
+
`A skill can only hold text files (the column that stores them is text, not bytes). Not text: ${notText.sort().join(", ")}. Remove them from the directory, or keep them out of the skill entirely.`
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
if (escaping.length > 0) {
|
|
614
|
+
throw new Error(
|
|
615
|
+
`These symlinks point outside the skill directory and were refused: ${escaping.sort().join(", ")}. Replace them with real files if they belong to the skill.`
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
if (tooLongPath.length > 0) {
|
|
619
|
+
throw new Error(
|
|
620
|
+
`These paths exceed the ${MAX_PATH_LENGTH}-character limit: ${tooLongPath.sort().join(", ")}.`
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
if (files.length > MAX_FILES) {
|
|
624
|
+
throw new Error(
|
|
625
|
+
`A skill bundle is capped at ${MAX_FILES} files; ${skillDir} has ${files.length}.`
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
const total = Buffer.byteLength(content, "utf8") + files.reduce((n, f) => n + digest(f.content).bytes, 0);
|
|
629
|
+
if (total > MAX_BUNDLE_BYTES) {
|
|
630
|
+
const biggest = digestFiles(files).sort((a, b) => b.bytes - a.bytes).slice(0, 5).map((f) => `${f.path} (${Math.round(f.bytes / 1024)} KB)`);
|
|
631
|
+
throw new Error(
|
|
632
|
+
`The bundle is ${Math.round(total / 1024)} KB, over the ${MAX_BUNDLE_BYTES / 1024 / 1024} MB cap. Largest files: ${biggest.join(", ")}.`
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
files.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
636
|
+
return { content, files };
|
|
637
|
+
}
|
|
638
|
+
async function isDirectory(path) {
|
|
639
|
+
try {
|
|
640
|
+
await readdir(path);
|
|
641
|
+
return true;
|
|
642
|
+
} catch {
|
|
643
|
+
return false;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
async function writeSkillFiles(outDir, files, opts) {
|
|
647
|
+
const root = resolve(outDir);
|
|
648
|
+
await mkdir(root, { recursive: true });
|
|
649
|
+
const realRoot = await realpath(root);
|
|
650
|
+
const targets = files.map((file) => {
|
|
651
|
+
const target = resolve(realRoot, file.path);
|
|
652
|
+
if (target !== realRoot && !target.startsWith(realRoot + sep)) {
|
|
653
|
+
throw new Error(`Refusing to write outside outDir: ${file.path}`);
|
|
654
|
+
}
|
|
655
|
+
return { file, target };
|
|
656
|
+
});
|
|
657
|
+
if (!opts.overwrite) {
|
|
658
|
+
const conflicts = [];
|
|
659
|
+
for (const { file, target } of targets) {
|
|
660
|
+
const existing = await readFile(target, "utf8").catch(() => void 0);
|
|
661
|
+
if (existing !== void 0 && existing !== file.content) conflicts.push(file.path);
|
|
662
|
+
}
|
|
663
|
+
if (conflicts.length > 0) {
|
|
664
|
+
throw new Error(
|
|
665
|
+
`These files already exist in ${root} with different content: ${conflicts.sort().join(", ")}. Nothing was written. Pass overwrite: true to replace them.`
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
for (const { file, target } of targets) {
|
|
670
|
+
await mkdir(dirname(target), { recursive: true });
|
|
671
|
+
await writeFile(target, file.content, "utf8");
|
|
672
|
+
}
|
|
673
|
+
return digestFiles(files);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// src/tools/skills.ts
|
|
677
|
+
function skillMetadata(skill) {
|
|
678
|
+
return {
|
|
679
|
+
id: skill.id,
|
|
680
|
+
name: skill.name,
|
|
681
|
+
description: skill.description ?? null,
|
|
682
|
+
status: skill.status,
|
|
683
|
+
origin: skill.origin,
|
|
684
|
+
currentVersion: skill.currentVersion ?? null,
|
|
685
|
+
updatedAt: skill.updatedAt,
|
|
686
|
+
canEdit: skill.canEdit,
|
|
687
|
+
fileCount: skill.files?.length ?? 0,
|
|
688
|
+
usedBy: skill.usedBy ?? []
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
function writeReceipt(skill) {
|
|
692
|
+
return {
|
|
693
|
+
id: skill.id,
|
|
694
|
+
name: skill.name,
|
|
695
|
+
currentVersion: skill.currentVersion ?? null,
|
|
696
|
+
updatedAt: skill.updatedAt,
|
|
697
|
+
content: digest(skill.content ?? ""),
|
|
698
|
+
files: digestFiles(skill.files ?? [])
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
async function resolveBundle(args) {
|
|
702
|
+
if (args.skillDir === void 0) return { content: args.content, files: args.files };
|
|
703
|
+
if (args.content !== void 0 || args.files !== void 0) {
|
|
704
|
+
throw new Error(
|
|
705
|
+
"skillDir cannot be combined with content or files: pass the directory alone (its SKILL.md becomes the content and everything else becomes the files), or pass content/files inline."
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
return readSkillDir(args.skillDir);
|
|
709
|
+
}
|
|
710
|
+
var FILES_SEMANTICS = "Omitting `files` keeps the existing ones; sending it REPLACES the whole set, so a file missing from the list is deleted (`files: []` deletes all of them). To delete a file, leave it out of the directory (or the array) and upload.";
|
|
711
|
+
var SKILL_DIR_HELP = "Path to a skill directory on disk \u2014 `SKILL.md` at its root becomes the content (frontmatter included) and every other file becomes an embedded file, path relative to the directory. This is the layout download-skill writes with outDir, so the loop is: download to a folder, edit, upload the folder. Prefer this over inline content for anything non-trivial \u2014 it costs the same whether the skill is 1 KB or 1 MB. Text files only; .git/node_modules/__pycache__/.venv are skipped.";
|
|
523
712
|
function registerSkillTools(server2, client, ctx2) {
|
|
524
713
|
server2.registerTool(
|
|
525
714
|
"list-skills",
|
|
526
715
|
{
|
|
527
|
-
description: "List skill definitions (active by default).
|
|
716
|
+
description: "List skill definitions (active by default). Metadata only \u2014 id, name, description, status, origin, version, file count and timestamps. Use get-skill or download-skill to read a specific skill.",
|
|
528
717
|
inputSchema: {
|
|
529
|
-
status: z2.enum(["active", "archived"]).optional().describe("Lifecycle filter \u2014 defaults to 'active'; pass 'archived' for archived skills")
|
|
718
|
+
status: z2.enum(["active", "archived"]).optional().describe("Lifecycle filter \u2014 defaults to 'active'; pass 'archived' for archived skills"),
|
|
719
|
+
query: z2.string().optional().describe("Case-insensitive substring match against the name and description")
|
|
530
720
|
},
|
|
531
721
|
annotations: { readOnlyHint: true }
|
|
532
722
|
},
|
|
533
|
-
async ({ status }) => {
|
|
534
|
-
const
|
|
535
|
-
const page = await client.get(`/api/skills${
|
|
536
|
-
|
|
723
|
+
async ({ status, query }) => {
|
|
724
|
+
const qs = status ? `?status=${status}` : "";
|
|
725
|
+
const page = await client.get(`/api/skills${qs}`);
|
|
726
|
+
const needle = query?.trim().toLowerCase();
|
|
727
|
+
const items = page.items.filter(
|
|
728
|
+
(s) => !needle || s.name.toLowerCase().includes(needle) || (s.description ?? "").toLowerCase().includes(needle)
|
|
729
|
+
).map(skillMetadata);
|
|
730
|
+
return jsonResult({ items, total: items.length, totalBeforeFilter: page.total });
|
|
537
731
|
}
|
|
538
732
|
);
|
|
539
733
|
server2.registerTool(
|
|
540
734
|
"get-skill",
|
|
541
735
|
{
|
|
542
|
-
description: "Get detailed information about a specific skill, including content, allowed tools, files, and version info.",
|
|
736
|
+
description: "Get detailed information about a specific skill, including content, allowed tools, files, and version info. For a large skill prefer download-skill with outDir, which puts the files on disk instead of in the conversation.",
|
|
543
737
|
inputSchema: {
|
|
544
|
-
id: z2.string().describe("Skill ID (UUID)")
|
|
738
|
+
id: z2.string().describe("Skill ID (UUID)"),
|
|
739
|
+
includeContent: z2.boolean().optional().describe(
|
|
740
|
+
"Defaults to true. With false, returns metadata plus a size and sha256 per file \u2014 enough to check what is stored without pulling the content in."
|
|
741
|
+
)
|
|
545
742
|
},
|
|
546
743
|
annotations: { readOnlyHint: true }
|
|
547
744
|
},
|
|
548
|
-
async ({ id }) => {
|
|
745
|
+
async ({ id, includeContent }) => {
|
|
549
746
|
const skill = await client.get(`/api/skills/${id}`);
|
|
747
|
+
if (includeContent === false) {
|
|
748
|
+
return jsonResult({
|
|
749
|
+
...skillMetadata(skill),
|
|
750
|
+
model: skill.model ?? null,
|
|
751
|
+
allowedTools: skill.allowedTools ?? [],
|
|
752
|
+
content: digest(skill.content ?? ""),
|
|
753
|
+
files: digestFiles(skill.files ?? [])
|
|
754
|
+
});
|
|
755
|
+
}
|
|
550
756
|
return jsonResult(skill);
|
|
551
757
|
}
|
|
552
758
|
);
|
|
@@ -567,18 +773,30 @@ function registerSkillTools(server2, client, ctx2) {
|
|
|
567
773
|
server2.registerTool(
|
|
568
774
|
"download-skill",
|
|
569
775
|
{
|
|
570
|
-
description: "Download a skill as files in the target tool's on-disk layout (e.g. .claude/skills/<name>/SKILL.md for Claude Code).
|
|
776
|
+
description: "Download a skill as files in the target tool's on-disk layout (e.g. .claude/skills/<name>/SKILL.md for Claude Code). With `outDir` the files are written to disk and only their paths, sizes and hashes come back \u2014 the way to handle a skill of any size. Without it, the content is returned inline. Then edit the folder and push it back with update-skill + skillDir.",
|
|
571
777
|
inputSchema: {
|
|
572
778
|
id: z2.string().describe("Skill ID (UUID)"),
|
|
573
779
|
format: z2.enum(["claude-code", "codex", "kimi-code", "opencode", "raw"]).optional().describe(
|
|
574
780
|
"Target tool layout \u2014 defaults to 'claude-code'; 'raw' is the dudamel-native <name>/SKILL.md layout"
|
|
781
|
+
),
|
|
782
|
+
outDir: z2.string().optional().describe(
|
|
783
|
+
"Directory to write the files into (created if missing). Paths from the chosen format are kept, so outDir is the root the layout hangs from."
|
|
784
|
+
),
|
|
785
|
+
overwrite: z2.boolean().optional().describe(
|
|
786
|
+
"Defaults to false: if a file already exists with different content the call is rejected and nothing is written, so a re-download cannot erase edits in flight."
|
|
575
787
|
)
|
|
576
788
|
},
|
|
577
789
|
annotations: { readOnlyHint: true }
|
|
578
790
|
},
|
|
579
|
-
async ({ id, format }) => {
|
|
580
|
-
const result = await client.get(
|
|
581
|
-
|
|
791
|
+
async ({ id, format, outDir, overwrite }) => {
|
|
792
|
+
const result = await client.get(
|
|
793
|
+
`/api/skills/${id}/export?format=${format ?? "claude-code"}`
|
|
794
|
+
);
|
|
795
|
+
if (outDir === void 0) return jsonResult(result);
|
|
796
|
+
const written = await writeSkillFiles(outDir, result.files, {
|
|
797
|
+
overwrite: overwrite ?? false
|
|
798
|
+
});
|
|
799
|
+
return jsonResult({ skillName: result.skillName, outDir, files: written });
|
|
582
800
|
}
|
|
583
801
|
);
|
|
584
802
|
server2.registerTool(
|
|
@@ -599,10 +817,13 @@ function registerSkillTools(server2, client, ctx2) {
|
|
|
599
817
|
server2.registerTool(
|
|
600
818
|
"create-skill",
|
|
601
819
|
{
|
|
602
|
-
description: "Create a new skill definition. The creator becomes the owner and can edit it and share edition with business roles via set-skill-roles.",
|
|
820
|
+
description: "Create a new skill definition. The creator becomes the owner and can edit it and share edition with business roles via set-skill-roles. Pass `skillDir` to upload a directory from disk instead of inlining every file. Returns the new version plus a size and sha256 per stored file. " + FILES_SEMANTICS,
|
|
603
821
|
inputSchema: {
|
|
604
822
|
name: z2.string().describe("Skill name (unique, kebab-case)"),
|
|
605
|
-
content: z2.string().describe(
|
|
823
|
+
content: z2.string().optional().describe(
|
|
824
|
+
"Skill content \u2014 the full SKILL.md is fine: its frontmatter (description, version, allowed-tools, model) is honored, and an argument passed explicitly here wins over it. Required unless skillDir is given."
|
|
825
|
+
),
|
|
826
|
+
skillDir: z2.string().optional().describe(SKILL_DIR_HELP),
|
|
606
827
|
description: z2.string().optional().describe("Skill description"),
|
|
607
828
|
disableModelInvocation: z2.boolean().optional().describe("Disable model-triggered invocation"),
|
|
608
829
|
userInvocable: z2.boolean().optional().describe("Allow user to invoke via slash command"),
|
|
@@ -614,13 +835,14 @@ function registerSkillTools(server2, client, ctx2) {
|
|
|
614
835
|
path: z2.string().describe("Relative file path within the skill"),
|
|
615
836
|
content: z2.string().describe("File content")
|
|
616
837
|
})
|
|
617
|
-
).optional().describe(
|
|
838
|
+
).optional().describe(`Embedded skill files. ${FILES_SEMANTICS}`)
|
|
618
839
|
},
|
|
619
840
|
annotations: { readOnlyHint: false, destructiveHint: false }
|
|
620
841
|
},
|
|
621
842
|
async ({
|
|
622
843
|
name,
|
|
623
844
|
content,
|
|
845
|
+
skillDir,
|
|
624
846
|
description,
|
|
625
847
|
disableModelInvocation,
|
|
626
848
|
userInvocable,
|
|
@@ -629,30 +851,37 @@ function registerSkillTools(server2, client, ctx2) {
|
|
|
629
851
|
currentVersion,
|
|
630
852
|
files
|
|
631
853
|
}) => {
|
|
854
|
+
const bundle = await resolveBundle({ content, files, skillDir });
|
|
855
|
+
if (bundle.content === void 0) {
|
|
856
|
+
throw new Error("A new skill needs either content or skillDir.");
|
|
857
|
+
}
|
|
632
858
|
const result = await client.post("/api/skills", {
|
|
633
859
|
name,
|
|
634
|
-
content,
|
|
860
|
+
content: bundle.content,
|
|
635
861
|
description,
|
|
636
862
|
disableModelInvocation,
|
|
637
863
|
userInvocable,
|
|
638
864
|
model,
|
|
639
865
|
allowedTools,
|
|
640
866
|
currentVersion,
|
|
641
|
-
files
|
|
867
|
+
files: bundle.files
|
|
642
868
|
});
|
|
643
|
-
return jsonResult(result);
|
|
869
|
+
return jsonResult(writeReceipt(result));
|
|
644
870
|
}
|
|
645
871
|
);
|
|
646
872
|
server2.registerTool(
|
|
647
873
|
"update-skill",
|
|
648
874
|
{
|
|
649
|
-
description: "Update a skill definition. Creates a new version entry.",
|
|
875
|
+
description: "Update a skill definition. Creates a new version entry. Pass `skillDir` to upload an edited directory from disk in one call instead of retyping every file. Returns the new version plus a size and sha256 per stored file, so the upload can be verified without downloading it back. " + FILES_SEMANTICS,
|
|
650
876
|
inputSchema: {
|
|
651
877
|
id: z2.string().describe("Skill ID (UUID)"),
|
|
652
878
|
name: z2.string().optional().describe(
|
|
653
879
|
"New skill name (unique, kebab-case) \u2014 rename the skill; agent assignments are by id and survive the rename"
|
|
654
880
|
),
|
|
655
|
-
content: z2.string().optional().describe(
|
|
881
|
+
content: z2.string().optional().describe(
|
|
882
|
+
"New skill content \u2014 the full SKILL.md is fine: its frontmatter (description, version, allowed-tools, model) is honored, and an argument passed explicitly here wins over it."
|
|
883
|
+
),
|
|
884
|
+
skillDir: z2.string().optional().describe(SKILL_DIR_HELP),
|
|
656
885
|
description: z2.string().optional().describe("New description"),
|
|
657
886
|
disableModelInvocation: z2.boolean().optional().describe("Disable model-triggered invocation"),
|
|
658
887
|
userInvocable: z2.boolean().optional().describe("Allow user to invoke via slash command"),
|
|
@@ -665,13 +894,22 @@ function registerSkillTools(server2, client, ctx2) {
|
|
|
665
894
|
path: z2.string().describe("Relative file path within the skill"),
|
|
666
895
|
content: z2.string().describe("File content")
|
|
667
896
|
})
|
|
668
|
-
).optional().describe(
|
|
897
|
+
).optional().describe(`Embedded skill files. ${FILES_SEMANTICS}`)
|
|
669
898
|
},
|
|
670
899
|
annotations: { readOnlyHint: false, destructiveHint: false }
|
|
671
900
|
},
|
|
672
|
-
async ({ id, ...updates }) => {
|
|
673
|
-
const
|
|
674
|
-
|
|
901
|
+
async ({ id, skillDir, ...updates }) => {
|
|
902
|
+
const bundle = await resolveBundle({
|
|
903
|
+
content: updates.content,
|
|
904
|
+
files: updates.files,
|
|
905
|
+
skillDir
|
|
906
|
+
});
|
|
907
|
+
const result = await client.put(`/api/skills/${id}`, {
|
|
908
|
+
...updates,
|
|
909
|
+
content: bundle.content,
|
|
910
|
+
files: bundle.files
|
|
911
|
+
});
|
|
912
|
+
return jsonResult(writeReceipt(result));
|
|
675
913
|
}
|
|
676
914
|
);
|
|
677
915
|
server2.registerTool(
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skills.d.ts","sourceRoot":"","sources":["../../src/tools/skills.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"skills.d.ts","sourceRoot":"","sources":["../../src/tools/skills.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AA6FvD,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,gBAAgB,EACxB,GAAG,EAAE,WAAW,GACf,IAAI,CA+XN"}
|
package/package.json
CHANGED