From 1eeb8ee1652b92df6524f7597b145336cefa3da6 Mon Sep 17 00:00:00 2001 From: Hive Advise Date: Sat, 30 May 2026 15:27:43 -0400 Subject: [PATCH 1/2] Reject invalid skill listing prices --- packages/cli/src/commands/skills.test.ts | 62 +++++++++++++++++++++++- packages/cli/src/commands/skills.ts | 19 ++++++-- 2 files changed, 77 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/commands/skills.test.ts b/packages/cli/src/commands/skills.test.ts index 0f23e4ca..c781e968 100644 --- a/packages/cli/src/commands/skills.test.ts +++ b/packages/cli/src/commands/skills.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'; +import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { @@ -103,6 +103,66 @@ describe('skills install command', () => { }); }); +describe('skills new command', () => { + let stdout: string[]; + let tempDir: string; + + beforeEach(() => { + stdout = []; + vi.spyOn(console, 'log').mockImplementation((...args: unknown[]) => { + stdout.push(args.map(String).join(' ')); + }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + if (tempDir) { + rmSync(tempDir, { recursive: true, force: true }); + tempDir = ''; + } + }); + + function skillFixture(): { skillFile: string; manifestFile: string } { + tempDir = mkdtempSync(join(tmpdir(), 'sh1pt-skills-new-')); + const skillFile = join(tempDir, 'SKILL.md'); + const manifestFile = join(tempDir, 'sh1pt.skill.json'); + writeFileSync(skillFile, '---\nname: paid-helper\ndescription: Paid helper skill\n---\n', 'utf8'); + return { skillFile, manifestFile }; + } + + it('preserves a valid integer price in the manifest and marketplace commands', async () => { + const { skillFile, manifestFile } = skillFixture(); + const newCmd = skillsCmd.commands.find((c) => c.name() === 'new'); + expect(newCmd).toBeDefined(); + + await newCmd?.parseAsync(['--skill-file', skillFile, '--out', manifestFile, '--price', '42'], { from: 'user' }); + + const manifest = JSON.parse(readFileSync(manifestFile, 'utf8')); + expect(manifest.price).toBe(42); + expect(manifest.marketplaces.ugig.command).toContain('--price 42'); + }); + + it('rejects negative prices without writing a manifest', async () => { + const { skillFile, manifestFile } = skillFixture(); + const newCmd = skillsCmd.commands.find((c) => c.name() === 'new'); + expect(newCmd).toBeDefined(); + + await expect(newCmd?.parseAsync(['--skill-file', skillFile, '--out', manifestFile, '--price', '-5'], { from: 'user' })) + .rejects.toThrow('--price must be a non-negative integer in sats'); + expect(existsSync(manifestFile)).toBe(false); + }); + + it('rejects fractional prices without truncating them', async () => { + const { skillFile, manifestFile } = skillFixture(); + const newCmd = skillsCmd.commands.find((c) => c.name() === 'new'); + expect(newCmd).toBeDefined(); + + await expect(newCmd?.parseAsync(['--skill-file', skillFile, '--out', manifestFile, '--price', '1.9'], { from: 'user' })) + .rejects.toThrow('--price must be a non-negative integer in sats'); + expect(existsSync(manifestFile)).toBe(false); + }); +}); + describe('skills marketplaces --json', () => { let stdout: string[]; diff --git a/packages/cli/src/commands/skills.ts b/packages/cli/src/commands/skills.ts index d7ecaf82..7230b6cd 100644 --- a/packages/cli/src/commands/skills.ts +++ b/packages/cli/src/commands/skills.ts @@ -78,6 +78,17 @@ function slugify(s: string): string { } function q(s: string): string { return JSON.stringify(s); } async function exists(path: string): Promise { try { await access(path); return true; } catch { return false; } } +function parsePriceSats(value: string): number { + const normalized = value.trim(); + if (!/^\d+$/.test(normalized)) { + throw new Error('--price must be a non-negative integer in sats'); + } + const price = Number(normalized); + if (!Number.isSafeInteger(price)) { + throw new Error('--price must be a safe non-negative integer in sats'); + } + return price; +} function frontmatterValue(text: string, key: string): string | undefined { const m = text.match(new RegExp(`^${key}:\\s*["']?([^"'\\n]+)["']?\\s*$`, 'm')); return m?.[1]?.trim(); @@ -349,17 +360,19 @@ skillsCmd const name = slugify(opts.name ?? inferred.name ?? basename(dirname(skillFile))); const title = opts.title ?? inferred.title ?? name; const description = opts.description ?? inferred.description ?? `Agent skill: ${title}`; + const tags = opts.tags.split(',').map(t => t.trim()).filter(Boolean).slice(0, 10); + const price = parsePriceSats(opts.price); const manifest: SkillManifest = { name, title, description, tagline: opts.tagline, category: opts.category, - tags: opts.tags.split(',').map(t => t.trim()).filter(Boolean).slice(0, 10), - price: Number.parseInt(opts.price, 10) || 0, + tags, + price, skillFile, sourceUrl: opts.sourceUrl, - marketplaces: Object.fromEntries(MARKETPLACES.map(mp => [mp.id, { enabled: true, status: 'pending', command: 'command' in mp && mp.command ? mp.command({ name, title, description, tagline: opts.tagline, category: opts.category, tags: opts.tags.split(',').map(t => t.trim()).filter(Boolean).slice(0, 10), price: Number.parseInt(opts.price, 10) || 0, skillFile, sourceUrl: opts.sourceUrl, marketplaces: {} }) : undefined, note: 'note' in mp ? mp.note : undefined }])) as SkillManifest['marketplaces'], + marketplaces: Object.fromEntries(MARKETPLACES.map(mp => [mp.id, { enabled: true, status: 'pending', command: 'command' in mp && mp.command ? mp.command({ name, title, description, tagline: opts.tagline, category: opts.category, tags, price, skillFile, sourceUrl: opts.sourceUrl, marketplaces: {} }) : undefined, note: 'note' in mp ? mp.note : undefined }])) as SkillManifest['marketplaces'], }; await mkdir(dirname(resolve(opts.out)), { recursive: true }); await saveManifest(opts.out, manifest); From 7ae3004a497f225cbc8eeacb6b7238b8e88d8aef Mon Sep 17 00:00:00 2001 From: Hive Advise Date: Sat, 30 May 2026 15:58:20 -0400 Subject: [PATCH 2/2] Cover zero and unsafe skill prices --- packages/cli/src/commands/skills.test.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/cli/src/commands/skills.test.ts b/packages/cli/src/commands/skills.test.ts index c781e968..86019893 100644 --- a/packages/cli/src/commands/skills.test.ts +++ b/packages/cli/src/commands/skills.test.ts @@ -142,6 +142,18 @@ describe('skills new command', () => { expect(manifest.marketplaces.ugig.command).toContain('--price 42'); }); + it('preserves zero as a valid price', async () => { + const { skillFile, manifestFile } = skillFixture(); + const newCmd = skillsCmd.commands.find((c) => c.name() === 'new'); + expect(newCmd).toBeDefined(); + + await newCmd?.parseAsync(['--skill-file', skillFile, '--out', manifestFile, '--price', '0'], { from: 'user' }); + + const manifest = JSON.parse(readFileSync(manifestFile, 'utf8')); + expect(manifest.price).toBe(0); + expect(manifest.marketplaces.ugig.command).toContain('--price 0'); + }); + it('rejects negative prices without writing a manifest', async () => { const { skillFile, manifestFile } = skillFixture(); const newCmd = skillsCmd.commands.find((c) => c.name() === 'new'); @@ -161,6 +173,16 @@ describe('skills new command', () => { .rejects.toThrow('--price must be a non-negative integer in sats'); expect(existsSync(manifestFile)).toBe(false); }); + + it('rejects prices larger than Number.MAX_SAFE_INTEGER', async () => { + const { skillFile, manifestFile } = skillFixture(); + const newCmd = skillsCmd.commands.find((c) => c.name() === 'new'); + expect(newCmd).toBeDefined(); + + await expect(newCmd?.parseAsync(['--skill-file', skillFile, '--out', manifestFile, '--price', '9007199254740992'], { from: 'user' })) + .rejects.toThrow('--price must be a safe non-negative integer in sats'); + expect(existsSync(manifestFile)).toBe(false); + }); }); describe('skills marketplaces --json', () => {