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.
- package/.claude-plugin/plugin.json +12 -0
- package/.codex-plugin/plugin.json +16 -0
- package/.cursor-plugin/plugin.json +14 -0
- package/.plugin/plugin.json +30 -0
- package/README.md +37 -0
- package/bin/buddy-agent-harness.mjs +7 -0
- package/dist/cli.d.mts +5 -0
- package/dist/cli.d.mts.map +1 -0
- package/dist/cli.mjs +20 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/index.d.mts +57 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/init.command-wxC4cgUc.mjs +203 -0
- package/dist/init.command-wxC4cgUc.mjs.map +1 -0
- package/package.json +69 -0
- package/plugin.json +14 -0
- package/skills/init/README.md +29 -0
- package/skills/init/SKILL.md +75 -0
- package/skills/init/agents/openai.yaml +4 -0
- package/skills/init/references/agents-md.md +131 -0
- package/skills/init/references/detection.md +34 -0
- package/skills/init/references/frontmatter.md +63 -0
- package/skills/init/references/harnesses/claude-code.md +51 -0
- package/skills/init/references/harnesses/codex.md +13 -0
- package/skills/init/references/harnesses/copilot-cli.md +16 -0
- package/skills/init/references/harnesses/cursor.md +23 -0
- package/skills/init/references/harnesses/devin-desktop.md +19 -0
- package/skills/init/references/harnesses/gemini-cli.md +33 -0
- package/skills/init/references/standard.md +26 -0
- package/src/cli.ts +16 -0
- package/src/harness-registry/harness-registry.ts +60 -0
- package/src/index.ts +5 -0
- package/src/initialize-harnesses/init.command.ts +76 -0
- package/src/initialize-harnesses/initialize-harnesses.ts +44 -0
- 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
|
+
}
|