diff --git a/hackmd-cli.skill b/hackmd-cli.skill new file mode 100644 index 0000000..be0cb01 Binary files /dev/null and b/hackmd-cli.skill differ diff --git a/hackmd-cli/SKILL.md b/hackmd-cli/SKILL.md new file mode 100644 index 0000000..d1f8ed4 --- /dev/null +++ b/hackmd-cli/SKILL.md @@ -0,0 +1,155 @@ +--- +name: hackmd-cli +description: HackMD command-line interface for managing notes and team notes. Use this skill when users want to create, read, update, delete, or export HackMD notes via CLI, manage team notes, list teams, view browsing history, or automate HackMD workflows. Triggers on mentions of hackmd-cli, HackMD CLI, or requests to interact with HackMD notes programmatically. +--- + +# HackMD CLI + +Command-line tool for managing HackMD notes and team notes via the HackMD API. + +## Setup + +### Install + +```bash +npm install -g @hackmd/hackmd-cli +``` + +### Configure Access Token + +Create an API token at [hackmd.io/settings#api](https://hackmd.io/settings#api), then configure: + +```bash +# Interactive login (saves to ~/.hackmd/config.json) +hackmd-cli login + +# Or via environment variable +export HMD_API_ACCESS_TOKEN=YOUR_TOKEN +``` + +For HackMD EE instances, also set the API endpoint: + +```bash +export HMD_API_ENDPOINT_URL=https://your.hackmd-ee.endpoint +``` + +## Commands + +### Authentication + +```bash +hackmd-cli login # Set access token interactively +hackmd-cli logout # Clear stored credentials +hackmd-cli whoami # Show current user info +``` + +### Personal Notes + +```bash +# List all notes +hackmd-cli notes + +# Get specific note +hackmd-cli notes --noteId= + +# Create note +hackmd-cli notes create --content='# Title' --title='My Note' +hackmd-cli notes create --readPermission=owner --writePermission=owner + +# Create from file/stdin +cat README.md | hackmd-cli notes create + +# Create with editor +hackmd-cli notes create -e + +# Update note +hackmd-cli notes update --noteId= --content='# New Content' + +# Delete note +hackmd-cli notes delete --noteId= +``` + +### Team Notes + +```bash +# List team notes +hackmd-cli team-notes --teamPath= + +# Create team note +hackmd-cli team-notes create --teamPath= --content='# Team Doc' + +# Update team note +hackmd-cli team-notes update --teamPath= --noteId= --content='# Updated' + +# Delete team note +hackmd-cli team-notes delete --teamPath= --noteId= +``` + +### Teams & History + +```bash +hackmd-cli teams # List accessible teams +hackmd-cli history # List browsing history +``` + +### Export + +```bash +hackmd-cli export --noteId= # Export note content to stdout +``` + +## Permissions + +Available permission values: + +| Permission Type | Values | +|----------------|--------| +| `--readPermission` | `owner`, `signed_in`, `guest` | +| `--writePermission` | `owner`, `signed_in`, `guest` | +| `--commentPermission` | `disabled`, `forbidden`, `owners`, `signed_in_users`, `everyone` | + +## Output Formats + +All list commands support: + +```bash +--output=json # JSON output +--output=yaml # YAML output +--output=csv # CSV output (or --csv) +--no-header # Hide table headers +--no-truncate # Don't truncate long values +--columns=id,title # Show specific columns +--filter=name=foo # Filter by property +--sort=title # Sort by property (prepend '-' for descending) +-x, --extended # Show additional columns +``` + +## Common Workflows + +### Sync local file to HackMD + +```bash +# Create new note from file +cat doc.md | hackmd-cli notes create --title="My Doc" + +# Update existing note from file +cat doc.md | hackmd-cli notes update --noteId= +``` + +### Export note to local file + +```bash +hackmd-cli export --noteId= > note.md +``` + +### List notes as JSON for scripting + +```bash +hackmd-cli notes --output=json | jq '.[] | .id' +``` + +### Find note by title + +```bash +hackmd-cli notes --filter=title=README +``` diff --git a/package.json b/package.json index 85c4f5a..7f4451e 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,9 @@ "test": "mocha --forbid-only \"test/**/*.test.ts\" --exclude \"test/smoke/**/*\"", "test:unit": "mocha --forbid-only \"test/**/*.test.ts\" --exclude \"test/integration/**/*\" --exclude \"test/smoke/**/*\"", "test:smoke": "pnpm run build && mocha --forbid-only \"test/smoke/**/*.test.ts\"", - "version": "oclif readme && git add README.md" + "version": "oclif readme && git add README.md", + "skill:package": "node scripts/package-skill.mjs", + "skill:watch": "node scripts/watch-skill.mjs" }, "types": "dist/index.d.ts" } diff --git a/scripts/package-skill.mjs b/scripts/package-skill.mjs new file mode 100644 index 0000000..41ea67e --- /dev/null +++ b/scripts/package-skill.mjs @@ -0,0 +1,150 @@ +#!/usr/bin/env node +/** + * Package the hackmd-cli skill into a .skill file (zip format) + * + * Usage: node scripts/package-skill.mjs + */ + +import {execSync} from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; +import {fileURLToPath} from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const SKILL_DIR = path.join(__dirname, '..', 'hackmd-cli'); +const SKILL_MD = path.join(SKILL_DIR, 'SKILL.md'); +const OUTPUT_FILE = path.join(__dirname, '..', 'hackmd-cli.skill'); + +function validateSkill() { + // Check SKILL.md exists + if (!fs.existsSync(SKILL_MD)) { + throw new Error('SKILL.md not found in hackmd-cli/'); + } + + const content = fs.readFileSync(SKILL_MD, 'utf8'); + + // Check frontmatter exists + if (!content.startsWith('---')) { + throw new Error('No YAML frontmatter found'); + } + + // Extract frontmatter + const match = content.match(/^---\n([\s\S]*?)\n---/); + if (!match) { + throw new Error('Invalid frontmatter format'); + } + + const frontmatter = match[1]; + + // Simple YAML parsing for required fields + const nameMatch = frontmatter.match(/^name:\s*(.+)$/m); + const descMatch = frontmatter.match(/^description:\s*(.+)$/m); + + if (!nameMatch) { + throw new Error("Missing 'name' in frontmatter"); + } + + if (!descMatch) { + throw new Error("Missing 'description' in frontmatter"); + } + + const name = nameMatch[1].trim(); + const description = descMatch[1].trim(); + + // Validate name format (hyphen-case) + if (!/^[a-z0-9-]+$/.test(name)) { + throw new Error(`Name '${name}' should be hyphen-case (lowercase letters, digits, and hyphens only)`); + } + + if (name.startsWith('-') || name.endsWith('-') || name.includes('--')) { + throw new Error(`Name '${name}' cannot start/end with hyphen or contain consecutive hyphens`); + } + + if (name.length > 64) { + throw new Error(`Name is too long (${name.length} characters). Maximum is 64 characters.`); + } + + // Validate description + if (description.includes('<') || description.includes('>')) { + throw new Error('Description cannot contain angle brackets (< or >)'); + } + + if (description.length > 1024) { + throw new Error(`Description is too long (${description.length} characters). Maximum is 1024 characters.`); + } + + return {description, name}; +} + +function packageSkill() { + console.log('šŸ“¦ Packaging skill: hackmd-cli\n'); + + // Validate + console.log('šŸ” Validating skill...'); + try { + validateSkill(); + console.log('āœ… Skill is valid!\n'); + } catch (error) { + console.error(`āŒ Validation failed: ${error.message}`); + throw error; + } + + // Remove existing .skill file + if (fs.existsSync(OUTPUT_FILE)) { + fs.unlinkSync(OUTPUT_FILE); + } + + // Create zip file using system zip command + try { + // Get all files in skill directory + const files = getAllFiles(SKILL_DIR); + + if (files.length === 0) { + throw new Error('No files found in skill directory'); + } + + // Use zip command from parent directory to maintain folder structure + const parentDir = path.dirname(SKILL_DIR); + const skillDirName = path.basename(SKILL_DIR); + + execSync(`zip -r "${OUTPUT_FILE}" "${skillDirName}"`, { + cwd: parentDir, + stdio: 'pipe', + }); + + // List what was added + for (const file of files) { + const relative = path.relative(parentDir, file); + console.log(` Added: ${relative}`); + } + + console.log(`\nāœ… Successfully packaged skill to: ${OUTPUT_FILE}`); + } catch (error) { + console.error(`āŒ Error creating .skill file: ${error.message}`); + throw error; + } +} + +function getAllFiles(dir) { + const files = []; + const entries = fs.readdirSync(dir, {withFileTypes: true}); + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + files.push(...getAllFiles(fullPath)); + } else { + files.push(fullPath); + } + } + + return files; +} + +try { + packageSkill(); +} catch { + process.exitCode = 1; +} diff --git a/scripts/watch-skill.mjs b/scripts/watch-skill.mjs new file mode 100644 index 0000000..a544f37 --- /dev/null +++ b/scripts/watch-skill.mjs @@ -0,0 +1,23 @@ +#!/usr/bin/env node +/** + * Watch for changes in hackmd-cli/ and auto-rebuild the .skill file + * + * Usage: node scripts/watch-skill.mjs + */ + +import {execSync} from 'node:child_process'; +import {watch} from 'node:fs'; + +const SKILL_DIR = './hackmd-cli'; + +console.log(`šŸ‘€ Watching ${SKILL_DIR} for changes...`); +console.log('Press Ctrl+C to stop\n'); + +watch(SKILL_DIR, {recursive: true}, (eventType, filename) => { + console.log(`Change detected: ${filename}`); + try { + execSync('npm run skill:package', {stdio: 'inherit'}); + } catch (error) { + console.error('Error packaging skill:', error.message); + } +});