diff --git a/.changeset/skill-import-prompt-symlink-copy.md b/.changeset/skill-import-prompt-symlink-copy.md new file mode 100644 index 00000000..33e05c10 --- /dev/null +++ b/.changeset/skill-import-prompt-symlink-copy.md @@ -0,0 +1,5 @@ +--- +"@inkeep/open-knowledge": patch +--- + +Clarify the skills import prompt. The one-time banner now states what Import actually does: it moves the editor-dir skills into `.ok/skills` and replaces the `.claude`, `.codex`, etc. copies with symlinks back to it, so the knowledge base is the single place to edit them and every editor stays in sync. It also flags the consequences a user needs before clicking: if those folders are committed to git the change should be reviewed, and symlinks can behave differently on some editors and on Windows. diff --git a/.changeset/skills-tool-builtin-skill-hint.md b/.changeset/skills-tool-builtin-skill-hint.md new file mode 100644 index 00000000..f59aa6cf --- /dev/null +++ b/.changeset/skills-tool-builtin-skill-hint.md @@ -0,0 +1,9 @@ +--- +"@inkeep/open-knowledge": patch +--- + +Stop two ways agents get derailed around the skill surface. + +- The `skills` MCP tool now short-circuits OpenKnowledge's own built-in skills instead of 404-ing. An agent told to "load the open-knowledge skill" would call `skills({ name: "open-knowledge" })`, hit a bare `Skill not found.`, and fall back to cat-ing the bundled SKILL.md. The built-ins (`open-knowledge`, `open-knowledge-discovery`, `open-knowledge-write-skill`) are runtime agent skills projected into editor host dirs, never KB content skills, so a READ aimed at one now returns a teaching error explaining it is already in the agent's loaded skill list and is not fetched through this tool. User-authored `open-knowledge-pack-*` skills are unaffected, and the tool description states the boundary up front. + +- The project SKILL.md escape hatch now tells agents that their initial tool list is not exhaustive: some clients (notably Codex) defer MCP tools behind a lazy `tool_search` step, so `mcp__open-knowledge__*` is absent until discovered. Absence from the visible list means "not discovered yet," not "not registered" — agents must run tool discovery before invoking the native-tools escape hatch. diff --git a/packages/app/src/components/SkillsSidebarSection.tsx b/packages/app/src/components/SkillsSidebarSection.tsx index 8ef71d28..cf3e0832 100644 --- a/packages/app/src/components/SkillsSidebarSection.tsx +++ b/packages/app/src/components/SkillsSidebarSection.tsx @@ -432,8 +432,10 @@ function SkillImportPrompt() {

- Open Knowledge can manage {state.importable} editor skill(s) for this project — import - them into the knowledge base? + Open Knowledge can manage {state.importable} editor skill(s) for this project. Import + moves them into .ok/skills and replaces the .claude, .codex, etc. copies with symlinks — + one place to edit, in sync everywhere. If those folders are committed to git, review the + change first; symlinks can behave differently on some editors and on Windows.

diff --git a/packages/app/src/locales/en/messages.json b/packages/app/src/locales/en/messages.json index 99f604c6..07c7e58f 100644 --- a/packages/app/src/locales/en/messages.json +++ b/packages/app/src/locales/en/messages.json @@ -1676,6 +1676,11 @@ "msOK6I": [ "Repositories you've already cloned may keep syncing through git's own saved credentials, even after you disconnect." ], + "mu1O0q": [ + "Open Knowledge can manage ", + ["0"], + " editor skill(s) for this project. Import moves them into .ok/skills and replaces the .claude, .codex, etc. copies with symlinks — one place to edit, in sync everywhere. If those folders are committed to git, review the change first; symlinks can behave differently on some editors and on Windows." + ], "mwMF9u": ["Open CodeMirror search in source editor mode."], "mxY24F": ["Duplicate the focused file-tree item when focus is in the Files sidebar."], "n2lRue": ["Commit or stash changes to switch:"], @@ -2051,11 +2056,6 @@ "y1t8bQ": ["Settings sections"], "y3aU20": ["Save changes"], "y46m5r": ["The desktop app isn't responding. Refresh and try again."], - "y48eD2": [ - "Open Knowledge can manage ", - ["0"], - " editor skill(s) for this project — import them into the knowledge base?" - ], "y5FUYi": ["Suggestion menu open"], "y8B54M": ["Couldn't send your prompt — please try again."], "yCX4jp": [" Your selection could not be restored."], diff --git a/packages/app/src/locales/en/messages.po b/packages/app/src/locales/en/messages.po index cb836daa..f0a213a0 100644 --- a/packages/app/src/locales/en/messages.po +++ b/packages/app/src/locales/en/messages.po @@ -4310,8 +4310,8 @@ msgstr "Open in current branch" #. placeholder {0}: state.importable #: src/components/SkillsSidebarSection.tsx -msgid "Open Knowledge can manage {0} editor skill(s) for this project — import them into the knowledge base?" -msgstr "Open Knowledge can manage {0} editor skill(s) for this project — import them into the knowledge base?" +msgid "Open Knowledge can manage {0} editor skill(s) for this project. Import moves them into .ok/skills and replaces the .claude, .codex, etc. copies with symlinks — one place to edit, in sync everywhere. If those folders are committed to git, review the change first; symlinks can behave differently on some editors and on Windows." +msgstr "Open Knowledge can manage {0} editor skill(s) for this project. Import moves them into .ok/skills and replaces the .claude, .codex, etc. copies with symlinks — one place to edit, in sync everywhere. If those folders are committed to git, review the change first; symlinks can behave differently on some editors and on Windows." #: src/components/GraphPanel.tsx msgid "Open link" diff --git a/packages/app/src/locales/pseudo/messages.json b/packages/app/src/locales/pseudo/messages.json index ef6e4c03..f3e59392 100644 --- a/packages/app/src/locales/pseudo/messages.json +++ b/packages/app/src/locales/pseudo/messages.json @@ -1676,6 +1676,11 @@ "msOK6I": [ "Ŕēƥōśĩţōŕĩēś ŷōũ'vē àĺŕēàďŷ ćĺōńēď ḿàŷ ķēēƥ śŷńćĩńĝ ţĥŕōũĝĥ ĝĩţ'ś ōŵń śàvēď ćŕēďēńţĩàĺś, ēvēń àƒţēŕ ŷōũ ďĩśćōńńēćţ." ], + "mu1O0q": [ + "Ōƥēń Ķńōŵĺēďĝē ćàń ḿàńàĝē ", + ["0"], + " ēďĩţōŕ śķĩĺĺ(ś) ƒōŕ ţĥĩś ƥŕōĴēćţ. Ĩḿƥōŕţ ḿōvēś ţĥēḿ ĩńţō .ōķ/śķĩĺĺś àńď ŕēƥĺàćēś ţĥē .ćĺàũďē, .ćōďēx, ēţć. ćōƥĩēś ŵĩţĥ śŷḿĺĩńķś — ōńē ƥĺàćē ţō ēďĩţ, ĩń śŷńć ēvēŕŷŵĥēŕē. Ĩƒ ţĥōśē ƒōĺďēŕś àŕē ćōḿḿĩţţēď ţō ĝĩţ, ŕēvĩēŵ ţĥē ćĥàńĝē ƒĩŕśţ; śŷḿĺĩńķś ćàń ƀēĥàvē ďĩƒƒēŕēńţĺŷ ōń śōḿē ēďĩţōŕś àńď ōń Ŵĩńďōŵś." + ], "mwMF9u": ["Ōƥēń ĆōďēḾĩŕŕōŕ śēàŕćĥ ĩń śōũŕćē ēďĩţōŕ ḿōďē."], "mxY24F": ["Ďũƥĺĩćàţē ţĥē ƒōćũśēď ƒĩĺē-ţŕēē ĩţēḿ ŵĥēń ƒōćũś ĩś ĩń ţĥē Ƒĩĺēś śĩďēƀàŕ."], "n2lRue": ["Ćōḿḿĩţ ōŕ śţàśĥ ćĥàńĝēś ţō śŵĩţćĥ:"], @@ -2051,11 +2056,6 @@ "y1t8bQ": ["Śēţţĩńĝś śēćţĩōńś"], "y3aU20": ["Śàvē ćĥàńĝēś"], "y46m5r": ["Ţĥē ďēśķţōƥ àƥƥ ĩśń'ţ ŕēśƥōńďĩńĝ. Ŕēƒŕēśĥ àńď ţŕŷ àĝàĩń."], - "y48eD2": [ - "Ōƥēń Ķńōŵĺēďĝē ćàń ḿàńàĝē ", - ["0"], - " ēďĩţōŕ śķĩĺĺ(ś) ƒōŕ ţĥĩś ƥŕōĴēćţ — ĩḿƥōŕţ ţĥēḿ ĩńţō ţĥē ķńōŵĺēďĝē ƀàśē?" - ], "y5FUYi": ["Śũĝĝēśţĩōń ḿēńũ ōƥēń"], "y8B54M": ["Ćōũĺďń'ţ śēńď ŷōũŕ ƥŕōḿƥţ — ƥĺēàśē ţŕŷ àĝàĩń."], "yCX4jp": [" Ŷōũŕ śēĺēćţĩōń ćōũĺď ńōţ ƀē ŕēśţōŕēď."], diff --git a/packages/app/src/locales/pseudo/messages.po b/packages/app/src/locales/pseudo/messages.po index 9187d4cb..2a401405 100644 --- a/packages/app/src/locales/pseudo/messages.po +++ b/packages/app/src/locales/pseudo/messages.po @@ -4305,7 +4305,7 @@ msgstr "" #. placeholder {0}: state.importable #: src/components/SkillsSidebarSection.tsx -msgid "Open Knowledge can manage {0} editor skill(s) for this project — import them into the knowledge base?" +msgid "Open Knowledge can manage {0} editor skill(s) for this project. Import moves them into .ok/skills and replaces the .claude, .codex, etc. copies with symlinks — one place to edit, in sync everywhere. If those folders are committed to git, review the change first; symlinks can behave differently on some editors and on Windows." msgstr "" #: src/components/GraphPanel.tsx diff --git a/packages/server/assets/skills/project/SKILL.md b/packages/server/assets/skills/project/SKILL.md index ebed91d3..62dd6198 100644 --- a/packages/server/assets/skills/project/SKILL.md +++ b/packages/server/assets/skills/project/SKILL.md @@ -48,7 +48,9 @@ Tools NOT in OK MCP (your host's): `preview_start`, `preview_screenshot`, `WebFe **MCP tool visibility — not seeing `exec` is NOT the escape hatch.** MCP wiring varies by client (Claude Code, Cursor, Codex, Windsurf, VS Code). Server labels are user-defined; tools may not appear as top-level symbols named `exec`. If OpenKnowledge is registered as an MCP server here, route markdown reads through its `exec` / `search` via your client's documented MCP invocation (including any generic "call MCP tool" flow). Registration is the test, not top-level-symbol visibility. -**Escape hatch.** Native `Read` / `Grep` / `Glob` on `.md` / `.mdx` is allowed **only** when no OpenKnowledge MCP server is registered for this project, **or** immediately after you tried an MCP call and it failed — then begin a user-visible sentence with `OpenKnowledge MCP unavailable:`. Never use the hatch because you skipped your client's MCP path, didn't see `exec` as a top-level tool, or rationalized the skill wasn't necessary. +**Your initial tool list is NOT exhaustive — run tool discovery before concluding the MCP is missing.** Some clients (notably Codex) defer MCP tools behind a lazy `tool_search` / tool-discovery step, so `mcp__open-knowledge__*` is absent from the upfront tool set and only appears after you search for it. Absence from the visible list means "I have not discovered it yet," NOT "it is not registered." If you don't see the tools, your first move is to run your client's tool-discovery / `tool_search` for `open-knowledge` (or `exec` / `search` / `write`) — do not infer unavailability from the initial list. + +**Escape hatch.** Native `Read` / `Grep` / `Glob` on `.md` / `.mdx` is allowed **only** when, after running tool discovery (above), no OpenKnowledge MCP server is registered for this project, **or** immediately after you actually invoked an MCP call and it failed — then begin a user-visible sentence with `OpenKnowledge MCP unavailable:`. "Not registered" is a conclusion you may only reach after tool discovery turned it up empty — never from the initial tool list alone. Never use the hatch because you skipped your client's MCP path, didn't see `exec` as a top-level tool, didn't run tool discovery, or rationalized the skill wasn't necessary. **Source code and non-markdown files** (`.ts`, `.py`, `package.json`, …): native `Read` / `Grep` / `Glob` always. diff --git a/packages/server/src/mcp/tools/skills.test.ts b/packages/server/src/mcp/tools/skills.test.ts index c851e03b..2dcda061 100644 --- a/packages/server/src/mcp/tools/skills.test.ts +++ b/packages/server/src/mcp/tools/skills.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from 'bun:test'; import { type Config, ConfigSchema } from '../../config/schema.ts'; -import type { ServerInstance } from './shared.ts'; +import { BUNDLE_SKILL_NAME } from '../../skill-bundles.ts'; +import { HOCUSPOCUS_NOT_RUNNING_ERROR, type ServerInstance } from './shared.ts'; import { register as registerSkills, type SkillsToolDeps } from './skills.ts'; const BASE_CONFIG: Config = ConfigSchema.parse({}); @@ -60,6 +61,39 @@ describe('skills read tool — server-required', () => { const handler = captureSkills(undefined); const r = await handler({ name: 'trip-log' }); expect(r.isError).toBe(true); - expect(text(r)).toContain('Hocuspocus server is not running'); + expect(text(r)).toContain(HOCUSPOCUS_NOT_RUNNING_ERROR); + }); +}); + +describe('skills read tool — built-in OK skills short-circuit before the network', () => { + test('READ open-knowledge teaches instead of looking it up', async () => { + const handler = captureSkills(undefined); + const r = await handler({ name: 'open-knowledge', scope: 'project' }); + expect(r.isError).toBe(true); + expect(text(r)).toContain('built-in agent skills'); + expect(text(r)).toContain('already provided to you in your loaded skill list'); + }); + + test('every shipped bundle name short-circuits (not just open-knowledge)', async () => { + const handler = captureSkills(undefined); + for (const name of Object.values(BUNDLE_SKILL_NAME)) { + const r = await handler({ name }); + expect(r.isError, `isError for "${name}"`).toBe(true); + expect(text(r), `teaching error for "${name}"`).toContain('NOT managed by this tool'); + } + }); + + test('READ-file on a built-in skill is short-circuited too', async () => { + const handler = captureSkills(undefined); + const r = await handler({ name: 'open-knowledge', file: 'references/x.md' }); + expect(r.isError).toBe(true); + expect(text(r)).toContain('built-in agent skills'); + }); + + test('a user-authored pack skill is NOT treated as built-in', async () => { + const handler = captureSkills(undefined); + const r = await handler({ name: 'open-knowledge-pack-fishing' }); + expect(r.isError).toBe(true); + expect(text(r)).toContain(HOCUSPOCUS_NOT_RUNNING_ERROR); }); }); diff --git a/packages/server/src/mcp/tools/skills.ts b/packages/server/src/mcp/tools/skills.ts index 9233a178..bbfdef83 100644 --- a/packages/server/src/mcp/tools/skills.ts +++ b/packages/server/src/mcp/tools/skills.ts @@ -1,4 +1,5 @@ import { z } from 'zod'; +import { BUNDLE_SKILL_NAME } from '../../skill-bundles.ts'; import { type ConfigOrResolver, HOCUSPOCUS_NOT_RUNNING_ERROR, @@ -18,6 +19,16 @@ function bundleFileKind(path: string): 'reference' | 'script' { return path.replace(/\\/g, '/').startsWith('scripts/') ? 'script' : 'reference'; } +const INTERNAL_BUNDLE_SKILL_NAMES = new Set(Object.values(BUNDLE_SKILL_NAME)); + +function internalSkillHint(name: string): string { + return [ + `"${name}" is one of OpenKnowledge's built-in agent skills — it is NOT managed by this tool and cannot be read or listed here.`, + "It is already provided to you in your loaded skill list (a hidden runtime skill projected into your editor); don't fetch or re-load it — just follow the skill you already have.", + 'The `skills` tool only covers skills authored as KB content (`.ok/skills` = project, `~/.ok/skills` = global). Built-in `open-knowledge*` skills never appear in either scope.', + ].join(' '); +} + const SCOPE_FIELD_DESCRIBE = "Which level the skill lives at. `project` = this KB's `.ok/skills/` (shared via git with everyone on the project); `global` = your user-level `~/.ok/skills/` (available in every project on this machine, not shared)."; @@ -63,6 +74,8 @@ const DESCRIPTION = [ '', 'This is how you find and read skills. Skills are addressed by `name` + `scope`, NOT by path — do NOT `ls`/`cat` `.ok/skills/` or pass raw `.ok/...` paths; `.ok/` is opaque internal state.', '', + "Covers only skills authored as KB content. OpenKnowledge's own built-in `open-knowledge*` skills (e.g. the `open-knowledge` project skill) are runtime skills already loaded in your skill list — they are NOT here, and you never fetch them through this tool.", + '', '**Three modes:**', '- **List** (omit `name`): every skill across BOTH levels — Project (this KB) and Global (user-level). Returns name, scope, description, installed/hosts, and (for starter packs) `updateAvailable`.', "- **Read skill** (pass `name`): that skill's description + body + a `files` list (`{ path, kind }`, no inline text) of its `references/**`+`scripts/**` bundle files. `scope` optional — omitted, it resolves by name (preferring Project when a name exists at both levels).", @@ -108,6 +121,10 @@ export function register(server: ServerInstance, deps: SkillsToolDeps): void { }), }, async (args: { name?: string; file?: string; scope?: SkillScope; cwd?: string }) => { + if (args.name !== undefined && INTERNAL_BUNDLE_SKILL_NAMES.has(args.name)) { + return textResult(`Error: ${internalSkillHint(args.name)}`, true); + } + const context = await resolveProjectServerContext( deps.resolveCwd, deps.config,