easymd-cli 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/easymd.js CHANGED
@@ -8,6 +8,7 @@ import { syncDir, watchDir } from '../src/cli/sync.js';
8
8
  import { autoOn, autoOff, autoStatus } from '../src/cli/auto.js';
9
9
  import { getCredentials } from '../src/cli/config.js';
10
10
  import { mcpInstall } from '../src/cli/mcp-install.js';
11
+ import { hookInstall, hookUninstall } from '../src/cli/hook-install.js';
11
12
 
12
13
  const HELP = `
13
14
  easymd — collaborate on markdown files in your repo, live with humans and AI agents
@@ -26,6 +27,8 @@ Usage:
26
27
 
27
28
  easymd mcp Run the MCP server (stdio) so AI agents can edit your docs
28
29
  easymd mcp-install [agent] Register the MCP server with Cursor / Claude (agent: --cursor | --claude-desktop)
30
+ easymd hook-install Auto-sync .md to your account after every Claude Code session (everywhere)
31
+ easymd hook-uninstall Remove the auto-sync hook
29
32
 
30
33
  easymd open <file> Open a local .md for real-time collaborative editing in the browser
31
34
  easymd open <file> --port N Use a fixed port (default: random)
@@ -164,6 +167,10 @@ async function main() {
164
167
  return;
165
168
  case 'mcp-install':
166
169
  return mcpInstall(rest[0]);
170
+ case 'hook-install':
171
+ return hookInstall();
172
+ case 'hook-uninstall':
173
+ return hookUninstall();
167
174
  case 'open':
168
175
  return cmdOpen(rest);
169
176
  default:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "easymd-cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Google Docs for markdown — collaborate on the actual .md file in your repo, live with humans and AI agents. CLI: login, auto-sync, and open .md files for real-time editing.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,64 @@
1
+ import { homedir } from 'os';
2
+ import { join } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { mkdir, readFile, writeFile, copyFile } from 'fs/promises';
5
+
6
+ const CLAUDE_DIR = join(homedir(), '.claude');
7
+ const SETTINGS = join(CLAUDE_DIR, 'settings.json');
8
+ const HOOK_PATH = fileURLToPath(new URL('./hooks/easymd-stop-sync.sh', import.meta.url));
9
+ const HOOK_CMD = `bash ${HOOK_PATH}`;
10
+ const MARKER = 'easymd-stop-sync';
11
+
12
+ async function readSettings() {
13
+ try {
14
+ return JSON.parse(await readFile(SETTINGS, 'utf8'));
15
+ } catch {
16
+ return {};
17
+ }
18
+ }
19
+ async function writeSettings(s) {
20
+ await mkdir(CLAUDE_DIR, { recursive: true });
21
+ // Back up first — never clobber an existing config silently.
22
+ try {
23
+ await copyFile(SETTINGS, `${SETTINGS}.easymd-bak`);
24
+ } catch {
25
+ /* no existing file */
26
+ }
27
+ await writeFile(SETTINGS, JSON.stringify(s, null, 2));
28
+ }
29
+
30
+ // Registers a Claude Code Stop hook so that after any session that edits .md files,
31
+ // they auto-sync to your easymd account — across every project, everywhere. Merges
32
+ // alongside any existing Stop hooks (e.g. the obsidian-wiki capture hook).
33
+ export async function hookInstall() {
34
+ const s = await readSettings();
35
+ s.hooks = s.hooks || {};
36
+ s.hooks.Stop = Array.isArray(s.hooks.Stop) ? s.hooks.Stop : [];
37
+
38
+ if (JSON.stringify(s.hooks.Stop).includes(MARKER)) {
39
+ console.log('✓ easymd auto-sync hook is already installed.');
40
+ return;
41
+ }
42
+ s.hooks.Stop.push({ matcher: '', hooks: [{ type: 'command', command: HOOK_CMD }] });
43
+ await writeSettings(s);
44
+ console.log('✓ Installed the easymd auto-sync hook in ~/.claude/settings.json');
45
+ console.log(' After any Claude Code session that edits .md files, they sync to your account.');
46
+ console.log(' Requires `easymd login`. A backup was saved to settings.json.easymd-bak.');
47
+ console.log(' Remove anytime with `easymd hook-uninstall`.');
48
+ }
49
+
50
+ export async function hookUninstall() {
51
+ const s = await readSettings();
52
+ if (!s.hooks?.Stop?.length) {
53
+ console.log('easymd auto-sync hook is not installed.');
54
+ return;
55
+ }
56
+ const before = s.hooks.Stop.length;
57
+ s.hooks.Stop = s.hooks.Stop.filter((e) => !JSON.stringify(e).includes(MARKER));
58
+ if (s.hooks.Stop.length === before) {
59
+ console.log('easymd auto-sync hook is not installed.');
60
+ return;
61
+ }
62
+ await writeSettings(s);
63
+ console.log('✓ Removed the easymd auto-sync hook.');
64
+ }
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env bash
2
+ # easymd auto-sync Stop hook (adapted from the obsidian-wiki Stop-capture pattern).
3
+ #
4
+ # Fires on the Claude Code Stop event. If the session edited any .md files and you're
5
+ # logged into easymd, it pushes those docs to your account — so your markdown stays
6
+ # updated in easymd after every agent session, everywhere, with no daemon running.
7
+ #
8
+ # Always exits 0 (silent): it runs the sync directly, it never nudges Claude.
9
+ set -euo pipefail
10
+
11
+ INPUT=$(cat)
12
+
13
+ # Need credentials — no-op if not logged in.
14
+ [[ -f "$HOME/.easymd/credentials.json" ]] || exit 0
15
+
16
+ # Pull cwd + transcript path from the hook payload.
17
+ read -r CWD TRANSCRIPT < <(printf '%s' "$INPUT" | python3 -c "
18
+ import json, sys
19
+ d = json.load(sys.stdin)
20
+ print(d.get('cwd', ''), d.get('transcript_path', ''))
21
+ " 2>/dev/null || echo " ")
22
+ [[ -n "$TRANSCRIPT" && -f "$TRANSCRIPT" ]] || exit 0
23
+
24
+ # Did this session write/edit any .md files?
25
+ EDITED_MD=$(python3 - "$TRANSCRIPT" <<'PYEOF'
26
+ import json, sys
27
+ n = 0
28
+ for line in open(sys.argv[1]):
29
+ line = line.strip()
30
+ if not line:
31
+ continue
32
+ try:
33
+ e = json.loads(line)
34
+ except json.JSONDecodeError:
35
+ continue
36
+ m = e.get("message") or {}
37
+ if m.get("role") != "assistant":
38
+ continue
39
+ for b in m.get("content") or []:
40
+ if isinstance(b, dict) and b.get("type") == "tool_use" and b.get("name") in ("Write", "Edit", "NotebookEdit"):
41
+ fp = (b.get("input") or {}).get("file_path", "")
42
+ if fp.endswith(".md"):
43
+ n += 1
44
+ print(n)
45
+ PYEOF
46
+ )
47
+ [[ "${EDITED_MD:-0}" -ge 1 ]] || exit 0
48
+
49
+ # Resolve the easymd CLI from this script's own package (no PATH dependency).
50
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
51
+ EASYMD_BIN="$SCRIPT_DIR/../../../bin/easymd.js"
52
+ [[ -f "$EASYMD_BIN" ]] || exit 0
53
+
54
+ cd "${CWD:-$PWD}" 2>/dev/null || exit 0
55
+ node "$EASYMD_BIN" sync . --quiet >/dev/null 2>&1 || true
56
+ exit 0