Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/sync-skill-descriptions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@bomb.sh/tools': patch
---

Fixes `bsh sync` writing garbled or truncated skill descriptions to `AGENTS.md`
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@
"publint": "^0.3.18",
"tinyexec": "^1.1.1",
"tsdown": "^0.22.3",
"ultramatter": "^0.0.4",
"vitest": "^4.1.2",
"vitest-ansi-serializer": "^0.2.1"
"vitest-ansi-serializer": "^0.2.1",
"yaml": "^2.9.1"
},
"devDependencies": {
"@changesets/cli": "^2.30.0",
Expand Down
52 changes: 27 additions & 25 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 50 additions & 1 deletion src/commands/sync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { lstat, readlink } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { describe, it, expect } from 'vitest';
import { createFixture, createMocks } from '../test-utils/index.ts';
import { copySkills, findParentPackage } from './sync.ts';
import { copySkills, findParentPackage, updateAgentsMd } from './sync.ts';

describe('copySkills', () => {
it('symlinks each skill into the destination', async () => {
Expand Down Expand Up @@ -53,6 +53,55 @@ describe('copySkills', () => {
expect(await fixture.text('source-skills/build/SKILL.md')).toContain('name: build');
expect(await fixture.text('project/skills/build/SKILL.md')).toContain('name: build');
});

it('reads block scalar descriptions without the indicator', async () => {
const fixture = await createFixture({
'source-skills': {
test: {
'SKILL.md':
'---\nname: test\ndescription: >\n Vitest test runner with colocated .test.ts files.\n Use when writing tests.\nmetadata:\n type: core\n---\nbody',
},
lint: {
'SKILL.md': '---\nname: lint\ndescription: |-\n Lint the project.\n---\nbody',
},
},
});

const skills = await copySkills({
source: new URL('source-skills/', fixture.root),
dest: new URL('project/skills/', fixture.root),
});

expect(skills).toEqual(
expect.arrayContaining([
{
name: 'test',
description: 'Vitest test runner with colocated .test.ts files. Use when writing tests.',
},
{ name: 'lint', description: 'Lint the project.' },
]),
);
});
});

describe('updateAgentsMd', () => {
it('summarizes each skill with its first full sentence', async () => {
const fixture = await createFixture({ 'AGENTS.md': '# Project\n' });

await updateAgentsMd({
root: fixture.root,
skills: [
{
name: 'test',
description: 'Vitest test runner with colocated .test.ts files. Use when writing tests.',
},
],
});

expect(await fixture.text('AGENTS.md')).toContain(
'- **test** — [skills/test/SKILL.md](skills/test/SKILL.md) - Vitest test runner with colocated .test.ts files\n',
);
});
});

describe('findParentPackage', () => {
Expand Down
10 changes: 6 additions & 4 deletions src/commands/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { findPackageJSON } from 'node:module';
import { cwd, env, platform } from 'node:process';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { NodeHfs } from '@humanfs/node';
import { parse } from 'ultramatter';
import { parse } from 'yaml';
import type { CommandContext } from '../context.ts';
import { relativeUrlPath, resolveLinkTarget } from '../utils.ts';

Expand Down Expand Up @@ -126,13 +126,13 @@ async function updateGitignore(options: { root: URL; skills: SkillInfo[] }): Pro
await hfs.write(gitignorePath, content);
}

async function updateAgentsMd(options: { root: URL; skills: SkillInfo[] }): Promise<void> {
export async function updateAgentsMd(options: { root: URL; skills: SkillInfo[] }): Promise<void> {
const { root, skills } = options;
const agentsPath = new URL('AGENTS.md', root);
let content = (await hfs.text(agentsPath)) ?? '';

const lines = skills.map((s) => {
const desc = s.description.split('.')[0]?.trim();
const desc = s.description.split(/\.(?:\s|$)/)[0]?.trim();
return `- **${s.name}** — [skills/${s.name}/SKILL.md](skills/${s.name}/SKILL.md)${desc ? ` - ${desc}` : ''}`;
});

Expand Down Expand Up @@ -160,7 +160,9 @@ async function updateAgentsMd(options: { root: URL; skills: SkillInfo[] }): Prom
}

function parseFrontmatter(content: string): SkillInfo | undefined {
const { frontmatter } = parse(content);
const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
if (!match) return undefined;
const frontmatter = parse(match[1]!) as Record<string, unknown> | null;
if (!frontmatter) return undefined;
const name = frontmatter.name as string | undefined;
const description =
Expand Down
Loading