Skip to content

Commit 05c7a3d

Browse files
committed
feat: add skill for hackmd-cli
1 parent 9122d5f commit 05c7a3d

File tree

5 files changed

+331
-1
lines changed

5 files changed

+331
-1
lines changed

hackmd-cli.skill

1.66 KB
Binary file not shown.

hackmd-cli/SKILL.md

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
---
2+
name: hackmd-cli
3+
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.
4+
---
5+
6+
# HackMD CLI
7+
8+
Command-line tool for managing HackMD notes and team notes via the HackMD API.
9+
10+
## Setup
11+
12+
### Install
13+
14+
```bash
15+
npm install -g @hackmd/hackmd-cli
16+
```
17+
18+
### Configure Access Token
19+
20+
Create an API token at [hackmd.io/settings#api](https://hackmd.io/settings#api), then configure:
21+
22+
```bash
23+
# Interactive login (saves to ~/.hackmd/config.json)
24+
hackmd-cli login
25+
26+
# Or via environment variable
27+
export HMD_API_ACCESS_TOKEN=YOUR_TOKEN
28+
```
29+
30+
For HackMD EE instances, also set the API endpoint:
31+
32+
```bash
33+
export HMD_API_ENDPOINT_URL=https://your.hackmd-ee.endpoint
34+
```
35+
36+
## Commands
37+
38+
### Authentication
39+
40+
```bash
41+
hackmd-cli login # Set access token interactively
42+
hackmd-cli logout # Clear stored credentials
43+
hackmd-cli whoami # Show current user info
44+
```
45+
46+
### Personal Notes
47+
48+
```bash
49+
# List all notes
50+
hackmd-cli notes
51+
52+
# Get specific note
53+
hackmd-cli notes --noteId=<id>
54+
55+
# Create note
56+
hackmd-cli notes create --content='# Title' --title='My Note'
57+
hackmd-cli notes create --readPermission=owner --writePermission=owner
58+
59+
# Create from file/stdin
60+
cat README.md | hackmd-cli notes create
61+
62+
# Create with editor
63+
hackmd-cli notes create -e
64+
65+
# Update note
66+
hackmd-cli notes update --noteId=<id> --content='# New Content'
67+
68+
# Delete note
69+
hackmd-cli notes delete --noteId=<id>
70+
```
71+
72+
### Team Notes
73+
74+
```bash
75+
# List team notes
76+
hackmd-cli team-notes --teamPath=<team-path>
77+
78+
# Create team note
79+
hackmd-cli team-notes create --teamPath=<team-path> --content='# Team Doc'
80+
81+
# Update team note
82+
hackmd-cli team-notes update --teamPath=<team-path> --noteId=<id> --content='# Updated'
83+
84+
# Delete team note
85+
hackmd-cli team-notes delete --teamPath=<team-path> --noteId=<id>
86+
```
87+
88+
### Teams & History
89+
90+
```bash
91+
hackmd-cli teams # List accessible teams
92+
hackmd-cli history # List browsing history
93+
```
94+
95+
### Export
96+
97+
```bash
98+
hackmd-cli export --noteId=<id> # Export note content to stdout
99+
```
100+
101+
## Permissions
102+
103+
Available permission values:
104+
105+
| Permission Type | Values |
106+
|----------------|--------|
107+
| `--readPermission` | `owner`, `signed_in`, `guest` |
108+
| `--writePermission` | `owner`, `signed_in`, `guest` |
109+
| `--commentPermission` | `disabled`, `forbidden`, `owners`, `signed_in_users`, `everyone` |
110+
111+
## Output Formats
112+
113+
All list commands support:
114+
115+
```bash
116+
--output=json # JSON output
117+
--output=yaml # YAML output
118+
--output=csv # CSV output (or --csv)
119+
--no-header # Hide table headers
120+
--no-truncate # Don't truncate long values
121+
--columns=id,title # Show specific columns
122+
--filter=name=foo # Filter by property
123+
--sort=title # Sort by property (prepend '-' for descending)
124+
-x, --extended # Show additional columns
125+
```
126+
127+
## Common Workflows
128+
129+
### Sync local file to HackMD
130+
131+
```bash
132+
# Create new note from file
133+
cat doc.md | hackmd-cli notes create --title="My Doc"
134+
135+
# Update existing note from file
136+
cat doc.md | hackmd-cli notes update --noteId=<id>
137+
```
138+
139+
### Export note to local file
140+
141+
```bash
142+
hackmd-cli export --noteId=<id> > note.md
143+
```
144+
145+
### List notes as JSON for scripting
146+
147+
```bash
148+
hackmd-cli notes --output=json | jq '.[] | .id'
149+
```
150+
151+
### Find note by title
152+
153+
```bash
154+
hackmd-cli notes --filter=title=README
155+
```

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,9 @@
7777
"test": "mocha --forbid-only \"test/**/*.test.ts\" --exclude \"test/smoke/**/*\"",
7878
"test:unit": "mocha --forbid-only \"test/**/*.test.ts\" --exclude \"test/integration/**/*\" --exclude \"test/smoke/**/*\"",
7979
"test:smoke": "pnpm run build && mocha --forbid-only \"test/smoke/**/*.test.ts\"",
80-
"version": "oclif readme && git add README.md"
80+
"version": "oclif readme && git add README.md",
81+
"skill:package": "node scripts/package-skill.mjs",
82+
"skill:watch": "node scripts/watch-skill.mjs"
8183
},
8284
"types": "dist/index.d.ts"
8385
}

scripts/package-skill.mjs

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Package the hackmd-cli skill into a .skill file (zip format)
4+
*
5+
* Usage: node scripts/package-skill.mjs
6+
*/
7+
8+
import {execSync} from 'node:child_process';
9+
import fs from 'node:fs';
10+
import path from 'node:path';
11+
import {fileURLToPath} from 'node:url';
12+
13+
const __filename = fileURLToPath(import.meta.url);
14+
const __dirname = path.dirname(__filename);
15+
16+
const SKILL_DIR = path.join(__dirname, '..', 'hackmd-cli');
17+
const SKILL_MD = path.join(SKILL_DIR, 'SKILL.md');
18+
const OUTPUT_FILE = path.join(__dirname, '..', 'hackmd-cli.skill');
19+
20+
function validateSkill() {
21+
// Check SKILL.md exists
22+
if (!fs.existsSync(SKILL_MD)) {
23+
throw new Error('SKILL.md not found in hackmd-cli/');
24+
}
25+
26+
const content = fs.readFileSync(SKILL_MD, 'utf8');
27+
28+
// Check frontmatter exists
29+
if (!content.startsWith('---')) {
30+
throw new Error('No YAML frontmatter found');
31+
}
32+
33+
// Extract frontmatter
34+
const match = content.match(/^---\n([\s\S]*?)\n---/);
35+
if (!match) {
36+
throw new Error('Invalid frontmatter format');
37+
}
38+
39+
const frontmatter = match[1];
40+
41+
// Simple YAML parsing for required fields
42+
const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
43+
const descMatch = frontmatter.match(/^description:\s*(.+)$/m);
44+
45+
if (!nameMatch) {
46+
throw new Error("Missing 'name' in frontmatter");
47+
}
48+
49+
if (!descMatch) {
50+
throw new Error("Missing 'description' in frontmatter");
51+
}
52+
53+
const name = nameMatch[1].trim();
54+
const description = descMatch[1].trim();
55+
56+
// Validate name format (hyphen-case)
57+
if (!/^[a-z0-9-]+$/.test(name)) {
58+
throw new Error(`Name '${name}' should be hyphen-case (lowercase letters, digits, and hyphens only)`);
59+
}
60+
61+
if (name.startsWith('-') || name.endsWith('-') || name.includes('--')) {
62+
throw new Error(`Name '${name}' cannot start/end with hyphen or contain consecutive hyphens`);
63+
}
64+
65+
if (name.length > 64) {
66+
throw new Error(`Name is too long (${name.length} characters). Maximum is 64 characters.`);
67+
}
68+
69+
// Validate description
70+
if (description.includes('<') || description.includes('>')) {
71+
throw new Error('Description cannot contain angle brackets (< or >)');
72+
}
73+
74+
if (description.length > 1024) {
75+
throw new Error(`Description is too long (${description.length} characters). Maximum is 1024 characters.`);
76+
}
77+
78+
return {description, name};
79+
}
80+
81+
function packageSkill() {
82+
console.log('📦 Packaging skill: hackmd-cli\n');
83+
84+
// Validate
85+
console.log('🔍 Validating skill...');
86+
try {
87+
validateSkill();
88+
console.log('✅ Skill is valid!\n');
89+
} catch (error) {
90+
console.error(`❌ Validation failed: ${error.message}`);
91+
throw error;
92+
}
93+
94+
// Remove existing .skill file
95+
if (fs.existsSync(OUTPUT_FILE)) {
96+
fs.unlinkSync(OUTPUT_FILE);
97+
}
98+
99+
// Create zip file using system zip command
100+
try {
101+
// Get all files in skill directory
102+
const files = getAllFiles(SKILL_DIR);
103+
104+
if (files.length === 0) {
105+
throw new Error('No files found in skill directory');
106+
}
107+
108+
// Use zip command from parent directory to maintain folder structure
109+
const parentDir = path.dirname(SKILL_DIR);
110+
const skillDirName = path.basename(SKILL_DIR);
111+
112+
execSync(`zip -r "${OUTPUT_FILE}" "${skillDirName}"`, {
113+
cwd: parentDir,
114+
stdio: 'pipe',
115+
});
116+
117+
// List what was added
118+
for (const file of files) {
119+
const relative = path.relative(parentDir, file);
120+
console.log(` Added: ${relative}`);
121+
}
122+
123+
console.log(`\n✅ Successfully packaged skill to: ${OUTPUT_FILE}`);
124+
} catch (error) {
125+
console.error(`❌ Error creating .skill file: ${error.message}`);
126+
throw error;
127+
}
128+
}
129+
130+
function getAllFiles(dir) {
131+
const files = [];
132+
const entries = fs.readdirSync(dir, {withFileTypes: true});
133+
134+
for (const entry of entries) {
135+
const fullPath = path.join(dir, entry.name);
136+
if (entry.isDirectory()) {
137+
files.push(...getAllFiles(fullPath));
138+
} else {
139+
files.push(fullPath);
140+
}
141+
}
142+
143+
return files;
144+
}
145+
146+
try {
147+
packageSkill();
148+
} catch (error) {
149+
process.exitCode = 1;
150+
}

scripts/watch-skill.mjs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Watch for changes in hackmd-cli/ and auto-rebuild the .skill file
4+
*
5+
* Usage: node scripts/watch-skill.mjs
6+
*/
7+
8+
import {watch} from 'node:fs';
9+
import {execSync} from 'node:child_process';
10+
11+
const SKILL_DIR = './hackmd-cli';
12+
13+
console.log(`👀 Watching ${SKILL_DIR} for changes...`);
14+
console.log('Press Ctrl+C to stop\n');
15+
16+
watch(SKILL_DIR, {recursive: true}, (eventType, filename) => {
17+
console.log(`Change detected: ${filename}`);
18+
try {
19+
execSync('npm run skill:package', {stdio: 'inherit'});
20+
} catch (error) {
21+
console.error('Error packaging skill:', error.message);
22+
}
23+
});

0 commit comments

Comments
 (0)