From 68178a52347825fbedf93967afac8c148e5dd1f1 Mon Sep 17 00:00:00 2001 From: Doug Richar Date: Tue, 10 Feb 2026 06:21:34 -0500 Subject: [PATCH 1/5] feat: add confirmation prompt to remove command The remove command now prompts for confirmation before deleting skills. Use -y/--yes to skip the prompt for scripted usage. Co-Authored-By: Claude Opus 4.6 --- src/cli.ts | 4 ++-- src/commands/remove.ts | 17 +++++++++++++++-- src/prompt.ts | 15 +++++++++++++++ 3 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 src/prompt.ts diff --git a/src/cli.ts b/src/cli.ts index 6d53219..8c66f46 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -63,8 +63,8 @@ program .option('-l, --local', 'Remove from project-level skill directory') .option('--all', 'Remove all skills') .option('-y, --yes', 'Skip confirmation prompts') - .action((skillNames: string[], options) => { - removeCommand(skillNames, options) + .action(async (skillNames: string[], options) => { + await removeCommand(skillNames, options) }) program diff --git a/src/commands/remove.ts b/src/commands/remove.ts index 62c69bd..04c2cbe 100644 --- a/src/commands/remove.ts +++ b/src/commands/remove.ts @@ -2,9 +2,10 @@ import chalk from 'chalk' import { discoverSkills } from '../skills.ts' import { detectAgents, findAgent } from '../agents.ts' import { uninstallSkill } from '../installer.ts' +import { confirm } from '../prompt.ts' import type { Agent } from '../types.ts' -export function removeCommand( +export async function removeCommand( skillNames: string[], options: { agent?: string[] @@ -13,7 +14,7 @@ export function removeCommand( all?: boolean yes?: boolean }, -): void { +): Promise { let names = skillNames if (options.all) { const allSkills = discoverSkills() @@ -37,6 +38,18 @@ export function removeCommand( process.exit(1) } + if (!options.yes) { + const skillList = names.join(', ') + const agentList = targetAgents.map((a) => a.displayName).join(', ') + const confirmed = await confirm( + `Remove ${options.all ? 'all skills' : skillList} from ${agentList}?`, + ) + if (!confirmed) { + console.log(chalk.dim('Cancelled.')) + return + } + } + const isLocal = options.local === true for (const name of names) { diff --git a/src/prompt.ts b/src/prompt.ts new file mode 100644 index 0000000..7d5288d --- /dev/null +++ b/src/prompt.ts @@ -0,0 +1,15 @@ +import { createInterface } from 'node:readline' + +export async function confirm(message: string): Promise { + const rl = createInterface({ + input: process.stdin, + output: process.stdout, + }) + + return new Promise((resolve) => { + rl.question(`${message} [y/N] `, (answer) => { + rl.close() + resolve(answer.trim().toLowerCase() === 'y') + }) + }) +} From 5e590fd6cd3a27befe18cb196c0f611c8db36052 Mon Sep 17 00:00:00 2001 From: Doug Richar Date: Tue, 10 Feb 2026 06:22:16 -0500 Subject: [PATCH 2/5] fix: truncate skill descriptions in list output Descriptions are now truncated to fit within 80 columns, breaking at sentence boundaries when possible. A new "Details" hint directs users to the info command for full descriptions. Co-Authored-By: Claude Opus 4.6 --- src/commands/list.ts | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/commands/list.ts b/src/commands/list.ts index 870f46f..74a01a8 100644 --- a/src/commands/list.ts +++ b/src/commands/list.ts @@ -14,13 +14,33 @@ export function listCommand(): void { console.log() const maxNameLen = Math.max(...skills.map((s) => s.name.length)) + const indent = 2 + const gap = 2 + const maxDescLen = 80 - indent - maxNameLen - gap for (const skill of skills) { - const name = chalk.cyan(skill.name.padEnd(maxNameLen + 2)) - console.log(` ${name}${skill.description}`) + const name = chalk.cyan(skill.name.padEnd(maxNameLen + gap)) + const desc = truncate(skill.description, maxDescLen) + console.log(` ${name}${desc}`) } console.log() + console.log( + chalk.dim( + ` Details: npx @txnlab/skills info `, + ), + ) console.log(chalk.dim(` Install: npx @txnlab/skills add `)) console.log() } + +function truncate(text: string, maxLen: number): string { + if (maxLen < 4) return text + // Truncate at first sentence boundary (period followed by space) within maxLen + const period = text.indexOf('. ') + if (period >= 0 && period + 1 <= maxLen) { + return text.slice(0, period + 1) + } + if (text.length <= maxLen) return text + return text.slice(0, maxLen - 3).trimEnd() + '...' +} From e0fcba62a65348be0cd6919b8f542b2b5e200d9d Mon Sep 17 00:00:00 2001 From: Doug Richar Date: Tue, 10 Feb 2026 06:23:07 -0500 Subject: [PATCH 3/5] fix: surface install method and handle re-install over copies The add command now shows "(copied)" when the symlink fallback is used. Re-installing a skill over an existing copy (non-symlink) now replaces it instead of failing, so users don't have to manually remove before re-adding. Co-Authored-By: Claude Opus 4.6 --- src/commands/add.ts | 4 +++- src/installer.ts | 11 ++--------- tests/installer.test.ts | 6 +++--- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/commands/add.ts b/src/commands/add.ts index 692fcf7..f24b14c 100644 --- a/src/commands/add.ts +++ b/src/commands/add.ts @@ -56,8 +56,10 @@ export function addCommand( for (const agent of targetAgents) { const result = installSkill(skill, agent, { local: isLocal }) if (result.success) { + const method = + result.method === 'copy' ? chalk.dim(' (copied)') : '' console.log( - ` ${chalk.green('✔')} ${agent.displayName} — ${result.skillName} → ${result.targetPath}`, + ` ${chalk.green('✔')} ${agent.displayName} — ${result.skillName} → ${result.targetPath}${method}`, ) } else { console.error( diff --git a/src/installer.ts b/src/installer.ts index 5bb3991..c9ad8ba 100644 --- a/src/installer.ts +++ b/src/installer.ts @@ -32,17 +32,10 @@ export function installSkill( if (existsSync(targetPath)) { const stat = lstatSync(targetPath) if (stat.isSymbolicLink()) { - // Replace existing symlink unlinkSync(targetPath) } else { - return { - success: false, - skillName: skill.name, - agent, - targetPath, - method: 'symlink', - error: `Target path already exists and is not a symlink: ${targetPath}`, - } + // Replace existing copy (e.g., from a previous install) + rmSync(targetPath, { recursive: true }) } } diff --git a/tests/installer.test.ts b/tests/installer.test.ts index 503a786..99a1328 100644 --- a/tests/installer.test.ts +++ b/tests/installer.test.ts @@ -78,14 +78,14 @@ describe('installSkill', () => { expect(result.success).toBe(true) }) - test('fails if target is a regular directory', () => { + test('replaces existing directory (previous copy)', () => { const targetPath = join(agentSkillsDir, 'my-skill') mkdirSync(targetPath, { recursive: true }) writeFileSync(join(targetPath, 'file.txt'), 'existing') const result = installSkill(mockSkill, mockAgent) - expect(result.success).toBe(false) - expect(result.error).toContain('not a symlink') + expect(result.success).toBe(true) + expect(lstatSync(targetPath).isSymbolicLink()).toBe(true) }) }) From ecc70e5af0bfbd371cbe37c59992cf107fc13359 Mon Sep 17 00:00:00 2001 From: Doug Richar Date: Tue, 10 Feb 2026 06:24:48 -0500 Subject: [PATCH 4/5] refactor: extract shared resolveAgents and show valid agent names Consolidates the duplicated resolveAgents function from add, remove, and dev commands into agents.ts. The "Unknown agent" error now lists valid agent names so users don't have to check the docs. Co-Authored-By: Claude Opus 4.6 --- src/agents.ts | 21 +++++++++++++++++++++ src/commands/add.ts | 37 +++++++++---------------------------- src/commands/dev.ts | 20 +------------------- src/commands/remove.ts | 20 +------------------- 4 files changed, 32 insertions(+), 66 deletions(-) diff --git a/src/agents.ts b/src/agents.ts index 39b8f3c..d0e4bd7 100644 --- a/src/agents.ts +++ b/src/agents.ts @@ -1,6 +1,7 @@ import { existsSync } from 'node:fs' import { join } from 'node:path' import { homedir } from 'node:os' +import chalk from 'chalk' import type { Agent } from './types.ts' const home = homedir() @@ -58,3 +59,23 @@ export function findAgent(name: string): Agent | undefined { export function getAgentNames(): string[] { return agents.map((a) => a.name) } + +export function resolveAgents(agentNames?: string[]): Agent[] { + if (agentNames && agentNames.length > 0) { + const resolved: Agent[] = [] + for (const name of agentNames) { + const agent = findAgent(name) + if (agent) { + resolved.push(agent) + } else { + const valid = getAgentNames().join(', ') + console.warn( + chalk.yellow(` Unknown agent: "${name}". Valid agents: ${valid}`), + ) + } + } + return resolved + } + + return detectAgents() +} diff --git a/src/commands/add.ts b/src/commands/add.ts index f24b14c..fdced31 100644 --- a/src/commands/add.ts +++ b/src/commands/add.ts @@ -1,8 +1,7 @@ import chalk from 'chalk' import { findSkill, discoverSkills } from '../skills.ts' -import { detectAgents, findAgent } from '../agents.ts' +import { resolveAgents } from '../agents.ts' import { installSkill } from '../installer.ts' -import type { Agent } from '../types.ts' export function addCommand( skillNames: string[], @@ -39,6 +38,14 @@ export function addCommand( process.exit(1) } + // Show detected agents when auto-detected (no explicit --agent flag) + if (!options.agent || options.agent.length === 0) { + console.log(chalk.dim(' Detected agents:')) + for (const a of targetAgents) { + console.log(` ${chalk.green('✔')} ${a.displayName}`) + } + } + const isLocal = options.local === true for (const name of names) { @@ -71,29 +78,3 @@ export function addCommand( console.log() } - -function resolveAgents(agentNames?: string[]): Agent[] { - if (agentNames && agentNames.length > 0) { - const resolved: Agent[] = [] - for (const name of agentNames) { - const agent = findAgent(name) - if (agent) { - resolved.push(agent) - } else { - console.warn(chalk.yellow(` Unknown agent: "${name}"`)) - } - } - return resolved - } - - const detected = detectAgents() - if (detected.length > 0) { - console.log(chalk.dim(' Detected agents:')) - for (const a of detected) { - console.log(` ${chalk.green('✔')} ${a.displayName}`) - } - return detected - } - - return [] -} diff --git a/src/commands/dev.ts b/src/commands/dev.ts index 7615ebd..5236e2f 100644 --- a/src/commands/dev.ts +++ b/src/commands/dev.ts @@ -1,8 +1,7 @@ import chalk from 'chalk' import { discoverSkills, findSkill } from '../skills.ts' -import { detectAgents, findAgent } from '../agents.ts' +import { resolveAgents } from '../agents.ts' import { devLink, devUnlink } from '../installer.ts' -import type { Agent } from '../types.ts' export function devLinkCommand( skillNames: string[], @@ -96,20 +95,3 @@ export function devUnlinkCommand( } } } - -function resolveAgents(agentNames?: string[]): Agent[] { - if (agentNames && agentNames.length > 0) { - const resolved: Agent[] = [] - for (const name of agentNames) { - const agent = findAgent(name) - if (agent) { - resolved.push(agent) - } else { - console.warn(chalk.yellow(` Unknown agent: "${name}"`)) - } - } - return resolved - } - - return detectAgents() -} diff --git a/src/commands/remove.ts b/src/commands/remove.ts index 04c2cbe..50069e2 100644 --- a/src/commands/remove.ts +++ b/src/commands/remove.ts @@ -1,9 +1,8 @@ import chalk from 'chalk' import { discoverSkills } from '../skills.ts' -import { detectAgents, findAgent } from '../agents.ts' +import { resolveAgents } from '../agents.ts' import { uninstallSkill } from '../installer.ts' import { confirm } from '../prompt.ts' -import type { Agent } from '../types.ts' export async function removeCommand( skillNames: string[], @@ -69,20 +68,3 @@ export async function removeCommand( console.log() } - -function resolveAgents(agentNames?: string[]): Agent[] { - if (agentNames && agentNames.length > 0) { - const resolved: Agent[] = [] - for (const name of agentNames) { - const agent = findAgent(name) - if (agent) { - resolved.push(agent) - } else { - console.warn(chalk.yellow(` Unknown agent: "${name}"`)) - } - } - return resolved - } - - return detectAgents() -} From 916ba6516bf1c5545490440bd12d773258981e77 Mon Sep 17 00:00:00 2001 From: Doug Richar Date: Tue, 10 Feb 2026 06:25:31 -0500 Subject: [PATCH 5/5] chore: add engines field and expand npm keywords Adds node >=18 engines constraint to catch incompatible runtimes early. Expands keywords with use-wallet, haystack, codex, cursor, and opencode for better npm discoverability. Co-Authored-By: Claude Opus 4.6 --- package.json | 8 ++++++++ src/commands/add.ts | 3 +-- src/commands/list.ts | 6 +----- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index ff35639..4447d87 100644 --- a/package.json +++ b/package.json @@ -28,12 +28,20 @@ "type": "git", "url": "git+https://github.com/TxnLab/skills.git" }, + "engines": { + "node": ">=18" + }, "keywords": [ "agent-skills", "algorand", "txnlab", "nfd", + "use-wallet", + "haystack", "claude-code", + "codex", + "cursor", + "opencode", "ai-agent" ], "devDependencies": { diff --git a/src/commands/add.ts b/src/commands/add.ts index fdced31..2c1cba1 100644 --- a/src/commands/add.ts +++ b/src/commands/add.ts @@ -63,8 +63,7 @@ export function addCommand( for (const agent of targetAgents) { const result = installSkill(skill, agent, { local: isLocal }) if (result.success) { - const method = - result.method === 'copy' ? chalk.dim(' (copied)') : '' + const method = result.method === 'copy' ? chalk.dim(' (copied)') : '' console.log( ` ${chalk.green('✔')} ${agent.displayName} — ${result.skillName} → ${result.targetPath}${method}`, ) diff --git a/src/commands/list.ts b/src/commands/list.ts index 74a01a8..639eb64 100644 --- a/src/commands/list.ts +++ b/src/commands/list.ts @@ -25,11 +25,7 @@ export function listCommand(): void { } console.log() - console.log( - chalk.dim( - ` Details: npx @txnlab/skills info `, - ), - ) + console.log(chalk.dim(` Details: npx @txnlab/skills info `)) console.log(chalk.dim(` Install: npx @txnlab/skills add `)) console.log() }