claude-home 1.8.1 → 1.8.3

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/README.md CHANGED
@@ -53,7 +53,7 @@ Direct links (`#/note/filename`) let you open a specific note instantly.
53
53
  Visual overview of all your Claude projects with session count, token usage, cost, and memory files. Drill into any project to browse its sessions, memory entries, and `CLAUDE.md` files.
54
54
 
55
55
  ### Plans
56
- Read and search your GSD/planning files from `~/.claude/plans/`. Export or publish as a Gist with one click.
56
+ Read and search your GSD/planning files from `~/.claude/plans/` (or your configured config dir). Export or publish as a Gist with one click.
57
57
 
58
58
  ### Memory
59
59
  Inspect your auto-memory entries across all projects.
@@ -68,7 +68,7 @@ Read and edit your `CLAUDE.md` files (global and per-project). Inspect permissio
68
68
 
69
69
  ## No tokens. No cloud. No tracking.
70
70
 
71
- claude-home is a local Express server that reads your `~/.claude/` directory directly. It never calls the Claude API, never sends data anywhere, and works completely offline (except for the marketplace and Gist sharing features).
71
+ claude-home is a local Express server that reads your Claude config directory (`~/.claude/` by default) directly. It never calls the Claude API, never sends data anywhere, and works completely offline (except for the marketplace and Gist sharing features).
72
72
 
73
73
  ---
74
74
 
@@ -86,15 +86,6 @@ npm install -g claude-home
86
86
 
87
87
  > Requires Node.js 18+.
88
88
 
89
- ### From source
90
-
91
- ```bash
92
- git clone https://github.com/ZenekeZene/claude-home
93
- cd claude-home
94
- npm install
95
- npm start
96
- ```
97
-
98
89
  ---
99
90
 
100
91
  ## Usage
@@ -123,10 +114,12 @@ Or toggle it directly from the **Hooks** section in the UI.
123
114
  ```
124
115
  claude-home [options]
125
116
 
126
- --port, -p <n> Port to use (default: 3141)
127
- --no-open Don't open the browser automatically
128
- --version, -v Print version
129
- --help, -h Show help
117
+ --port, -p <n> Port to use (default: 3141)
118
+ --no-open Don't open the browser automatically
119
+ --config-dir <path> Claude config directory (default: ~/.claude)
120
+ Also: CLAUDE_CONFIG_DIR env var
121
+ --version, -v Print version
122
+ --help, -h Show help
130
123
 
131
124
  Commands:
132
125
  setup-hook Add SessionStart hook + /claude-home slash command
@@ -134,11 +127,28 @@ Commands:
134
127
  stop Stop the running claude-home server
135
128
  ```
136
129
 
130
+ ### Multiple Claude installations
131
+
132
+ If you use separate Claude Code configurations (e.g. `~/.claude` for personal and `~/.claude-work` for work), you can point claude-home at any of them:
133
+
134
+ ```bash
135
+ # Via flag
136
+ claude-home --config-dir ~/.claude-work
137
+
138
+ # Via environment variable
139
+ CLAUDE_CONFIG_DIR=~/.claude-work claude-home
140
+
141
+ # Install the auto-start hook into the work config
142
+ claude-home --config-dir ~/.claude-work setup-hook
143
+ ```
144
+
145
+ The dashboard will read sessions, settings, skills, commands, agents, and plans from the specified directory. The UI will reflect the correct paths throughout.
146
+
137
147
  ---
138
148
 
139
149
  ## Claude integration (Notes & Today)
140
150
 
141
- claude-home can receive content directly from Claude during active sessions. Go to **Config → Integrations** and click **Set up** — this adds instructions to your `~/.claude/CLAUDE.md` and grants the necessary write permissions automatically.
151
+ claude-home can receive content directly from Claude during active sessions. Go to **Config → Integrations** and click **Set up** — this adds instructions to your `CLAUDE.md` and grants the necessary write permissions automatically.
142
152
 
143
153
  Once set up, from any Claude session you can say:
144
154
  - *"Save this as a note"* → creates a note in the Notes view
@@ -157,7 +167,7 @@ Publish sessions or plans as public GitHub Gists. Go to **Config → Sharing**,
157
167
 
158
168
  claude-home includes a marketplace to discover and install skills from GitHub repositories.
159
169
 
160
- On first run it comes pre-configured with the [Anthropic Official skills repo](https://github.com/anthropics/skills). You can add your own sources (including private repos) by editing `~/.claude/claude-home/marketplace.json`:
170
+ On first run it comes pre-configured with the [Anthropic Official skills repo](https://github.com/anthropics/skills). You can add your own sources (including private repos) by editing `~/.claude/claude-home/marketplace.json` (or the equivalent in your configured config dir):
161
171
 
162
172
  ```json
163
173
  {
@@ -175,7 +185,7 @@ On first run it comes pre-configured with the [Anthropic Official skills repo](h
175
185
  }
176
186
  ```
177
187
 
178
- For private repos, set your token in `~/.claude/claude-home/.env`:
188
+ For private repos, set your token in `~/.claude/claude-home/.env` (or your config dir equivalent):
179
189
 
180
190
  ```
181
191
  GITHUB_TOKEN=ghp_your_token_here
package/bin/cli.js CHANGED
@@ -9,18 +9,7 @@ const os = require('os');
9
9
 
10
10
  const pkg = require('../package.json');
11
11
 
12
- // ─── Ensure data dir exists (always, before any subcommand) ──────────────────
13
- const DATA_DIR = path.join(os.homedir(), '.claude', 'claude-home');
14
- try {
15
- if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });
16
- const marketplaceDest = path.join(DATA_DIR, 'marketplace.json');
17
- if (!fs.existsSync(marketplaceDest)) {
18
- const defaultSrc = path.join(__dirname, '..', 'marketplace.default.json');
19
- if (fs.existsSync(defaultSrc)) fs.copyFileSync(defaultSrc, marketplaceDest);
20
- }
21
- } catch { /* ignore */ }
22
-
23
- // ─── Arg parsing ─────────────────────────────────────────────────────────────
12
+ // ─── Arg parsing (must happen before path derivation) ────────────────────────
24
13
 
25
14
  const args = process.argv.slice(2);
26
15
  const subcommand = args[0];
@@ -31,9 +20,25 @@ function getFlag(name, def) {
31
20
  return args[idx + 1];
32
21
  }
33
22
 
23
+ const configDirFlag = getFlag('--config-dir', '');
24
+ if (configDirFlag) process.env.CLAUDE_CONFIG_DIR = path.resolve(configDirFlag);
25
+
34
26
  const port = parseInt(getFlag('--port', getFlag('-p', process.env.PORT || 3141)), 10);
35
27
  const noOpen = args.includes('--no-open');
36
28
 
29
+ // ─── Config directory + data dir ─────────────────────────────────────────────
30
+ const CLAUDE_DIR = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
31
+ const DATA_DIR = path.join(CLAUDE_DIR, 'claude-home');
32
+
33
+ try {
34
+ if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });
35
+ const marketplaceDest = path.join(DATA_DIR, 'marketplace.json');
36
+ if (!fs.existsSync(marketplaceDest)) {
37
+ const defaultSrc = path.join(__dirname, '..', 'marketplace.default.json');
38
+ if (fs.existsSync(defaultSrc)) fs.copyFileSync(defaultSrc, marketplaceDest);
39
+ }
40
+ } catch { /* ignore */ }
41
+
37
42
  if (args.includes('--version') || args.includes('-v')) {
38
43
  console.log(pkg.version);
39
44
  process.exit(0);
@@ -48,10 +53,12 @@ if (args.includes('--help') || args.includes('-h')) {
48
53
  claude-home setup-hook
49
54
 
50
55
  Options:
51
- --port, -p <n> Port to use (default: 3141)
52
- --no-open Don't open the browser automatically
53
- --version, -v Print version
54
- --help, -h Show help
56
+ --port, -p <n> Port to use (default: 3141)
57
+ --no-open Don't open the browser automatically
58
+ --config-dir <path> Claude config directory (default: ~/.claude)
59
+ Also: CLAUDE_CONFIG_DIR env var
60
+ --version, -v Print version
61
+ --help, -h Show help
55
62
 
56
63
  Commands:
57
64
  setup-hook Add SessionStart hook and /claude-home slash command
@@ -102,7 +109,7 @@ promptSetupHookIfNeeded().then(() => {
102
109
  // ─── Update check ────────────────────────────────────────────────────────────
103
110
 
104
111
  function checkForUpdates() {
105
- const cacheFile = path.join(os.homedir(), '.claude', 'claude-home', '.update-check');
112
+ const cacheFile = path.join(DATA_DIR, '.update-check');
106
113
  const TTL = 24 * 60 * 60 * 1000; // 24h
107
114
 
108
115
  try {
@@ -149,8 +156,8 @@ function isPortFree(p) {
149
156
  }
150
157
 
151
158
  function promptSetupHookIfNeeded() {
152
- const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
153
- const sentinelPath = path.join(os.homedir(), '.claude', 'claude-home', '.hook-prompted');
159
+ const settingsPath = path.join(CLAUDE_DIR, 'settings.json');
160
+ const sentinelPath = path.join(DATA_DIR, '.hook-prompted');
154
161
 
155
162
  // Already prompted before, or hook already set up
156
163
  if (fs.existsSync(sentinelPath)) return Promise.resolve();
@@ -186,8 +193,8 @@ function openBrowser(url) {
186
193
  }
187
194
 
188
195
  function setupHook() {
189
- const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
190
- const commandsDir = path.join(os.homedir(), '.claude', 'commands');
196
+ const settingsPath = path.join(CLAUDE_DIR, 'settings.json');
197
+ const commandsDir = path.join(CLAUDE_DIR, 'commands');
191
198
  const commandPath = path.join(commandsDir, 'claude-home.md');
192
199
  const hookCommand = `lsof -ti:${port} >/dev/null 2>&1 || (claude-home --no-open &>/dev/null &)`;
193
200
 
@@ -235,9 +242,9 @@ function setupHook() {
235
242
  }
236
243
 
237
244
  function removeHook() {
238
- const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
239
- const commandPath = path.join(os.homedir(), '.claude', 'commands', 'claude-home.md');
240
- const sentinelPath = path.join(os.homedir(), '.claude', 'claude-home', '.hook-prompted');
245
+ const settingsPath = path.join(CLAUDE_DIR, 'settings.json');
246
+ const commandPath = path.join(CLAUDE_DIR, 'commands', 'claude-home.md');
247
+ const sentinelPath = path.join(DATA_DIR, '.hook-prompted');
241
248
 
242
249
  // Remove SessionStart hook
243
250
  let removed = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-home",
3
- "version": "1.8.1",
3
+ "version": "1.8.3",
4
4
  "description": "Web dashboard for Claude Code — browse sessions, manage skills, hooks, commands, and agents",
5
5
  "main": "server.js",
6
6
  "bin": {
@@ -14,7 +14,12 @@
14
14
  "README.md"
15
15
  ],
16
16
  "scripts": {
17
- "start": "node server.js"
17
+ "start": "node server.js",
18
+ "dev": "vite",
19
+ "build": "vite build",
20
+ "preview": "vite preview",
21
+ "test": "vitest run",
22
+ "test:watch": "vitest"
18
23
  },
19
24
  "engines": {
20
25
  "node": ">=18"
@@ -35,6 +40,20 @@
35
40
  "author": "ZenekeZene",
36
41
  "license": "MIT",
37
42
  "dependencies": {
38
- "express": "^4.18.2"
43
+ "@tanstack/react-query": "^5.96.2",
44
+ "express": "^4.18.2",
45
+ "highlight.js": "^11.11.1",
46
+ "marked": "^17.0.6",
47
+ "react": "^18.3.1",
48
+ "react-dom": "^18.3.1",
49
+ "react-router-dom": "^6.30.3"
50
+ },
51
+ "devDependencies": {
52
+ "@testing-library/jest-dom": "^6.9.1",
53
+ "@testing-library/react": "^16.3.2",
54
+ "@testing-library/user-event": "^14.6.1",
55
+ "@vitejs/plugin-react": "^6.0.1",
56
+ "jsdom": "^29.0.2",
57
+ "vitest": "^3.2.4"
39
58
  }
40
59
  }
package/public/index.html CHANGED
@@ -2182,7 +2182,7 @@
2182
2182
  </div>
2183
2183
  </div>
2184
2184
 
2185
- <template x-if="showToday || showInsights || showNotes || showTemplates">
2185
+ <template x-if="showToday || showInsights || showNotes || showTemplates || showWebmd">
2186
2186
  <div>
2187
2187
  <div class="nav-divider"></div>
2188
2188
  <div class="nav-section-label" @click="widgetsOpen=!widgetsOpen;localStorage.setItem('cs:widgetsOpen',widgetsOpen)" style="cursor:pointer;display:flex;align-items:center;justify-content:space-between;padding-right:12px;user-select:none">
@@ -2235,6 +2235,19 @@
2235
2235
  <span class="nav-text">Prompts</span>
2236
2236
  <span class="nav-count" x-show="templates.length>0" x-text="templates.length"></span>
2237
2237
  </div>
2238
+ <div x-show="showWebmd" class="nav-item nav-widget" :class="{ active: view === 'webmd' }" @click="view='webmd';selectedSession=null;loadWebmd()">
2239
+ <span class="nav-icon">
2240
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5">
2241
+ <path d="M9 2H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V6z"/>
2242
+ <polyline points="9 2 9 6 13 6"/>
2243
+ <line x1="5" y1="9" x2="11" y2="9"/>
2244
+ <line x1="5" y1="12" x2="8" y2="12"/>
2245
+ <path d="M10.5 11.5 L12 10 L13.5 11.5" stroke-linecap="round"/>
2246
+ </svg>
2247
+ </span>
2248
+ <span class="nav-text">URL to MD</span>
2249
+ <span class="nav-count" x-show="webmdItems.length>0" x-text="webmdItems.length"></span>
2250
+ </div>
2238
2251
  </div>
2239
2252
  </div>
2240
2253
  </template>
@@ -4141,14 +4154,14 @@
4141
4154
  </div>
4142
4155
  </template>
4143
4156
  </div>
4144
- <span class="section-sub">Implementation plans Claude saves to ~/.claude/plans</span>
4157
+ <span class="section-sub" x-text="'Implementation plans Claude saves to ' + (status?.claudeDir || '~/.claude') + '/plans'"></span>
4145
4158
  <div style="flex:1;overflow:hidden;">
4146
4159
  <template x-if="plansLoading"><div class="loading"><div class="spinner"></div> Loading…</div></template>
4147
4160
  <template x-if="!plansLoading && plans.length === 0">
4148
4161
  <div class="empty">
4149
4162
  <div class="empty-mark"><svg width="20" height="20" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2" y="2" width="12" height="12"/><line x1="5" y1="6" x2="11" y2="6"/><line x1="5" y1="9" x2="9" y2="9"/></svg></div>
4150
4163
  <div style="font-size:13px;color:var(--ink-2);font-weight:500;margin-bottom:4px">No plans yet</div>
4151
- <div style="font-size:12px;color:var(--ink-4);max-width:300px;text-align:center;line-height:1.5">When Claude creates a plan, it saves a Markdown file to <code style="font-size:11px;background:var(--canvas-2);padding:1px 4px;border-radius:3px">~/.claude/plans/</code>. Plans will appear here automatically.</div>
4164
+ <div style="font-size:12px;color:var(--ink-4);max-width:300px;text-align:center;line-height:1.5">When Claude creates a plan, it saves a Markdown file to <code style="font-size:11px;background:var(--canvas-2);padding:1px 4px;border-radius:3px" x-text="(status?.claudeDir || '~/.claude') + '/plans/'"></code>. Plans will appear here automatically.</div>
4152
4165
  </div>
4153
4166
  </template>
4154
4167
  <template x-if="!plansLoading && plans.length > 0 && filteredPlans().length === 0">
@@ -5141,7 +5154,7 @@ Los **agents**, **slash commands** (\`/spec\`, \`/plan\`, \`/build\`, \`/test\`,
5141
5154
  </template>
5142
5155
  </select>
5143
5156
  </div>
5144
- <span class="section-sub">Subagents Claude can delegate tasks to — stored in ~/.claude/agents</span>
5157
+ <span class="section-sub" x-text="'Subagents Claude can delegate tasks to — stored in ' + (status?.claudeDir || '~/.claude') + '/agents'"></span>
5145
5158
  <!-- Tabs -->
5146
5159
  <div class="tools-tabs" style="border-bottom:1px solid var(--rule);flex-shrink:0">
5147
5160
  <div class="tools-tab" :class="{active:agentsTab==='marketplace'}" @click="agentsTab='marketplace';loadAgentsMarketplace();loadAgentsMarketplaceSources()">
@@ -5598,6 +5611,97 @@ Los **agents**, **slash commands** (\`/spec\`, \`/plan\`, \`/build\`, \`/test\`,
5598
5611
  </div>
5599
5612
  </template>
5600
5613
 
5614
+ <!-- URL to Markdown widget -->
5615
+ <template x-if="view === 'webmd' && !selectedSession">
5616
+ <div style="display:flex;flex-direction:column;height:100%;overflow:hidden;">
5617
+ <div class="topbar">
5618
+ <span class="topbar-title">URL to MD</span>
5619
+ <span style="font-size:11px;color:var(--ink-3)" x-show="webmdItems.length>0" x-text="webmdItems.length + ' saved'"></span>
5620
+ </div>
5621
+ <div style="flex:1;overflow-y:auto;padding:20px;display:flex;flex-direction:column;gap:16px">
5622
+
5623
+ <!-- Input area -->
5624
+ <div style="display:flex;gap:8px">
5625
+ <input type="text" x-model="webmdUrl" placeholder="https://example.com/article" @keydown.enter="fetchWebmd()"
5626
+ style="flex:1;font-size:13px;padding:6px 10px;border:1px solid var(--rule-2);border-radius:6px;background:var(--white);color:var(--ink)">
5627
+ <button class="btn btn-primary btn-sm" @click="fetchWebmd()" :disabled="webmdLoading||!webmdUrl.trim()">
5628
+ <span x-show="!webmdLoading">Convertir</span>
5629
+ <span x-show="webmdLoading" style="display:flex;align-items:center;gap:5px"><span class="spinner" style="width:12px;height:12px;border-width:2px"></span>Cargando…</span>
5630
+ </button>
5631
+ </div>
5632
+ <!-- Options -->
5633
+ <div style="display:flex;gap:16px;flex-wrap:wrap">
5634
+ <label style="display:flex;align-items:center;gap:5px;font-size:12px;color:var(--ink-2);cursor:pointer;user-select:none">
5635
+ <input type="checkbox" x-model="webmdOptTitle" @change="localStorage.setItem('cs:webmdOptTitle',webmdOptTitle)">
5636
+ Include Title
5637
+ </label>
5638
+ <label style="display:flex;align-items:center;gap:5px;font-size:12px;color:var(--ink-2);cursor:pointer;user-select:none">
5639
+ <input type="checkbox" x-model="webmdOptLinks" @change="localStorage.setItem('cs:webmdOptLinks',webmdOptLinks)">
5640
+ Ignore Links
5641
+ </label>
5642
+ <label style="display:flex;align-items:center;gap:5px;font-size:12px;color:var(--ink-2);cursor:pointer;user-select:none">
5643
+ <input type="checkbox" x-model="webmdOptClean" @change="localStorage.setItem('cs:webmdOptClean',webmdOptClean)">
5644
+ Clean / Filter
5645
+ </label>
5646
+ </div>
5647
+ <div x-show="webmdError" style="font-size:12px;color:var(--red);padding:6px 10px;background:rgba(220,50,50,.07);border-radius:5px" x-text="webmdError"></div>
5648
+
5649
+ <!-- Result -->
5650
+ <template x-if="webmdResult">
5651
+ <div style="display:flex;flex-direction:column;gap:10px">
5652
+ <div style="display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap">
5653
+ <div style="font-size:13px;font-weight:600;color:var(--ink);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" x-text="webmdResult.title"></div>
5654
+ <div style="display:flex;gap:6px;flex-shrink:0">
5655
+ <button class="btn btn-outline btn-sm" @click="webmdShowRaw=!webmdShowRaw" x-text="webmdShowRaw ? 'Ver preview' : 'Ver raw'"></button>
5656
+ <button class="btn btn-outline btn-sm" @click="copyWebmd()" title="Copiar Markdown">
5657
+ <svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5">
5658
+ <rect x="5" y="5" width="9" height="9" rx="1"/>
5659
+ <path d="M3 11H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v1"/>
5660
+ </svg>
5661
+ Copiar MD
5662
+ </button>
5663
+ <button class="btn btn-primary btn-sm" @click="saveWebmd()">Guardar</button>
5664
+ </div>
5665
+ </div>
5666
+ <!-- Raw view -->
5667
+ <template x-if="webmdShowRaw">
5668
+ <pre style="background:var(--canvas);border:1px solid var(--rule);border-radius:6px;padding:12px;font-size:12px;overflow-y:auto;white-space:pre-wrap;word-break:break-word;color:var(--ink-2);margin:0;height:calc(100vh - 300px)" x-text="processedWebmd()"></pre>
5669
+ </template>
5670
+ <!-- Rendered view -->
5671
+ <template x-if="!webmdShowRaw">
5672
+ <div class="markdown-body" style="font-size:13px;line-height:1.6;overflow-y:auto;padding:12px;border:1px solid var(--rule);border-radius:6px;background:var(--white);flex:1;min-height:0" x-html="marked.parse(processedWebmd())"></div>
5673
+ </template>
5674
+ </div>
5675
+ </template>
5676
+
5677
+ <!-- Saved items -->
5678
+ <template x-if="webmdItems.length > 0">
5679
+ <div style="border-top:1px solid var(--rule);padding-top:16px;display:flex;flex-direction:column;gap:8px">
5680
+ <div style="font-size:11px;font-weight:600;color:var(--ink-3);text-transform:uppercase;letter-spacing:0.05em">Guardadas</div>
5681
+ <template x-for="item in webmdItems" :key="item.slug">
5682
+ <div style="padding:10px 12px;border:1px solid var(--rule);border-radius:6px;background:var(--white);display:flex;align-items:center;gap:10px">
5683
+ <div style="flex:1;min-width:0">
5684
+ <div style="font-size:13px;font-weight:500;color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap" x-text="item.title"></div>
5685
+ <div style="font-size:11px;color:var(--ink-3);margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" x-text="item.url"></div>
5686
+ <div style="font-size:11px;color:var(--ink-3);margin-top:1px" x-text="new Date(item.savedAt).toLocaleDateString('es-ES',{day:'2-digit',month:'short',year:'numeric'})"></div>
5687
+ </div>
5688
+ <div style="display:flex;gap:5px;flex-shrink:0">
5689
+ <button class="btn btn-outline btn-sm" @click="webmdResult={title:item.title,markdown:item.markdown};webmdUrl=item.url;webmdShowRaw=false">Ver</button>
5690
+ <button class="btn btn-outline btn-sm" @click="navigator.clipboard.writeText(item.markdown)" title="Copiar">
5691
+ <svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="5" y="5" width="9" height="9" rx="1"/><path d="M3 11H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v1"/></svg>
5692
+ </button>
5693
+ <button class="btn btn-outline btn-sm" style="color:var(--red)" @click="deleteWebmd(item.slug)">×</button>
5694
+ </div>
5695
+ </div>
5696
+ </template>
5697
+ </div>
5698
+ </template>
5699
+ <div x-show="webmdItemsLoading" class="loading"><div class="spinner"></div> Cargando…</div>
5700
+
5701
+ </div>
5702
+ </div>
5703
+ </template>
5704
+
5601
5705
  <!-- Commands / Skills / Config -->
5602
5706
  <template x-if="(view === 'commands' || view === 'skills' || view === 'config' || view === 'instructions' || view === 'permissions' || view === 'hooks') && !selectedSession">
5603
5707
  <div style="display:flex;flex-direction:column;height:100%;overflow:hidden;">
@@ -5624,7 +5728,7 @@ Los **agents**, **slash commands** (\`/spec\`, \`/plan\`, \`/build\`, \`/test\`,
5624
5728
  <button @click="showCliRef=true;cliRefTab='slash'" style="background:none;border:1px solid var(--rule);border-radius:50%;width:20px;height:20px;font-size:11px;color:var(--ink-3);cursor:pointer;display:flex;align-items:center;justify-content:center;line-height:1;flex-shrink:0;margin-left:auto" title="CLI Reference">?</button>
5625
5729
  </template>
5626
5730
  </div>
5627
- <span class="section-sub" x-text="view==='commands' ? 'Custom slash commands in ~/.claude/commands' : view==='skills' ? 'Reusable capabilities for Claude Code' : view==='instructions' ? 'Global and per-project CLAUDE.md files' : view==='permissions' ? 'Tool permission controls' : view==='hooks' ? 'Scripts triggered by Claude Code events' : 'Application settings'"></span>
5731
+ <span class="section-sub" x-text="view==='commands' ? 'Custom slash commands in ' + (status?.claudeDir || '~/.claude') + '/commands' : view==='skills' ? 'Reusable capabilities for Claude Code' : view==='instructions' ? 'Global and per-project CLAUDE.md files' : view==='permissions' ? 'Tool permission controls' : view==='hooks' ? 'Scripts triggered by Claude Code events' : 'Application settings'"></span>
5628
5732
  <div style="flex:1;overflow:hidden;">
5629
5733
  <template x-if="toolsLoading||(configLoading&&(view==='config'||view==='instructions'||view==='permissions'))"><div class="loading"><div class="spinner"></div> Loading…</div></template>
5630
5734
  <template x-if="!toolsLoading&&(view==='commands'||view==='skills')">
@@ -6768,7 +6872,7 @@ Los **agents**, **slash commands** (\`/spec\`, \`/plan\`, \`/build\`, \`/test\`,
6768
6872
  <div>
6769
6873
  <label style="font-size:11px;color:var(--ink-3);display:block;margin-bottom:6px">Save to</label>
6770
6874
  <div style="display:flex;gap:16px;flex-wrap:wrap">
6771
- <template x-for="sc in [{v:'user',d:'~/.claude/settings.json'},{v:'project',d:'.claude/settings.json'},{v:'local',d:'.claude/settings.local.json'}]" :key="sc.v">
6875
+ <template x-for="sc in [{v:'user',d:(status?.claudeDir||'~/.claude')+'/settings.json'},{v:'project',d:'.claude/settings.json'},{v:'local',d:'.claude/settings.local.json'}]" :key="sc.v">
6772
6876
  <label style="display:flex;align-items:center;gap:5px;cursor:pointer">
6773
6877
  <input type="radio" :value="sc.v" x-model="hookBuilder.scope">
6774
6878
  <span style="font-size:12px;color:var(--ink-2)" x-text="sc.v"></span>
@@ -7018,7 +7122,7 @@ Los **agents**, **slash commands** (\`/spec\`, \`/plan\`, \`/build\`, \`/test\`,
7018
7122
  <input type="radio" value="global" x-model="instrNew.audience" style="margin-top:2px;accent-color:var(--accent,#4a9);flex-shrink:0">
7019
7123
  <div style="flex:1;min-width:0">
7020
7124
  <div style="font-size:12px;font-weight:600;color:var(--ink-1)">Solo yo, en todos mis proyectos</div>
7021
- <div style="font-size:11px;color:var(--ink-3);margin-top:1px">Se guarda en <code>~/.claude/CLAUDE.md</code>. Claude lo lee en cualquier proyecto.</div>
7125
+ <div style="font-size:11px;color:var(--ink-3);margin-top:1px">Se guarda en <code x-text="(status?.claudeDir || '~/.claude') + '/CLAUDE.md'"></code>. Claude lo lee en cualquier proyecto.</div>
7022
7126
  </div>
7023
7127
  </label>
7024
7128
 
@@ -7743,6 +7847,17 @@ Los **agents**, **slash commands** (\`/spec\`, \`/plan\`, \`/build\`, \`/test\`,
7743
7847
  widgetsOpen: localStorage.getItem('cs:widgetsOpen') !== 'false',
7744
7848
  showNotes: localStorage.getItem('cs:showNotes') !== 'false',
7745
7849
  showTemplates: localStorage.getItem('cs:showTemplates') !== 'false',
7850
+ showWebmd: localStorage.getItem('cs:showWebmd') !== 'false',
7851
+ webmdUrl: '',
7852
+ webmdResult: null,
7853
+ webmdLoading: false,
7854
+ webmdError: '',
7855
+ webmdShowRaw: true,
7856
+ webmdOptTitle: localStorage.getItem('cs:webmdOptTitle') !== 'false',
7857
+ webmdOptLinks: localStorage.getItem('cs:webmdOptLinks') === 'true',
7858
+ webmdOptClean: localStorage.getItem('cs:webmdOptClean') === 'true',
7859
+ webmdItems: [],
7860
+ webmdItemsLoading: false,
7746
7861
  defaultView: localStorage.getItem('cs:defaultView') || '',
7747
7862
 
7748
7863
  // ── Live session monitor ──────────────────────────────
@@ -9352,6 +9467,60 @@ Los **agents**, **slash commands** (\`/spec\`, \`/plan\`, \`/build\`, \`/test\`,
9352
9467
  this.templatesLoading = false;
9353
9468
  },
9354
9469
 
9470
+ async loadWebmd() {
9471
+ if (this.webmdItems.length > 0) return;
9472
+ this.webmdItemsLoading = true;
9473
+ this.webmdItems = await fetch('/api/webmd').then(r => r.json()).catch(() => []);
9474
+ this.webmdItemsLoading = false;
9475
+ },
9476
+
9477
+ async fetchWebmd() {
9478
+ if (!this.webmdUrl.trim()) return;
9479
+ this.webmdLoading = true;
9480
+ this.webmdError = '';
9481
+ this.webmdResult = null;
9482
+ try {
9483
+ const r = await fetch('/api/webmd/fetch', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: this.webmdUrl.trim() }) });
9484
+ const data = await r.json();
9485
+ if (!r.ok) throw new Error(data.error || 'Error desconocido');
9486
+ this.webmdResult = data;
9487
+ } catch (e) { this.webmdError = e.message; }
9488
+ this.webmdLoading = false;
9489
+ },
9490
+
9491
+ async saveWebmd() {
9492
+ if (!this.webmdResult) return;
9493
+ this.webmdError = '';
9494
+ try {
9495
+ const r = await fetch('/api/webmd/save', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: this.webmdUrl.trim(), title: this.webmdResult.title, markdown: this.processedWebmd() }) });
9496
+ const entry = await r.json();
9497
+ if (!r.ok) throw new Error(entry.error || 'Error desconocido');
9498
+ this.webmdItems.unshift(entry);
9499
+ } catch (e) { this.webmdError = e.message; }
9500
+ },
9501
+
9502
+ async deleteWebmd(slug) {
9503
+ await fetch('/api/webmd/' + slug, { method: 'DELETE' });
9504
+ this.webmdItems = this.webmdItems.filter(i => i.slug !== slug);
9505
+ },
9506
+
9507
+ processedWebmd() {
9508
+ if (!this.webmdResult) return '';
9509
+ let md = this.webmdResult.markdown;
9510
+ if (!this.webmdOptTitle) md = md.replace(/^#[^\n]*\n+/, '');
9511
+ if (this.webmdOptLinks) md = md.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
9512
+ if (this.webmdOptClean) {
9513
+ md = md.replace(/!\[[^\]]*\]\([^)]*\)/g, ''); // remove images
9514
+ md = md.replace(/^(Title:|URL:|Published:|Source:)[^\n]*\n?/gm, ''); // Jina metadata lines
9515
+ md = md.replace(/\n{3,}/g, '\n\n').trim(); // collapse blank lines
9516
+ }
9517
+ return md;
9518
+ },
9519
+
9520
+ copyWebmd() {
9521
+ navigator.clipboard.writeText(this.processedWebmd()).catch(() => {});
9522
+ },
9523
+
9355
9524
  async createTemplate() {
9356
9525
  this.templateNew.saving = true; this.templateNew.msg = '';
9357
9526
  try {
@@ -9792,12 +9961,12 @@ Los **agents**, **slash commands** (\`/spec\`, \`/plan\`, \`/build\`, \`/test\`,
9792
9961
  hookTemplates() {
9793
9962
  return [
9794
9963
  { name: 'Notify on finish', desc: 'macOS notification when Claude stops responding', event: 'Stop', matcher: '', type: 'command', command: `osascript -e 'display notification "Claude finished" with title "Claude Code" sound name "Glass"'` },
9795
- { name: 'Log session start', desc: 'Append session start + working dir to hooks.log', event: 'SessionStart',matcher: '', type: 'command', command: `echo "[$(date -Iseconds)] Session started in $PWD" >> ~/.claude/hooks.log` },
9964
+ { name: 'Log session start', desc: 'Append session start + working dir to hooks.log', event: 'SessionStart',matcher: '', type: 'command', command: `echo "[$(date -Iseconds)] Session started in $PWD" >> ${this.status?.claudeDir || '~/.claude'}/hooks.log` },
9796
9965
  { name: 'Git status on start', desc: 'Print git status when a session begins', event: 'SessionStart',matcher: '', type: 'command', command: `git status --short 2>/dev/null || true` },
9797
9966
  { name: 'Block rm -rf', desc: 'Cancel any Bash command containing rm -rf (exit 2)',event: 'PreToolUse', matcher: 'Bash', type: 'command', command: `python3 -c "import json,sys; d=json.load(sys.stdin); c=d.get('tool_input',{}).get('command',''); (sys.stderr.write('BLOCKED: rm -rf\\n'), sys.exit(2)) if 'rm -rf' in c else None"` },
9798
9967
  { name: 'Block force push', desc: 'Cancel git push --force commands (exit 2)', event: 'PreToolUse', matcher: 'Bash', type: 'command', command: `python3 -c "import json,sys; d=json.load(sys.stdin); c=d.get('tool_input',{}).get('command',''); (sys.stderr.write('BLOCKED: force push\\n'), sys.exit(2)) if 'git push' in c and ('--force' in c or ' -f ' in c) else None"` },
9799
- { name: 'Audit Bash commands', desc: 'Append every shell command run to bash-audit.log', event: 'PostToolUse', matcher: 'Bash', type: 'command', command: `python3 -c "import json,sys,os,datetime; d=json.load(sys.stdin); c=d.get('tool_input',{}).get('command','')[:200]; open(os.path.expanduser('~/.claude/bash-audit.log'),'a').write(datetime.datetime.now().isoformat()+' '+c+'\\n')"` },
9800
- { name: 'Audit file edits', desc: 'Log every edited file path to edit-audit.log', event: 'PostToolUse', matcher: 'Edit', type: 'command', command: `python3 -c "import json,sys,os,datetime; d=json.load(sys.stdin); p=d.get('tool_input',{}).get('file_path','?'); open(os.path.expanduser('~/.claude/edit-audit.log'),'a').write(datetime.datetime.now().isoformat()+' '+p+'\\n')"` },
9968
+ { name: 'Audit Bash commands', desc: 'Append every shell command run to bash-audit.log', event: 'PostToolUse', matcher: 'Bash', type: 'command', command: `python3 -c "import json,sys,os,datetime; d=json.load(sys.stdin); c=d.get('tool_input',{}).get('command','')[:200]; open(os.path.expanduser('${this.status?.claudeDir || '~/.claude'}/bash-audit.log'),'a').write(datetime.datetime.now().isoformat()+' '+c+'\\n')"` },
9969
+ { name: 'Audit file edits', desc: 'Log every edited file path to edit-audit.log', event: 'PostToolUse', matcher: 'Edit', type: 'command', command: `python3 -c "import json,sys,os,datetime; d=json.load(sys.stdin); p=d.get('tool_input',{}).get('file_path','?'); open(os.path.expanduser('${this.status?.claudeDir || '~/.claude'}/edit-audit.log'),'a').write(datetime.datetime.now().isoformat()+' '+p+'\\n')"` },
9801
9970
  { name: 'HTTP webhook on stop',desc: 'POST to a webhook URL when Claude finishes', event: 'Stop', matcher: '', type: 'http', url: 'https://your-webhook.example.com/claude-stop' },
9802
9971
  ];
9803
9972
  },
package/server.js CHANGED
@@ -5,8 +5,17 @@ const os = require('os');
5
5
  const readline = require('readline');
6
6
  const https = require('https');
7
7
 
8
- // ─── Data directory (user config, separate from package install location) ─────
9
- const DATA_DIR = path.join(os.homedir(), '.claude', 'claude-home');
8
+ // ─── Config directory (supports CLAUDE_CONFIG_DIR override) ──────────────────
9
+ const CLAUDE_DIR = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
10
+ const DATA_DIR = path.join(CLAUDE_DIR, 'claude-home');
11
+ const CLAUDE_SETTINGS_PATH = path.join(CLAUDE_DIR, 'settings.json');
12
+ const PROJECTS_DIR = path.join(CLAUDE_DIR, 'projects');
13
+
14
+ function tildePrefix(p) {
15
+ const home = os.homedir();
16
+ return p.startsWith(home + path.sep) ? '~' + p.slice(home.length) : p;
17
+ }
18
+ const CLAUDE_DIR_DISPLAY = tildePrefix(CLAUDE_DIR);
10
19
 
11
20
  function ensureDataDir() {
12
21
  if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });
@@ -24,10 +33,9 @@ function ensureDataDir() {
24
33
  ensureDataDir();
25
34
 
26
35
  // ─── Claude Code permissions (auto-allow notes/todos writes) ──────────────────
27
- const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
28
36
  const CLAUDE_HOME_PERMISSIONS = [
29
- 'Write(~/.claude/claude-home/notes/*)',
30
- 'Write(~/.claude/claude-home/todos/*)',
37
+ `Write(${tildePrefix(path.join(DATA_DIR, 'notes', '*'))})`,
38
+ `Write(${tildePrefix(path.join(DATA_DIR, 'todos', '*'))})`,
31
39
  ];
32
40
 
33
41
  function ensureClaudePermissions() {
@@ -64,8 +72,6 @@ try {
64
72
  const app = express();
65
73
  app.use(express.json({ limit: '1mb' }));
66
74
  const PORT = parseInt(process.env.PORT, 10) || 3141;
67
- const PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects');
68
- const CLAUDE_DIR = path.join(os.homedir(), '.claude');
69
75
 
70
76
  // ─── Marketplace ──────────────────────────────────────────────────────────────
71
77
  const MARKETPLACE_CONFIG_PATH = path.join(DATA_DIR, 'marketplace.json');
@@ -1341,7 +1347,7 @@ app.get('/api/costs', async (req, res) => {
1341
1347
 
1342
1348
  // GET /api/plans
1343
1349
  app.get('/api/plans', (req, res) => {
1344
- const plansDir = path.join(os.homedir(), '.claude', 'plans');
1350
+ const plansDir = path.join(CLAUDE_DIR, 'plans');
1345
1351
  try {
1346
1352
  if (!fs.existsSync(plansDir)) return res.json([]);
1347
1353
  const files = fs.readdirSync(plansDir).filter(f => f.endsWith('.md'));
@@ -1367,7 +1373,7 @@ app.get('/api/plans', (req, res) => {
1367
1373
  // GET /api/history
1368
1374
  app.get('/api/history', async (req, res) => {
1369
1375
  const { q, project } = req.query;
1370
- const historyPath = path.join(os.homedir(), '.claude', 'history.jsonl');
1376
+ const historyPath = path.join(CLAUDE_DIR, 'history.jsonl');
1371
1377
  try {
1372
1378
  const entries = [];
1373
1379
  const rl = readline.createInterface({ input: fs.createReadStream(historyPath), crlfDelay: Infinity });
@@ -1409,7 +1415,7 @@ function writeScope(scope, projectPath, data) {
1409
1415
 
1410
1416
  // GET /api/config
1411
1417
  app.get('/api/config', (req, res) => {
1412
- const claudeDir = path.join(os.homedir(), '.claude');
1418
+ const claudeDir = CLAUDE_DIR;
1413
1419
  const projectPath = req.query.projectPath || '';
1414
1420
  try {
1415
1421
  const settings = readScope('user', '');
@@ -1496,10 +1502,10 @@ function buildPluginAttribution() {
1496
1502
  const agentSlugs = {}; // slug → pluginId
1497
1503
  const commandSlugs = {}; // "namespace:slug" → pluginId
1498
1504
 
1499
- const cacheBase = path.join(os.homedir(), '.claude', 'plugins', 'cache');
1505
+ const cacheBase = path.join(CLAUDE_DIR, 'plugins', 'cache');
1500
1506
  let installedPluginIds = [];
1501
1507
  try {
1502
- const installedJson = path.join(os.homedir(), '.claude', 'plugins', 'installed_plugins.json');
1508
+ const installedJson = path.join(CLAUDE_DIR, 'plugins', 'installed_plugins.json');
1503
1509
  if (fs.existsSync(installedJson)) {
1504
1510
  const installed = JSON.parse(fs.readFileSync(installedJson, 'utf8'));
1505
1511
  installedPluginIds = Object.keys(installed.plugins || {});
@@ -1564,13 +1570,13 @@ app.get('/api/tools/commands', (req, res) => {
1564
1570
  }
1565
1571
 
1566
1572
  try {
1567
- readCommandsDir(path.join(os.homedir(), '.claude', 'commands'), 'user');
1573
+ readCommandsDir(path.join(CLAUDE_DIR, 'commands'), 'user');
1568
1574
  if (projectPath) {
1569
1575
  readCommandsDir(path.join(projectPath, '.claude', 'commands'), 'project');
1570
1576
  }
1571
1577
  // Also include commands from installed plugins
1572
1578
  try {
1573
- const installedJson = path.join(os.homedir(), '.claude', 'plugins', 'installed_plugins.json');
1579
+ const installedJson = path.join(CLAUDE_DIR, 'plugins', 'installed_plugins.json');
1574
1580
  if (fs.existsSync(installedJson)) {
1575
1581
  const installed = JSON.parse(fs.readFileSync(installedJson, 'utf8'));
1576
1582
  for (const [pluginId, entries] of Object.entries(installed.plugins || {})) {
@@ -1610,7 +1616,7 @@ app.delete('/api/tools/commands/:slug', (req, res) => {
1610
1616
  if (!pp.startsWith(os.homedir())) return res.status(403).json({ error: 'Forbidden' });
1611
1617
  baseDir = path.join(pp, '.claude', 'commands');
1612
1618
  } else {
1613
- baseDir = path.join(os.homedir(), '.claude', 'commands');
1619
+ baseDir = path.join(CLAUDE_DIR, 'commands');
1614
1620
  }
1615
1621
 
1616
1622
  const filePath = namespace
@@ -1647,14 +1653,14 @@ app.get('/api/tools/agents', (req, res) => {
1647
1653
  }
1648
1654
 
1649
1655
  try {
1650
- readAgentsDir(path.join(os.homedir(), '.claude', 'agents'), 'user');
1656
+ readAgentsDir(path.join(CLAUDE_DIR, 'agents'), 'user');
1651
1657
  if (projectPath) {
1652
1658
  const pp = path.resolve(projectPath);
1653
1659
  if (pp.startsWith(os.homedir())) readAgentsDir(path.join(pp, '.claude', 'agents'), 'project');
1654
1660
  }
1655
1661
  // Also include agents from installed plugins
1656
1662
  try {
1657
- const installedJson = path.join(os.homedir(), '.claude', 'plugins', 'installed_plugins.json');
1663
+ const installedJson = path.join(CLAUDE_DIR, 'plugins', 'installed_plugins.json');
1658
1664
  if (fs.existsSync(installedJson)) {
1659
1665
  const installed = JSON.parse(fs.readFileSync(installedJson, 'utf8'));
1660
1666
  for (const [pluginId, entries] of Object.entries(installed.plugins || {})) {
@@ -1692,7 +1698,7 @@ app.post('/api/tools/agents', (req, res) => {
1692
1698
  if (!pp.startsWith(os.homedir())) return res.status(403).json({ error: 'Forbidden' });
1693
1699
  baseDir = path.join(pp, '.claude', 'agents');
1694
1700
  } else {
1695
- baseDir = path.join(os.homedir(), '.claude', 'agents');
1701
+ baseDir = path.join(CLAUDE_DIR, 'agents');
1696
1702
  }
1697
1703
 
1698
1704
  const agentPath = path.join(baseDir, slug + '.md');
@@ -1719,7 +1725,7 @@ app.put('/api/tools/agents/:slug', (req, res) => {
1719
1725
  if (!pp.startsWith(os.homedir())) return res.status(403).json({ error: 'Forbidden' });
1720
1726
  baseDir = path.join(pp, '.claude', 'agents');
1721
1727
  } else {
1722
- baseDir = path.join(os.homedir(), '.claude', 'agents');
1728
+ baseDir = path.join(CLAUDE_DIR, 'agents');
1723
1729
  }
1724
1730
 
1725
1731
  const agentPath = path.join(baseDir, slug + '.md');
@@ -1750,7 +1756,7 @@ app.delete('/api/tools/agents/:slug', (req, res) => {
1750
1756
  if (!pp.startsWith(os.homedir())) return res.status(403).json({ error: 'Forbidden' });
1751
1757
  baseDir = path.join(pp, '.claude', 'agents');
1752
1758
  } else {
1753
- baseDir = path.join(os.homedir(), '.claude', 'agents');
1759
+ baseDir = path.join(CLAUDE_DIR, 'agents');
1754
1760
  }
1755
1761
 
1756
1762
  const agentPath = path.join(baseDir, slug + '.md');
@@ -1784,13 +1790,13 @@ app.get('/api/tools/skills', (req, res) => {
1784
1790
  }
1785
1791
 
1786
1792
  try {
1787
- readSkillsDir(path.join(os.homedir(), '.claude', 'skills'), 'user');
1793
+ readSkillsDir(path.join(CLAUDE_DIR, 'skills'), 'user');
1788
1794
  if (projectPath) {
1789
1795
  readSkillsDir(path.join(projectPath, '.claude', 'skills'), 'project');
1790
1796
  }
1791
1797
  // Also include skills from installed plugins (plugin items live in cache, not ~/.claude/skills)
1792
1798
  try {
1793
- const installedJson = path.join(os.homedir(), '.claude', 'plugins', 'installed_plugins.json');
1799
+ const installedJson = path.join(CLAUDE_DIR, 'plugins', 'installed_plugins.json');
1794
1800
  if (fs.existsSync(installedJson)) {
1795
1801
  const installed = JSON.parse(fs.readFileSync(installedJson, 'utf8'));
1796
1802
  for (const [pluginId, entries] of Object.entries(installed.plugins || {})) {
@@ -1829,7 +1835,7 @@ app.post('/api/tools/skills', (req, res) => {
1829
1835
  if (!pp.startsWith(os.homedir())) return res.status(403).json({ error: 'Forbidden' });
1830
1836
  baseDir = path.join(pp, '.claude', 'skills');
1831
1837
  } else {
1832
- baseDir = path.join(os.homedir(), '.claude', 'skills');
1838
+ baseDir = path.join(CLAUDE_DIR, 'skills');
1833
1839
  }
1834
1840
 
1835
1841
  const skillDir = path.join(baseDir, slug);
@@ -1855,7 +1861,7 @@ app.put('/api/tools/skills/:slug', (req, res) => {
1855
1861
  if (!pp.startsWith(os.homedir())) return res.status(403).json({ error: 'Forbidden' });
1856
1862
  baseDir = path.join(pp, '.claude', 'skills');
1857
1863
  } else {
1858
- baseDir = path.join(os.homedir(), '.claude', 'skills');
1864
+ baseDir = path.join(CLAUDE_DIR, 'skills');
1859
1865
  }
1860
1866
 
1861
1867
  const skillMdPath = path.join(baseDir, slug, 'SKILL.md');
@@ -1885,7 +1891,7 @@ app.delete('/api/tools/skills/:slug', (req, res) => {
1885
1891
  if (!pp.startsWith(os.homedir())) return res.status(403).json({ error: 'Forbidden' });
1886
1892
  baseDir = path.join(pp, '.claude', 'skills');
1887
1893
  } else {
1888
- baseDir = path.join(os.homedir(), '.claude', 'skills');
1894
+ baseDir = path.join(CLAUDE_DIR, 'skills');
1889
1895
  }
1890
1896
 
1891
1897
  const skillDir = path.join(baseDir, slug);
@@ -1909,7 +1915,7 @@ app.get('/api/status', (req, res) => {
1909
1915
  claudeVersion = out.split('\n')[0];
1910
1916
  } catch {}
1911
1917
  const appVersion = require('./package.json').version;
1912
- res.json({ claudeVersion, appVersion });
1918
+ res.json({ claudeVersion, appVersion, claudeDir: CLAUDE_DIR_DISPLAY });
1913
1919
  });
1914
1920
 
1915
1921
  // ─── Marketplace endpoints ───────────────────────────────────────────────────
@@ -2017,7 +2023,7 @@ app.post('/api/marketplace/install', (req, res) => {
2017
2023
  if (!pp.startsWith(os.homedir())) return res.status(403).json({ error: 'Forbidden' });
2018
2024
  baseDir = path.join(pp, '.claude', 'skills');
2019
2025
  } else {
2020
- baseDir = path.join(os.homedir(), '.claude', 'skills');
2026
+ baseDir = path.join(CLAUDE_DIR, 'skills');
2021
2027
  }
2022
2028
 
2023
2029
  const skillDir = path.join(baseDir, slug);
@@ -2037,7 +2043,7 @@ app.get('/api/marketplace/check-installed', (req, res) => {
2037
2043
  if (!slugs) return res.json({});
2038
2044
  const slugList = slugs.split(',').map(s => s.trim()).filter(Boolean);
2039
2045
  const result = {};
2040
- const userSkillsDir = path.join(os.homedir(), '.claude', 'skills');
2046
+ const userSkillsDir = path.join(CLAUDE_DIR, 'skills');
2041
2047
  let projectSkillsDir = null;
2042
2048
  if (projectPath) {
2043
2049
  const pp = path.resolve(projectPath);
@@ -2804,7 +2810,7 @@ app.post('/api/share/plan/:filename', async (req, res) => {
2804
2810
  if (!githubToken) return res.status(400).json({ error: 'GitHub token not configured. Add it in Settings → Sharing.' });
2805
2811
  const filename = path.basename(req.params.filename);
2806
2812
  if (!filename.endsWith('.md')) return res.status(400).json({ error: 'invalid filename' });
2807
- const filePath = path.join(os.homedir(), '.claude', 'plans', filename);
2813
+ const filePath = path.join(CLAUDE_DIR, 'plans', filename);
2808
2814
  try {
2809
2815
  const content = fs.readFileSync(filePath, 'utf8');
2810
2816
  const titleMatch = content.match(/^#\s+(.+)$/m);
@@ -2859,27 +2865,29 @@ function scanNotes(dir, base) {
2859
2865
  return results;
2860
2866
  }
2861
2867
 
2862
- const NOTES_CLAUDE_MD_SNIPPET = `
2868
+ function notesClaudeMdSnippet() {
2869
+ const d = CLAUDE_DIR_DISPLAY;
2870
+ return `
2863
2871
  ## Personal Notes
2864
2872
 
2865
2873
  When the user asks you to "save a note", "add to notes", "guarda esto como nota", or similar:
2866
- 1. Create a markdown file in \`~/.claude/claude-home/notes/\` with this exact format:
2874
+ 1. Create a markdown file in \`${d}/claude-home/notes/\` with this exact format:
2867
2875
  - Filename: \`YYYY-MM-DD-short-slug.md\` (e.g. \`2026-03-31-bug-fix-auth.md\`)
2868
2876
  - Content:
2869
2877
  \`\`\`
2870
2878
  ---
2871
2879
  title: <descriptive title>
2872
2880
  date: <current ISO date>
2873
- session: <run: ls -t ~/.claude/projects/$(pwd | sed 's|/|-|g' | sed 's|^-||')/*.jsonl 2>/dev/null | head -1 | xargs basename 2>/dev/null | sed 's/\.jsonl//'>
2881
+ session: <run: ls -t ${d}/projects/$(pwd | sed 's|/|-|g' | sed 's|^-||')/*.jsonl 2>/dev/null | head -1 | xargs basename 2>/dev/null | sed 's/\\.jsonl//'>
2874
2882
  ---
2875
2883
 
2876
2884
  <the content the user wants to save>
2877
2885
  \`\`\`
2878
2886
  2. **Folder**: Save in a subfolder when appropriate:
2879
- - Project-specific note → \`~/.claude/claude-home/notes/<basename-of-pwd>/\` (e.g. working in \`mono-genially\` → folder \`mono-genially\`)
2880
- - Global/cross-project note → root \`~/.claude/claude-home/notes/\`
2887
+ - Project-specific note → \`${d}/claude-home/notes/<basename-of-pwd>/\` (e.g. working in \`mono-genially\` → folder \`mono-genially\`)
2888
+ - Global/cross-project note → root \`${d}/claude-home/notes/\`
2881
2889
  - User-specified folder → use that folder name
2882
- 3. **Note linking**: If the note references concepts in other notes, use \`#slug\` syntax (\`2026-03-31-my-slug.md\` → \`#my-slug\`). Glob \`~/.claude/claude-home/notes/**/*.md\` to discover existing slugs.
2890
+ 3. **Note linking**: If the note references concepts in other notes, use \`#slug\` syntax (\`2026-03-31-my-slug.md\` → \`#my-slug\`). Glob \`${d}/claude-home/notes/**/*.md\` to discover existing slugs.
2883
2891
  4. Use the Write tool to create the file (not Bash).
2884
2892
  5. Confirm with: "Saved to Notes: http://localhost:3141/#/note/<folder/filename or filename>"
2885
2893
 
@@ -2889,7 +2897,7 @@ The notes directory may not exist yet — the app creates it automatically on fi
2889
2897
 
2890
2898
  When the user asks to add a task "for today", "for tomorrow", "to review later", or similar:
2891
2899
  1. Determine the target date (today = current date, tomorrow = current date + 1 day)
2892
- 2. Read the existing file if it exists: \`~/.claude/claude-home/todos/YYYY-MM-DD.json\`
2900
+ 2. Read the existing file if it exists: \`${d}/claude-home/todos/YYYY-MM-DD.json\`
2893
2901
  3. Use the Write tool to save the updated file with this format:
2894
2902
  \`\`\`json
2895
2903
  {
@@ -2910,9 +2918,10 @@ When the user asks to add a task "for today", "for tomorrow", "to review later",
2910
2918
 
2911
2919
  The todos directory may not exist yet — the app creates it automatically on first load.
2912
2920
  `;
2921
+ }
2913
2922
 
2914
2923
  app.get('/api/notes/claude-md-status', (req, res) => {
2915
- const claudeMdPath = path.join(os.homedir(), '.claude', 'CLAUDE.md');
2924
+ const claudeMdPath = path.join(CLAUDE_DIR, 'CLAUDE.md');
2916
2925
  try {
2917
2926
  const content = fs.existsSync(claudeMdPath) ? fs.readFileSync(claudeMdPath, 'utf8') : '';
2918
2927
  res.json({ installed: content.includes('Personal Notes') });
@@ -2920,22 +2929,19 @@ app.get('/api/notes/claude-md-status', (req, res) => {
2920
2929
  });
2921
2930
 
2922
2931
  app.post('/api/notes/setup-claude', (req, res) => {
2923
- const claudeMdPath = path.join(os.homedir(), '.claude', 'CLAUDE.md');
2924
- const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
2932
+ const claudeMdPath = path.join(CLAUDE_DIR, 'CLAUDE.md');
2933
+ const settingsPath = CLAUDE_SETTINGS_PATH;
2925
2934
  try {
2926
2935
  const current = fs.existsSync(claudeMdPath) ? fs.readFileSync(claudeMdPath, 'utf8') : '';
2927
2936
  if (current.includes('Personal Notes')) return res.json({ ok: true, alreadyInstalled: true });
2928
2937
  // Append CLAUDE.md snippet
2929
- fs.writeFileSync(claudeMdPath, current + '\n' + NOTES_CLAUDE_MD_SNIPPET, 'utf8');
2938
+ fs.writeFileSync(claudeMdPath, current + '\n' + notesClaudeMdSnippet(), 'utf8');
2930
2939
  // Add Write permission for notes dir to settings.json
2931
2940
  try {
2932
2941
  const settings = fs.existsSync(settingsPath) ? JSON.parse(fs.readFileSync(settingsPath, 'utf8')) : {};
2933
2942
  if (!settings.permissions) settings.permissions = {};
2934
2943
  if (!settings.permissions.allow) settings.permissions.allow = [];
2935
- const rules = [
2936
- `Write(${path.join(os.homedir(), '.claude', 'claude-home', 'notes', '*')})`,
2937
- `Write(${path.join(os.homedir(), '.claude', 'claude-home', 'todos', '*')})`,
2938
- ];
2944
+ const rules = CLAUDE_HOME_PERMISSIONS;
2939
2945
  let changed = false;
2940
2946
  for (const rule of rules) {
2941
2947
  if (!settings.permissions.allow.includes(rule)) {
@@ -3286,6 +3292,62 @@ app.post('/api/today/pull', (req, res) => {
3286
3292
  res.json({ ok: true });
3287
3293
  });
3288
3294
 
3295
+ // ─── URL to Markdown ─────────────────────────────────────────────────────────
3296
+ const WEBMD_DIR = path.join(DATA_DIR, 'webmd');
3297
+ function ensureWebmdDir() { if (!fs.existsSync(WEBMD_DIR)) fs.mkdirSync(WEBMD_DIR, { recursive: true }); }
3298
+
3299
+ function slugify(url) {
3300
+ return url.replace(/^https?:\/\//, '').replace(/[^a-z0-9]/gi, '-').toLowerCase().slice(0, 60)
3301
+ + '-' + Date.now();
3302
+ }
3303
+
3304
+ app.post('/api/webmd/fetch', async (req, res) => {
3305
+ try {
3306
+ const { url } = req.body;
3307
+ if (!url || !/^https?:\/\//i.test(url)) return res.status(400).json({ error: 'URL inválida' });
3308
+ const jinaUrl = 'https://r.jina.ai/' + url;
3309
+ const raw = await fetchRemote(jinaUrl);
3310
+ if (raw.length > 500_000) return res.status(413).json({ error: 'Página demasiado grande (>500KB)' });
3311
+ // Extract title from first markdown heading or URL
3312
+ const titleMatch = raw.match(/^#\s+(.+)$/m);
3313
+ const title = titleMatch ? titleMatch[1].trim() : url;
3314
+ res.json({ title, markdown: raw });
3315
+ } catch (e) { res.status(500).json({ error: e.message }); }
3316
+ });
3317
+
3318
+ app.get('/api/webmd', (req, res) => {
3319
+ ensureWebmdDir();
3320
+ try {
3321
+ const files = fs.readdirSync(WEBMD_DIR).filter(f => f.endsWith('.json')).sort().reverse();
3322
+ const items = files.map(f => {
3323
+ try { return JSON.parse(fs.readFileSync(path.join(WEBMD_DIR, f), 'utf8')); } catch { return null; }
3324
+ }).filter(Boolean);
3325
+ res.json(items);
3326
+ } catch (e) { res.status(500).json({ error: e.message }); }
3327
+ });
3328
+
3329
+ app.post('/api/webmd/save', (req, res) => {
3330
+ ensureWebmdDir();
3331
+ try {
3332
+ const { url, title, markdown } = req.body;
3333
+ if (!url || !markdown) return res.status(400).json({ error: 'url y markdown requeridos' });
3334
+ const slug = slugify(url);
3335
+ const entry = { slug, url, title: title || url, markdown, savedAt: new Date().toISOString() };
3336
+ fs.writeFileSync(path.join(WEBMD_DIR, `${slug}.json`), JSON.stringify(entry, null, 2), 'utf8');
3337
+ res.json(entry);
3338
+ } catch (e) { res.status(500).json({ error: e.message }); }
3339
+ });
3340
+
3341
+ app.delete('/api/webmd/:slug', (req, res) => {
3342
+ try {
3343
+ const slug = req.params.slug.replace(/[^a-z0-9-]/gi, '');
3344
+ const filePath = path.join(WEBMD_DIR, `${slug}.json`);
3345
+ if (!filePath.startsWith(WEBMD_DIR + path.sep)) return res.status(400).json({ error: 'Invalid slug' });
3346
+ if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
3347
+ res.json({ ok: true });
3348
+ } catch (e) { res.status(500).json({ error: e.message }); }
3349
+ });
3350
+
3289
3351
  // ─── Start ────────────────────────────────────────────────────────────────────
3290
3352
 
3291
3353
  function startServer(port) {
@@ -3449,7 +3511,7 @@ async function fetchToolItemContent(source, treePath) {
3449
3511
 
3450
3512
  // Scan local plugin marketplace dirs for .md files in a given subdir
3451
3513
  function getPluginToolItems(subdir) {
3452
- const marketplacesDir = path.join(os.homedir(), '.claude', 'plugins', 'marketplaces');
3514
+ const marketplacesDir = path.join(CLAUDE_DIR, 'plugins', 'marketplaces');
3453
3515
  const items = [];
3454
3516
  try {
3455
3517
  for (const mkt of fs.readdirSync(marketplacesDir)) {
@@ -3555,7 +3617,7 @@ function makeToolMarketplaceRoutes(prefix, configPath, itemsPathKey, localSubdir
3555
3617
  if (!pp.startsWith(os.homedir())) return res.status(403).json({ error: 'Forbidden' });
3556
3618
  baseDir = path.join(pp, '.claude', localSubdir);
3557
3619
  } else {
3558
- baseDir = path.join(os.homedir(), '.claude', localSubdir);
3620
+ baseDir = path.join(CLAUDE_DIR, localSubdir);
3559
3621
  }
3560
3622
  const dest = path.join(baseDir, slug + '.md');
3561
3623
  if (fs.existsSync(dest)) return res.status(409).json({ error: `"${slug}" ya está instalado` });
@@ -3571,7 +3633,7 @@ function makeToolMarketplaceRoutes(prefix, configPath, itemsPathKey, localSubdir
3571
3633
  app.get(`/api/${prefix}/check-installed`, (req, res) => {
3572
3634
  const { slugs, projectPath } = req.query;
3573
3635
  if (!slugs) return res.json({});
3574
- const dirs = [path.join(os.homedir(), '.claude', localSubdir)];
3636
+ const dirs = [path.join(CLAUDE_DIR, localSubdir)];
3575
3637
  if (projectPath) dirs.push(path.join(path.resolve(projectPath), '.claude', localSubdir));
3576
3638
  const result = {};
3577
3639
  for (const slug of slugs.split(',')) {
@@ -3615,7 +3677,7 @@ app.get('/api/plugins/detail', (req, res) => {
3615
3677
  const name = atIdx >= 0 ? pluginId.slice(0, atIdx) : pluginId;
3616
3678
  const marketplace = atIdx >= 0 ? pluginId.slice(atIdx + 1) : null;
3617
3679
 
3618
- const pluginsBase = path.join(os.homedir(), '.claude', 'plugins', 'marketplaces');
3680
+ const pluginsBase = path.join(CLAUDE_DIR, 'plugins', 'marketplaces');
3619
3681
  const candidates = [];
3620
3682
 
3621
3683
  if (marketplace) {
@@ -3636,7 +3698,7 @@ app.get('/api/plugins/detail', (req, res) => {
3636
3698
 
3637
3699
  // Also check install cache from installed_plugins.json
3638
3700
  try {
3639
- const installedJson = path.join(os.homedir(), '.claude', 'plugins', 'installed_plugins.json');
3701
+ const installedJson = path.join(CLAUDE_DIR, 'plugins', 'installed_plugins.json');
3640
3702
  if (fs.existsSync(installedJson)) {
3641
3703
  const installed = JSON.parse(fs.readFileSync(installedJson, 'utf8'));
3642
3704
  const entries = installed.plugins?.[pluginId] || [];
@@ -3691,7 +3753,7 @@ app.get('/api/plugins/items', (req, res) => {
3691
3753
  const pName = atIdx >= 0 ? pluginId.slice(0, atIdx) : pluginId;
3692
3754
  const marketplace = atIdx >= 0 ? pluginId.slice(atIdx + 1) : null;
3693
3755
 
3694
- const cacheBase = path.join(os.homedir(), '.claude', 'plugins', 'cache');
3756
+ const cacheBase = path.join(CLAUDE_DIR, 'plugins', 'cache');
3695
3757
  let pluginDir = null;
3696
3758
 
3697
3759
  // Find from versioned cache: cache/<marketplace>/<plugin>/<latest-version>