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
Binary file added hackmd-cli.skill
Binary file not shown.
155 changes: 155 additions & 0 deletions hackmd-cli/SKILL.md
Original file line number Diff line number Diff line change
@@ -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=<id>

# 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=<id> --content='# New Content'

# Delete note
hackmd-cli notes delete --noteId=<id>
```

### Team Notes

```bash
# List team notes
hackmd-cli team-notes --teamPath=<team-path>

# Create team note
hackmd-cli team-notes create --teamPath=<team-path> --content='# Team Doc'

# Update team note
hackmd-cli team-notes update --teamPath=<team-path> --noteId=<id> --content='# Updated'

# Delete team note
hackmd-cli team-notes delete --teamPath=<team-path> --noteId=<id>
```

### Teams & History

```bash
hackmd-cli teams # List accessible teams
hackmd-cli history # List browsing history
```

### Export

```bash
hackmd-cli export --noteId=<id> # 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=<id>
```

### Export note to local file

```bash
hackmd-cli export --noteId=<id> > 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
```
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
150 changes: 150 additions & 0 deletions scripts/package-skill.mjs
Original file line number Diff line number Diff line change
@@ -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;
}
23 changes: 23 additions & 0 deletions scripts/watch-skill.mjs
Original file line number Diff line number Diff line change
@@ -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);
}
});