Merge origin/master into skill system branch
Use the new filesystem seam for skill file reads and system skill writes when ctx.fs is available, and include the skill tool in the generated tool catalog.
This commit is contained in:
@@ -23,6 +23,7 @@
|
||||
"license": "BSD-3-Clause",
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-agent": "^0.0.1",
|
||||
"@deepseek-ai/dsh-fs": "^0.0.1",
|
||||
"@deepseek-ai/dsh-llm": "^0.0.1",
|
||||
"cordis": "^4.0.0-rc.6"
|
||||
},
|
||||
@@ -31,6 +32,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||
"@deepseek-ai/dsh-fs": "workspace:^",
|
||||
"@deepseek-ai/dsh-fs-local": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||
"cordis": "^4.0.0-rc.6"
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { dirname, join, resolve } from 'node:path'
|
||||
import { homedir } from 'node:os'
|
||||
import { Context, Service } from 'cordis'
|
||||
import { parse as parseYaml } from 'yaml'
|
||||
import type { FileSystem, FsTarget } from '@deepseek-ai/dsh-fs'
|
||||
import type { GenerateOptions } from '@deepseek-ai/dsh-llm'
|
||||
import type {} from '@deepseek-ai/dsh-agent'
|
||||
|
||||
@@ -249,18 +250,13 @@ export class SkillService extends Service {
|
||||
}
|
||||
|
||||
async function writeSystemSkills(systemRoot: string, ctx: Context): Promise<void> {
|
||||
await mkdir(systemRoot, { recursive: true })
|
||||
await Promise.all(SYSTEM_SKILLS.map(async (skill) => {
|
||||
const dir = join(systemRoot, skill.name)
|
||||
const file = join(dir, 'SKILL.md')
|
||||
try {
|
||||
await access(file)
|
||||
if (await skillFileExists(ctx, file)) {
|
||||
return
|
||||
} catch {
|
||||
// Expected first-run path: the bundled system skill has not been installed.
|
||||
}
|
||||
await mkdir(dir, { recursive: true })
|
||||
await writeFile(file, renderSkillFile(skill))
|
||||
await writeSkillText(ctx, file, renderSkillFile(skill))
|
||||
ctx.logger.debug(`installed system skill ${skill.name} at ${file}`)
|
||||
}))
|
||||
}
|
||||
@@ -300,10 +296,8 @@ async function discoverRoot(root: SkillRoot, ctx: Context): Promise<SkillDefinit
|
||||
}
|
||||
|
||||
async function parseSkillFile(path: string, directory: string, source: SkillSource, ctx: Context): Promise<SkillDefinition | undefined> {
|
||||
let raw: string
|
||||
try {
|
||||
raw = await readFile(path, 'utf8')
|
||||
} catch {
|
||||
const raw = await readSkillText(ctx, path)
|
||||
if (raw === undefined) {
|
||||
return undefined
|
||||
}
|
||||
const parsed = parseFrontmatter(raw)
|
||||
@@ -334,6 +328,63 @@ async function parseSkillFile(path: string, directory: string, source: SkillSour
|
||||
}
|
||||
}
|
||||
|
||||
function optionalFileSystem(ctx: Context): FileSystem | undefined {
|
||||
return ctx.get('fs')
|
||||
}
|
||||
|
||||
async function skillFileExists(ctx: Context, path: string): Promise<boolean> {
|
||||
const fs = optionalFileSystem(ctx)
|
||||
if (fs !== undefined) {
|
||||
const target = await fs.resolve(path)
|
||||
return await fs.stat(target) !== undefined
|
||||
}
|
||||
try {
|
||||
await access(path)
|
||||
return true
|
||||
} catch {
|
||||
// Expected first-run path: the bundled system skill has not been installed.
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function writeSkillText(ctx: Context, path: string, content: string): Promise<void> {
|
||||
const fs = optionalFileSystem(ctx)
|
||||
if (fs !== undefined) {
|
||||
await fs.writeText(await fs.resolve(path), content)
|
||||
return
|
||||
}
|
||||
await mkdir(dirname(path), { recursive: true })
|
||||
await writeFile(path, content)
|
||||
}
|
||||
|
||||
async function readSkillText(ctx: Context, path: string): Promise<string | undefined> {
|
||||
const fs = optionalFileSystem(ctx)
|
||||
if (fs !== undefined) {
|
||||
return await readSkillTextFromFileSystem(ctx, fs, path)
|
||||
}
|
||||
try {
|
||||
return await readFile(path, 'utf8')
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
async function readSkillTextFromFileSystem(ctx: Context, fs: FileSystem, path: string): Promise<string | undefined> {
|
||||
const target = await fs.resolve(path)
|
||||
const info = await fs.stat(target)
|
||||
if (info === undefined || info.type !== 'file') return undefined
|
||||
try {
|
||||
return await fs.readText(target)
|
||||
} catch (error) {
|
||||
ctx.logger.warn(`skill file ${path} ignored: ${fsReadErrorMessage(target, error)}`)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
function fsReadErrorMessage(target: FsTarget, error: unknown): string {
|
||||
return `failed to read text file at ${target.displayPath}: ${errorMessage(error)}`
|
||||
}
|
||||
|
||||
function parseFrontmatter(raw: string): { data: Record<string, unknown>; body: string } | undefined {
|
||||
if (!raw.startsWith('---\n')) return undefined
|
||||
const end = raw.indexOf('\n---', 4)
|
||||
|
||||
@@ -4,6 +4,7 @@ import { join } from 'node:path'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { Context } from 'cordis'
|
||||
import SkillService from '@deepseek-ai/dsh-skill'
|
||||
import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
|
||||
|
||||
async function tempDir(name: string): Promise<string> {
|
||||
return await import('node:fs/promises').then(fs => fs.mkdtemp(join(tmpdir(), `dsh-${name}-`)))
|
||||
@@ -194,6 +195,24 @@ describe('SkillService', () => {
|
||||
expect(await readFile(join(home, '.dsh/skills/.system/dsh-skill-creator/SKILL.md'), 'utf8')).toContain('dsh-skill-creator')
|
||||
})
|
||||
|
||||
it('uses the filesystem service when installing bundled system skills', async () => {
|
||||
const home = await tempDir('skill-install-fs')
|
||||
const existing = join(home, '.dsh/skills/.system/dsh-plugin-creator/SKILL.md')
|
||||
await mkdir(join(home, '.dsh/skills/.system/dsh-plugin-creator'), { recursive: true })
|
||||
await writeFile(existing, '---\nname: dsh-plugin-creator\ndescription: Existing system skill\n---\n\nExisting body.\n')
|
||||
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LocalFileSystem, { cwd: home })
|
||||
await ctx.plugin(SkillService, { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents') })
|
||||
|
||||
expect((await ctx.skills.list()).map(skill => [skill.name, skill.description])).toEqual([
|
||||
['dsh-plugin-creator', 'Existing system skill'],
|
||||
['dsh-skill-creator', 'Create or update DeepSeek Harness SKILL.md instructions.'],
|
||||
])
|
||||
expect(await readFile(existing, 'utf8')).toContain('Existing body.')
|
||||
expect(await readFile(join(home, '.dsh/skills/.system/dsh-skill-creator/SKILL.md'), 'utf8')).toContain('dsh-skill-creator')
|
||||
})
|
||||
|
||||
it('renders bundled system skill files with and without routing metadata', async () => {
|
||||
const home = await tempDir('skill-install-render')
|
||||
|
||||
@@ -205,6 +224,26 @@ describe('SkillService', () => {
|
||||
expect(await readFile(join(home, '.dsh/skills/.system/dsh-skill-creator/SKILL.md'), 'utf8')).toContain('whenToUse:')
|
||||
})
|
||||
|
||||
it('uses the filesystem service for skill file reads when it is available', async () => {
|
||||
const home = await tempDir('skill-read-fs')
|
||||
const root = join(home, '.dsh/skills')
|
||||
await writeFlatSkill(root, 'text-skill', 'Text skill', 'Text body.')
|
||||
await mkdir(join(root, 'empty-dir'), { recursive: true })
|
||||
await mkdir(join(root, 'directory-skill/SKILL.md'), { recursive: true })
|
||||
await writeFile(join(root, 'binary-skill.md'), Buffer.concat([
|
||||
Buffer.from('---\nname: binary-skill\ndescription: Binary skill\n---\n\n'),
|
||||
Buffer.from([0xff]),
|
||||
Buffer.from('\n'),
|
||||
]))
|
||||
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LocalFileSystem, { cwd: home })
|
||||
await ctx.plugin(SkillService, { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents'), installSystemSkills: false })
|
||||
|
||||
expect((await ctx.skills.list()).map(skill => skill.name)).toEqual(['text-skill'])
|
||||
expect(await ctx.skills.get('binary-skill')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('degrades when bundled system skill installation fails', async () => {
|
||||
const home = await tempDir('skill-install-fail')
|
||||
await writeFile(join(home, '.dsh'), 'not a directory')
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"references": [
|
||||
{ "path": "../../../vendor/cosmokit" },
|
||||
{ "path": "../../../vendor/cordis" },
|
||||
{ "path": "../../fs/fs" },
|
||||
{ "path": "../../llm/llm" },
|
||||
{ "path": "../agent" }
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user