@yeyuan98/opencode-bioresearcher-plugin 1.3.0-alpha.0 → 1.3.0

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/README.md CHANGED
@@ -75,6 +75,32 @@ Download pubmed article data from https://ftp.ncbi.nlm.nih.gov/pubmed/updatefile
75
75
 
76
76
  Reference: [PubMed Download Data](https://pubmed.ncbi.nlm.nih.gov/download/).
77
77
 
78
+ ## Skills
79
+
80
+ Skills are reusable prompt templates discovered from multiple paths:
81
+
82
+ | Path | Scope |
83
+ |------|-------|
84
+ | `.opencode/skills/` | Project |
85
+ | `~/.config/opencode/skills/` | Global |
86
+ | `.claude/skills/` | Claude Code compatible |
87
+ | `.agents/skills/` | Agents compatible |
88
+
89
+ This plugin provides a skill tool that overrides Opencode's built-in to support plugin-shipped skills.
90
+
91
+ See [skill-tools/README.md](skill-tools/README.md) for full documentation.
92
+
93
+ ### Supplied skills
94
+
95
+ - `demo-skill`: showcase skill tool mechanisms.
96
+ - `python-setup-uv`: setup python runtime in your working directory with uv.
97
+
98
+ Prompt the following and follow along:
99
+
100
+ ```txt
101
+ Setup python uv with skill
102
+ ```
103
+
78
104
  ## Installation
79
105
 
80
106
  Add the plugin to your `opencode.json`:
@@ -42,6 +42,9 @@ export const AGENT_TOOL_RESTRICTIONS = {
42
42
  "read",
43
43
  "write",
44
44
  "edit",
45
+ "question",
46
+ "todowrite",
47
+ "task"
45
48
  ]),
46
49
  bioresearcherDR_worker: createAllowlist([
47
50
  "biomcp*",
@@ -53,6 +56,7 @@ export const AGENT_TOOL_RESTRICTIONS = {
53
56
  "read",
54
57
  "write",
55
58
  "edit",
59
+ "todowrite"
56
60
  ]),
57
61
  };
58
62
  /**
@@ -1,3 +1,3 @@
1
1
  export { SkillTool } from "./tool";
2
- export { getAllSkills, getSkill, clearCache, SkillConflictError } from "./registry";
2
+ export { getAllSkills, getSkill, SkillConflictError } from "./registry";
3
3
  export type { ExtendedSkill, ExtendedSkillFrontmatter, ParsedSkill } from "./types";
@@ -1,2 +1,2 @@
1
1
  export { SkillTool } from "./tool";
2
- export { getAllSkills, getSkill, clearCache, SkillConflictError } from "./registry";
2
+ export { getAllSkills, getSkill, SkillConflictError } from "./registry";
@@ -6,6 +6,11 @@ export declare class SkillConflictError extends Error {
6
6
  constructor(skillName: string, pluginLocation: string, userLocation: string);
7
7
  }
8
8
  export declare function loadPluginSkills(): Promise<ExtendedSkill[]>;
9
- export declare function getAllSkills(): Promise<ExtendedSkill[]>;
10
- export declare function getSkill(name: string): Promise<ExtendedSkill | undefined>;
11
- export declare function clearCache(): void;
9
+ export declare function discoverOpencodeProjectSkills(directory?: string): Promise<ExtendedSkill[]>;
10
+ export declare function discoverOpencodeGlobalSkills(): Promise<ExtendedSkill[]>;
11
+ export declare function discoverClaudeProjectSkills(directory?: string): Promise<ExtendedSkill[]>;
12
+ export declare function discoverClaudeGlobalSkills(): Promise<ExtendedSkill[]>;
13
+ export declare function discoverAgentsProjectSkills(directory?: string): Promise<ExtendedSkill[]>;
14
+ export declare function discoverAgentsGlobalSkills(): Promise<ExtendedSkill[]>;
15
+ export declare function getAllSkills(directory?: string): Promise<ExtendedSkill[]>;
16
+ export declare function getSkill(name: string, directory?: string): Promise<ExtendedSkill | undefined>;
@@ -1,7 +1,19 @@
1
1
  import path from "path";
2
+ import { homedir } from "os";
3
+ import { existsSync } from "fs";
2
4
  import { fileURLToPath } from "url";
5
+ import { xdgConfig } from "xdg-basedir";
3
6
  import { parseSkillFrontmatter } from "./frontmatter";
4
7
  const SKILL_GLOB = new Bun.Glob("**/SKILL.md");
8
+ function getOpenCodeConfigDir() {
9
+ const envDir = process.env.OPENCODE_CONFIG_DIR;
10
+ if (envDir)
11
+ return envDir;
12
+ return path.join(xdgConfig, "opencode");
13
+ }
14
+ function getClaudeConfigDir() {
15
+ return process.env.CLAUDE_CONFIG_DIR || path.join(homedir(), ".claude");
16
+ }
5
17
  export class SkillConflictError extends Error {
6
18
  skillName;
7
19
  pluginLocation;
@@ -19,12 +31,13 @@ function getPluginSkillsDir() {
19
31
  const currentDir = path.dirname(fileURLToPath(import.meta.url));
20
32
  return path.join(currentDir, "..", "skills");
21
33
  }
22
- export async function loadPluginSkills() {
23
- const skillsDir = getPluginSkillsDir();
34
+ async function loadSkillsFromDir(dir, source) {
35
+ if (!existsSync(dir))
36
+ return [];
24
37
  const skills = [];
25
38
  try {
26
39
  for await (const match of SKILL_GLOB.scan({
27
- cwd: skillsDir,
40
+ cwd: dir,
28
41
  absolute: true,
29
42
  onlyFiles: true,
30
43
  })) {
@@ -38,27 +51,82 @@ export async function loadPluginSkills() {
38
51
  content: parsed.content,
39
52
  agent: parsed.frontmatter.agent,
40
53
  allowedTools: parsed.frontmatter.allowedTools,
41
- source: "plugin",
54
+ source,
42
55
  });
43
56
  }
44
57
  }
45
58
  catch {
46
- // Plugin skills directory may not exist
59
+ // Directory may not be readable
47
60
  }
48
61
  return skills;
49
62
  }
50
- let cachedSkills = null;
51
- export async function getAllSkills() {
52
- if (cachedSkills)
53
- return cachedSkills;
54
- const pluginSkills = await loadPluginSkills();
55
- cachedSkills = pluginSkills;
56
- return pluginSkills;
63
+ export async function loadPluginSkills() {
64
+ return loadSkillsFromDir(getPluginSkillsDir(), "plugin");
57
65
  }
58
- export async function getSkill(name) {
59
- const skills = await getAllSkills();
60
- return skills.find((s) => s.name === name);
66
+ export async function discoverOpencodeProjectSkills(directory) {
67
+ const dir = directory ?? process.cwd();
68
+ return loadSkillsFromDir(path.join(dir, ".opencode", "skills"), "opencode-project");
69
+ }
70
+ export async function discoverOpencodeGlobalSkills() {
71
+ return loadSkillsFromDir(path.join(getOpenCodeConfigDir(), "skills"), "opencode-global");
72
+ }
73
+ export async function discoverClaudeProjectSkills(directory) {
74
+ const dir = directory ?? process.cwd();
75
+ return loadSkillsFromDir(path.join(dir, ".claude", "skills"), "claude-project");
61
76
  }
62
- export function clearCache() {
63
- cachedSkills = null;
77
+ export async function discoverClaudeGlobalSkills() {
78
+ return loadSkillsFromDir(path.join(getClaudeConfigDir(), "skills"), "claude-global");
79
+ }
80
+ export async function discoverAgentsProjectSkills(directory) {
81
+ const dir = directory ?? process.cwd();
82
+ return loadSkillsFromDir(path.join(dir, ".agents", "skills"), "agents-project");
83
+ }
84
+ export async function discoverAgentsGlobalSkills() {
85
+ return loadSkillsFromDir(path.join(homedir(), ".agents", "skills"), "agents-global");
86
+ }
87
+ export async function getAllSkills(directory) {
88
+ const dir = directory ?? process.cwd();
89
+ const [opencodeProject, opencodeGlobal, claudeProject, claudeGlobal, agentsProject, agentsGlobal, plugin] = await Promise.all([
90
+ discoverOpencodeProjectSkills(dir),
91
+ discoverOpencodeGlobalSkills(),
92
+ discoverClaudeProjectSkills(dir),
93
+ discoverClaudeGlobalSkills(),
94
+ discoverAgentsProjectSkills(dir),
95
+ discoverAgentsGlobalSkills(),
96
+ loadPluginSkills(),
97
+ ]);
98
+ const userSkills = [
99
+ ...opencodeProject,
100
+ ...opencodeGlobal,
101
+ ...claudeProject,
102
+ ...claudeGlobal,
103
+ ...agentsProject,
104
+ ...agentsGlobal,
105
+ ];
106
+ const pluginNames = new Set(plugin.map((s) => s.name));
107
+ const conflicts = userSkills.filter((s) => pluginNames.has(s.name));
108
+ if (conflicts.length > 0) {
109
+ throw new SkillConflictError(conflicts[0].name, "", conflicts[0].location);
110
+ }
111
+ const seen = new Set();
112
+ return [
113
+ ...opencodeProject,
114
+ ...opencodeGlobal,
115
+ ...claudeProject,
116
+ ...claudeGlobal,
117
+ ...agentsProject,
118
+ ...agentsGlobal,
119
+ ...plugin,
120
+ ].filter((skill) => {
121
+ if (seen.has(skill.name)) {
122
+ console.warn(`[skill] Duplicate skill "${skill.name}" - using higher priority version`);
123
+ return false;
124
+ }
125
+ seen.add(skill.name);
126
+ return true;
127
+ });
128
+ }
129
+ export async function getSkill(name, directory) {
130
+ const skills = await getAllSkills(directory);
131
+ return skills.find((s) => s.name === name);
64
132
  }
@@ -53,8 +53,8 @@ export const SkillTool = tool({
53
53
  name: tool.schema.string().describe("The name of the skill from available_skills"),
54
54
  },
55
55
  async execute(params, ctx) {
56
- const skills = await getAllSkills();
57
- const skill = await getSkill(params.name);
56
+ const skills = await getAllSkills(ctx.directory);
57
+ const skill = await getSkill(params.name, ctx.directory);
58
58
  if (!skill) {
59
59
  const available = skills.map((s) => s.name).join(", ");
60
60
  throw new Error(`Skill "${params.name}" not found. Available skills: ${available || "none"}`);
@@ -6,6 +6,7 @@ export declare const ExtendedSkillFrontmatter: z.ZodObject<{
6
6
  allowedTools: z.ZodOptional<z.ZodArray<z.ZodString>>;
7
7
  }, z.core.$strip>;
8
8
  export type ExtendedSkillFrontmatter = z.infer<typeof ExtendedSkillFrontmatter>;
9
+ export type SkillSource = "plugin" | "opencode-project" | "opencode-global" | "claude-project" | "claude-global" | "agents-project" | "agents-global";
9
10
  export interface ExtendedSkill {
10
11
  name: string;
11
12
  description: string;
@@ -13,7 +14,7 @@ export interface ExtendedSkill {
13
14
  content: string;
14
15
  agent?: string;
15
16
  allowedTools?: string[];
16
- source: "plugin";
17
+ source: SkillSource;
17
18
  }
18
19
  export interface ParsedSkill {
19
20
  frontmatter: ExtendedSkillFrontmatter;
@@ -21,7 +21,7 @@ This skill demonstrates the plugin skill integration system.
21
21
  Run the bundled script to verify resources are properly resolved:
22
22
 
23
23
  ```bash
24
- python3 demo_script.py
24
+ python demo_script.py
25
25
  ```
26
26
 
27
27
  Expected output:
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env python3
1
+ #!/usr/bin/env python
2
2
  """Demo script to verify skill resource resolution."""
3
3
 
4
4
  import os
@@ -0,0 +1,141 @@
1
+ ---
2
+ name: python-setup-uv
3
+ description: Setup Python environment with uv package manager - download binaries, create symlink, and install packages
4
+ allowedTools:
5
+ - Bash
6
+ - Read
7
+ ---
8
+
9
+ # Python Environment Setup with uv
10
+
11
+ This skill sets up a Python environment using the uv package manager.
12
+
13
+ ## Prerequisites
14
+ - Internet connection for downloading uv
15
+ - Python 3.8+ should be available on PATH (or uv will prompt to install it)
16
+
17
+ ## Steps
18
+
19
+ **ABSOLUTE RULE:** Follow steps below EXACTLY AS IS. Do NOT skip/modify steps (nor detailed subtasks in each step) nor assume anything based on user platform information. Use URLs below EXACTLY AS IS. Follow steps below INCLUDING ALL DETAILS AND SUBSTEPS EXACTLY AS IS.
20
+
21
+ ### Step 1: Ask user question about which installer to use
22
+
23
+ Use the question tool to ask which installer should be used:
24
+
25
+ - Official astral-uv installer (https://astral.sh)
26
+ - China mainland uv-custom installer (https://gitee.com/wangnov/uv-custom)
27
+
28
+ ### Step 2: Detect Shell and Download uv Binary
29
+
30
+ First, detect your shell environment:
31
+
32
+ ```bash
33
+ # Detect shell type
34
+ # MSYSTEM is set by Git Bash, MINGW_PREFIX by MSYS2
35
+ if [ -n "$MSYSTEM" ] || [ -n "$MINGW_PREFIX" ] || command -v curl >/dev/null 2>&1; then
36
+ echo "Unix-like shell detected (Git Bash, bash, zsh, etc.)"
37
+ IS_UNIX_SHELL=true
38
+ else
39
+ echo "Windows cmd.exe detected"
40
+ IS_UNIX_SHELL=false
41
+ fi
42
+ ```
43
+
44
+ Then download uv based on your shell (see below).
45
+
46
+ Choose the correct `UV_INSTALLER_URL` depending on answer you received in Step 1 from the user:
47
+
48
+ - If opted "Official astral-uv", UV_INSTALLER_URL should be `https://astral.sh/uv/install.sh` (Unix-like) or `https://astral.sh/uv/install.ps1` (Windows)
49
+ - If opted "China mainland uv-custom", UV_INSTALLER_URL should be `https://gitee.com/wangnov/uv-custom/releases/download/latest/uv-installer-custom.sh` (Unix-like) or `https://gitee.com/wangnov/uv-custom/releases/download/latest/uv-installer-custom.ps1` (Windows)
50
+
51
+ **For Unix-like shells (Git Bash / macOS / Linux; use correct UV_INSTALLER_URL):**
52
+ ```bash
53
+ mkdir -p .uv
54
+ curl -LsSf UV_INSTALLER_URL | UV_INSTALL_DIR="$(pwd)/.uv" sh
55
+ ```
56
+
57
+ **For Windows cmd.exe (if Git Bash unavailable; use correct UV_INSTALLER_URL):**
58
+ ```bash
59
+ powershell -NoProfile -Command "New-Item -ItemType Directory -Force -Path .uv | Out-Null; $env:UV_INSTALL_DIR = (Get-Location).Path + '\.uv'; Invoke-RestMethod UV_INSTALLER_URL | Invoke-Expression"
60
+ ```
61
+
62
+ ### Step 3: Create Symlink or Copy uv to Working Directory
63
+
64
+ **For Unix-like shells (Git Bash / macOS / Linux):**
65
+ ```bash
66
+ ln -sf .uv/uv uv
67
+ ```
68
+
69
+ **For Windows cmd.exe:**
70
+
71
+ Try symlink first, fall back to copy if no Admin rights:
72
+ ```bash
73
+ cmd /c "(mklink uv .uv\uv.exe) 2>nul || copy /Y .uv\uv.exe uv.exe"
74
+ ```
75
+
76
+ ### Step 4: Create Virtual Environment and Install pandas
77
+
78
+ NOTE: this step (package installation) may timeout. If timed out, use the question tool to ask if the user would like to retry package installation. If successful, do NOT ask any question and continue to Step 5.
79
+
80
+ **For Unix-like shells:**
81
+ ```bash
82
+ ./uv venv
83
+ ./uv pip install pandas
84
+ ```
85
+
86
+ **For Windows cmd.exe:**
87
+ ```bash
88
+ uv.exe venv
89
+ uv.exe pip install pandas
90
+ ```
91
+
92
+ ### Step 5: Verification
93
+
94
+ **For Unix-like shells:**
95
+ ```bash
96
+ ./uv --version
97
+ ./uv run python -c "import pandas; print(pandas.__version__)"
98
+ ```
99
+
100
+ **For Windows cmd.exe:**
101
+ ```bash
102
+ uv.exe --version
103
+ uv.exe run python -c "import pandas; print(pandas.__version__)"
104
+ ```
105
+
106
+ ### Step 6: Update AGENTS.md
107
+
108
+ Use the question tool to ask if the user want to update AGENTS.md in WORKING DIRECTORY to "direct agents to use the installed UV Python" (options: "Yes" / "No"). If received no answer, continue to Step 7 (do NOT modify AGENTS.md NOR create directories). If received yes answer, follow steps belows.
109
+
110
+ 1. If AGENTS.md not found in WORKING DIR, create an empty AGENTS.md.
111
+ 2. Inspect AGENTS.md content. If you do not see the content block below, APPEND EXACTLY AS IS to end of AGENTS.md.
112
+ 3. Check if `./.code/py` exist. If not, create the directories.
113
+
114
+ Content block:
115
+
116
+ ```md
117
+ ## Important note about Python
118
+
119
+ ALWAYS use the uv package manager available in WORKING DIRECTORY, including `uv add ...` or `uv pip ...` for package management and `uv run ...` to run python package executables.
120
+
121
+ ALWAYS save python scripts under path `./.code/py/` and run the script with `uv run python ...` whenever your work involves executing python scripts. Your script MUST contain concise docstrings and comments and use good engineering practices including separation of concerns.
122
+ ```
123
+
124
+ ### Step 7: Return summary to user (Usage After Setup)
125
+
126
+ **For Unix-like shells:**
127
+ ```bash
128
+ ./uv run python your_script.py
129
+ ```
130
+
131
+ **For Windows cmd.exe:**
132
+ ```bash
133
+ uv.exe run python your_script.py
134
+ ```
135
+
136
+ ## Notes
137
+ - Add `.uv/` and `.venv/` to `.gitignore`
138
+ - `uv run` handles venv activation automatically
139
+ - Use `./uv add <package>` (Unix) or `uv.exe add <package>` (Windows cmd.exe) for project dependencies
140
+ - Windows with Git Bash: Follow Unix-like shell instructions
141
+ - Windows cmd.exe without Admin rights: `uv.exe` is copied instead of symlinked
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeyuan98/opencode-bioresearcher-plugin",
3
- "version": "1.3.0-alpha.0",
3
+ "version": "1.3.0",
4
4
  "description": "OpenCode plugin that adds a bioresearcher agent",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -32,6 +32,7 @@
32
32
  "dependencies": {
33
33
  "@opencode-ai/plugin": "^1.2.6",
34
34
  "fast-xml-parser": "^5.3.5",
35
+ "xdg-basedir": "^5.1.0",
35
36
  "xlsx": "^0.18.5",
36
37
  "zod": "^4.1.8"
37
38
  },