feat(plugin): official Claude Code plugin with marketplace and seven workflow skills (#1088)
Ships an installable Claude Code plugin (/plugin marketplace add erp-mafia/accounted) that bundles the MCP connection (OAuth, zero-key) with seven short workflow skills following the Swedish bookkeeping rhythm: start, bookkeep, check, month-close, vat, payroll, year-end. Wrappers are deliberately thin: they ground in the agent briefing and Accounted:// resources, load server-side workflow skills and regulatory atoms via gnubok_load_skill at need, and stage every write for user approval. No knowledge is duplicated into the plugin. A vitest cross-checks every skill slug, atom id, resource URI, and tool name the wrappers reference against the MCP server source, so a server rename fails CI instead of a user's chat session. Assessment and follow-ups in dev_docs/claude_plugin.md (local, dev_docs is unpublished by design). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
b420f3e1d9
commit
e33cc2428d
@@ -0,0 +1,71 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { readFileSync, readdirSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { workflowSkills } from '../skills'
|
||||
import { dataResources } from '../resources'
|
||||
import { discoverAtoms } from '@/scripts/lib/atom-discovery'
|
||||
|
||||
/**
|
||||
* The Claude Code plugin (claude-plugin/) ships thin wrapper skills whose value
|
||||
* is that every slug, resource URI, and tool name they mention resolves against
|
||||
* this MCP server. A rename on the server side must fail here, not in a user's
|
||||
* chat session.
|
||||
*/
|
||||
|
||||
const repoRoot = join(__dirname, '..', '..', '..', '..')
|
||||
const skillsDir = join(repoRoot, 'claude-plugin', 'skills')
|
||||
|
||||
function readPluginSkills(): { file: string; body: string }[] {
|
||||
return readdirSync(skillsDir).map((dir) => {
|
||||
const file = join(skillsDir, dir, 'SKILL.md')
|
||||
return { file: `${dir}/SKILL.md`, body: readFileSync(file, 'utf8') }
|
||||
})
|
||||
}
|
||||
|
||||
const pluginSkills = readPluginSkills()
|
||||
const serverSource = readFileSync(join(__dirname, '..', 'server.ts'), 'utf8')
|
||||
|
||||
describe('claude-plugin wrapper references', () => {
|
||||
it('ships the seven v1 skills', () => {
|
||||
expect(pluginSkills.map((s) => s.file).sort()).toEqual([
|
||||
'bookkeep/SKILL.md',
|
||||
'check/SKILL.md',
|
||||
'month-close/SKILL.md',
|
||||
'payroll/SKILL.md',
|
||||
'start/SKILL.md',
|
||||
'vat/SKILL.md',
|
||||
'year-end/SKILL.md',
|
||||
])
|
||||
})
|
||||
|
||||
it('every gnubok_load_skill slug resolves to a workflow skill or a registry atom', async () => {
|
||||
const workflowSlugs = new Set(workflowSkills.map((s) => s.slug))
|
||||
const atomIds = new Set((await discoverAtoms(repoRoot)).map((a) => a.id))
|
||||
for (const { file, body } of pluginSkills) {
|
||||
for (const [, slug] of body.matchAll(/gnubok_load_skill\("([^"]+)"\)/g)) {
|
||||
const known = workflowSlugs.has(slug) || atomIds.has(slug)
|
||||
expect(known, `${file} references unknown skill slug "${slug}"`).toBe(true)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('every Accounted:// resource URI exists on the server', () => {
|
||||
const uris = new Set(dataResources.map((r) => r.uri))
|
||||
for (const { file, body } of pluginSkills) {
|
||||
for (const [uri] of body.matchAll(/Accounted:\/\/[a-z/-]+/g)) {
|
||||
expect(uris.has(uri), `${file} references unknown resource "${uri}"`).toBe(true)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('every gnubok_* tool name exists on the server', () => {
|
||||
for (const { file, body } of pluginSkills) {
|
||||
for (const [tool] of body.matchAll(/gnubok_[a-z_]+/g)) {
|
||||
expect(
|
||||
serverSource.includes(`name: '${tool}'`),
|
||||
`${file} references unknown tool "${tool}"`,
|
||||
).toBe(true)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user