mirror of
https://github.com/sst/opencode.git
synced 2025-12-23 10:11:41 +00:00
Merge branch 'dev' into polish-desktop-styles-and-ui-updates
This commit is contained in:
commit
721a810b2e
24 changed files with 518 additions and 46 deletions
30
bun.lock
30
bun.lock
|
|
@ -21,7 +21,7 @@
|
|||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.0.184",
|
||||
"version": "1.0.185",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
|
|
@ -49,7 +49,7 @@
|
|||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.0.184",
|
||||
"version": "1.0.185",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
|
|
@ -76,7 +76,7 @@
|
|||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.0.184",
|
||||
"version": "1.0.185",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "2.0.0",
|
||||
"@ai-sdk/openai": "2.0.2",
|
||||
|
|
@ -100,7 +100,7 @@
|
|||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.0.184",
|
||||
"version": "1.0.185",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
|
@ -124,7 +124,7 @@
|
|||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.0.184",
|
||||
"version": "1.0.185",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
|
|
@ -172,7 +172,7 @@
|
|||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.0.184",
|
||||
"version": "1.0.185",
|
||||
"dependencies": {
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
|
|
@ -201,7 +201,7 @@
|
|||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.0.184",
|
||||
"version": "1.0.185",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
|
|
@ -217,7 +217,7 @@
|
|||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.0.184",
|
||||
"version": "1.0.185",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
|
|
@ -309,7 +309,7 @@
|
|||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.0.184",
|
||||
"version": "1.0.185",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"zod": "catalog:",
|
||||
|
|
@ -329,7 +329,7 @@
|
|||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.0.184",
|
||||
"version": "1.0.185",
|
||||
"devDependencies": {
|
||||
"@hey-api/openapi-ts": "0.88.1",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
|
|
@ -340,7 +340,7 @@
|
|||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.0.184",
|
||||
"version": "1.0.185",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
|
|
@ -353,7 +353,7 @@
|
|||
},
|
||||
"packages/tauri": {
|
||||
"name": "@opencode-ai/tauri",
|
||||
"version": "1.0.184",
|
||||
"version": "1.0.185",
|
||||
"dependencies": {
|
||||
"@opencode-ai/desktop": "workspace:*",
|
||||
"@solid-primitives/storage": "catalog:",
|
||||
|
|
@ -380,7 +380,7 @@
|
|||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.0.184",
|
||||
"version": "1.0.185",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
|
|
@ -415,7 +415,7 @@
|
|||
},
|
||||
"packages/util": {
|
||||
"name": "@opencode-ai/util",
|
||||
"version": "1.0.184",
|
||||
"version": "1.0.185",
|
||||
"dependencies": {
|
||||
"zod": "catalog:",
|
||||
},
|
||||
|
|
@ -426,7 +426,7 @@
|
|||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.0.184",
|
||||
"version": "1.0.185",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.0.184",
|
||||
"version": "1.0.185",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.0.184",
|
||||
"version": "1.0.185",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.0.184",
|
||||
"version": "1.0.185",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.0.184",
|
||||
"version": "1.0.185",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.0.184",
|
||||
"version": "1.0.185",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.0.184",
|
||||
"version": "1.0.185",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
id = "opencode"
|
||||
name = "OpenCode"
|
||||
description = "The open source coding agent."
|
||||
version = "1.0.184"
|
||||
version = "1.0.185"
|
||||
schema_version = 1
|
||||
authors = ["Anomaly"]
|
||||
repository = "https://github.com/sst/opencode"
|
||||
|
|
@ -11,26 +11,26 @@ name = "OpenCode"
|
|||
icon = "./icons/opencode.svg"
|
||||
|
||||
[agent_servers.opencode.targets.darwin-aarch64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.184/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.185/opencode-darwin-arm64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.darwin-x86_64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.184/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.185/opencode-darwin-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-aarch64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.184/opencode-linux-arm64.tar.gz"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.185/opencode-linux-arm64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-x86_64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.184/opencode-linux-x64.tar.gz"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.185/opencode-linux-x64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.windows-x86_64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.184/opencode-windows-x64.zip"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.185/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.0.184",
|
||||
"version": "1.0.185",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.0.184",
|
||||
"version": "1.0.185",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
|
|
|
|||
|
|
@ -697,7 +697,7 @@ export namespace LSPServer {
|
|||
})
|
||||
if (!ok) return
|
||||
} else {
|
||||
await $`tar -xf ${tempPath}`.cwd(Global.Path.bin).nothrow()
|
||||
await $`tar -xf ${tempPath}`.cwd(Global.Path.bin).quiet().nothrow()
|
||||
}
|
||||
|
||||
await fs.rm(tempPath, { force: true })
|
||||
|
|
@ -710,7 +710,7 @@ export namespace LSPServer {
|
|||
}
|
||||
|
||||
if (platform !== "win32") {
|
||||
await $`chmod +x ${bin}`.nothrow()
|
||||
await $`chmod +x ${bin}`.quiet().nothrow()
|
||||
}
|
||||
|
||||
log.info(`installed zls`, { bin })
|
||||
|
|
@ -1003,7 +1003,7 @@ export namespace LSPServer {
|
|||
if (!ok) return
|
||||
}
|
||||
if (tar) {
|
||||
await $`tar -xf ${archive}`.cwd(Global.Path.bin).nothrow()
|
||||
await $`tar -xf ${archive}`.cwd(Global.Path.bin).quiet().nothrow()
|
||||
}
|
||||
await fs.rm(archive, { force: true })
|
||||
|
||||
|
|
@ -1014,7 +1014,7 @@ export namespace LSPServer {
|
|||
}
|
||||
|
||||
if (platform !== "win32") {
|
||||
await $`chmod +x ${bin}`.nothrow()
|
||||
await $`chmod +x ${bin}`.quiet().nothrow()
|
||||
}
|
||||
|
||||
await fs.unlink(path.join(Global.Path.bin, "clangd")).catch(() => {})
|
||||
|
|
@ -1580,7 +1580,7 @@ export namespace LSPServer {
|
|||
}
|
||||
|
||||
if (platform !== "win32") {
|
||||
await $`chmod +x ${bin}`.nothrow()
|
||||
await $`chmod +x ${bin}`.quiet().nothrow()
|
||||
}
|
||||
|
||||
log.info(`installed terraform-ls`, { bin })
|
||||
|
|
@ -1663,7 +1663,7 @@ export namespace LSPServer {
|
|||
if (!ok) return
|
||||
}
|
||||
if (ext === "tar.gz") {
|
||||
await $`tar -xzf ${tempPath}`.cwd(Global.Path.bin).nothrow()
|
||||
await $`tar -xzf ${tempPath}`.cwd(Global.Path.bin).quiet().nothrow()
|
||||
}
|
||||
|
||||
await fs.rm(tempPath, { force: true })
|
||||
|
|
@ -1676,7 +1676,7 @@ export namespace LSPServer {
|
|||
}
|
||||
|
||||
if (platform !== "win32") {
|
||||
await $`chmod +x ${bin}`.nothrow()
|
||||
await $`chmod +x ${bin}`.quiet().nothrow()
|
||||
}
|
||||
|
||||
log.info("installed texlab", { bin })
|
||||
|
|
|
|||
|
|
@ -532,7 +532,11 @@ export namespace SessionPrompt {
|
|||
agent,
|
||||
abort,
|
||||
sessionID,
|
||||
system: [...(await SystemPrompt.environment()), ...(await SystemPrompt.custom())],
|
||||
system: [
|
||||
...(await SystemPrompt.environment()),
|
||||
...(await SystemPrompt.skills()),
|
||||
...(await SystemPrompt.custom()),
|
||||
],
|
||||
messages: [
|
||||
...MessageV2.toModelMessage(sessionMessages),
|
||||
...(isLastStep
|
||||
|
|
@ -1309,7 +1313,7 @@ export namespace SessionPrompt {
|
|||
const results = await Promise.all(
|
||||
shell.map(async ([, cmd]) => {
|
||||
try {
|
||||
return await $`${{ raw: cmd }}`.nothrow().text()
|
||||
return await $`${{ raw: cmd }}`.quiet().nothrow().text()
|
||||
} catch (error) {
|
||||
return `Error executing command: ${error instanceof Error ? error.message : String(error)}`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { Ripgrep } from "../file/ripgrep"
|
|||
import { Global } from "../global"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
import { Config } from "../config/config"
|
||||
import { Skill } from "../skill"
|
||||
|
||||
import { Instance } from "../project/instance"
|
||||
import path from "path"
|
||||
|
|
@ -117,4 +118,25 @@ export namespace SystemPrompt {
|
|||
)
|
||||
return Promise.all(found).then((result) => result.filter(Boolean))
|
||||
}
|
||||
|
||||
export async function skills() {
|
||||
const all = await Skill.all()
|
||||
if (all.length === 0) return []
|
||||
|
||||
const lines = [
|
||||
"You have access to skills listed in `<available_skills>`. When a task matches a skill's description, read its SKILL.md file to get detailed instructions.",
|
||||
"",
|
||||
"<available_skills>",
|
||||
]
|
||||
for (const skill of all) {
|
||||
lines.push(" <skill>")
|
||||
lines.push(` <name>${skill.name}</name>`)
|
||||
lines.push(` <description>${skill.description}</description>`)
|
||||
lines.push(` <location>${skill.location}</location>`)
|
||||
lines.push(" </skill>")
|
||||
}
|
||||
lines.push("</available_skills>")
|
||||
|
||||
return [lines.join("\n")]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
1
packages/opencode/src/skill/index.ts
Normal file
1
packages/opencode/src/skill/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "./skill"
|
||||
154
packages/opencode/src/skill/skill.ts
Normal file
154
packages/opencode/src/skill/skill.ts
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
import path from "path"
|
||||
import z from "zod"
|
||||
import { Config } from "../config/config"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
import { Instance } from "../project/instance"
|
||||
import { NamedError } from "@opencode-ai/util/error"
|
||||
import { ConfigMarkdown } from "../config/markdown"
|
||||
import { Log } from "../util/log"
|
||||
|
||||
export namespace Skill {
|
||||
const log = Log.create({ service: "skill" })
|
||||
|
||||
// Name: 1-64 chars, lowercase alphanumeric and hyphens, no consecutive hyphens, can't start/end with hyphen
|
||||
const NAME_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/
|
||||
|
||||
export const Frontmatter = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(64)
|
||||
.refine((val) => NAME_REGEX.test(val), {
|
||||
message:
|
||||
"Name must be lowercase alphanumeric with hyphens, no consecutive hyphens, cannot start or end with hyphen",
|
||||
}),
|
||||
description: z.string().min(1).max(1024),
|
||||
license: z.string().optional(),
|
||||
compatibility: z.string().max(500).optional(),
|
||||
metadata: z.record(z.string(), z.string()).optional(),
|
||||
})
|
||||
|
||||
export type Frontmatter = z.infer<typeof Frontmatter>
|
||||
|
||||
export interface Info {
|
||||
name: string
|
||||
description: string
|
||||
location: string
|
||||
license?: string
|
||||
compatibility?: string
|
||||
metadata?: Record<string, string>
|
||||
}
|
||||
|
||||
export const InvalidError = NamedError.create(
|
||||
"SkillInvalidError",
|
||||
z.object({
|
||||
path: z.string(),
|
||||
message: z.string().optional(),
|
||||
issues: z.custom<z.core.$ZodIssue[]>().optional(),
|
||||
}),
|
||||
)
|
||||
|
||||
export const NameMismatchError = NamedError.create(
|
||||
"SkillNameMismatchError",
|
||||
z.object({
|
||||
path: z.string(),
|
||||
expected: z.string(),
|
||||
actual: z.string(),
|
||||
}),
|
||||
)
|
||||
|
||||
const SKILL_GLOB = new Bun.Glob("skill/*/SKILL.md")
|
||||
const CLAUDE_SKILL_GLOB = new Bun.Glob("*/SKILL.md")
|
||||
|
||||
async function discover(): Promise<string[]> {
|
||||
const directories = await Config.directories()
|
||||
|
||||
const paths: string[] = []
|
||||
|
||||
// Scan skill/ subdirectory in config directories (.opencode/, ~/.opencode/, etc.)
|
||||
for (const dir of directories) {
|
||||
for await (const match of SKILL_GLOB.scan({
|
||||
cwd: dir,
|
||||
absolute: true,
|
||||
onlyFiles: true,
|
||||
followSymlinks: true,
|
||||
})) {
|
||||
paths.push(match)
|
||||
}
|
||||
}
|
||||
|
||||
// Also scan .claude/skills/ walking up from cwd to worktree
|
||||
for await (const dir of Filesystem.up({
|
||||
targets: [".claude/skills"],
|
||||
start: Instance.directory,
|
||||
stop: Instance.worktree,
|
||||
})) {
|
||||
for await (const match of CLAUDE_SKILL_GLOB.scan({
|
||||
cwd: dir,
|
||||
absolute: true,
|
||||
onlyFiles: true,
|
||||
followSymlinks: true,
|
||||
})) {
|
||||
paths.push(match)
|
||||
}
|
||||
}
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
async function load(skillMdPath: string): Promise<Info> {
|
||||
const md = await ConfigMarkdown.parse(skillMdPath)
|
||||
if (!md.data) {
|
||||
throw new InvalidError({
|
||||
path: skillMdPath,
|
||||
message: "SKILL.md must have YAML frontmatter",
|
||||
})
|
||||
}
|
||||
|
||||
const parsed = Frontmatter.safeParse(md.data)
|
||||
if (!parsed.success) {
|
||||
throw new InvalidError({
|
||||
path: skillMdPath,
|
||||
issues: parsed.error.issues,
|
||||
})
|
||||
}
|
||||
|
||||
const frontmatter = parsed.data
|
||||
const skillDir = path.dirname(skillMdPath)
|
||||
const dirName = path.basename(skillDir)
|
||||
|
||||
if (frontmatter.name !== dirName) {
|
||||
throw new NameMismatchError({
|
||||
path: skillMdPath,
|
||||
expected: dirName,
|
||||
actual: frontmatter.name,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
name: frontmatter.name,
|
||||
description: frontmatter.description,
|
||||
location: skillMdPath,
|
||||
license: frontmatter.license,
|
||||
compatibility: frontmatter.compatibility,
|
||||
metadata: frontmatter.metadata,
|
||||
}
|
||||
}
|
||||
|
||||
export const state = Instance.state(async () => {
|
||||
const paths = await discover()
|
||||
const skills: Info[] = []
|
||||
|
||||
for (const skillPath of paths) {
|
||||
const info = await load(skillPath)
|
||||
log.info("loaded skill", { name: info.name, location: info.location })
|
||||
skills.push(info)
|
||||
}
|
||||
|
||||
return skills
|
||||
})
|
||||
|
||||
export async function all(): Promise<Info[]> {
|
||||
return state()
|
||||
}
|
||||
}
|
||||
291
packages/opencode/test/skill/skill.test.ts
Normal file
291
packages/opencode/test/skill/skill.test.ts
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
import { test, expect } from "bun:test"
|
||||
import { Skill } from "../../src/skill"
|
||||
import { SystemPrompt } from "../../src/session/system"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import path from "path"
|
||||
|
||||
test("discovers skills from .opencode/skill/ directory", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
git: true,
|
||||
init: async (dir) => {
|
||||
const skillDir = path.join(dir, ".opencode", "skill", "test-skill")
|
||||
await Bun.write(
|
||||
path.join(skillDir, "SKILL.md"),
|
||||
`---
|
||||
name: test-skill
|
||||
description: A test skill for verification.
|
||||
---
|
||||
|
||||
# Test Skill
|
||||
|
||||
Instructions here.
|
||||
`,
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const skills = await Skill.all()
|
||||
expect(skills.length).toBe(1)
|
||||
expect(skills[0].name).toBe("test-skill")
|
||||
expect(skills[0].description).toBe("A test skill for verification.")
|
||||
expect(skills[0].location).toContain("skill/test-skill/SKILL.md")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("discovers multiple skills from .opencode/skill/ directory", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
git: true,
|
||||
init: async (dir) => {
|
||||
const skillDir = path.join(dir, ".opencode", "skill", "my-skill")
|
||||
await Bun.write(
|
||||
path.join(skillDir, "SKILL.md"),
|
||||
`---
|
||||
name: my-skill
|
||||
description: Another test skill.
|
||||
---
|
||||
|
||||
# My Skill
|
||||
`,
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const skills = await Skill.all()
|
||||
expect(skills.length).toBe(1)
|
||||
expect(skills[0].name).toBe("my-skill")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("throws error for invalid skill name format", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
git: true,
|
||||
init: async (dir) => {
|
||||
const skillDir = path.join(dir, ".opencode", "skill", "InvalidName")
|
||||
await Bun.write(
|
||||
path.join(skillDir, "SKILL.md"),
|
||||
`---
|
||||
name: InvalidName
|
||||
description: A skill with invalid name.
|
||||
---
|
||||
`,
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await expect(Skill.all()).rejects.toThrow()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("throws error when name doesn't match directory", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
git: true,
|
||||
init: async (dir) => {
|
||||
const skillDir = path.join(dir, ".opencode", "skill", "dir-name")
|
||||
await Bun.write(
|
||||
path.join(skillDir, "SKILL.md"),
|
||||
`---
|
||||
name: different-name
|
||||
description: A skill with mismatched name.
|
||||
---
|
||||
`,
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await expect(Skill.all()).rejects.toThrow("SkillNameMismatchError")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("throws error for missing frontmatter", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
git: true,
|
||||
init: async (dir) => {
|
||||
const skillDir = path.join(dir, ".opencode", "skill", "no-frontmatter")
|
||||
await Bun.write(
|
||||
path.join(skillDir, "SKILL.md"),
|
||||
`# No Frontmatter
|
||||
|
||||
Just some content without YAML frontmatter.
|
||||
`,
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await expect(Skill.all()).rejects.toThrow("SkillInvalidError")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("parses optional fields", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
git: true,
|
||||
init: async (dir) => {
|
||||
const skillDir = path.join(dir, ".opencode", "skill", "full-skill")
|
||||
await Bun.write(
|
||||
path.join(skillDir, "SKILL.md"),
|
||||
`---
|
||||
name: full-skill
|
||||
description: A skill with all optional fields.
|
||||
license: MIT
|
||||
compatibility: Requires Node.js 18+
|
||||
metadata:
|
||||
author: test-author
|
||||
version: "1.0"
|
||||
---
|
||||
|
||||
# Full Skill
|
||||
`,
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const skills = await Skill.all()
|
||||
expect(skills.length).toBe(1)
|
||||
expect(skills[0].license).toBe("MIT")
|
||||
expect(skills[0].compatibility).toBe("Requires Node.js 18+")
|
||||
expect(skills[0].metadata).toEqual({
|
||||
author: "test-author",
|
||||
version: "1.0",
|
||||
})
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("ignores unknown frontmatter fields", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
git: true,
|
||||
init: async (dir) => {
|
||||
const skillDir = path.join(dir, ".opencode", "skill", "extra-fields")
|
||||
await Bun.write(
|
||||
path.join(skillDir, "SKILL.md"),
|
||||
`---
|
||||
name: extra-fields
|
||||
description: A skill with extra unknown fields.
|
||||
allowed-tools: Bash Read Write
|
||||
some-other-field: ignored
|
||||
---
|
||||
|
||||
# Extra Fields Skill
|
||||
`,
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const skills = await Skill.all()
|
||||
expect(skills.length).toBe(1)
|
||||
expect(skills[0].name).toBe("extra-fields")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("returns empty array when no skills exist", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const skills = await Skill.all()
|
||||
expect(skills).toEqual([])
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("SystemPrompt.skills() returns empty array when no skills", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const result = await SystemPrompt.skills()
|
||||
expect(result).toEqual([])
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("SystemPrompt.skills() returns XML block with skills", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
git: true,
|
||||
init: async (dir) => {
|
||||
const skillDir = path.join(dir, ".opencode", "skill", "example-skill")
|
||||
await Bun.write(
|
||||
path.join(skillDir, "SKILL.md"),
|
||||
`---
|
||||
name: example-skill
|
||||
description: An example skill for testing XML output.
|
||||
---
|
||||
|
||||
# Example
|
||||
`,
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const result = await SystemPrompt.skills()
|
||||
expect(result.length).toBe(1)
|
||||
expect(result[0]).toContain("<available_skills>")
|
||||
expect(result[0]).toContain("<name>example-skill</name>")
|
||||
expect(result[0]).toContain("<description>An example skill for testing XML output.</description>")
|
||||
expect(result[0]).toContain("SKILL.md</location>")
|
||||
expect(result[0]).toContain("</available_skills>")
|
||||
expect(result[0]).toContain("When a task matches a skill's description")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("discovers skills from .claude/skills/ directory", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
git: true,
|
||||
init: async (dir) => {
|
||||
const skillDir = path.join(dir, ".claude", "skills", "claude-skill")
|
||||
await Bun.write(
|
||||
path.join(skillDir, "SKILL.md"),
|
||||
`---
|
||||
name: claude-skill
|
||||
description: A skill in the .claude/skills directory.
|
||||
---
|
||||
|
||||
# Claude Skill
|
||||
`,
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const skills = await Skill.all()
|
||||
expect(skills.length).toBe(1)
|
||||
expect(skills[0].name).toBe("claude-skill")
|
||||
expect(skills[0].location).toContain(".claude/skills/claude-skill/SKILL.md")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.0.184",
|
||||
"version": "1.0.185",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.0.184",
|
||||
"version": "1.0.185",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.0.184",
|
||||
"version": "1.0.185",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "bun run src/index.ts",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@opencode-ai/tauri",
|
||||
"private": true,
|
||||
"version": "1.0.184",
|
||||
"version": "1.0.185",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo -b",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.0.184",
|
||||
"version": "1.0.185",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./*": "./src/components/*.tsx",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/util",
|
||||
"version": "1.0.184",
|
||||
"version": "1.0.185",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@opencode-ai/web",
|
||||
"type": "module",
|
||||
"version": "1.0.184",
|
||||
"version": "1.0.185",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"name": "opencode",
|
||||
"displayName": "opencode",
|
||||
"description": "opencode for VS Code",
|
||||
"version": "1.0.184",
|
||||
"version": "1.0.185",
|
||||
"publisher": "sst-dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue