feat(server): skills v1 — parser, tools, /api/skills, mount
- /data/skills mount (host: /opt/skills) - skill_find, skill_use, skill_resource added to default read-only tool set; opt-in for agents with explicit tools: whitelist - AGENTS.md builtin agents drop explicit tools: arrays to inherit the new default (now includes skill tools) - POST /api/chats/:id/skill_invoke for slash-command flow - 19 SKILL.md files seeded at /opt/skills/ across 6 source groups
This commit is contained in:
@@ -4,6 +4,7 @@ import { z } from 'zod';
|
||||
import { pathGuard, PathScopeError } from './path_guard.js';
|
||||
import { grep as fileOpsGrep, findFiles as fileOpsFindFiles } from './file_ops.js';
|
||||
import { getGitMeta } from './git_meta.js';
|
||||
import { findSkills, getSkillBody, getSkillResource } from './skills.js';
|
||||
|
||||
const MAX_FILE_BYTES = 5 * 1024 * 1024;
|
||||
const DEFAULT_VIEW_LINES = 200;
|
||||
@@ -300,12 +301,119 @@ export const gitStatus: ToolDef<GitStatusInputT> = {
|
||||
},
|
||||
};
|
||||
|
||||
// Batch 9.6: skill_find, skill_use, skill_resource. Lazy-loaded markdown
|
||||
// playbooks at /data/skills/. Three tools rather than one to keep each call
|
||||
// cheap — the model lists, then loads, then optionally pulls support files.
|
||||
|
||||
const SkillFindInput = z.object({
|
||||
query: z.string().optional(),
|
||||
});
|
||||
type SkillFindInputT = z.infer<typeof SkillFindInput>;
|
||||
|
||||
export const skillFind: ToolDef<SkillFindInputT> = {
|
||||
name: 'skill_find',
|
||||
description:
|
||||
'Find skills (markdown playbooks under /data/skills) by name or description. Returns up to 5 matches. Empty query or "*" returns all available skills. Call this first to discover what skills are available.',
|
||||
inputSchema: SkillFindInput,
|
||||
jsonSchema: {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'skill_find',
|
||||
description:
|
||||
'Find skills by name or description. Returns up to 5 matches. Empty or "*" returns all.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: { type: 'string', description: 'substring matched against skill name and description' },
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
async execute(input) {
|
||||
return await findSkills(input.query ?? '');
|
||||
},
|
||||
};
|
||||
|
||||
const SkillUseInput = z.object({
|
||||
name: z.string().min(1),
|
||||
});
|
||||
type SkillUseInputT = z.infer<typeof SkillUseInput>;
|
||||
|
||||
export const skillUse: ToolDef<SkillUseInputT> = {
|
||||
name: 'skill_use',
|
||||
description:
|
||||
"Load the full body of a skill's SKILL.md by name. Returns the markdown playbook to follow. Discover names via skill_find. Errors: unknown_skill.",
|
||||
inputSchema: SkillUseInput,
|
||||
jsonSchema: {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'skill_use',
|
||||
description: "Load the full body of a skill's SKILL.md by name.",
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', description: 'skill name from skill_find' },
|
||||
},
|
||||
required: ['name'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
async execute(input) {
|
||||
const body = await getSkillBody(input.name);
|
||||
if (body === null) {
|
||||
return { error: 'unknown_skill', message: `unknown skill: ${input.name}` };
|
||||
}
|
||||
return { body };
|
||||
},
|
||||
};
|
||||
|
||||
const SkillResourceInput = z.object({
|
||||
name: z.string().min(1),
|
||||
path: z.string().min(1),
|
||||
});
|
||||
type SkillResourceInputT = z.infer<typeof SkillResourceInput>;
|
||||
|
||||
export const skillResource: ToolDef<SkillResourceInputT> = {
|
||||
name: 'skill_resource',
|
||||
description:
|
||||
"Read a support file inside a skill's folder (e.g. references/root-cause-tracing.md). Path is relative to the skill folder. Use skill_use to read SKILL.md itself. Errors: unknown_skill, unknown_resource, path_escape.",
|
||||
inputSchema: SkillResourceInput,
|
||||
jsonSchema: {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'skill_resource',
|
||||
description: "Read a support file inside a skill's folder. Path is relative to the skill folder.",
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', description: 'skill name' },
|
||||
path: { type: 'string', description: 'relative path under the skill folder' },
|
||||
},
|
||||
required: ['name', 'path'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
async execute(input) {
|
||||
const result = await getSkillResource(input.name, input.path);
|
||||
if (!result.ok) {
|
||||
return { error: result.code, message: result.message };
|
||||
}
|
||||
return { content: result.content };
|
||||
},
|
||||
};
|
||||
|
||||
export const ALL_TOOLS: ReadonlyArray<ToolDef<unknown>> = [
|
||||
viewFile as ToolDef<unknown>,
|
||||
listDir as ToolDef<unknown>,
|
||||
grep as ToolDef<unknown>,
|
||||
findFiles as ToolDef<unknown>,
|
||||
gitStatus as ToolDef<unknown>,
|
||||
skillFind as ToolDef<unknown>,
|
||||
skillUse as ToolDef<unknown>,
|
||||
skillResource as ToolDef<unknown>,
|
||||
];
|
||||
|
||||
// v1.8.2: forward-compatible read-only whitelist. An agent whose `tools` is
|
||||
@@ -313,12 +421,16 @@ export const ALL_TOOLS: ReadonlyArray<ToolDef<unknown>> = [
|
||||
// anything outside means the agent can mutate state and gets a tighter
|
||||
// default (10). Every tool in v1.8.2 happens to be read-only, so the
|
||||
// non-RO branch only takes effect once BooCoder lands write tools.
|
||||
// Batch 9.6: skill_* added; all still read-only.
|
||||
export const READ_ONLY_TOOL_NAMES = [
|
||||
'view_file',
|
||||
'list_dir',
|
||||
'grep',
|
||||
'find_files',
|
||||
'git_status',
|
||||
'skill_find',
|
||||
'skill_use',
|
||||
'skill_resource',
|
||||
] as const;
|
||||
|
||||
export const TOOLS_BY_NAME: Record<string, ToolDef<unknown>> = Object.fromEntries(
|
||||
|
||||
Reference in New Issue
Block a user