claude-home 1.5.9 → 1.5.27
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/package.json +1 -1
- package/public/index.html +362 -12
- package/server.js +228 -0
package/package.json
CHANGED
package/public/index.html
CHANGED
|
@@ -1548,6 +1548,17 @@
|
|
|
1548
1548
|
Plans
|
|
1549
1549
|
<span class="nav-count" x-show="plans.length>0" x-text="plans.length"></span>
|
|
1550
1550
|
</div>
|
|
1551
|
+
<div class="nav-item" :class="{ active: view === 'notes' }" @click="view='notes';selectedSession=null;loadPersonalNotes()">
|
|
1552
|
+
<span class="nav-icon">
|
|
1553
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke-width="1.5" stroke="currentColor">
|
|
1554
|
+
<path d="M3 2h8l3 3v9a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1z"/>
|
|
1555
|
+
<polyline points="11 2 11 5 14 5"/>
|
|
1556
|
+
<line x1="5" y1="7" x2="11" y2="7"/><line x1="5" y1="10" x2="9" y2="10"/>
|
|
1557
|
+
</svg>
|
|
1558
|
+
</span>
|
|
1559
|
+
Notes
|
|
1560
|
+
<span class="nav-count" x-show="personalNotes.length>0" x-text="personalNotes.length"></span>
|
|
1561
|
+
</div>
|
|
1551
1562
|
</div>
|
|
1552
1563
|
<div class="nav-divider"></div>
|
|
1553
1564
|
<div class="nav-section">
|
|
@@ -2252,25 +2263,32 @@
|
|
|
2252
2263
|
</div>
|
|
2253
2264
|
</div>
|
|
2254
2265
|
<div style="display:flex;gap:6px;align-items:center;flex-shrink:0">
|
|
2255
|
-
<button class="btn btn-sm" :
|
|
2266
|
+
<button class="btn btn-sm" :style="cleanMode ? 'background:var(--blue);color:#fff' : 'background:var(--canvas-2);color:var(--ink)'" @click="cleanMode=!cleanMode" title="Toggle focus view">Focus</button>
|
|
2267
|
+
<div style="flex:1"></div>
|
|
2268
|
+
<span x-show="exportMsg" x-text="exportMsg" style="font-size:12px;color:var(--green)" x-transition></span>
|
|
2269
|
+
<span x-show="deletingSession" style="font-size:12px;color:var(--ink-3)">Deleting…</span>
|
|
2270
|
+
<button class="btn btn-primary btn-sm" id="resume-detail" :disabled="!sessionDetail?.resumable" :title="!sessionDetail?.resumable ? 'Session file deleted — cannot resume' : 'Copy command to resume this session'" @click="sessionDetail?.resumable && resumeSession(selectedSession.sessionId, 'resume-detail', selectedSession.projectPath)">
|
|
2271
|
+
Resume →
|
|
2272
|
+
</button>
|
|
2273
|
+
<!-- ⋮ dropdown -->
|
|
2256
2274
|
<div style="position:relative">
|
|
2257
|
-
<button class="btn btn-sm" style="background:var(--canvas-2);color:var(--ink);display:flex;align-items:center
|
|
2258
|
-
|
|
2275
|
+
<button class="btn btn-sm" style="background:var(--canvas-2);color:var(--ink);border:1px solid var(--rule);padding:4px 10px;display:flex;align-items:center" @click="exportDropOpen=!exportDropOpen" @click.outside="exportDropOpen=false" title="More options">
|
|
2276
|
+
<svg width="3" height="15" viewBox="0 0 3 15" fill="currentColor"><circle cx="1.5" cy="1.5" r="1.5"/><circle cx="1.5" cy="7.5" r="1.5"/><circle cx="1.5" cy="13.5" r="1.5"/></svg>
|
|
2259
2277
|
</button>
|
|
2260
|
-
<div x-show="exportDropOpen" x-transition style="position:absolute;top:calc(100% + 4px);right:0;background:var(--
|
|
2278
|
+
<div x-show="exportDropOpen" x-transition style="position:absolute;top:calc(100% + 4px);right:0;background:var(--canvas,#fff);border:1px solid var(--rule);border-radius:6px;box-shadow:0 4px 16px rgba(0,0,0,.2);z-index:50;min-width:180px;overflow:hidden">
|
|
2261
2279
|
<button class="export-drop-item" @click="exportSession(true); exportDropOpen=false">Download .md</button>
|
|
2262
|
-
<button class="export-drop-item" @click="copySessionMd(); exportDropOpen=false">Copy
|
|
2280
|
+
<button class="export-drop-item" @click="copySessionMd(); exportDropOpen=false">Copy as Markdown</button>
|
|
2281
|
+
<button class="export-drop-item" @click="shareSession(); exportDropOpen=false" :disabled="!!shareMsg"><span x-text="shareMsg || 'Publish to Gist'"></span></button>
|
|
2282
|
+
<button class="export-drop-item" @click="navigator.clipboard.writeText(location.origin+location.pathname+'#/session/'+selectedSession.projectDir+'/'+selectedSession.sessionId);exportDropOpen=false">Copy local link</button>
|
|
2283
|
+
<button class="export-drop-item" @click="saveNoteFromSession();exportDropOpen=false">Save as note</button>
|
|
2284
|
+
<div style="height:1px;background:var(--rule);margin:4px 0"></div>
|
|
2285
|
+
<button class="export-drop-item" style="color:var(--red)" @click="deleteSession();exportDropOpen=false" x-show="!deletingSession">Delete session</button>
|
|
2263
2286
|
</div>
|
|
2264
2287
|
</div>
|
|
2265
|
-
<button class="btn btn-sm" style="background:var(--red-dim,#3a1a1a);color:var(--red);border:1px solid var(--red)" @click="deleteSession()" x-show="!deletingSession">Delete</button>
|
|
2266
|
-
<span x-show="deletingSession" style="font-size:12px;color:var(--ink-3)">Deleting…</span>
|
|
2267
|
-
<button class="btn btn-primary btn-sm" id="resume-detail" :disabled="!sessionDetail?.resumable" :title="!sessionDetail?.resumable ? 'Session file deleted — cannot resume' : 'Copy command to resume this session'" @click="sessionDetail?.resumable && resumeSession(selectedSession.sessionId, 'resume-detail', selectedSession.projectPath)">
|
|
2268
|
-
Resume →
|
|
2269
|
-
</button>
|
|
2270
|
-
<span x-show="exportMsg" x-text="exportMsg" style="font-size:12px;color:var(--green)" x-transition></span>
|
|
2271
2288
|
</div>
|
|
2272
2289
|
</div>
|
|
2273
2290
|
|
|
2291
|
+
|
|
2274
2292
|
<div class="chat-messages">
|
|
2275
2293
|
<template x-if="loadingDetail">
|
|
2276
2294
|
<div class="loading"><div class="spinner"></div> Loading conversation…</div>
|
|
@@ -2528,8 +2546,10 @@
|
|
|
2528
2546
|
<template x-if="selectedPlan">
|
|
2529
2547
|
<div style="display:flex;align-items:center;gap:8px;margin-left:auto">
|
|
2530
2548
|
<span class="save-msg" x-show="planExportMsg" x-text="planExportMsg"></span>
|
|
2549
|
+
<span class="save-msg" x-show="planShareMsg" x-text="planShareMsg"></span>
|
|
2531
2550
|
<button class="btn btn-sm btn-outline" @click="copyPlanMd()">Copy MD</button>
|
|
2532
2551
|
<button class="btn btn-sm btn-outline" @click="downloadPlan()">Download</button>
|
|
2552
|
+
<button class="btn btn-sm" style="background:var(--canvas-2);color:var(--ink)" title="Publish to GitHub Gist and copy public URL" @click="sharePlan(selectedPlan.filename)" :disabled="!!planShareMsg">Share</button>
|
|
2533
2553
|
<button class="btn btn-sm" style="background:var(--red-dim,#3a1a1a);color:var(--red);border:1px solid var(--red)" @click="deletePlan()">Delete</button>
|
|
2534
2554
|
</div>
|
|
2535
2555
|
</template>
|
|
@@ -2575,6 +2595,103 @@
|
|
|
2575
2595
|
</div>
|
|
2576
2596
|
</template>
|
|
2577
2597
|
|
|
2598
|
+
<!-- Notes view -->
|
|
2599
|
+
<template x-if="view === 'notes' && !selectedSession">
|
|
2600
|
+
<div style="display:flex;flex-direction:column;height:100%;overflow:hidden;">
|
|
2601
|
+
<div class="topbar">
|
|
2602
|
+
<span class="topbar-title">Notes</span>
|
|
2603
|
+
<span style="font-size:11px;color:var(--ink-3);padding:2px 8px;background:var(--canvas-2);border:1px solid var(--rule);border-radius:4px" title="These are your personal notes — Claude doesn't read or use them as context. They're only for you.">Your notepad · not Claude's memory</span>
|
|
2604
|
+
<input class="search-input" type="text" placeholder="Search notes…" x-model="noteSearch" style="font-size:12px;width:200px;padding:4px 10px" />
|
|
2605
|
+
<span style="font-size:11px;color:var(--ink-3)" x-show="personalNotes.length>0" x-text="personalNotes.length + ' notes'"></span>
|
|
2606
|
+
<div style="display:flex;align-items:center;gap:8px;margin-left:auto">
|
|
2607
|
+
<template x-if="selectedNote && noteEditing">
|
|
2608
|
+
<div style="display:flex;align-items:center;gap:8px">
|
|
2609
|
+
<span class="save-msg" x-show="noteMsg==='Saved'" x-text="noteMsg"></span>
|
|
2610
|
+
<span class="err-msg" x-show="noteMsg && noteMsg!=='Saved'" x-text="noteMsg"></span>
|
|
2611
|
+
<button class="btn btn-sm btn-outline" @click="noteEditing=false">Cancel</button>
|
|
2612
|
+
<button class="btn btn-primary btn-sm" @click="savePersonalNote()" :disabled="noteSaving" x-text="noteSaving?'Saving…':'Save'"></button>
|
|
2613
|
+
</div>
|
|
2614
|
+
</template>
|
|
2615
|
+
<template x-if="selectedNote && !noteEditing">
|
|
2616
|
+
<div style="display:flex;align-items:center;gap:8px">
|
|
2617
|
+
<button class="btn btn-sm btn-outline" @click="noteEditing=true;noteTitleDraft=selectedNote.title;noteBodyDraft=selectedNote.content">Edit</button>
|
|
2618
|
+
<button class="btn btn-sm" style="background:var(--red-dim,#3a1a1a);color:var(--red);border:1px solid var(--red)" @click="deletePersonalNote()">Delete</button>
|
|
2619
|
+
</div>
|
|
2620
|
+
</template>
|
|
2621
|
+
<button class="btn btn-primary btn-sm" @click="noteCreating=true;noteNewTitle='';noteNewBody=''">+ New note</button>
|
|
2622
|
+
</div>
|
|
2623
|
+
</div>
|
|
2624
|
+
|
|
2625
|
+
<!-- New note form -->
|
|
2626
|
+
<template x-if="noteCreating">
|
|
2627
|
+
<div style="padding:16px 20px;border-bottom:1px solid var(--rule);background:var(--canvas-2)">
|
|
2628
|
+
<input class="settings-input" type="text" placeholder="Note title…" x-model="noteNewTitle" style="width:100%;margin-bottom:8px;font-size:13px" @keydown.enter="createPersonalNote()" />
|
|
2629
|
+
<textarea class="settings-input" placeholder="Content (optional)…" x-model="noteNewBody" rows="3" style="width:100%;resize:vertical;font-size:12px;font-family:inherit"></textarea>
|
|
2630
|
+
<div style="display:flex;gap:8px;margin-top:8px;justify-content:flex-end">
|
|
2631
|
+
<button class="btn btn-sm btn-outline" @click="noteCreating=false">Cancel</button>
|
|
2632
|
+
<button class="btn btn-primary btn-sm" @click="createPersonalNote()" :disabled="noteSaving||!noteNewTitle.trim()">Create</button>
|
|
2633
|
+
</div>
|
|
2634
|
+
</div>
|
|
2635
|
+
</template>
|
|
2636
|
+
|
|
2637
|
+
<div style="flex:1;overflow:hidden;">
|
|
2638
|
+
<template x-if="personalNotesLoading"><div class="loading"><div class="spinner"></div> Loading…</div></template>
|
|
2639
|
+
<template x-if="!personalNotesLoading && noteClaudeInstalled === false">
|
|
2640
|
+
<div style="margin:12px 20px;padding:10px 14px;background:var(--canvas-2);border:1px solid var(--rule);border-radius:8px;display:flex;align-items:center;justify-content:space-between;gap:12px">
|
|
2641
|
+
<div>
|
|
2642
|
+
<div style="font-size:12px;font-weight:600;margin-bottom:2px">Claude doesn't know about Notes yet</div>
|
|
2643
|
+
<div style="font-size:11px;color:var(--ink-3)">Add one instruction to your CLAUDE.md so Claude can save notes when you ask.</div>
|
|
2644
|
+
</div>
|
|
2645
|
+
<div style="display:flex;align-items:center;gap:8px;flex-shrink:0">
|
|
2646
|
+
<span x-show="noteSetupMsg" x-text="noteSetupMsg" style="font-size:11px;color:var(--green)"></span>
|
|
2647
|
+
<button class="btn btn-primary btn-sm" @click="setupClaudeNotes()" :disabled="!!noteSetupMsg">Set up</button>
|
|
2648
|
+
</div>
|
|
2649
|
+
</div>
|
|
2650
|
+
</template>
|
|
2651
|
+
<template x-if="!personalNotesLoading && personalNotes.length === 0">
|
|
2652
|
+
<div class="empty"><div class="empty-mark">✎</div>No notes yet. Create one or ask Claude to save one for you.</div>
|
|
2653
|
+
</template>
|
|
2654
|
+
<template x-if="!personalNotesLoading && personalNotes.length > 0">
|
|
2655
|
+
<div class="memory-layout">
|
|
2656
|
+
<div class="memory-sidebar" :style="'width:'+sidebarW+'px'">
|
|
2657
|
+
<template x-for="n in personalNotes.filter(n => !noteSearch || n.title.toLowerCase().includes(noteSearch.toLowerCase()) || n.content.toLowerCase().includes(noteSearch.toLowerCase()))" :key="n.filename">
|
|
2658
|
+
<div class="plan-row" :class="{active: selectedNote?.filename === n.filename}" @click="selectedNote=n;noteEditing=false;window.location.hash='#/note/'+n.filename">
|
|
2659
|
+
<div class="plan-row-title" x-text="n.title"></div>
|
|
2660
|
+
<div class="plan-row-meta">
|
|
2661
|
+
<span x-text="formatDate(n.modified)"></span>
|
|
2662
|
+
<template x-if="n.session"><span style="color:var(--blue);font-size:10px">session</span></template>
|
|
2663
|
+
</div>
|
|
2664
|
+
</div>
|
|
2665
|
+
</template>
|
|
2666
|
+
</div>
|
|
2667
|
+
<div class="resize-handle" @mousedown.prevent="startResize($event)"></div>
|
|
2668
|
+
<div class="memory-detail">
|
|
2669
|
+
<template x-if="!selectedNote"><div class="memory-empty"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M5 12h14M12 5l7 7-7 7"/></svg>Select a note</div></template>
|
|
2670
|
+
<template x-if="selectedNote && !noteEditing">
|
|
2671
|
+
<div>
|
|
2672
|
+
<div class="memory-detail-title" x-text="selectedNote.title"></div>
|
|
2673
|
+
<div class="memory-detail-meta">
|
|
2674
|
+
<span style="font-size:11px;color:var(--ink-3)" x-text="formatDate(selectedNote.modified)"></span>
|
|
2675
|
+
<template x-if="selectedNote.session">
|
|
2676
|
+
<span style="font-size:11px;color:var(--blue);cursor:pointer" @click="openSessionById(selectedNote.session)" x-text="'→ Session ' + selectedNote.session.slice(0,8) + '…'"></span>
|
|
2677
|
+
</template>
|
|
2678
|
+
</div>
|
|
2679
|
+
<div class="memory-content md-content" x-html="selectedNote.content ? renderMd(selectedNote.content) : '<span style=\'color:var(--ink-3)\'>Empty note. Click Edit to add content.</span>'"></div>
|
|
2680
|
+
</div>
|
|
2681
|
+
</template>
|
|
2682
|
+
<template x-if="selectedNote && noteEditing">
|
|
2683
|
+
<div style="display:flex;flex-direction:column;height:100%;gap:10px">
|
|
2684
|
+
<input class="settings-input" type="text" x-model="noteTitleDraft" style="font-size:14px;font-weight:600;width:100%" />
|
|
2685
|
+
<textarea class="settings-input" x-model="noteBodyDraft" style="flex:1;resize:none;font-family:monospace;font-size:12px;min-height:300px;width:100%"></textarea>
|
|
2686
|
+
</div>
|
|
2687
|
+
</template>
|
|
2688
|
+
</div>
|
|
2689
|
+
</div>
|
|
2690
|
+
</template>
|
|
2691
|
+
</div>
|
|
2692
|
+
</div>
|
|
2693
|
+
</template>
|
|
2694
|
+
|
|
2578
2695
|
<!-- Memory view -->
|
|
2579
2696
|
<template x-if="view === 'memory' && !selectedSession">
|
|
2580
2697
|
<div style="display:flex;flex-direction:column;height:100%;overflow:hidden;">
|
|
@@ -3380,6 +3497,58 @@
|
|
|
3380
3497
|
</div>
|
|
3381
3498
|
<template x-if="config.settings?.statusLine?.command"><div class="config-row"><div class="config-key">Status line</div><div class="config-val" x-text="config.settings.statusLine.command"></div></div></template>
|
|
3382
3499
|
</div>
|
|
3500
|
+
|
|
3501
|
+
<div class="config-section">
|
|
3502
|
+
<div class="config-section-title" style="display:flex;align-items:center;justify-content:space-between">
|
|
3503
|
+
Sharing
|
|
3504
|
+
<div style="display:flex;align-items:center;gap:8px">
|
|
3505
|
+
<span class="save-msg" x-show="githubTokenMsg==='Saved'" x-text="githubTokenMsg"></span>
|
|
3506
|
+
<span class="err-msg" x-show="githubTokenMsg && githubTokenMsg!=='Saved'" x-text="githubTokenMsg"></span>
|
|
3507
|
+
<button class="btn btn-primary btn-sm" @click="saveGithubToken()" :disabled="githubTokenSaving || !githubTokenDraft">
|
|
3508
|
+
<span x-text="githubTokenSaving?'Saving…':'Save'"></span>
|
|
3509
|
+
</button>
|
|
3510
|
+
</div>
|
|
3511
|
+
</div>
|
|
3512
|
+
<div class="config-row">
|
|
3513
|
+
<div class="config-key">GitHub token</div>
|
|
3514
|
+
<div class="config-val" style="flex:1">
|
|
3515
|
+
<input type="password" class="settings-input" x-model="githubTokenDraft" placeholder="ghp_…" style="width:100%;font-family:monospace" />
|
|
3516
|
+
<div style="font-size:11px;color:var(--ink-3);margin-top:4px">
|
|
3517
|
+
<template x-if="githubTokenSet">
|
|
3518
|
+
<span>Token saved (<span x-text="githubTokenMasked"></span>). Enter a new value to replace it.</span>
|
|
3519
|
+
</template>
|
|
3520
|
+
<template x-if="!githubTokenSet">
|
|
3521
|
+
<span>Required to publish sessions and plans as public GitHub Gists. Generate a classic token at github.com/settings/tokens with the <strong>gist</strong> scope checked.</span>
|
|
3522
|
+
</template>
|
|
3523
|
+
</div>
|
|
3524
|
+
</div>
|
|
3525
|
+
</div>
|
|
3526
|
+
</div>
|
|
3527
|
+
|
|
3528
|
+
<div class="config-section">
|
|
3529
|
+
<div class="config-section-title">Integrations</div>
|
|
3530
|
+
<div class="config-row">
|
|
3531
|
+
<div class="config-key">Notes</div>
|
|
3532
|
+
<div class="config-val" style="flex:1">
|
|
3533
|
+
<div style="display:flex;align-items:center;gap:10px">
|
|
3534
|
+
<template x-if="noteClaudeInstalled">
|
|
3535
|
+
<span style="font-size:12px;color:var(--green);display:flex;align-items:center;gap:4px">
|
|
3536
|
+
<svg width="13" height="13" viewBox="0 0 13 13" fill="none" stroke="currentColor" stroke-width="2"><polyline points="2 7 5 10 11 3"/></svg>
|
|
3537
|
+
Enabled
|
|
3538
|
+
</span>
|
|
3539
|
+
</template>
|
|
3540
|
+
<template x-if="!noteClaudeInstalled">
|
|
3541
|
+
<div style="display:flex;align-items:center;gap:8px">
|
|
3542
|
+
<span x-show="noteSetupMsg" x-text="noteSetupMsg" style="font-size:12px;color:var(--green)"></span>
|
|
3543
|
+
<button class="btn btn-primary btn-sm" @click="setupClaudeNotes()" :disabled="!!noteSetupMsg">Set up</button>
|
|
3544
|
+
</div>
|
|
3545
|
+
</template>
|
|
3546
|
+
</div>
|
|
3547
|
+
<div style="font-size:11px;color:var(--ink-3);margin-top:4px">Adds an instruction to your CLAUDE.md so Claude can save notes when you ask. Notes are your personal notepad — Claude won't read them as context.</div>
|
|
3548
|
+
</div>
|
|
3549
|
+
</div>
|
|
3550
|
+
</div>
|
|
3551
|
+
|
|
3383
3552
|
</div>
|
|
3384
3553
|
</div>
|
|
3385
3554
|
</template>
|
|
@@ -4165,8 +4334,24 @@
|
|
|
4165
4334
|
exportDropOpen: false,
|
|
4166
4335
|
exportMsg: '',
|
|
4167
4336
|
cleanMode: true,
|
|
4337
|
+
|
|
4168
4338
|
planExportMsg: '',
|
|
4169
4339
|
planExportOpen: false,
|
|
4340
|
+
planShareMsg: '',
|
|
4341
|
+
personalNotes: [],
|
|
4342
|
+
personalNotesLoading: false,
|
|
4343
|
+
selectedNote: null,
|
|
4344
|
+
noteEditing: false,
|
|
4345
|
+
noteTitleDraft: '',
|
|
4346
|
+
noteBodyDraft: '',
|
|
4347
|
+
noteMsg: '',
|
|
4348
|
+
noteSaving: false,
|
|
4349
|
+
noteSearch: '',
|
|
4350
|
+
noteCreating: false,
|
|
4351
|
+
noteNewTitle: '',
|
|
4352
|
+
noteNewBody: '',
|
|
4353
|
+
noteClaudeInstalled: null,
|
|
4354
|
+
noteSetupMsg: '',
|
|
4170
4355
|
sidebarW: parseInt(localStorage.getItem('cm:sidebarW') || '260'),
|
|
4171
4356
|
historyEntries: [],
|
|
4172
4357
|
historyLoading: false,
|
|
@@ -4210,6 +4395,13 @@
|
|
|
4210
4395
|
settingsDraft: { model: '', language: '', voiceEnabled: false, outputStyle: '', effortLevel: '', defaultMode: '' },
|
|
4211
4396
|
settingsSaving: false,
|
|
4212
4397
|
settingsMsg: '',
|
|
4398
|
+
githubTokenDraft: '',
|
|
4399
|
+
githubTokenSet: false,
|
|
4400
|
+
githubTokenMasked: '',
|
|
4401
|
+
githubTokenSaving: false,
|
|
4402
|
+
githubTokenMsg: '',
|
|
4403
|
+
shareMsg: '',
|
|
4404
|
+
shareUrl: '',
|
|
4213
4405
|
permsDraft: { allow: [], deny: [], ask: [] },
|
|
4214
4406
|
permsNewRule: { allow: '', deny: '', ask: '' },
|
|
4215
4407
|
ruleBuilder: { type: 'deny', tool: 'Bash', specifier: '', pathType: './' },
|
|
@@ -4325,16 +4517,37 @@
|
|
|
4325
4517
|
this.loadCosts();
|
|
4326
4518
|
this.loadStatus();
|
|
4327
4519
|
this.loadInsights();
|
|
4520
|
+
// Load counts for nav badges without blocking
|
|
4521
|
+
this.loadPlans();
|
|
4522
|
+
this.loadAgents();
|
|
4523
|
+
this.loadTools();
|
|
4524
|
+
this.loadMemory();
|
|
4525
|
+
this.loadPersonalNotes();
|
|
4526
|
+
// Parse URL hash for direct links
|
|
4527
|
+
const hash = window.location.hash;
|
|
4528
|
+
const mSession = hash.match(/^#\/session\/([^/]+)\/([^/]+)$/);
|
|
4529
|
+
const mNote = hash.match(/^#\/note\/(.+)$/);
|
|
4530
|
+
if (mSession) { this.view = 'sessions'; await this.openSessionById(mSession[2], mSession[1]); }
|
|
4531
|
+
if (mNote) {
|
|
4532
|
+
const notes = await fetch('/api/notes').then(r => r.json()).catch(() => []);
|
|
4533
|
+
this.personalNotes = notes;
|
|
4534
|
+
const note = this.personalNotes.find(n => n.filename === mNote[1]);
|
|
4535
|
+
if (note) { this.view = 'notes'; this.selectedNote = note; }
|
|
4536
|
+
}
|
|
4537
|
+
// Clear hash when closing
|
|
4538
|
+
this.$watch('selectedSession', v => { if (!v) { if (window.location.hash.startsWith('#/session/')) window.location.hash = ''; } });
|
|
4539
|
+
this.$watch('selectedNote', v => { if (!v) { if (window.location.hash.startsWith('#/note/')) window.location.hash = ''; } });
|
|
4328
4540
|
},
|
|
4329
4541
|
|
|
4330
4542
|
initView(v) {
|
|
4331
4543
|
if (v === 'dashboard') { this.loadStats(); this.loadInsights(); }
|
|
4332
4544
|
if (v === 'projects') { this.loadProjects(); }
|
|
4333
4545
|
if (v === 'memory') { this.loadMemory(); }
|
|
4546
|
+
if (v === 'notes') { this.loadPersonalNotes(); }
|
|
4334
4547
|
if (v === 'plans') { this.loadPlans(); }
|
|
4335
4548
|
if (v === 'commands' || v === 'skills') { this.loadTools(); }
|
|
4336
4549
|
if (v === 'agents') { this.loadAgents(); }
|
|
4337
|
-
if (v === 'config' || v === 'instructions' || v === 'permissions' || v === 'hooks') { this.loadConfig(); }
|
|
4550
|
+
if (v === 'config' || v === 'instructions' || v === 'permissions' || v === 'hooks') { this.loadConfig(); this.checkNoteClaudeStatus(); }
|
|
4338
4551
|
},
|
|
4339
4552
|
|
|
4340
4553
|
async loadProjects() {
|
|
@@ -4425,6 +4638,8 @@
|
|
|
4425
4638
|
this.selectedSession = session;
|
|
4426
4639
|
this.loadingDetail = true;
|
|
4427
4640
|
this.sessionDetail = null;
|
|
4641
|
+
|
|
4642
|
+
window.location.hash = `#/session/${session.projectDir}/${session.sessionId}`;
|
|
4428
4643
|
const data = await fetch(`/api/sessions/${session.projectDir}/${session.sessionId}`).then(r => r.json());
|
|
4429
4644
|
this.sessionDetail = data;
|
|
4430
4645
|
this.loadingDetail = false;
|
|
@@ -4439,6 +4654,7 @@
|
|
|
4439
4654
|
await this.openSession(s);
|
|
4440
4655
|
},
|
|
4441
4656
|
|
|
4657
|
+
|
|
4442
4658
|
async resumeSession(sessionId, btnId, projectPath) {
|
|
4443
4659
|
const cmd = projectPath
|
|
4444
4660
|
? `cd "${projectPath}" && claude -r ${sessionId}`
|
|
@@ -5147,6 +5363,136 @@
|
|
|
5147
5363
|
} catch (e) { alert('Error: ' + e.message); }
|
|
5148
5364
|
},
|
|
5149
5365
|
|
|
5366
|
+
async saveGithubToken() {
|
|
5367
|
+
this.githubTokenSaving = true; this.githubTokenMsg = '';
|
|
5368
|
+
try {
|
|
5369
|
+
const r = await fetch('/api/app-settings', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ githubToken: this.githubTokenDraft }) });
|
|
5370
|
+
if (!r.ok) throw new Error((await r.json()).error);
|
|
5371
|
+
this.githubTokenSet = !!this.githubTokenDraft;
|
|
5372
|
+
this.githubTokenMasked = this.githubTokenDraft ? this.githubTokenDraft.slice(0,4) + '…' + this.githubTokenDraft.slice(-4) : '';
|
|
5373
|
+
this.githubTokenDraft = '';
|
|
5374
|
+
this.githubTokenMsg = 'Saved';
|
|
5375
|
+
setTimeout(() => this.githubTokenMsg = '', 2000);
|
|
5376
|
+
} catch (e) { this.githubTokenMsg = 'Error: ' + e.message; }
|
|
5377
|
+
this.githubTokenSaving = false;
|
|
5378
|
+
},
|
|
5379
|
+
|
|
5380
|
+
async shareSession() {
|
|
5381
|
+
this.shareMsg = 'Sharing…'; this.shareUrl = '';
|
|
5382
|
+
try {
|
|
5383
|
+
const r = await fetch(`/api/share/session/${this.selectedSession.projectDir}/${this.selectedSession.sessionId}`, { method: 'POST' });
|
|
5384
|
+
const data = await r.json();
|
|
5385
|
+
if (!r.ok) throw new Error(data.error);
|
|
5386
|
+
this.shareUrl = data.url;
|
|
5387
|
+
await navigator.clipboard.writeText(data.url);
|
|
5388
|
+
this.shareMsg = 'Link copied!';
|
|
5389
|
+
} catch (e) { this.shareMsg = e.message; }
|
|
5390
|
+
setTimeout(() => { this.shareMsg = ''; this.shareUrl = ''; }, 5000);
|
|
5391
|
+
},
|
|
5392
|
+
|
|
5393
|
+
async sharePlan(filename) {
|
|
5394
|
+
this.planShareMsg = 'Sharing…';
|
|
5395
|
+
try {
|
|
5396
|
+
const r = await fetch(`/api/share/plan/${filename}`, { method: 'POST' });
|
|
5397
|
+
const data = await r.json();
|
|
5398
|
+
if (!r.ok) throw new Error(data.error);
|
|
5399
|
+
await navigator.clipboard.writeText(data.url);
|
|
5400
|
+
this.planShareMsg = 'Link copied!';
|
|
5401
|
+
} catch (e) { this.planShareMsg = e.message; }
|
|
5402
|
+
setTimeout(() => this.planShareMsg = '', 5000);
|
|
5403
|
+
},
|
|
5404
|
+
|
|
5405
|
+
async loadPersonalNotes() {
|
|
5406
|
+
if (this.personalNotesLoading) return;
|
|
5407
|
+
this.personalNotesLoading = true;
|
|
5408
|
+
const [notes, status] = await Promise.all([
|
|
5409
|
+
fetch('/api/notes').then(r => r.json()).catch(() => []),
|
|
5410
|
+
fetch('/api/notes/claude-md-status').then(r => r.json()).catch(() => ({})),
|
|
5411
|
+
]);
|
|
5412
|
+
this.personalNotes = notes;
|
|
5413
|
+
this.noteClaudeInstalled = status.installed ?? null;
|
|
5414
|
+
this.personalNotesLoading = false;
|
|
5415
|
+
},
|
|
5416
|
+
|
|
5417
|
+
async checkNoteClaudeStatus() {
|
|
5418
|
+
const s = await fetch('/api/notes/claude-md-status').then(r => r.json()).catch(() => ({}));
|
|
5419
|
+
this.noteClaudeInstalled = s.installed ?? null;
|
|
5420
|
+
},
|
|
5421
|
+
|
|
5422
|
+
async setupClaudeNotes() {
|
|
5423
|
+
this.noteSetupMsg = 'Installing…';
|
|
5424
|
+
try {
|
|
5425
|
+
const r = await fetch('/api/notes/setup-claude', { method: 'POST' });
|
|
5426
|
+
const d = await r.json();
|
|
5427
|
+
this.noteClaudeInstalled = true;
|
|
5428
|
+
this.noteSetupMsg = d.alreadyInstalled ? 'Already installed' : 'Installed!';
|
|
5429
|
+
} catch (e) { this.noteSetupMsg = 'Error: ' + e.message; }
|
|
5430
|
+
setTimeout(() => this.noteSetupMsg = '', 3000);
|
|
5431
|
+
},
|
|
5432
|
+
|
|
5433
|
+
async createPersonalNote() {
|
|
5434
|
+
if (!this.noteNewTitle.trim()) return;
|
|
5435
|
+
this.noteSaving = true;
|
|
5436
|
+
try {
|
|
5437
|
+
const note = await fetch('/api/notes', {
|
|
5438
|
+
method: 'POST',
|
|
5439
|
+
headers: { 'Content-Type': 'application/json' },
|
|
5440
|
+
body: JSON.stringify({ title: this.noteNewTitle, content: this.noteNewBody }),
|
|
5441
|
+
}).then(r => r.json());
|
|
5442
|
+
this.personalNotes.unshift(note);
|
|
5443
|
+
this.selectedNote = note;
|
|
5444
|
+
this.noteCreating = false;
|
|
5445
|
+
this.noteNewTitle = '';
|
|
5446
|
+
this.noteNewBody = '';
|
|
5447
|
+
this.noteEditing = false;
|
|
5448
|
+
} catch (e) { this.noteMsg = 'Error: ' + e.message; }
|
|
5449
|
+
this.noteSaving = false;
|
|
5450
|
+
},
|
|
5451
|
+
|
|
5452
|
+
async savePersonalNote() {
|
|
5453
|
+
if (!this.selectedNote) return;
|
|
5454
|
+
this.noteSaving = true; this.noteMsg = '';
|
|
5455
|
+
try {
|
|
5456
|
+
const updated = await fetch(`/api/notes/${this.selectedNote.filename}`, {
|
|
5457
|
+
method: 'PUT',
|
|
5458
|
+
headers: { 'Content-Type': 'application/json' },
|
|
5459
|
+
body: JSON.stringify({ title: this.noteTitleDraft, content: this.noteBodyDraft }),
|
|
5460
|
+
}).then(r => r.json());
|
|
5461
|
+
const idx = this.personalNotes.findIndex(n => n.filename === this.selectedNote.filename);
|
|
5462
|
+
if (idx !== -1) this.personalNotes[idx] = updated;
|
|
5463
|
+
this.selectedNote = updated;
|
|
5464
|
+
this.noteEditing = false;
|
|
5465
|
+
this.noteMsg = 'Saved';
|
|
5466
|
+
setTimeout(() => this.noteMsg = '', 2000);
|
|
5467
|
+
} catch (e) { this.noteMsg = 'Error: ' + e.message; }
|
|
5468
|
+
this.noteSaving = false;
|
|
5469
|
+
},
|
|
5470
|
+
|
|
5471
|
+
async deletePersonalNote() {
|
|
5472
|
+
if (!this.selectedNote || !confirm(`Delete "${this.selectedNote.title}"?`)) return;
|
|
5473
|
+
await fetch(`/api/notes/${this.selectedNote.filename}`, { method: 'DELETE' });
|
|
5474
|
+
this.personalNotes = this.personalNotes.filter(n => n.filename !== this.selectedNote.filename);
|
|
5475
|
+
this.selectedNote = null;
|
|
5476
|
+
this.noteEditing = false;
|
|
5477
|
+
},
|
|
5478
|
+
|
|
5479
|
+
async saveNoteFromSession() {
|
|
5480
|
+
const title = this.selectedSession?.firstPrompt?.slice(0, 60) || 'Session note';
|
|
5481
|
+
const sessionId = this.selectedSession?.sessionId || '';
|
|
5482
|
+
const note = await fetch('/api/notes', {
|
|
5483
|
+
method: 'POST',
|
|
5484
|
+
headers: { 'Content-Type': 'application/json' },
|
|
5485
|
+
body: JSON.stringify({ title, content: '', session: sessionId }),
|
|
5486
|
+
}).then(r => r.json());
|
|
5487
|
+
this.personalNotes.unshift(note);
|
|
5488
|
+
this.selectedNote = note;
|
|
5489
|
+
this.noteEditing = true;
|
|
5490
|
+
this.noteTitleDraft = note.title;
|
|
5491
|
+
this.noteBodyDraft = note.content;
|
|
5492
|
+
this.view = 'notes';
|
|
5493
|
+
this.selectedSession = null;
|
|
5494
|
+
},
|
|
5495
|
+
|
|
5150
5496
|
async saveSettings() {
|
|
5151
5497
|
this.settingsSaving = true; this.settingsMsg = '';
|
|
5152
5498
|
try {
|
|
@@ -5576,6 +5922,10 @@
|
|
|
5576
5922
|
const proj = this.projects?.find(p => p.dirName === this.configProject);
|
|
5577
5923
|
const pp = proj?.projectPath ? '?projectPath=' + encodeURIComponent(proj.projectPath) : '';
|
|
5578
5924
|
this.config = await fetch('/api/config' + pp).then(r => r.json());
|
|
5925
|
+
const as = await fetch('/api/app-settings').then(r => r.json()).catch(() => ({}));
|
|
5926
|
+
this.githubTokenSet = as.githubTokenSet || false;
|
|
5927
|
+
this.githubTokenMasked = as.githubTokenMasked || '';
|
|
5928
|
+
this.githubTokenDraft = '';
|
|
5579
5929
|
this.settingsDraft = {
|
|
5580
5930
|
model: this.config.settings?.model || '',
|
|
5581
5931
|
language: this.config.settings?.language || '',
|
package/server.js
CHANGED
|
@@ -1767,6 +1767,234 @@ app.get('/api/sessions/:project/:sessionId/export', async (req, res) => {
|
|
|
1767
1767
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
|
1768
1768
|
});
|
|
1769
1769
|
|
|
1770
|
+
// ─── App settings (claude-home specific, not Claude's settings.json) ─────────
|
|
1771
|
+
const APP_SETTINGS_FILE = path.join(DATA_DIR, 'app-settings.json');
|
|
1772
|
+
|
|
1773
|
+
function readAppSettings() {
|
|
1774
|
+
try { return JSON.parse(fs.readFileSync(APP_SETTINGS_FILE, 'utf8')); } catch { return {}; }
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
app.get('/api/app-settings', (req, res) => {
|
|
1778
|
+
const s = readAppSettings();
|
|
1779
|
+
// Mask token for display
|
|
1780
|
+
const masked = s.githubToken ? s.githubToken.slice(0, 4) + '…' + s.githubToken.slice(-4) : '';
|
|
1781
|
+
res.json({ githubTokenSet: !!s.githubToken, githubTokenMasked: masked });
|
|
1782
|
+
});
|
|
1783
|
+
|
|
1784
|
+
app.put('/api/app-settings', (req, res) => {
|
|
1785
|
+
try {
|
|
1786
|
+
const current = readAppSettings();
|
|
1787
|
+
const { githubToken } = req.body;
|
|
1788
|
+
if (githubToken !== undefined) current.githubToken = githubToken;
|
|
1789
|
+
ensureDataDir();
|
|
1790
|
+
fs.writeFileSync(APP_SETTINGS_FILE, JSON.stringify(current, null, 2), 'utf8');
|
|
1791
|
+
res.json({ ok: true });
|
|
1792
|
+
} catch (e) { res.status(500).json({ error: e.message }); }
|
|
1793
|
+
});
|
|
1794
|
+
|
|
1795
|
+
// ─── Gist sharing ─────────────────────────────────────────────────────────────
|
|
1796
|
+
function postGist(token, description, filename, content) {
|
|
1797
|
+
return new Promise((resolve, reject) => {
|
|
1798
|
+
const body = JSON.stringify({ description, public: true, files: { [filename]: { content } } });
|
|
1799
|
+
const opts = {
|
|
1800
|
+
hostname: 'api.github.com',
|
|
1801
|
+
path: '/gists',
|
|
1802
|
+
method: 'POST',
|
|
1803
|
+
headers: {
|
|
1804
|
+
'Authorization': `Bearer ${token}`,
|
|
1805
|
+
'Content-Type': 'application/json',
|
|
1806
|
+
'User-Agent': 'claude-home',
|
|
1807
|
+
'Accept': 'application/vnd.github+json',
|
|
1808
|
+
'Content-Length': Buffer.byteLength(body),
|
|
1809
|
+
},
|
|
1810
|
+
};
|
|
1811
|
+
const req = https.request(opts, res => {
|
|
1812
|
+
let data = '';
|
|
1813
|
+
res.on('data', c => data += c);
|
|
1814
|
+
res.on('end', () => {
|
|
1815
|
+
try {
|
|
1816
|
+
const json = JSON.parse(data);
|
|
1817
|
+
if (json.html_url) resolve(json.html_url);
|
|
1818
|
+
else {
|
|
1819
|
+
const msg = json.message || 'GitHub API error';
|
|
1820
|
+
const hint = (msg === 'Not Found' || msg === 'Bad credentials')
|
|
1821
|
+
? `${msg} — check your token has the "gist" scope`
|
|
1822
|
+
: msg;
|
|
1823
|
+
reject(new Error(hint));
|
|
1824
|
+
}
|
|
1825
|
+
} catch { reject(new Error('Invalid response from GitHub')); }
|
|
1826
|
+
});
|
|
1827
|
+
});
|
|
1828
|
+
req.on('error', reject);
|
|
1829
|
+
req.write(body);
|
|
1830
|
+
req.end();
|
|
1831
|
+
});
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
app.post('/api/share/session/:project/:sessionId', async (req, res) => {
|
|
1835
|
+
const { githubToken } = readAppSettings();
|
|
1836
|
+
if (!githubToken) return res.status(400).json({ error: 'GitHub token not configured. Add it in Settings → Sharing.' });
|
|
1837
|
+
const { project, sessionId } = req.params;
|
|
1838
|
+
const filePath = path.join(PROJECTS_DIR, project, `${sessionId}.jsonl`);
|
|
1839
|
+
if (!filePath.startsWith(PROJECTS_DIR)) return res.status(400).json({ error: 'invalid path' });
|
|
1840
|
+
try {
|
|
1841
|
+
const messages = await parseJsonl(filePath);
|
|
1842
|
+
const lines = [`# Session ${sessionId}`, `**Project:** ${project}`, ''];
|
|
1843
|
+
for (const m of messages) {
|
|
1844
|
+
if (m.type === 'user') {
|
|
1845
|
+
const text = typeof m.message?.content === 'string' ? m.message.content
|
|
1846
|
+
: (m.message?.content || []).filter(b => b.type === 'text').map(b => b.text).join('\n');
|
|
1847
|
+
if (text.trim()) lines.push(`## Human\n\n${text.trim()}`, '');
|
|
1848
|
+
} else if (m.type === 'assistant') {
|
|
1849
|
+
const text = typeof m.message?.content === 'string' ? m.message.content
|
|
1850
|
+
: (m.message?.content || []).filter(b => b.type === 'text').map(b => b.text).join('\n');
|
|
1851
|
+
if (text.trim()) lines.push(`## Claude\n\n${text.trim()}`, '');
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
const url = await postGist(githubToken, `Claude session — ${project}`, `${sessionId}.md`, lines.join('\n'));
|
|
1855
|
+
res.json({ url });
|
|
1856
|
+
} catch (e) { res.status(500).json({ error: e.message }); }
|
|
1857
|
+
});
|
|
1858
|
+
|
|
1859
|
+
app.post('/api/share/plan/:filename', async (req, res) => {
|
|
1860
|
+
const { githubToken } = readAppSettings();
|
|
1861
|
+
if (!githubToken) return res.status(400).json({ error: 'GitHub token not configured. Add it in Settings → Sharing.' });
|
|
1862
|
+
const filename = path.basename(req.params.filename);
|
|
1863
|
+
if (!filename.endsWith('.md')) return res.status(400).json({ error: 'invalid filename' });
|
|
1864
|
+
const filePath = path.join(os.homedir(), '.claude', 'plans', filename);
|
|
1865
|
+
try {
|
|
1866
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
1867
|
+
const titleMatch = content.match(/^#\s+(.+)$/m);
|
|
1868
|
+
const description = titleMatch ? titleMatch[1].trim() : filename;
|
|
1869
|
+
const url = await postGist(githubToken, `Claude plan — ${description}`, filename, content);
|
|
1870
|
+
res.json({ url });
|
|
1871
|
+
} catch (e) { res.status(500).json({ error: e.message }); }
|
|
1872
|
+
});
|
|
1873
|
+
|
|
1874
|
+
// ─── Personal Notes ───────────────────────────────────────────────────────────
|
|
1875
|
+
const NOTES_DIR = path.join(DATA_DIR, 'notes');
|
|
1876
|
+
function ensureNotesDir() { if (!fs.existsSync(NOTES_DIR)) fs.mkdirSync(NOTES_DIR, { recursive: true }); }
|
|
1877
|
+
|
|
1878
|
+
function parseNoteFile(filename) {
|
|
1879
|
+
const filePath = path.join(NOTES_DIR, filename);
|
|
1880
|
+
const stat = fs.statSync(filePath);
|
|
1881
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
1882
|
+
const { meta, content: body } = parseFrontmatter(raw);
|
|
1883
|
+
return {
|
|
1884
|
+
filename,
|
|
1885
|
+
title: meta.title || filename.replace('.md', ''),
|
|
1886
|
+
date: meta.date || stat.mtimeMs,
|
|
1887
|
+
session: meta.session || '',
|
|
1888
|
+
content: body.trim(),
|
|
1889
|
+
modified: new Date(stat.mtimeMs).toISOString(),
|
|
1890
|
+
};
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
const NOTES_CLAUDE_MD_SNIPPET = `
|
|
1894
|
+
## Personal Notes
|
|
1895
|
+
|
|
1896
|
+
When the user asks you to "save a note", "add to notes", "guarda esto como nota", or similar:
|
|
1897
|
+
1. Create a markdown file in \`~/.claude/claude-home/notes/\` with this exact format:
|
|
1898
|
+
- Filename: \`YYYY-MM-DD-short-slug.md\` (e.g. \`2026-03-31-bug-fix-auth.md\`)
|
|
1899
|
+
- Content:
|
|
1900
|
+
\`\`\`
|
|
1901
|
+
---
|
|
1902
|
+
title: <descriptive title>
|
|
1903
|
+
date: <current ISO date>
|
|
1904
|
+
session: <current session ID if known>
|
|
1905
|
+
---
|
|
1906
|
+
|
|
1907
|
+
<the content the user wants to save>
|
|
1908
|
+
\`\`\`
|
|
1909
|
+
2. Use the Write tool to create the file (not Bash).
|
|
1910
|
+
3. Confirm with: "Saved to Notes: http://localhost:3141/#/note/<filename>"
|
|
1911
|
+
|
|
1912
|
+
The notes directory may not exist yet — create it if needed with \`mkdir -p\`.
|
|
1913
|
+
`;
|
|
1914
|
+
|
|
1915
|
+
app.get('/api/notes/claude-md-status', (req, res) => {
|
|
1916
|
+
const claudeMdPath = path.join(os.homedir(), '.claude', 'CLAUDE.md');
|
|
1917
|
+
try {
|
|
1918
|
+
const content = fs.existsSync(claudeMdPath) ? fs.readFileSync(claudeMdPath, 'utf8') : '';
|
|
1919
|
+
res.json({ installed: content.includes('Personal Notes') });
|
|
1920
|
+
} catch (e) { res.status(500).json({ error: e.message }); }
|
|
1921
|
+
});
|
|
1922
|
+
|
|
1923
|
+
app.post('/api/notes/setup-claude', (req, res) => {
|
|
1924
|
+
const claudeMdPath = path.join(os.homedir(), '.claude', 'CLAUDE.md');
|
|
1925
|
+
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
1926
|
+
try {
|
|
1927
|
+
const current = fs.existsSync(claudeMdPath) ? fs.readFileSync(claudeMdPath, 'utf8') : '';
|
|
1928
|
+
if (current.includes('Personal Notes')) return res.json({ ok: true, alreadyInstalled: true });
|
|
1929
|
+
// Append CLAUDE.md snippet
|
|
1930
|
+
fs.writeFileSync(claudeMdPath, current + '\n' + NOTES_CLAUDE_MD_SNIPPET, 'utf8');
|
|
1931
|
+
// Add Write permission for notes dir to settings.json
|
|
1932
|
+
try {
|
|
1933
|
+
const settings = fs.existsSync(settingsPath) ? JSON.parse(fs.readFileSync(settingsPath, 'utf8')) : {};
|
|
1934
|
+
if (!settings.permissions) settings.permissions = {};
|
|
1935
|
+
if (!settings.permissions.allow) settings.permissions.allow = [];
|
|
1936
|
+
const rule = `Write(${path.join(os.homedir(), '.claude', 'claude-home', 'notes', '*')})`;
|
|
1937
|
+
if (!settings.permissions.allow.includes(rule)) {
|
|
1938
|
+
settings.permissions.allow.push(rule);
|
|
1939
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
|
|
1940
|
+
}
|
|
1941
|
+
} catch {}
|
|
1942
|
+
res.json({ ok: true });
|
|
1943
|
+
} catch (e) { res.status(500).json({ error: e.message }); }
|
|
1944
|
+
});
|
|
1945
|
+
|
|
1946
|
+
app.get('/api/notes', (req, res) => {
|
|
1947
|
+
ensureNotesDir();
|
|
1948
|
+
try {
|
|
1949
|
+
const files = fs.readdirSync(NOTES_DIR).filter(f => f.endsWith('.md'));
|
|
1950
|
+
const notes = files.map(f => parseNoteFile(f)).sort((a, b) => b.modified.localeCompare(a.modified));
|
|
1951
|
+
res.json(notes);
|
|
1952
|
+
} catch (e) { res.status(500).json({ error: e.message }); }
|
|
1953
|
+
});
|
|
1954
|
+
|
|
1955
|
+
app.post('/api/notes', (req, res) => {
|
|
1956
|
+
ensureNotesDir();
|
|
1957
|
+
const { title, content, session } = req.body;
|
|
1958
|
+
if (!title) return res.status(400).json({ error: 'title required' });
|
|
1959
|
+
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 40) || 'note';
|
|
1960
|
+
const date = new Date().toISOString();
|
|
1961
|
+
const datePrefix = date.slice(0, 10);
|
|
1962
|
+
let filename = `${datePrefix}-${slug}.md`;
|
|
1963
|
+
// avoid collisions
|
|
1964
|
+
let i = 1;
|
|
1965
|
+
while (fs.existsSync(path.join(NOTES_DIR, filename))) { filename = `${datePrefix}-${slug}-${i++}.md`; }
|
|
1966
|
+
const sessionLine = session ? `\nsession: ${session}` : '';
|
|
1967
|
+
const raw = `---\ntitle: ${title}\ndate: ${date}${sessionLine}\n---\n\n${content || ''}`;
|
|
1968
|
+
fs.writeFileSync(path.join(NOTES_DIR, filename), raw, 'utf8');
|
|
1969
|
+
res.json(parseNoteFile(filename));
|
|
1970
|
+
});
|
|
1971
|
+
|
|
1972
|
+
app.put('/api/notes/:filename', (req, res) => {
|
|
1973
|
+
const filename = path.basename(req.params.filename);
|
|
1974
|
+
if (!filename.endsWith('.md')) return res.status(400).json({ error: 'invalid filename' });
|
|
1975
|
+
const filePath = path.join(NOTES_DIR, filename);
|
|
1976
|
+
if (!filePath.startsWith(NOTES_DIR)) return res.status(400).json({ error: 'invalid path' });
|
|
1977
|
+
const { title, content } = req.body;
|
|
1978
|
+
try {
|
|
1979
|
+
const existing = parseNoteFile(filename);
|
|
1980
|
+
const sessionLine = existing.session ? `\nsession: ${existing.session}` : '';
|
|
1981
|
+
const raw = `---\ntitle: ${title || existing.title}\ndate: ${existing.date}${sessionLine}\n---\n\n${content ?? existing.content}`;
|
|
1982
|
+
fs.writeFileSync(filePath, raw, 'utf8');
|
|
1983
|
+
res.json(parseNoteFile(filename));
|
|
1984
|
+
} catch (e) { res.status(500).json({ error: e.message }); }
|
|
1985
|
+
});
|
|
1986
|
+
|
|
1987
|
+
app.delete('/api/notes/:filename', (req, res) => {
|
|
1988
|
+
const filename = path.basename(req.params.filename);
|
|
1989
|
+
if (!filename.endsWith('.md')) return res.status(400).json({ error: 'invalid filename' });
|
|
1990
|
+
const filePath = path.join(NOTES_DIR, filename);
|
|
1991
|
+
if (!filePath.startsWith(NOTES_DIR)) return res.status(400).json({ error: 'invalid path' });
|
|
1992
|
+
try {
|
|
1993
|
+
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
|
|
1994
|
+
res.json({ ok: true });
|
|
1995
|
+
} catch (e) { res.status(500).json({ error: e.message }); }
|
|
1996
|
+
});
|
|
1997
|
+
|
|
1770
1998
|
// ─── Start ────────────────────────────────────────────────────────────────────
|
|
1771
1999
|
|
|
1772
2000
|
function startServer(port) {
|