feat: publish Agent Studio integrations and setup guide

This commit is contained in:
Wenxin Duan
2026-09-16 16:32:16 +08:00
parent 2ad089d68a
commit 05e6060490
106 changed files with 4942 additions and 198 deletions
+36 -2
View File
@@ -1,6 +1,7 @@
import { access, realpath, stat } from "node:fs/promises";
import { access, mkdir, mkdtemp, readFile, realpath, stat, writeFile } from "node:fs/promises";
import { realpathSync } from "node:fs";
import { delimiter, isAbsolute, relative, resolve, sep } from "node:path";
import { delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
import { SafeWorkspace } from "../workspace/safe-workspace.js";
import {
DefaultResourceLoader,
SettingsManager,
@@ -49,13 +50,16 @@ export type SkillCatalogOptions = {
cwd: string;
agentDir: string;
extraDirsValue?: string;
resources?: Record<string, readonly string[]>;
};
export class SkillCatalog {
readonly #cwd: string;
readonly #agentDir: string;
readonly #extraDirsValue: string | undefined;
readonly #resources: Record<string, readonly string[]>;
#entries: SkillCatalogEntry[] = [];
#skills: Skill[] = [];
#indexedSkills: IndexedSkill[] = [];
#diagnostics: SkillDiagnostic[] = [];
@@ -63,6 +67,7 @@ export class SkillCatalog {
this.#cwd = canonicalPathSync(options.cwd);
this.#agentDir = canonicalPathSync(options.agentDir);
this.#extraDirsValue = options.extraDirsValue;
this.#resources = Object.fromEntries(Object.entries(options.resources ?? {}).map(([path, files]) => [canonicalPathSync(path), files]));
}
public async reload(): Promise<void> {
@@ -81,6 +86,7 @@ export class SkillCatalog {
await loader.reload();
const loaded = loader.getSkills();
this.#skills = loaded.skills;
const loaderDiagnostics = loaded.diagnostics.map((diagnostic) =>
mapDiagnostic(diagnostic, this.#cwd),
);
@@ -106,6 +112,34 @@ export class SkillCatalog {
return new SkillIndex(this.#indexedSkills);
}
/** Copy only catalogued instruction bodies, never arbitrary project files or credentials. */
public async createSessionSnapshot(workspaceRoot: string): Promise<{ skills: Skill[]; index: SkillIndex }> {
const root = await realpath(workspaceRoot);
const snapshotRoot = await mkdtemp(join(root, ".skills-"));
const skills = await Promise.all(this.#skills.map(async (skill, index) => {
const baseDir = join(snapshotRoot, String(index));
await mkdir(baseDir, { mode: 0o700 });
const filePath = join(baseDir, "SKILL.md");
await writeFile(filePath, await readFile(skill.filePath), { flag: "wx", mode: 0o400 });
const source = await SafeWorkspace.create(skill.baseDir);
for (const resource of this.#resources[canonicalPathSync(skill.filePath)] ?? []) {
if (isAbsolute(resource) || resource.split(/[\\/]/).includes("..")) throw new Error("Unsafe Skill resource path");
const from = await source.resolveReadable(resource);
if ((await stat(from)).size > 1_048_576) throw new Error("Skill resource exceeds size limit");
const target = join(baseDir, resource);
await mkdir(dirname(target), { recursive: true, mode: 0o700 });
await writeFile(target, await readFile(from), { flag: "wx", mode: 0o400 });
}
return { ...skill, baseDir, filePath };
}));
return {
skills,
index: new SkillIndex(skills.map((skill) => ({
name: skill.name, source: displaySource(skill), canonicalFilePath: skill.filePath,
}))),
};
}
public readonly diagnostics = (): SkillDiagnostic[] => {
return this.#diagnostics.map((diagnostic) => ({ ...diagnostic }));
};