Ship a Logbuch skill and record token use

- docs/skill.md teaches a Claude what Logbuch is for, how an entry sounds
  and in which order the calls go, served at GET /api/v1/skill and as a
  download next to the spec and the guide
- a test keeps the skill from drifting: frontmatter, only endpoints that
  exist, house rules
- resolveClient stamps last_used_at, the access list said never used even
  after eleven entries
This commit is contained in:
Matthias G
2026-08-03 10:40:48 +02:00
parent 06d0184965
commit 5072ff7249
12 changed files with 315 additions and 7 deletions
+59
View File
@@ -0,0 +1,59 @@
import { readFileSync, readdirSync, statSync } from 'node:fs'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
const skill = readFileSync(join(process.cwd(), 'docs', 'skill.md'), 'utf8')
function routes(): string[] {
const found: string[] = []
function walk(dir: string, url: string) {
for (const name of readdirSync(dir)) {
const path = join(dir, name)
if (statSync(path).isDirectory()) {
walk(path, `${url}/${name}`)
continue
}
if (name === 'route.ts') {
found.push(url)
}
}
}
walk(join(process.cwd(), 'src', 'app', 'api', 'v1'), '/api/v1')
return found
}
describe('Skill für Claude', () => {
it('trägt Frontmatter mit Namen und Beschreibung', () => {
const head = skill.split('---')[1] ?? ''
expect(head).toMatch(/\nname: logbuch\n/u)
expect(head).toMatch(/\ndescription: .{40,}/u)
})
it('nennt nur Endpunkte, die es gibt', () => {
const known = new Set(routes().map(url => url.replace(/\/\[[^\]]+\]/gu, '')))
const mentioned = [...skill.matchAll(/`?(?:GET|POST|PUT|PATCH|DELETE) (\/api\/v1[a-z0-9./-]*)/giu)]
.map(match => match[1]!.replace(/\/(?::[a-z]+|<[a-z-]+>)/giu, '').replace(/\/$/u, ''))
expect(mentioned.length).toBeGreaterThan(5)
for (const url of new Set(mentioned)) {
expect(known.has(url), `${url} steht im Skill, aber nicht im Code`).toBe(true)
}
})
it('hält sich an die Hausregeln', () => {
expect(skill).not.toMatch(/[]/u)
expect(skill).not.toMatch(/\p{Extended_Pictographic}/u)
expect(skill).not.toMatch(/\b(fuer|ueber|koennen|muessen|loeschen|aendern|groesser|gehoert|laesst|veroeffentlich\w*)\b/iu)
})
it('sagt, dass ein Zugang nicht veröffentlichen kann', () => {
expect(skill).toMatch(/niemals veröffentlichen/u)
})
})