Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
ff85d28
feat(skills): add shared editable skills with V2 bridge and GUI
EyJunge1 Oct 12, 2026
83885e3
fix(web): reuse FormField/Textarea for skill dialog labels
EyJunge1 Oct 12, 2026
d5fd2a2
fix(web): remove skills bridge hint from skills view
EyJunge1 Oct 12, 2026
6f2a228
fix(web): align skills empty state with memory explorer
EyJunge1 Oct 12, 2026
a6dd60f
fix(web): rename skills section heading to Skills
EyJunge1 Oct 12, 2026
303fb4d
fix(web): show lucide icon on skills empty state
EyJunge1 Oct 12, 2026
c5f0c2b
fix(web): show skills Total chip in page header
EyJunge1 Oct 12, 2026
0046369
fix(web): move skills UI to /skill-library to avoid catalog clash
EyJunge1 Oct 12, 2026
562490e
docs: note skills UI path vs HTTP catalog prefix
EyJunge1 Oct 12, 2026
3789f96
fix(web): keep skills section label without Shared prefix
EyJunge1 Oct 12, 2026
a6cf23f
fix(web): use icon-only add button on skills page
EyJunge1 Oct 12, 2026
d6f4d4e
feat(web): add client-side skills search bar
EyJunge1 Oct 12, 2026
849e95a
fix(web): use scroll-text icon for skills
EyJunge1 Oct 12, 2026
80a1844
fix(web): replace Sparkles icon with ScrollText in skills components
EyJunge1 Oct 12, 2026
469afb8
feat(web): implement search functionality and enhance skills display …
EyJunge1 Oct 12, 2026
f8cf085
refactor(web): streamline SkillDialog and SkillsView components, remo…
EyJunge1 Oct 12, 2026
1e9f7c1
refactor(web): replace Input with Textarea for skill description in S…
EyJunge1 Oct 12, 2026
25b1dba
refactor(web): update layout and styling in various components for im…
EyJunge1 Oct 12, 2026
0629761
refactor(web): remove duplicate skill content field in SkillDialog fo…
EyJunge1 Oct 12, 2026
6efa1bf
feat(web): add CRUD functionality for skills in memory simulation, in…
EyJunge1 Oct 12, 2026
ee6b708
feat(web): enhance SkillDialog with edit and preview modes for skill …
EyJunge1 Oct 12, 2026
7dcfeea
fix(skills): materialize SKILL.md and cover OpenCode skill.transform
EyJunge1 Oct 12, 2026
4a7cde9
fix(skills): ensure Skill.Info.path is correctly written to disk and …
EyJunge1 Oct 12, 2026
41b5184
fix(i18n): add skill strings for zh, ar, tr, and nl
EyJunge1 Oct 12, 2026
9420ecd
fix(test): assert absolute skill paths in a Windows-safe way
EyJunge1 Oct 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,15 +161,23 @@ You do **not** need to ask OpenCode to “remember” things for the plugin to w

Manual search/add/list still work even if auto-capture has no provider configured. Auto-capture and user profile learning need a provider that can return structured/tool-call output.

### Memory vs AGENTS.md / project docs
### Memory vs AGENTS.md vs Skills

| Store in **memory** | Store in **AGENTS.md** / static docs |
| -------------------------------------------------------------------- | ----------------------------------------------------------- |
| Project-specific decisions, bug patterns, “we tried X and it failed” | Stable rules and workflows that rarely change |
| User preferences discovered over sessions | Always-on coding conventions and process |
| Facts that should follow you across chats | Instructions every agent should see regardless of retrieval |
| Store in **memory** | Store in **AGENTS.md** / static docs | Store as **Skills** (web UI / `skills.db`) |
| -------------------------------------------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------- |
| Project-specific decisions, bug patterns, “we tried X and it failed” | Stable rules and workflows that rarely change | Reusable procedures the agent loads on demand |
| User preferences discovered over sessions | Always-on coding conventions and process | Team-editable skill packs shared via the same storage |
| Facts that should follow you across chats | Instructions every agent should see regardless of retrieval | OpenCode-native `skill` tool (V2 plugin bridge) |

Rule of thumb: if it is a lasting project instruction, put it in AGENTS.md; if it is context that grows from real work, let memory (or auto-capture) hold it.
Rule of thumb: lasting always-on project instructions → `AGENTS.md`; growing work context → memory; reusable on-demand procedures → **Skills**.

#### Shared Skills (V2)

- Create/edit skills in the web explorer (**Skills** tab at `/skill-library`).
- Persisted in `skills.db` under the plugin storage path (not as memory vectors).
- On OpenCode **V2**, the plugin registers enabled skills via `ctx.skill.transform` so the agent can load them with the native `skill` tool.
- Optional HTTP catalog for other OpenCode instances: `"skills": ["http://127.0.0.1:4747/skills/"]` (serves `/skills/index.json` and `/skills/<id>/<id>.md` — separate from the UI path).
- REST: `GET/POST /api/skills`, `GET/PUT/DELETE /api/skills/:id`.

### Intelligent prompt-based memory extraction

Expand Down
145 changes: 145 additions & 0 deletions src/services/api-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1669,3 +1669,148 @@ export async function handleRunTagMigrationBatch(
return { success: false, error: String(error) };
}
}

function skillToApi(skill: {
id: string;
name: string;
description: string;
content: string;
autoinvoke: boolean;
enabled: boolean;
containerTag: string | null;
createdAt: number;
updatedAt: number;
}) {
return {
id: skill.id,
name: skill.name,
description: skill.description,
content: skill.content,
autoinvoke: skill.autoinvoke,
enabled: skill.enabled,
containerTag: skill.containerTag,
createdAt: safeToISOString(skill.createdAt),
updatedAt: safeToISOString(skill.updatedAt),
};
}

export async function handleListSkills(options?: {
containerTag?: string;
enabledOnly?: boolean;
}): Promise<ApiResponse<{ items: ReturnType<typeof skillToApi>[] }>> {
try {
const { skillsManager } = await import("./skills/skills-manager.js");
const items = await skillsManager.list({
containerTag: options?.containerTag,
enabledOnly: options?.enabledOnly,
});
return { success: true, data: { items: items.map(skillToApi) } };
} catch (error) {
log("handleListSkills: error", { error: String(error) });
return { success: false, error: String(error) };
}
}

export async function handleGetSkill(
id: string
): Promise<ApiResponse<ReturnType<typeof skillToApi>>> {
try {
const { skillsManager } = await import("./skills/skills-manager.js");
const skill = await skillsManager.get(id);
if (!skill) return { success: false, error: `Skill not found: ${id}` };
return { success: true, data: skillToApi(skill) };
} catch (error) {
log("handleGetSkill: error", { error: String(error) });
return { success: false, error: String(error) };
}
}

export async function handleCreateSkill(body: {
id?: string;
name?: string;
description?: string;
content?: string;
autoinvoke?: boolean;
enabled?: boolean;
containerTag?: string | null;
}): Promise<ApiResponse<ReturnType<typeof skillToApi>>> {
try {
if (!body?.id || !body?.name || !body?.content) {
return { success: false, error: "id, name, and content are required" };
}
const { skillsManager } = await import("./skills/skills-manager.js");
const skill = await skillsManager.create({
id: body.id,
name: body.name,
description: body.description,
content: body.content,
autoinvoke: body.autoinvoke,
enabled: body.enabled,
containerTag: body.containerTag,
});
return { success: true, data: skillToApi(skill) };
} catch (error) {
log("handleCreateSkill: error", { error: String(error) });
return { success: false, error: String(error) };
}
}

export async function handleUpdateSkill(
id: string,
body: {
name?: string;
description?: string;
content?: string;
autoinvoke?: boolean;
enabled?: boolean;
containerTag?: string | null;
}
): Promise<ApiResponse<ReturnType<typeof skillToApi>>> {
try {
const { skillsManager } = await import("./skills/skills-manager.js");
const skill = await skillsManager.update(id, body);
return { success: true, data: skillToApi(skill) };
} catch (error) {
log("handleUpdateSkill: error", { error: String(error) });
return { success: false, error: String(error) };
}
}

export async function handleDeleteSkill(id: string): Promise<ApiResponse<{ deleted: boolean }>> {
try {
const { skillsManager } = await import("./skills/skills-manager.js");
const deleted = await skillsManager.delete(id);
if (!deleted) return { success: false, error: `Skill not found: ${id}` };
return { success: true, data: { deleted: true } };
} catch (error) {
log("handleDeleteSkill: error", { error: String(error) });
return { success: false, error: String(error) };
}
}

export async function handleSkillsCatalogIndex(): Promise<
ApiResponse<{ skills: Array<{ name: string; version: string; files: string[] }> }>
> {
try {
const { skillsManager } = await import("./skills/skills-manager.js");
const catalog = await skillsManager.toHttpCatalog();
return { success: true, data: catalog };
} catch (error) {
log("handleSkillsCatalogIndex: error", { error: String(error) });
return { success: false, error: String(error) };
}
}

export async function handleSkillsCatalogFile(
id: string
): Promise<ApiResponse<{ markdown: string }>> {
try {
const { skillsManager } = await import("./skills/skills-manager.js");
const markdown = await skillsManager.toCatalogMarkdown(id);
if (!markdown) return { success: false, error: `Skill not found: ${id}` };
return { success: true, data: { markdown } };
} catch (error) {
log("handleSkillsCatalogFile: error", { error: String(error) });
return { success: false, error: String(error) };
}
}
Loading
Loading