buddy-agent-harness 0.1.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.
Files changed (36) hide show
  1. package/.claude-plugin/plugin.json +12 -0
  2. package/.codex-plugin/plugin.json +16 -0
  3. package/.cursor-plugin/plugin.json +14 -0
  4. package/.plugin/plugin.json +30 -0
  5. package/README.md +37 -0
  6. package/bin/buddy-agent-harness.mjs +7 -0
  7. package/dist/cli.d.mts +5 -0
  8. package/dist/cli.d.mts.map +1 -0
  9. package/dist/cli.mjs +20 -0
  10. package/dist/cli.mjs.map +1 -0
  11. package/dist/index.d.mts +57 -0
  12. package/dist/index.d.mts.map +1 -0
  13. package/dist/index.mjs +2 -0
  14. package/dist/init.command-wxC4cgUc.mjs +203 -0
  15. package/dist/init.command-wxC4cgUc.mjs.map +1 -0
  16. package/package.json +69 -0
  17. package/plugin.json +14 -0
  18. package/skills/init/README.md +29 -0
  19. package/skills/init/SKILL.md +75 -0
  20. package/skills/init/agents/openai.yaml +4 -0
  21. package/skills/init/references/agents-md.md +131 -0
  22. package/skills/init/references/detection.md +34 -0
  23. package/skills/init/references/frontmatter.md +63 -0
  24. package/skills/init/references/harnesses/claude-code.md +51 -0
  25. package/skills/init/references/harnesses/codex.md +13 -0
  26. package/skills/init/references/harnesses/copilot-cli.md +16 -0
  27. package/skills/init/references/harnesses/cursor.md +23 -0
  28. package/skills/init/references/harnesses/devin-desktop.md +19 -0
  29. package/skills/init/references/harnesses/gemini-cli.md +33 -0
  30. package/skills/init/references/standard.md +26 -0
  31. package/src/cli.ts +16 -0
  32. package/src/harness-registry/harness-registry.ts +60 -0
  33. package/src/index.ts +5 -0
  34. package/src/initialize-harnesses/init.command.ts +76 -0
  35. package/src/initialize-harnesses/initialize-harnesses.ts +44 -0
  36. package/src/skill-projection/skill-projection.ts +78 -0
@@ -0,0 +1,76 @@
1
+ import { encode } from '@toon-format/toon'
2
+ import type { cli } from 'clibuilder'
3
+ import { command, z } from 'clibuilder'
4
+ import { type HarnessName, harnessRegistry } from '../harness-registry/harness-registry.ts'
5
+ import { initializeHarnesses } from './initialize-harnesses.ts'
6
+
7
+ function write(value: object, format: 'json' | 'toon' | undefined): void {
8
+ process.stdout.write(`${format === 'json' ? JSON.stringify(value) : encode(value)}\n`)
9
+ }
10
+
11
+ export const initCommand: cli.Command = command({
12
+ name: 'init',
13
+ description: 'Set up canonical agent skills for the harnesses already enabled in a repository.',
14
+ options: {
15
+ root: {
16
+ description: 'Repository or package directory. Defaults to the current directory.',
17
+ type: z.optional(z.string()),
18
+ },
19
+ harness: {
20
+ description: 'Comma-separated harnesses to enable in addition to Claude Code and Cursor, e.g. codex,windsurf.',
21
+ type: z.optional(z.string()),
22
+ },
23
+ copy: {
24
+ description: 'Copy skills instead of linking them.',
25
+ type: z.optional(z.boolean()),
26
+ },
27
+ force: {
28
+ description: 'Replace conflicting target skill directories.',
29
+ type: z.optional(z.boolean()),
30
+ },
31
+ format: {
32
+ description: 'Output format: toon (default) or json.',
33
+ type: z.optional(z.string()),
34
+ default: 'toon',
35
+ },
36
+ },
37
+ run(args) {
38
+ try {
39
+ const format = args.format
40
+ if (format !== 'toon' && format !== 'json') throw new Error('--format must be toon or json.')
41
+ const requested = args.harness
42
+ ?.split(',')
43
+ .map((name) => name.trim())
44
+ .filter(Boolean)
45
+ const unsupported = requested?.filter((name) => !harnessRegistry.some((harness) => harness.name === name))
46
+ if (unsupported?.length)
47
+ throw new Error(
48
+ `Unsupported harness: ${unsupported.join(', ')}. Supported: ${harnessRegistry
49
+ .map((harness) => harness.name)
50
+ .join(', ')}.`,
51
+ )
52
+ write(
53
+ initializeHarnesses({
54
+ root: args.root ?? process.cwd(),
55
+ ...(requested?.length ? { harnesses: requested as HarnessName[] } : {}),
56
+ ...(args.copy === undefined ? {} : { copy: args.copy }),
57
+ ...(args.force === undefined ? {} : { force: args.force }),
58
+ }),
59
+ format,
60
+ )
61
+ } catch (error) {
62
+ process.stderr.write(`error: ${error instanceof Error ? error.message : 'Harness initialization failed.'}\n`)
63
+ process.exitCode = 1
64
+ }
65
+ },
66
+ })
67
+
68
+ export const harnessCommand: cli.Command = command({
69
+ name: 'harness',
70
+ description: 'Commands for configuring agent harness compatibility.',
71
+ commands: [initCommand],
72
+ })
73
+
74
+ export function activate({ addCommand }: { addCommand(command: typeof harnessCommand): void }): void {
75
+ addCommand(harnessCommand)
76
+ }
@@ -0,0 +1,44 @@
1
+ import { join } from 'node:path'
2
+ import { type HarnessName, selectHarnesses } from '../harness-registry/harness-registry.ts'
3
+ import { countSkills, projectSkills } from '../skill-projection/skill-projection.ts'
4
+
5
+ export type InitializeOptions = {
6
+ root: string
7
+ harnesses?: HarnessName[]
8
+ copy?: boolean
9
+ force?: boolean
10
+ }
11
+ export type InitializeResult = {
12
+ root: string
13
+ harnesses: HarnessName[]
14
+ native: HarnessName[]
15
+ linked: HarnessName[]
16
+ /** Enabled harnesses whose name has been superseded, as `{ name, replacedBy }`. */
17
+ deprecated: { name: HarnessName; replacedBy: HarnessName }[]
18
+ skills: number
19
+ copied: boolean
20
+ }
21
+
22
+ export function initializeHarnesses({
23
+ root,
24
+ harnesses: preferred = [],
25
+ copy = false,
26
+ force = false,
27
+ }: InitializeOptions): InitializeResult {
28
+ const canonicalSkills = join(root, '.agents', 'skills')
29
+ const skills = countSkills(canonicalSkills)
30
+ const harnesses = selectHarnesses(root, preferred)
31
+ const linked = projectSkills({ root, canonicalSkills, harnesses, copy, force })
32
+
33
+ return {
34
+ root,
35
+ harnesses: harnesses.map((harness) => harness.name),
36
+ native: harnesses.filter((harness) => !harness.skillsDirectory).map((harness) => harness.name),
37
+ linked,
38
+ deprecated: harnesses
39
+ .filter((harness) => harness.deprecated)
40
+ .map((harness) => ({ name: harness.name, replacedBy: harness.deprecated as HarnessName })),
41
+ skills,
42
+ copied: copy,
43
+ }
44
+ }
@@ -0,0 +1,78 @@
1
+ import { cpSync, lstatSync, mkdirSync, readdirSync, readlinkSync, rmSync, symlinkSync } from 'node:fs'
2
+ import { dirname, join, relative } from 'node:path'
3
+ import type { Harness, HarnessName } from '../harness-registry/harness-registry.ts'
4
+
5
+ function skillNames(skillsDirectory: string): string[] {
6
+ return readdirSync(skillsDirectory, { withFileTypes: true })
7
+ .filter((entry) => entry.isDirectory())
8
+ .map((entry) => entry.name)
9
+ .sort()
10
+ }
11
+
12
+ function pathOccupied(path: string): boolean {
13
+ try {
14
+ lstatSync(path)
15
+ return true
16
+ } catch {
17
+ return false
18
+ }
19
+ }
20
+
21
+ function isCorrectLink(target: string, source: string): boolean {
22
+ return lstatSync(target).isSymbolicLink() && readlinkSync(target) === relative(dirname(target), source)
23
+ }
24
+
25
+ /** Creates the canonical directory when absent, so a fresh repository reports zero skills. */
26
+ export function countSkills(canonicalSkills: string): number {
27
+ mkdirSync(canonicalSkills, { recursive: true })
28
+ return skillNames(canonicalSkills).length
29
+ }
30
+
31
+ export type ProjectSkillsOptions = {
32
+ root: string
33
+ canonicalSkills: string
34
+ harnesses: readonly Harness[]
35
+ copy: boolean
36
+ force: boolean
37
+ }
38
+
39
+ /**
40
+ * Points every harness that cannot read `.agents/skills` at the canonical directory. Symlinking is
41
+ * preferred; a platform that refuses the link falls back to a copy. Returns the projected harnesses.
42
+ */
43
+ export function projectSkills({ root, canonicalSkills, harnesses, copy, force }: ProjectSkillsOptions): HarnessName[] {
44
+ const projections = harnesses
45
+ .filter((harness) => harness.skillsDirectory)
46
+ .map((harness) => ({
47
+ harness,
48
+ target: join(root, harness.skillsDirectory as string),
49
+ }))
50
+
51
+ const conflicts = projections
52
+ .filter(({ target }) => pathOccupied(target) && !isCorrectLink(target, canonicalSkills))
53
+ .map(({ target }) => target)
54
+
55
+ if (conflicts.length && !force) {
56
+ throw new Error(
57
+ `Refusing to replace existing skill targets:\n${conflicts.map((target) => `- ${target}`).join('\n')}`,
58
+ )
59
+ }
60
+
61
+ for (const { target } of projections) {
62
+ mkdirSync(dirname(target), { recursive: true })
63
+ if (pathOccupied(target) && isCorrectLink(target, canonicalSkills)) continue
64
+ if (pathOccupied(target)) rmSync(target, { recursive: true, force: true })
65
+ if (copy) {
66
+ cpSync(canonicalSkills, target, { recursive: true })
67
+ continue
68
+ }
69
+ try {
70
+ symlinkSync(relative(dirname(target), canonicalSkills), target, 'junction')
71
+ } catch (error) {
72
+ if (pathOccupied(target)) throw error
73
+ cpSync(canonicalSkills, target, { recursive: true })
74
+ }
75
+ }
76
+
77
+ return projections.map(({ harness }) => harness.name)
78
+ }