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 +28 -18
- package/bin/cli.js +31 -24
- package/package.json +22 -3
- package/public/index.html +179 -10
- package/server.js +112 -50
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
|
|
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/`
|
|
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>
|
|
127
|
-
--no-open
|
|
128
|
-
--
|
|
129
|
-
|
|
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
|
|
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
|
-
// ───
|
|
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>
|
|
52
|
-
--no-open
|
|
53
|
-
--
|
|
54
|
-
|
|
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(
|
|
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(
|
|
153
|
-
const sentinelPath = path.join(
|
|
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(
|
|
190
|
-
const commandsDir = path.join(
|
|
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(
|
|
239
|
-
const commandPath = path.join(
|
|
240
|
-
const sentinelPath = path.join(
|
|
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.
|
|
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
|
-
"
|
|
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"
|
|
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"
|
|
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"
|
|
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
|
|
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
|
-
// ───
|
|
9
|
-
const
|
|
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
|
-
|
|
30
|
-
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
1505
|
+
const cacheBase = path.join(CLAUDE_DIR, 'plugins', 'cache');
|
|
1500
1506
|
let installedPluginIds = [];
|
|
1501
1507
|
try {
|
|
1502
|
-
const installedJson = path.join(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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 →
|
|
2880
|
-
- Global/cross-project note → root
|
|
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
|
|
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:
|
|
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(
|
|
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(
|
|
2924
|
-
const settingsPath =
|
|
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' +
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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>
|