claude-home 1.5.37 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -17,11 +17,15 @@ Claude Code is powerful but headless. Everything lives in files scattered across
17
17
  ### Sessions
18
18
  Browse every conversation you've had with Claude. Search by content, filter by project or branch, resume from where you left off, export as Markdown, or publish as a public GitHub Gist. Direct shareable links (`#/session/...`) let you bookmark or share specific conversations.
19
19
 
20
+ **Session snapshot** — save a structured summary of any conversation as a note. Opens the **Export** menu and click *Snapshot → Note*. The note includes project/branch/date metadata, a numbered list of all your prompts, and an empty *Notes* section for your annotations. Tagged `snapshot` automatically with a backlink to the original session.
21
+
20
22
  ### Today
21
23
  A daily task list that lives alongside your Claude work. Add tasks, provide context (hours available, meetings, energy), and hit **Copy for Claude** to get a formatted prompt ready to paste for prioritization. Uncompleted tasks carry over automatically to the next day.
22
24
 
23
25
  Claude can add tasks directly from any session — just say "add this to today" or "remind me tomorrow to…"
24
26
 
27
+ Tasks created from notes show a **↗ Note title** chip that navigates directly to the source note.
28
+
25
29
  ### Notes
26
30
  Your personal notepad — separate from Claude's memory. Notes are for you, not for Claude's context. Capture decisions, TILs, bug solutions, runbooks, snippets, or anything worth keeping.
27
31
 
@@ -37,6 +41,14 @@ Direct links (`#/note/filename`) let you open a specific note instantly.
37
41
 
38
42
  **Clipboard capture** — opening *New note* automatically pre-fills the content with whatever is in your clipboard.
39
43
 
44
+ **Folders** — organize notes in folders (per project or custom). Create folders from the sidebar, move notes between folders via the toolbar dropdown, and rename folders inline with a hover button (✎).
45
+
46
+ **Tags** — add `tags: [bug, idea]` in frontmatter. Filter by multiple tags in the sidebar. Hover any tag chip to rename or delete it globally across all notes.
47
+
48
+ **Pinned notes** — mark important notes with 📌 to keep them always at the top of the list, both in root and inside folders.
49
+
50
+ **Note → Today task** — hover any paragraph or list item in a note to reveal a **+** button that adds it directly to today's task list. A **+ Today** button in the toolbar adds the note title as a task. All tasks created from notes carry a link back to the source note.
51
+
40
52
  ### Projects
41
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.
42
54
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-home",
3
- "version": "1.5.37",
3
+ "version": "1.6.1",
4
4
  "description": "Web dashboard for Claude Code — browse sessions, manage skills, hooks, commands, and agents",
5
5
  "main": "server.js",
6
6
  "bin": {
package/public/index.html CHANGED
@@ -484,6 +484,96 @@
484
484
  .md-content th { background: var(--canvas); font-weight: 600; }
485
485
  .md-content hr { border: none; border-top: 1px solid var(--rule); margin: 10px 0; }
486
486
 
487
+ /* Note folders */
488
+ .note-folder-row {
489
+ display: flex;
490
+ align-items: center;
491
+ gap: 7px;
492
+ padding: 7px 12px;
493
+ cursor: pointer;
494
+ font-size: 12.5px;
495
+ color: var(--ink);
496
+ }
497
+ .note-folder-row:hover { background: var(--canvas); }
498
+ .note-folder-row:hover .note-folder-rename-btn { opacity: 1 !important; }
499
+ .note-folder-row:hover .note-folder-delete-btn { opacity: 1 !important; }
500
+ .note-folder-delete-btn:hover { color: var(--red) !important; }
501
+ .note-folder-icon { font-size: 8px; color: var(--ink-3); }
502
+ .note-folder-name { flex: 1; font-weight: 500; }
503
+ .note-folder-count { font-size: 10px; color: var(--ink-3); background: var(--canvas); border: 1px solid var(--rule); border-radius: 10px; padding: 0 5px; }
504
+ .note-folder-create-row { padding: 4px 10px 6px; }
505
+ .note-folder-add { font-size: 11px; color: var(--ink-3); cursor: pointer; }
506
+ .note-folder-add:hover { color: var(--blue); }
507
+ .note-folder-back {
508
+ display: flex;
509
+ align-items: center;
510
+ padding: 7px 12px;
511
+ font-size: 12px;
512
+ cursor: pointer;
513
+ border-bottom: 1px solid var(--rule);
514
+ }
515
+ .note-folder-back:hover { background: var(--canvas); }
516
+
517
+ /* Note tags */
518
+ .note-tag-filter {
519
+ display: flex;
520
+ flex-wrap: wrap;
521
+ gap: 4px;
522
+ padding: 6px 10px;
523
+ border-bottom: 1px solid var(--rule);
524
+ }
525
+ .note-tag-chip {
526
+ font-size: 10px;
527
+ padding: 2px 7px;
528
+ border-radius: 20px;
529
+ border: 1px solid var(--rule-2);
530
+ background: var(--canvas);
531
+ color: var(--ink-3);
532
+ cursor: pointer;
533
+ user-select: none;
534
+ }
535
+ .note-tag-chip:hover { border-color: var(--ink-3); color: var(--ink); }
536
+ .note-tag-chip.active { background: var(--ink); color: var(--white); border-color: var(--ink); }
537
+ .note-tag-mgmt-btn { font-size: 12px; color: var(--ink-3); cursor: pointer; padding: 0 2px; opacity: 0; transition: opacity .15s; line-height: 1; }
538
+ .note-tag-chip-wrap:hover .note-tag-mgmt-btn { opacity: 1; }
539
+ .note-tag-menu { position: absolute; top: 100%; left: 0; z-index: 50; background: var(--canvas); border: 1px solid var(--rule-2); border-radius: 6px; box-shadow: 0 4px 12px rgba(0,0,0,.15); min-width: 160px; padding: 4px 0; margin-top: 2px; }
540
+ .note-tag-menu-item { font-size: 12px; padding: 5px 12px; cursor: pointer; color: var(--ink-2); }
541
+ .note-tag-menu-item:hover { background: var(--canvas-2); color: var(--ink); }
542
+ .note-tag-menu-danger { color: var(--red) !important; }
543
+ .note-tag-menu-danger:hover { background: var(--red-dim, #3a1a1a) !important; }
544
+ .note-row-tags { display: flex; gap: 3px; flex-wrap: wrap; margin-top: 3px; }
545
+ .note-row-tag {
546
+ font-size: 9px;
547
+ padding: 1px 5px;
548
+ border-radius: 20px;
549
+ background: color-mix(in srgb, var(--blue) 10%, transparent);
550
+ color: var(--blue);
551
+ cursor: pointer;
552
+ }
553
+
554
+ .md-content p, .md-content li { position: relative; }
555
+ .para-add-task-btn {
556
+ display: inline-flex; align-items: center; justify-content: center;
557
+ position: absolute; left: -22px; top: 2px;
558
+ width: 16px; height: 16px; border-radius: 50%;
559
+ background: var(--blue); color: #fff;
560
+ border: none; cursor: pointer; font-size: 12px; line-height: 1;
561
+ opacity: 0; transition: opacity .15s; padding: 0;
562
+ font-weight: 700;
563
+ }
564
+ .md-content p:hover .para-add-task-btn,
565
+ .md-content li:hover .para-add-task-btn { opacity: 1; }
566
+ .note-task-toast {
567
+ position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
568
+ background: var(--ink); color: var(--white);
569
+ padding: 8px 16px; border-radius: 20px; font-size: 12px;
570
+ z-index: 300; pointer-events: none;
571
+ animation: toastIn .2s ease;
572
+ }
573
+ @keyframes toastIn { from { opacity:0; transform: translateX(-50%) translateY(8px); } to { opacity:1; transform: translateX(-50%) translateY(0); } }
574
+ .note-ref-chip { display:inline-flex;align-items:center;gap:3px;font-size:10px;color:var(--blue);background:color-mix(in srgb,var(--blue) 10%,transparent);border-radius:4px;padding:1px 5px;cursor:pointer;text-decoration:none; }
575
+ .note-row-tag:hover { background: color-mix(in srgb, var(--blue) 20%, transparent); }
576
+
487
577
  /* Scratch row */
488
578
  .scratch-row {
489
579
  border-bottom: 1px solid var(--rule);
@@ -2362,7 +2452,7 @@
2362
2452
  <button class="export-drop-item" @click="copySessionMd(); exportDropOpen=false">Copy as Markdown</button>
2363
2453
  <button class="export-drop-item" @click="shareSession(); exportDropOpen=false" :disabled="!!shareMsg"><span x-text="shareMsg || 'Publish to Gist'"></span></button>
2364
2454
  <button class="export-drop-item" @click="navigator.clipboard.writeText(location.origin+location.pathname+'#/session/'+selectedSession.projectDir+'/'+selectedSession.sessionId);exportDropOpen=false">Copy local link</button>
2365
- <button class="export-drop-item" @click="saveNoteFromSession();exportDropOpen=false">Save as note</button>
2455
+ <button class="export-drop-item" @click="snapshotSession();exportDropOpen=false" :disabled="snapshottingSession" x-text="snapshottingSession ? 'Saving…' : 'Snapshot → Note'"></button>
2366
2456
  <div style="height:1px;background:var(--rule);margin:4px 0"></div>
2367
2457
  <button class="export-drop-item" style="color:var(--red)" @click="deleteSession();exportDropOpen=false" x-show="!deletingSession">Delete session</button>
2368
2458
  </div>
@@ -2651,7 +2741,10 @@
2651
2741
  <template x-for="task in todayData.tasks.filter(t => t.carriedOver && !t.done)" :key="task.id">
2652
2742
  <div style="display:flex;align-items:center;gap:8px;padding:6px 10px;margin-bottom:4px;background:var(--canvas-2);border:1px solid var(--rule);border-left:3px solid var(--ink-3);border-radius:4px">
2653
2743
  <input type="checkbox" :checked="task.done" @change="toggleTodayTask(task.id)" style="flex-shrink:0;cursor:pointer" />
2654
- <span x-text="task.text" style="flex:1;font-size:13px;color:var(--ink-2)"></span>
2744
+ <span style="flex:1;font-size:13px;color:var(--ink-2)">
2745
+ <span x-text="task.text"></span>
2746
+ <template x-if="task.noteRef"><a class="note-ref-chip" @click.prevent="view='notes';selectedNote=personalNotes.find(n=>n.path===task.noteRef.path)||null" href="#" x-text="'↗ '+task.noteRef.title"></a></template>
2747
+ </span>
2655
2748
  <button @click="deleteTodayTask(task.id)" style="background:none;border:none;color:var(--ink-3);cursor:pointer;font-size:14px;padding:0 2px;line-height:1" title="Remove">×</button>
2656
2749
  </div>
2657
2750
  </template>
@@ -2662,7 +2755,10 @@
2662
2755
  <template x-for="task in todayData.tasks.filter(t => !t.carriedOver && !t.done)" :key="task.id">
2663
2756
  <div style="display:flex;align-items:center;gap:8px;padding:6px 0;border-bottom:1px solid var(--rule)">
2664
2757
  <input type="checkbox" :checked="task.done" @change="toggleTodayTask(task.id)" style="flex-shrink:0;cursor:pointer" />
2665
- <span x-text="task.text" style="flex:1;font-size:13px;color:var(--ink)"></span>
2758
+ <span style="flex:1;font-size:13px;color:var(--ink)">
2759
+ <span x-text="task.text"></span>
2760
+ <template x-if="task.noteRef"><a class="note-ref-chip" @click.prevent="view='notes';selectedNote=personalNotes.find(n=>n.path===task.noteRef.path)||null" href="#" x-text="'↗ '+task.noteRef.title"></a></template>
2761
+ </span>
2666
2762
  <button @click="deleteTodayTask(task.id)" style="background:none;border:none;color:var(--ink-3);cursor:pointer;font-size:14px;padding:0 2px;line-height:1" title="Remove">×</button>
2667
2763
  </div>
2668
2764
  </template>
@@ -2674,7 +2770,10 @@
2674
2770
  <template x-for="task in todayData.tasks.filter(t => t.done)" :key="task.id">
2675
2771
  <div style="display:flex;align-items:center;gap:8px;padding:6px 0;border-bottom:1px solid var(--rule);opacity:0.5">
2676
2772
  <input type="checkbox" :checked="task.done" @change="toggleTodayTask(task.id)" style="flex-shrink:0;cursor:pointer" />
2677
- <span x-text="task.text" style="flex:1;font-size:13px;text-decoration:line-through;color:var(--ink-3)"></span>
2773
+ <span style="flex:1;font-size:13px;text-decoration:line-through;color:var(--ink-3)">
2774
+ <span x-text="task.text"></span>
2775
+ <template x-if="task.noteRef"><a class="note-ref-chip" @click.prevent="view='notes';selectedNote=personalNotes.find(n=>n.path===task.noteRef.path)||null" href="#" x-text="'↗ '+task.noteRef.title"></a></template>
2776
+ </span>
2678
2777
  <button @click="deleteTodayTask(task.id)" style="background:none;border:none;color:var(--ink-3);cursor:pointer;font-size:14px;padding:0 2px;line-height:1" title="Remove">×</button>
2679
2778
  </div>
2680
2779
  </template>
@@ -2774,7 +2873,16 @@
2774
2873
  </template>
2775
2874
  <template x-if="selectedNote && !noteEditing">
2776
2875
  <div style="display:flex;align-items:center;gap:8px">
2777
- <button class="btn btn-sm btn-outline" @click="noteEditing=true;noteTitleDraft=selectedNote.title;noteBodyDraft=selectedNote.content">Edit</button>
2876
+ <button class="btn btn-sm btn-outline" :style="selectedNote.pinned ? 'color:var(--yellow,#f59e0b);border-color:var(--yellow,#f59e0b)' : ''" @click="togglePinNote()" :title="selectedNote.pinned ? 'Unpin' : 'Pin note'">📌</button>
2877
+ <button class="btn btn-sm btn-outline" @click="addNoteTask(selectedNote.title, selectedNote)" title="Add note title as Today task">+ Today</button>
2878
+ <button class="btn btn-sm btn-outline" @click="noteEditing=true;noteTitleDraft=selectedNote.title;noteBodyDraft=selectedNote.content;noteTagsDraft=(selectedNote.tags||[]).join(', ')">Edit</button>
2879
+ <select class="filter-select" style="font-size:12px" @change="movePersonalNote($event.target.value);$event.target.value='__current__'" x-init="">
2880
+ <option value="__current__">Move to…</option>
2881
+ <option value="">/ root</option>
2882
+ <template x-for="f in noteAllFolders().filter(f=>f!==(selectedNote.folder??''))" :key="f">
2883
+ <option :value="f" x-text="'/ '+f"></option>
2884
+ </template>
2885
+ </select>
2778
2886
  <button class="btn btn-sm" style="background:var(--red-dim,#3a1a1a);color:var(--red);border:1px solid var(--red)" @click="deletePersonalNote()">Delete</button>
2779
2887
  </div>
2780
2888
  </template>
@@ -2787,7 +2895,16 @@
2787
2895
  <div style="padding:16px 20px;border-bottom:1px solid var(--rule);background:var(--canvas-2)">
2788
2896
  <input class="settings-input" type="text" placeholder="Note title…" x-model="noteNewTitle" style="width:100%;margin-bottom:8px;font-size:13px" @keydown.enter="createPersonalNote()" x-init="$nextTick(()=>$el.focus())" />
2789
2897
  <div style="position:relative">
2790
- <textarea class="settings-input" placeholder="Content (optional)…" x-model="noteNewBody" rows="3" style="width:100%;resize:vertical;font-size:12px;font-family:inherit"></textarea>
2898
+ <div style="display:flex;gap:8px;margin-bottom:8px">
2899
+ <input class="settings-input" type="text" placeholder="Tags (bug, idea, trabajo)…" x-model="noteNewTags" style="flex:1;font-size:12px" />
2900
+ <select class="filter-select" x-model="noteNewFolder" style="font-size:12px;min-width:100px">
2901
+ <option value="">/ root</option>
2902
+ <template x-for="f in noteAllFolders()" :key="f">
2903
+ <option :value="f" x-text="'/ '+f"></option>
2904
+ </template>
2905
+ </select>
2906
+ </div>
2907
+ <textarea class="settings-input" placeholder="Content (optional)…" x-model="noteNewBody" rows="3" style="width:100%;resize:vertical;font-size:12px;font-family:inherit"></textarea>
2791
2908
  <template x-if="noteFromClipboard">
2792
2909
  <span style="position:absolute;top:6px;right:8px;font-size:10px;background:var(--canvas);border:1px solid var(--rule-2);border-radius:3px;padding:1px 5px;color:var(--ink-3);pointer-events:none">from clipboard</span>
2793
2910
  </template>
@@ -2825,16 +2942,116 @@
2825
2942
  </div>
2826
2943
  <div x-show="scratchContent.length > 0" style="font-size:10px;color:var(--ink-3)" x-text="scratchContent.slice(0,40) + (scratchContent.length>40?'…':'')"></div>
2827
2944
  </div>
2828
- <template x-if="personalNotes.length > 0">
2829
- <div style="border-top:1px solid var(--rule);margin:4px 0"></div>
2945
+ <div style="border-top:1px solid var(--rule)"></div>
2946
+ <!-- Folder nav: at root show folders + create; inside folder show back -->
2947
+ <template x-if="noteFolderPath === ''">
2948
+ <div>
2949
+ <template x-for="f in noteAllFolders()" :key="f">
2950
+ <div class="note-folder-row" @click="if(noteRenamingFolder!==f)noteFolderPath=f;selectedNote=null;scratchActive=false">
2951
+ <template x-if="noteRenamingFolder !== f">
2952
+ <span class="note-folder-icon">▶</span>
2953
+ </template>
2954
+ <template x-if="noteRenamingFolder === f">
2955
+ <input class="settings-input" style="flex:1;font-size:11px;padding:2px 5px;margin-right:4px" x-model="noteRenameFolderDraft"
2956
+ @click.stop
2957
+ x-init="$nextTick(()=>$el.focus())"
2958
+ @keydown.enter.stop="renameFolderConfirm(f)"
2959
+ @keydown.escape.stop="noteRenamingFolder=null;noteRenameFolderDraft=''" />
2960
+ </template>
2961
+ <template x-if="noteRenamingFolder !== f">
2962
+ <span class="note-folder-name" x-text="f"></span>
2963
+ </template>
2964
+ <template x-if="noteRenamingFolder !== f">
2965
+ <span class="note-folder-count" x-text="personalNotes.filter(n=>(n.folder??'')===f).length"></span>
2966
+ </template>
2967
+ <template x-if="noteRenamingFolder !== f">
2968
+ <span style="font-size:10px;color:var(--ink-3);padding:0 2px;opacity:0;transition:opacity .15s" class="note-folder-rename-btn"
2969
+ @click.stop="noteRenamingFolder=f;noteRenameFolderDraft=f" title="Rename">✎</span>
2970
+ </template>
2971
+ <template x-if="noteRenamingFolder !== f">
2972
+ <span style="font-size:10px;color:var(--ink-3);padding:0 1px;opacity:0;transition:opacity .15s" class="note-folder-delete-btn"
2973
+ @click.stop="deletingFolder=f" title="Delete folder">✕</span>
2974
+ </template>
2975
+ <template x-if="noteRenamingFolder === f">
2976
+ <button class="btn btn-sm btn-primary" style="padding:1px 7px;font-size:11px" @click.stop="renameFolderConfirm(f)">OK</button>
2977
+ </template>
2978
+ </div>
2979
+ </template>
2980
+ <div class="note-folder-create-row">
2981
+ <template x-if="!noteCreatingFolder">
2982
+ <span class="note-folder-add" @click="noteCreatingFolder=true">+ New folder</span>
2983
+ </template>
2984
+ <template x-if="noteCreatingFolder">
2985
+ <div style="display:flex;gap:4px;padding:4px 10px">
2986
+ <input class="settings-input" style="flex:1;font-size:11px;padding:3px 6px" x-model="newFolderName" placeholder="folder name…" x-init="$nextTick(()=>$el.focus())" @keydown.enter="createFolder()" @keydown.escape="noteCreatingFolder=false;newFolderName=''" />
2987
+ <button class="btn btn-sm btn-primary" style="padding:2px 8px;font-size:11px" @click="createFolder()">Create</button>
2988
+ </div>
2989
+ </template>
2990
+ </div>
2991
+ <template x-if="noteAllFolders().length > 0">
2992
+ <div style="border-top:1px solid var(--rule);margin:2px 0"></div>
2993
+ </template>
2994
+ </div>
2995
+ </template>
2996
+ <template x-if="noteFolderPath !== ''">
2997
+ <div class="note-folder-back" @click="noteFolderPath='';selectedNote=null">
2998
+ <span style="color:var(--blue)">← All notes</span>
2999
+ <span style="color:var(--ink-3)">&nbsp;/&nbsp;</span>
3000
+ <span style="font-weight:500" x-text="noteFolderPath"></span>
3001
+ </div>
3002
+ </template>
3003
+ <!-- Tag filter (scoped to current folder) -->
3004
+ <template x-if="noteAllTags().length > 0">
3005
+ <div class="note-tag-filter" @click.window="noteTagMenu=null;noteTagRenaming=false">
3006
+ <template x-for="t in noteAllTags()" :key="t.tag">
3007
+ <span class="note-tag-chip-wrap" style="position:relative;display:inline-flex;align-items:center">
3008
+ <span class="note-tag-chip" :class="{active: noteTagFilter.includes(t.tag)}"
3009
+ @click="noteTagFilter.includes(t.tag) ? noteTagFilter.splice(noteTagFilter.indexOf(t.tag),1) : noteTagFilter.push(t.tag)"
3010
+ x-text="t.tag + ' (' + t.count + ')'"></span>
3011
+ <span class="note-tag-mgmt-btn" @click.stop="noteTagMenu===t.tag ? noteTagMenu=null : (noteTagMenu=t.tag, noteTagRenameDraft=t.tag)" title="Manage tag">⋮</span>
3012
+ <template x-if="noteTagMenu === t.tag">
3013
+ <div class="note-tag-menu" @click.stop>
3014
+ <template x-if="!noteTagRenaming">
3015
+ <div>
3016
+ <div class="note-tag-menu-item" @click="noteTagRenaming=true;$nextTick(()=>$refs['tagRenameInput_'+t.tag]?.focus())">Rename…</div>
3017
+ <div class="note-tag-menu-item note-tag-menu-danger" @click="deleteTag(t.tag)">Delete from all notes</div>
3018
+ </div>
3019
+ </template>
3020
+ <template x-if="noteTagRenaming">
3021
+ <div style="display:flex;gap:4px;padding:2px">
3022
+ <input class="settings-input" style="font-size:11px;padding:2px 5px;width:100px"
3023
+ x-model="noteTagRenameDraft"
3024
+ :ref="'tagRenameInput_'+t.tag"
3025
+ x-init="$nextTick(()=>$el.focus())"
3026
+ @keydown.enter="renameTag(t.tag, noteTagRenameDraft)"
3027
+ @keydown.escape="noteTagRenaming=false" />
3028
+ <button class="btn btn-sm btn-primary" style="padding:1px 6px;font-size:11px" @click="renameTag(t.tag, noteTagRenameDraft)">OK</button>
3029
+ </div>
3030
+ </template>
3031
+ </div>
3032
+ </template>
3033
+ </span>
3034
+ </template>
3035
+ </div>
2830
3036
  </template>
2831
- <template x-for="n in personalNotes.filter(n => !noteSearch || n.title.toLowerCase().includes(noteSearch.toLowerCase()) || n.content.toLowerCase().includes(noteSearch.toLowerCase()))" :key="n.filename">
2832
- <div class="plan-row" :class="{active: selectedNote?.filename === n.filename}" @click="selectedNote=n;scratchActive=false;noteEditing=false;window.location.hash='#/note/'+n.filename">
2833
- <div class="plan-row-title" x-text="n.title"></div>
3037
+ <!-- Note list (scoped to current folder) -->
3038
+ <template x-for="n in noteFolderNotes()" :key="n.path">
3039
+ <div class="plan-row" :class="{active: selectedNote?.path === n.path}" @click="selectedNote=n;scratchActive=false;noteEditing=false;window.location.hash='#/note/'+n.path"
3040
+ <div class="plan-row-title" style="display:flex;align-items:center;gap:4px">
3041
+ <template x-if="n.pinned"><span style="font-size:10px;line-height:1" title="Pinned">📌</span></template>
3042
+ <span x-text="n.title"></span>
3043
+ </div>
2834
3044
  <div class="plan-row-meta">
2835
3045
  <span x-text="formatDate(n.modified)"></span>
2836
- <template x-if="n.session"><span style="color:var(--blue);font-size:10px">session</span></template>
3046
+ <template x-if="n.session && n.session.length>=32"><span style="color:var(--blue);font-size:10px">session</span></template>
2837
3047
  </div>
3048
+ <template x-if="(n.tags||[]).length > 0">
3049
+ <div class="note-row-tags">
3050
+ <template x-for="tag in n.tags" :key="tag">
3051
+ <span class="note-row-tag" x-text="tag" @click.stop="noteTagFilter.includes(tag) ? noteTagFilter.splice(noteTagFilter.indexOf(tag),1) : noteTagFilter.push(tag)"></span>
3052
+ </template>
3053
+ </div>
3054
+ </template>
2838
3055
  </div>
2839
3056
  </template>
2840
3057
  </div>
@@ -2857,14 +3074,24 @@
2857
3074
  <div style="padding:24px;max-width:380px">
2858
3075
  <div style="font-size:13px;font-weight:600;margin-bottom:6px;color:var(--ink)">Your personal notepad</div>
2859
3076
  <div style="font-size:12px;color:var(--ink-3);margin-bottom:16px;line-height:1.6">Notes are <strong>for you</strong>, not for Claude. He won't read them as context — they're yours to capture, review and revisit.</div>
2860
- <div style="font-size:11px;font-weight:600;color:var(--ink-3);text-transform:uppercase;letter-spacing:.05em;margin-bottom:8px">Ask Claude to save one</div>
2861
- <div style="display:flex;flex-direction:column;gap:6px">
3077
+ <div style="font-size:11px;font-weight:600;color:var(--ink-3);text-transform:uppercase;letter-spacing:.05em;margin-bottom:6px">Capture</div>
3078
+ <div style="display:flex;flex-direction:column;gap:5px;margin-bottom:14px">
2862
3079
  <code style="font-size:11px;background:var(--canvas-2);border:1px solid var(--rule);padding:3px 7px;border-radius:4px;display:block">"Save this as a note"</code>
2863
- <code style="font-size:11px;background:var(--canvas-2);border:1px solid var(--rule);padding:3px 7px;border-radius:4px;display:block">"Save this last output as a note"</code>
3080
+ <code style="font-size:11px;background:var(--canvas-2);border:1px solid var(--rule);padding:3px 7px;border-radius:4px;display:block">"Save this as a TIL in my notes"</code>
2864
3081
  <code style="font-size:11px;background:var(--canvas-2);border:1px solid var(--rule);padding:3px 7px;border-radius:4px;display:block">"Add this decision to my notes"</code>
2865
- <code style="font-size:11px;background:var(--canvas-2);border:1px solid var(--rule);padding:3px 7px;border-radius:4px;display:block">"Save this as a TIL"</code>
3082
+ <code style="font-size:11px;background:var(--canvas-2);border:1px solid var(--rule);padding:3px 7px;border-radius:4px;display:block">"Save this in my <em>project</em> notes folder"</code>
3083
+ </div>
3084
+ <div style="font-size:11px;font-weight:600;color:var(--ink-3);text-transform:uppercase;letter-spacing:.05em;margin-bottom:6px">Organise</div>
3085
+ <div style="display:flex;flex-direction:column;gap:5px;margin-bottom:14px">
3086
+ <code style="font-size:11px;background:var(--canvas-2);border:1px solid var(--rule);padding:3px 7px;border-radius:4px;display:block">"Tag my last note as <em>bug</em> and <em>auth</em>"</code>
3087
+ <code style="font-size:11px;background:var(--canvas-2);border:1px solid var(--rule);padding:3px 7px;border-radius:4px;display:block">"Move the note about X to the <em>project</em> folder"</code>
3088
+ <code style="font-size:11px;background:var(--canvas-2);border:1px solid var(--rule);padding:3px 7px;border-radius:4px;display:block">"Pin my runbook note"</code>
3089
+ </div>
3090
+ <div style="font-size:11px;font-weight:600;color:var(--ink-3);text-transform:uppercase;letter-spacing:.05em;margin-bottom:6px">Retrieve &amp; act</div>
3091
+ <div style="display:flex;flex-direction:column;gap:5px">
2866
3092
  <code style="font-size:11px;background:var(--canvas-2);border:1px solid var(--rule);padding:3px 7px;border-radius:4px;display:block">"Show me my last note"</code>
2867
- <code style="font-size:11px;background:var(--canvas-2);border:1px solid var(--rule);padding:3px 7px;border-radius:4px;display:block">"What notes do I have?"</code>
3093
+ <code style="font-size:11px;background:var(--canvas-2);border:1px solid var(--rule);padding:3px 7px;border-radius:4px;display:block">"What notes do I have tagged <em>bug</em>?"</code>
3094
+ <code style="font-size:11px;background:var(--canvas-2);border:1px solid var(--rule);padding:3px 7px;border-radius:4px;display:block">"Add the first step of my deploy note to today's tasks"</code>
2868
3095
  </div>
2869
3096
  </div>
2870
3097
  </template>
@@ -2877,12 +3104,19 @@
2877
3104
  <span style="font-size:11px;color:var(--blue);cursor:pointer" @click="openSessionById(selectedNote.session)" x-text="'→ Session ' + selectedNote.session.slice(0,8) + '…'"></span>
2878
3105
  </template>
2879
3106
  </div>
3107
+ <template x-if="(selectedNote.tags||[]).length > 0">
3108
+ <div style="display:flex;gap:5px;flex-wrap:wrap;padding:0 20px 10px">
3109
+ <template x-for="tag in selectedNote.tags" :key="tag">
3110
+ <span class="note-tag-chip active" style="cursor:pointer" @click="noteTagFilter.includes(tag) ? noteTagFilter.splice(noteTagFilter.indexOf(tag),1) : noteTagFilter.push(tag)" x-text="tag"></span>
3111
+ </template>
3112
+ </div>
3113
+ </template>
2880
3114
  <div class="memory-content md-content" @click="handleNoteLinkClick($event)" x-html="selectedNote.content ? renderNotesMd(selectedNote.content) : '<span style=\'color:var(--ink-3)\'>Empty note. Click Edit to add content.</span>'"></div>
2881
3115
  <template x-if="noteBacklinks(selectedNote).length > 0">
2882
3116
  <div class="note-backlinks">
2883
3117
  <span class="note-backlinks-label">Referenced by</span>
2884
3118
  <template x-for="bl in noteBacklinks(selectedNote)" :key="bl.filename">
2885
- <span class="note-backlink-chip" @click="selectedNote=bl;noteEditing=false;window.location.hash='#/note/'+bl.filename" x-text="bl.title"></span>
3119
+ <span class="note-backlink-chip" @click="selectedNote=bl;noteEditing=false;window.location.hash='#/note/'+bl.path" x-text="bl.title"></span>
2886
3120
  </template>
2887
3121
  </div>
2888
3122
  </template>
@@ -2890,7 +3124,7 @@
2890
3124
  <div class="note-backlinks">
2891
3125
  <span class="note-backlinks-label">Related</span>
2892
3126
  <template x-for="rn in noteRelated(selectedNote)" :key="rn.filename">
2893
- <span class="note-backlink-chip note-related-chip" @click="selectedNote=rn;noteEditing=false;window.location.hash='#/note/'+rn.filename" x-text="rn.title"></span>
3127
+ <span class="note-backlink-chip note-related-chip" @click="selectedNote=rn;noteEditing=false;window.location.hash='#/note/'+rn.path" x-text="rn.title"></span>
2894
3128
  </template>
2895
3129
  </div>
2896
3130
  </template>
@@ -2899,6 +3133,7 @@
2899
3133
  <template x-if="selectedNote && noteEditing">
2900
3134
  <div style="display:flex;flex-direction:column;height:100%;gap:10px;position:relative">
2901
3135
  <input class="settings-input" type="text" x-model="noteTitleDraft" style="font-size:14px;font-weight:600;width:100%" />
3136
+ <input class="settings-input" type="text" x-model="noteTagsDraft" placeholder="Tags (comma separated: bug, idea, trabajo)…" style="font-size:12px;width:100%" />
2902
3137
  <div style="position:relative;flex:1;display:flex;flex-direction:column">
2903
3138
  <textarea class="settings-input note-body-ta" x-model="noteBodyDraft" @input="noteEditorInput($event)" @keydown="noteEditorKeydown($event)" @blur="setTimeout(()=>noteAutocomplete.visible=false,150)" style="flex:1;resize:none;font-family:monospace;font-size:12px;min-height:300px;width:100%"></textarea>
2904
3139
  <template x-if="noteAutocomplete.visible">
@@ -4465,6 +4700,24 @@
4465
4700
 
4466
4701
  </div>
4467
4702
 
4703
+ <!-- Delete folder modal -->
4704
+ <template x-if="deletingFolder !== null">
4705
+ <div class="memory-new-modal" @click.self="deletingFolder=null">
4706
+ <div class="memory-new-form" style="width:360px">
4707
+ <h3>Borrar carpeta "<span x-text="deletingFolder"></span>"</h3>
4708
+ <p style="font-size:12px;color:var(--ink-2);line-height:1.5;margin-bottom:20px">
4709
+ Contiene <strong x-text="personalNotes.filter(n=>(n.folder??'')===deletingFolder).length"></strong> nota(s).
4710
+ ¿Borras también las notas o las dejas sueltas (sin carpeta)?
4711
+ </p>
4712
+ <div style="display:flex;gap:8px;justify-content:flex-end">
4713
+ <button class="btn btn-sm btn-outline" @click="deletingFolder=null">Cancelar</button>
4714
+ <button class="btn btn-sm" style="background:var(--canvas-2,#f5f5f5);border:1px solid var(--rule);color:var(--ink)" @click="deleteFolderConfirm('orphan')">Soltar notas</button>
4715
+ <button class="btn btn-sm" style="background:var(--red-dim,#3a1a1a);color:var(--red);border:1px solid var(--red)" @click="deleteFolderConfirm('delete')">Borrar todo</button>
4716
+ </div>
4717
+ </div>
4718
+ </div>
4719
+ </template>
4720
+
4468
4721
  <!-- New Memory modal -->
4469
4722
  <template x-if="memoryNewModal">
4470
4723
  <div class="memory-new-modal" @click.self="memoryNewModal=false">
@@ -4560,6 +4813,7 @@
4560
4813
  selectedPlan: null,
4561
4814
  plansSearch: '',
4562
4815
  deletingSession: false,
4816
+ snapshottingSession: false,
4563
4817
  exportDropOpen: false,
4564
4818
  exportMsg: '',
4565
4819
  cleanMode: true,
@@ -4590,6 +4844,20 @@
4590
4844
  noteFromClipboard: false,
4591
4845
  scratchContent: sessionStorage.getItem('cs:scratch') || '',
4592
4846
  scratchActive: false,
4847
+ noteTagFilter: [],
4848
+ noteTagsDraft: '',
4849
+ noteNewTags: '',
4850
+ noteFolderPath: '',
4851
+ noteNewFolder: '',
4852
+ noteCreatingFolder: false,
4853
+ newFolderName: '',
4854
+ noteFolders: [],
4855
+ noteRenamingFolder: null,
4856
+ noteRenameFolderDraft: '',
4857
+ deletingFolder: null,
4858
+ noteTagMenu: null,
4859
+ noteTagRenaming: false,
4860
+ noteTagRenameDraft: '',
4593
4861
  sidebarW: parseInt(localStorage.getItem('cm:sidebarW') || '260'),
4594
4862
  historyEntries: [],
4595
4863
  historyLoading: false,
@@ -4774,7 +5042,7 @@
4774
5042
  if (mNote) {
4775
5043
  const notes = await fetch('/api/notes').then(r => r.json()).catch(() => []);
4776
5044
  this.personalNotes = notes;
4777
- const note = this.personalNotes.find(n => n.filename === mNote[1]);
5045
+ const note = this.personalNotes.find(n => n.path === mNote[1]);
4778
5046
  if (note) { this.view = 'notes'; this.selectedNote = note; }
4779
5047
  }
4780
5048
  // Clear hash when closing
@@ -5167,22 +5435,166 @@
5167
5435
  return slugPart === slug || base === slug;
5168
5436
  });
5169
5437
  if (!note) return match;
5170
- return `${pre}[#${slug}](#/note/${note.filename})`;
5438
+ return `${pre}[#${slug}](#/note/${note.path})`;
5171
5439
  });
5172
5440
  let html;
5173
5441
  try { html = marked.parse(processed); } catch { html = processed; }
5174
5442
  // Mark note-ref links so we can style/intercept them
5175
5443
  html = html.replace(/href="#\/note\//g, 'class="note-ref-link" href="#/note/');
5444
+ // Inject + buttons into paragraphs and list items
5445
+ const btn = '<button class="para-add-task-btn" title="Add to Today">+</button>';
5446
+ html = html.replace(/<p>/g, `<p>${btn}`);
5447
+ html = html.replace(/<li>/g, `<li>${btn}`);
5176
5448
  return html;
5177
5449
  },
5178
5450
 
5179
5451
  handleNoteLinkClick(e) {
5452
+ const btn = e.target.closest('.para-add-task-btn');
5453
+ if (btn) {
5454
+ e.preventDefault();
5455
+ const block = btn.closest('p,li');
5456
+ const text = block ? block.innerText.replace('+', '').trim() : '';
5457
+ if (text) this.addNoteTask(text, this.selectedNote || this.noteHovered);
5458
+ return;
5459
+ }
5180
5460
  const a = e.target.closest('a.note-ref-link');
5181
5461
  if (!a) return;
5182
5462
  e.preventDefault();
5183
- const filename = a.getAttribute('href').replace('#/note/', '');
5184
- const note = this.personalNotes.find(n => n.filename === filename);
5185
- if (note) { this.selectedNote = note; this.noteEditing = false; window.location.hash = '#/note/' + filename; }
5463
+ const notepath = a.getAttribute('href').replace('#/note/', '');
5464
+ const note = this.personalNotes.find(n => n.path === notepath);
5465
+ if (note) { this.selectedNote = note; this.noteEditing = false; window.location.hash = '#/note/' + note.path; }
5466
+ },
5467
+
5468
+ noteAllFolders() {
5469
+ const fromNotes = this.personalNotes.map(n => n.folder ?? '').filter(Boolean);
5470
+ return [...new Set([...this.noteFolders, ...fromNotes])].sort();
5471
+ },
5472
+
5473
+ noteFolderNotes() {
5474
+ const filtered = this.personalNotes.filter(n => {
5475
+ if ((n.folder ?? '') !== this.noteFolderPath) return false;
5476
+ if (this.noteTagFilter.length > 0 && !this.noteTagFilter.some(t => (n.tags||[]).includes(t))) return false;
5477
+ if (!this.noteSearch) return true;
5478
+ return n.title.toLowerCase().includes(this.noteSearch.toLowerCase()) || (n.content||'').toLowerCase().includes(this.noteSearch.toLowerCase());
5479
+ });
5480
+ return filtered.sort((a, b) => (b.pinned ? 1 : 0) - (a.pinned ? 1 : 0));
5481
+ },
5482
+
5483
+ async createFolder() {
5484
+ if (!this.newFolderName.trim()) return;
5485
+ const res = await fetch('/api/notes/folders', {
5486
+ method: 'POST',
5487
+ headers: { 'Content-Type': 'application/json' },
5488
+ body: JSON.stringify({ name: this.newFolderName.trim() }),
5489
+ }).then(r => r.json()).catch(() => null);
5490
+ if (res?.name && !this.noteFolders.includes(res.name)) {
5491
+ this.noteFolders.push(res.name);
5492
+ this.noteFolders.sort();
5493
+ }
5494
+ this.noteCreatingFolder = false;
5495
+ this.newFolderName = '';
5496
+ },
5497
+
5498
+ async renameFolderConfirm(oldName) {
5499
+ const newName = this.noteRenameFolderDraft.trim();
5500
+ if (!newName || newName === oldName) { this.noteRenamingFolder = null; return; }
5501
+ const res = await fetch(`/api/notes/folders/${encodeURIComponent(oldName)}/rename`, {
5502
+ method: 'PATCH',
5503
+ headers: { 'Content-Type': 'application/json' },
5504
+ body: JSON.stringify({ newName }),
5505
+ }).then(r => r.json()).catch(() => null);
5506
+ if (!res?.name) { alert(res?.error || 'Rename failed'); return; }
5507
+ // Update noteFolders list
5508
+ const idx = this.noteFolders.indexOf(oldName);
5509
+ if (idx !== -1) this.noteFolders.splice(idx, 1, res.name);
5510
+ else { this.noteFolders.push(res.name); }
5511
+ this.noteFolders.sort();
5512
+ // Update folder field in all affected notes
5513
+ for (const n of this.personalNotes) {
5514
+ if ((n.folder ?? '') === oldName) {
5515
+ n.folder = res.name;
5516
+ n.path = res.name + '/' + n.filename;
5517
+ }
5518
+ }
5519
+ if (this.noteFolderPath === oldName) this.noteFolderPath = res.name;
5520
+ if (this.selectedNote?.folder === oldName) {
5521
+ this.selectedNote.folder = res.name;
5522
+ this.selectedNote.path = res.name + '/' + this.selectedNote.filename;
5523
+ }
5524
+ this.noteRenamingFolder = null;
5525
+ this.noteRenameFolderDraft = '';
5526
+ },
5527
+
5528
+ async deleteFolderConfirm(action) {
5529
+ const folder = this.deletingFolder;
5530
+ if (!folder) return;
5531
+ const res = await fetch(`/api/notes/folders/${encodeURIComponent(folder)}?action=${action}`, {
5532
+ method: 'DELETE',
5533
+ }).then(r => r.json()).catch(() => null);
5534
+ if (!res?.ok) { alert(res?.error || 'Error al borrar la carpeta'); return; }
5535
+ this.noteFolders = this.noteFolders.filter(f => f !== folder);
5536
+ if (action === 'orphan') {
5537
+ for (const n of this.personalNotes) {
5538
+ if ((n.folder ?? '') === folder) { n.folder = ''; n.path = n.filename; }
5539
+ }
5540
+ if (this.selectedNote?.folder === folder) {
5541
+ this.selectedNote.folder = ''; this.selectedNote.path = this.selectedNote.filename;
5542
+ }
5543
+ } else {
5544
+ this.personalNotes = this.personalNotes.filter(n => (n.folder ?? '') !== folder);
5545
+ if (this.selectedNote?.folder === folder) { this.selectedNote = null; this.noteEditing = false; }
5546
+ }
5547
+ if (this.noteFolderPath === folder) { this.noteFolderPath = ''; this.selectedNote = null; }
5548
+ this.deletingFolder = null;
5549
+ },
5550
+
5551
+ noteAllTags() {
5552
+ const counts = {};
5553
+ const scoped = this.personalNotes.filter(n => (n.folder ?? '') === this.noteFolderPath);
5554
+ for (const n of scoped) {
5555
+ for (const t of (n.tags || [])) counts[t] = (counts[t] || 0) + 1;
5556
+ }
5557
+ return Object.entries(counts).sort((a, b) => b[1] - a[1]).map(([tag, count]) => ({ tag, count }));
5558
+ },
5559
+
5560
+ async renameTag(oldName, newName) {
5561
+ newName = newName.trim();
5562
+ if (!newName || newName === oldName) { this.noteTagMenu = null; this.noteTagRenaming = false; return; }
5563
+ const res = await fetch(`/api/notes/tags/${encodeURIComponent(oldName)}/rename`, {
5564
+ method: 'PATCH',
5565
+ headers: { 'Content-Type': 'application/json' },
5566
+ body: JSON.stringify({ newName }),
5567
+ }).then(r => r.json()).catch(() => null);
5568
+ if (!res || res.error) { alert(res?.error || 'Rename failed'); return; }
5569
+ for (const n of this.personalNotes) {
5570
+ if ((n.tags || []).includes(oldName)) {
5571
+ n.tags = n.tags.map(t => t === oldName ? res.to : t);
5572
+ }
5573
+ }
5574
+ if (this.noteTagFilter.includes(oldName)) {
5575
+ this.noteTagFilter.splice(this.noteTagFilter.indexOf(oldName), 1, res.to);
5576
+ }
5577
+ if (this.selectedNote?.tags?.includes(oldName)) {
5578
+ this.selectedNote.tags = this.selectedNote.tags.map(t => t === oldName ? res.to : t);
5579
+ }
5580
+ this.noteTagMenu = null;
5581
+ this.noteTagRenaming = false;
5582
+ },
5583
+
5584
+ async deleteTag(name) {
5585
+ if (!confirm(`Remove tag "${name}" from all notes?`)) return;
5586
+ const res = await fetch(`/api/notes/tags/${encodeURIComponent(name)}`, { method: 'DELETE' })
5587
+ .then(r => r.json()).catch(() => null);
5588
+ if (!res || res.error) { alert(res?.error || 'Delete failed'); return; }
5589
+ for (const n of this.personalNotes) {
5590
+ if ((n.tags || []).includes(name)) n.tags = n.tags.filter(t => t !== name);
5591
+ }
5592
+ const fi = this.noteTagFilter.indexOf(name);
5593
+ if (fi !== -1) this.noteTagFilter.splice(fi, 1);
5594
+ if (this.selectedNote?.tags?.includes(name)) {
5595
+ this.selectedNote.tags = this.selectedNote.tags.filter(t => t !== name);
5596
+ }
5597
+ this.noteTagMenu = null;
5186
5598
  },
5187
5599
 
5188
5600
  noteTokens(note) {
@@ -5751,6 +6163,8 @@
5751
6163
  async openNewNote() {
5752
6164
  this.noteNewTitle = '';
5753
6165
  this.noteNewBody = '';
6166
+ this.noteNewTags = '';
6167
+ this.noteNewFolder = this.noteFolderPath;
5754
6168
  this.noteFromClipboard = false;
5755
6169
  this.noteCreating = true;
5756
6170
  try {
@@ -5773,12 +6187,14 @@
5773
6187
  async loadPersonalNotes() {
5774
6188
  if (this.personalNotesLoading) return;
5775
6189
  this.personalNotesLoading = true;
5776
- const [notes, status] = await Promise.all([
6190
+ const [notes, status, folders] = await Promise.all([
5777
6191
  fetch('/api/notes').then(r => r.json()).catch(() => []),
5778
6192
  fetch('/api/notes/claude-md-status').then(r => r.json()).catch(() => ({})),
6193
+ fetch('/api/notes/folders').then(r => r.json()).catch(() => []),
5779
6194
  ]);
5780
6195
  this.personalNotes = notes;
5781
6196
  this.noteClaudeInstalled = status.installed ?? null;
6197
+ this.noteFolders = Array.isArray(folders) ? folders : [];
5782
6198
  this.personalNotesLoading = false;
5783
6199
  },
5784
6200
 
@@ -5805,13 +6221,14 @@
5805
6221
  const note = await fetch('/api/notes', {
5806
6222
  method: 'POST',
5807
6223
  headers: { 'Content-Type': 'application/json' },
5808
- body: JSON.stringify({ title: this.noteNewTitle, content: this.noteNewBody }),
6224
+ body: JSON.stringify({ title: this.noteNewTitle, content: this.noteNewBody, tags: this.noteNewTags ? this.noteNewTags.split(',').map(t=>t.trim()).filter(Boolean) : [], folder: this.noteNewFolder }),
5809
6225
  }).then(r => r.json());
5810
6226
  this.personalNotes.unshift(note);
5811
6227
  this.selectedNote = note;
5812
6228
  this.noteCreating = false;
5813
6229
  this.noteNewTitle = '';
5814
6230
  this.noteNewBody = '';
6231
+ this.noteNewTags = '';
5815
6232
  this.noteEditing = false;
5816
6233
  } catch (e) { this.noteMsg = 'Error: ' + e.message; }
5817
6234
  this.noteSaving = false;
@@ -5821,10 +6238,10 @@
5821
6238
  if (!this.selectedNote) return;
5822
6239
  this.noteSaving = true; this.noteMsg = '';
5823
6240
  try {
5824
- const updated = await fetch(`/api/notes/${this.selectedNote.filename}`, {
6241
+ const updated = await fetch(`/api/notes/${this.selectedNote.path}`, {
5825
6242
  method: 'PUT',
5826
6243
  headers: { 'Content-Type': 'application/json' },
5827
- body: JSON.stringify({ title: this.noteTitleDraft, content: this.noteBodyDraft }),
6244
+ body: JSON.stringify({ title: this.noteTitleDraft, content: this.noteBodyDraft, tags: this.noteTagsDraft.split(',').map(t=>t.trim()).filter(Boolean) }),
5828
6245
  }).then(r => r.json());
5829
6246
  const idx = this.personalNotes.findIndex(n => n.filename === this.selectedNote.filename);
5830
6247
  if (idx !== -1) this.personalNotes[idx] = updated;
@@ -5838,27 +6255,61 @@
5838
6255
 
5839
6256
  async deletePersonalNote() {
5840
6257
  if (!this.selectedNote || !confirm(`Delete "${this.selectedNote.title}"?`)) return;
5841
- await fetch(`/api/notes/${this.selectedNote.filename}`, { method: 'DELETE' });
6258
+ await fetch(`/api/notes/${this.selectedNote.path}`, { method: 'DELETE' });
5842
6259
  this.personalNotes = this.personalNotes.filter(n => n.filename !== this.selectedNote.filename);
5843
6260
  this.selectedNote = null;
5844
6261
  this.noteEditing = false;
5845
6262
  },
5846
6263
 
5847
- async saveNoteFromSession() {
5848
- const title = this.selectedSession?.firstPrompt?.slice(0, 60) || 'Session note';
5849
- const sessionId = this.selectedSession?.sessionId || '';
5850
- const note = await fetch('/api/notes', {
5851
- method: 'POST',
6264
+ async togglePinNote() {
6265
+ if (!this.selectedNote) return;
6266
+ const newPinned = !this.selectedNote.pinned;
6267
+ const updated = await fetch(`/api/notes/${this.selectedNote.path}`, {
6268
+ method: 'PUT',
6269
+ headers: { 'Content-Type': 'application/json' },
6270
+ body: JSON.stringify({ pinned: newPinned }),
6271
+ }).then(r => r.json());
6272
+ if (updated.error) { alert(updated.error); return; }
6273
+ const idx = this.personalNotes.findIndex(n => n.path === this.selectedNote.path);
6274
+ if (idx !== -1) this.personalNotes.splice(idx, 1, updated);
6275
+ this.selectedNote = updated;
6276
+ },
6277
+
6278
+
6279
+ async movePersonalNote(targetFolder) {
6280
+ if (!this.selectedNote || targetFolder === '__current__') return;
6281
+ const currentFolder = this.selectedNote.folder ?? '';
6282
+ if (targetFolder === currentFolder) return;
6283
+ const updated = await fetch(`/api/notes/${this.selectedNote.path}/move`, {
6284
+ method: 'PATCH',
5852
6285
  headers: { 'Content-Type': 'application/json' },
5853
- body: JSON.stringify({ title, content: '', session: sessionId }),
6286
+ body: JSON.stringify({ folder: targetFolder }),
5854
6287
  }).then(r => r.json());
5855
- this.personalNotes.unshift(note);
5856
- this.selectedNote = note;
5857
- this.noteEditing = true;
5858
- this.noteTitleDraft = note.title;
5859
- this.noteBodyDraft = note.content;
5860
- this.view = 'notes';
5861
- this.selectedSession = null;
6288
+ if (updated.error) { alert(updated.error); return; }
6289
+ const idx = this.personalNotes.findIndex(n => n.path === this.selectedNote.path);
6290
+ if (idx !== -1) this.personalNotes.splice(idx, 1, updated);
6291
+ this.selectedNote = updated;
6292
+ this.noteFolderPath = targetFolder;
6293
+ window.location.hash = `#/note/${updated.path}`;
6294
+ },
6295
+
6296
+ async snapshotSession() {
6297
+ if (!this.selectedSession || this.snapshottingSession) return;
6298
+ this.snapshottingSession = true;
6299
+ try {
6300
+ const { projectDir, sessionId } = this.selectedSession;
6301
+ const note = await fetch(`/api/sessions/${projectDir}/${sessionId}/snapshot`, {
6302
+ method: 'POST',
6303
+ }).then(r => r.json());
6304
+ if (note.error) { alert(note.error); return; }
6305
+ if (!this.personalNotes.find(n => n.path === note.path)) this.personalNotes.unshift(note);
6306
+ this.selectedNote = note;
6307
+ this.noteEditing = false;
6308
+ this.view = 'notes';
6309
+ this.selectedSession = null;
6310
+ } finally {
6311
+ this.snapshottingSession = false;
6312
+ }
5862
6313
  },
5863
6314
 
5864
6315
  async loadToday() {
@@ -5894,6 +6345,25 @@
5894
6345
  this.saveToday();
5895
6346
  },
5896
6347
 
6348
+ addNoteTask(text, note) {
6349
+ if (!text || !this.todayData) return;
6350
+ this.todayData.tasks.push({
6351
+ id: Math.random().toString(36).slice(2),
6352
+ text: text.slice(0, 200),
6353
+ done: false,
6354
+ carriedOver: false,
6355
+ createdAt: new Date().toISOString(),
6356
+ noteRef: note ? { title: note.title, path: note.path } : undefined,
6357
+ });
6358
+ this.saveToday();
6359
+ // Toast
6360
+ const el = document.createElement('div');
6361
+ el.className = 'note-task-toast';
6362
+ el.textContent = '✓ Added to Today';
6363
+ document.body.appendChild(el);
6364
+ setTimeout(() => el.remove(), 2000);
6365
+ },
6366
+
5897
6367
  toggleTodayTask(id) {
5898
6368
  const task = this.todayData?.tasks.find(t => t.id === id);
5899
6369
  if (task) { task.done = !task.done; this.saveToday(); }
package/server.js CHANGED
@@ -399,7 +399,12 @@ function parseFrontmatter(raw) {
399
399
  const kv = line.match(/^([\w-]+):\s*(.*)$/);
400
400
  if (kv) {
401
401
  currentKey = kv[1];
402
- meta[currentKey] = kv[2].trim();
402
+ const val = kv[2].trim();
403
+ if (val.startsWith('[') && val.endsWith(']')) {
404
+ meta[currentKey] = val.slice(1, -1).split(',').map(s => s.trim()).filter(Boolean);
405
+ } else {
406
+ meta[currentKey] = val;
407
+ }
403
408
  } else if (currentKey && line.match(/^\s+-\s+(.+)$/)) {
404
409
  const val = line.match(/^\s+-\s+(.+)$/)[1].trim();
405
410
  if (!Array.isArray(meta[currentKey])) meta[currentKey] = meta[currentKey] ? [meta[currentKey]] : [];
@@ -1794,6 +1799,58 @@ app.get('/api/sessions/:project/:sessionId/export', async (req, res) => {
1794
1799
  } catch (e) { res.status(500).json({ error: e.message }); }
1795
1800
  });
1796
1801
 
1802
+ // POST /api/sessions/:project/:sessionId/snapshot — create a note with session summary
1803
+ app.post('/api/sessions/:project/:sessionId/snapshot', async (req, res) => {
1804
+ const { project, sessionId } = req.params;
1805
+ const filePath = path.join(PROJECTS_DIR, project, `${sessionId}.jsonl`);
1806
+ if (!filePath.startsWith(PROJECTS_DIR)) return res.status(400).json({ error: 'invalid path' });
1807
+ try {
1808
+ const messages = await parseJsonl(filePath);
1809
+ if (!messages.length) return res.status(404).json({ error: 'session not found or empty' });
1810
+
1811
+ // Extract metadata
1812
+ const first = messages[0];
1813
+ const branch = first.gitBranch || '';
1814
+ const date = first.timestamp ? first.timestamp.slice(0, 10) : new Date().toISOString().slice(0, 10);
1815
+
1816
+ // Extract user prompts (skip <command-name> tool messages)
1817
+ const prompts = [];
1818
+ for (const m of messages) {
1819
+ if (m.type !== 'user') continue;
1820
+ const c = m.message?.content;
1821
+ let text = '';
1822
+ if (typeof c === 'string') text = c;
1823
+ else if (Array.isArray(c)) {
1824
+ text = c.filter(b => b.type === 'text' && !b.text?.includes('<command-name>')).map(b => b.text).join('\n');
1825
+ }
1826
+ text = text.trim();
1827
+ if (text && text.length > 2) prompts.push(text);
1828
+ }
1829
+
1830
+ const firstPrompt = prompts[0] || sessionId;
1831
+ const title = `Snapshot: ${firstPrompt.slice(0, 60)}${firstPrompt.length > 60 ? '…' : ''}`;
1832
+
1833
+ const projectLabel = project.replace(/^-Users-[^-]+-/, '').replace(/-/g, '/');
1834
+ const meta = [`**Project:** ${projectLabel}`, branch ? `**Branch:** ${branch}` : '', `**Date:** ${date}`, `**Session:** ${sessionId.slice(0, 8)}…`].filter(Boolean).join(' · ');
1835
+
1836
+ const promptList = prompts.map((p, i) => `${i + 1}. ${p.replace(/\n+/g, ' ').slice(0, 200)}${p.length > 200 ? '…' : ''}`).join('\n');
1837
+
1838
+ const content = `${meta}\n\n## Prompts\n\n${promptList}\n\n## Notes\n\n`;
1839
+
1840
+ ensureNotesDir();
1841
+ const slug = firstPrompt.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 40) || 'snapshot';
1842
+ const datePrefix = date;
1843
+ let filename = `${datePrefix}-snapshot-${slug}.md`;
1844
+ let i = 1;
1845
+ while (fs.existsSync(path.join(NOTES_DIR, filename))) { filename = `${datePrefix}-snapshot-${slug}-${i++}.md`; }
1846
+ const tagsLine = `\ntags: [snapshot]`;
1847
+ const sessionLine = `\nsession: ${sessionId}`;
1848
+ const raw = `---\ntitle: ${title}\ndate: ${new Date().toISOString()}${sessionLine}${tagsLine}\n---\n\n${content}`;
1849
+ fs.writeFileSync(path.join(NOTES_DIR, filename), raw, 'utf8');
1850
+ res.json(parseNoteFile(filename));
1851
+ } catch (e) { res.status(500).json({ error: e.message }); }
1852
+ });
1853
+
1797
1854
  // ─── App settings (claude-home specific, not Claude's settings.json) ─────────
1798
1855
  const APP_SETTINGS_FILE = path.join(DATA_DIR, 'app-settings.json');
1799
1856
 
@@ -1902,21 +1959,47 @@ app.post('/api/share/plan/:filename', async (req, res) => {
1902
1959
  const NOTES_DIR = path.join(DATA_DIR, 'notes');
1903
1960
  function ensureNotesDir() { if (!fs.existsSync(NOTES_DIR)) fs.mkdirSync(NOTES_DIR, { recursive: true }); }
1904
1961
 
1905
- function parseNoteFile(filename) {
1906
- const filePath = path.join(NOTES_DIR, filename);
1962
+ function parseNoteFile(notepath) {
1963
+ const filePath = path.join(NOTES_DIR, notepath);
1907
1964
  const stat = fs.statSync(filePath);
1908
1965
  const raw = fs.readFileSync(filePath, 'utf8');
1909
1966
  const { meta, content: body } = parseFrontmatter(raw);
1967
+ const tags = Array.isArray(meta.tags) ? meta.tags : (meta.tags ? [meta.tags] : []);
1968
+ const parts = notepath.replace(/\\/g, '/').split('/');
1969
+ const filename = parts[parts.length - 1];
1970
+ const folder = parts.slice(0, -1).join('/');
1910
1971
  return {
1911
1972
  filename,
1973
+ path: notepath,
1974
+ folder,
1912
1975
  title: meta.title || filename.replace('.md', ''),
1913
1976
  date: meta.date || stat.mtimeMs,
1914
1977
  session: meta.session || '',
1978
+ tags,
1979
+ pinned: meta.pinned === true || meta.pinned === 'true',
1915
1980
  content: body.trim(),
1916
1981
  modified: new Date(stat.mtimeMs).toISOString(),
1917
1982
  };
1918
1983
  }
1919
1984
 
1985
+ function scanNotes(dir, base) {
1986
+ if (dir === undefined) dir = NOTES_DIR;
1987
+ if (base === undefined) base = '';
1988
+ const results = [];
1989
+ try {
1990
+ for (const entry of fs.readdirSync(dir)) {
1991
+ const fullPath = path.join(dir, entry);
1992
+ const relPath = base ? `${base}/${entry}` : entry;
1993
+ if (fs.statSync(fullPath).isDirectory()) {
1994
+ results.push(...scanNotes(fullPath, relPath));
1995
+ } else if (entry.endsWith('.md')) {
1996
+ results.push(parseNoteFile(relPath));
1997
+ }
1998
+ }
1999
+ } catch (e) { /* ignore unreadable dirs */ }
2000
+ return results;
2001
+ }
2002
+
1920
2003
  const NOTES_CLAUDE_MD_SNIPPET = `
1921
2004
  ## Personal Notes
1922
2005
 
@@ -1933,9 +2016,13 @@ When the user asks you to "save a note", "add to notes", "guarda esto como nota"
1933
2016
 
1934
2017
  <the content the user wants to save>
1935
2018
  \`\`\`
1936
- 2. **Note linking**: If the note references concepts that likely exist in other notes, use \`#slug\` syntax to link them. The slug is the part of the filename after the date (\`2026-03-31-\`**\`my-slug\`**\`.md\` → \`#my-slug\`). To discover existing notes and their slugs, \`Glob ~/.claude/claude-home/notes/*.md\` before writing.
1937
- 3. Use the Write tool to create the file (not Bash).
1938
- 4. Confirm with: "Saved to Notes: http://localhost:3141/#/note/<filename>"
2019
+ 2. **Folder**: Save in a subfolder when appropriate:
2020
+ - Project-specific note \`~/.claude/claude-home/notes/<basename-of-pwd>/\` (e.g. working in \`mono-genially\` folder \`mono-genially\`)
2021
+ - Global/cross-project note root \`~/.claude/claude-home/notes/\`
2022
+ - User-specified folder → use that folder name
2023
+ 3. **Note linking**: If the note references concepts in other notes, use \`#slug\` syntax (\`2026-03-31-my-slug.md\` → \`#my-slug\`). Glob \`~/.claude/claude-home/notes/**/*.md\` to discover existing slugs.
2024
+ 4. Use the Write tool to create the file (not Bash).
2025
+ 5. Confirm with: "Saved to Notes: http://localhost:3141/#/note/<folder/filename or filename>"
1939
2026
 
1940
2027
  The notes directory may not exist yet — the app creates it automatically on first load.
1941
2028
 
@@ -2003,51 +2090,186 @@ app.post('/api/notes/setup-claude', (req, res) => {
2003
2090
  } catch (e) { res.status(500).json({ error: e.message }); }
2004
2091
  });
2005
2092
 
2093
+ app.get('/api/notes/folders', (req, res) => {
2094
+ ensureNotesDir();
2095
+ try {
2096
+ const folders = fs.readdirSync(NOTES_DIR).filter(e => fs.statSync(path.join(NOTES_DIR, e)).isDirectory());
2097
+ res.json(folders.sort());
2098
+ } catch (e) { res.status(500).json({ error: e.message }); }
2099
+ });
2100
+
2101
+ app.post('/api/notes/folders', (req, res) => {
2102
+ ensureNotesDir();
2103
+ const { name } = req.body;
2104
+ if (!name) return res.status(400).json({ error: 'name required' });
2105
+ const safeName = name.trim().replace(/[^a-zA-Z0-9_-]/g, '-').replace(/^-+|-+$/g, '').slice(0, 50);
2106
+ if (!safeName) return res.status(400).json({ error: 'invalid name' });
2107
+ const folderPath = path.join(NOTES_DIR, safeName);
2108
+ if (!folderPath.startsWith(NOTES_DIR)) return res.status(400).json({ error: 'invalid name' });
2109
+ try {
2110
+ if (!fs.existsSync(folderPath)) fs.mkdirSync(folderPath, { recursive: true });
2111
+ res.json({ name: safeName });
2112
+ } catch (e) { res.status(500).json({ error: e.message }); }
2113
+ });
2114
+
2115
+ app.patch('/api/notes/folders/:name/rename', (req, res) => {
2116
+ ensureNotesDir();
2117
+ const { name } = req.params;
2118
+ const { newName } = req.body;
2119
+ if (!name || !newName) return res.status(400).json({ error: 'name and newName required' });
2120
+ const safeName = newName.trim().replace(/[^a-zA-Z0-9_-]/g, '-').replace(/^-+|-+$/g, '').slice(0, 50);
2121
+ if (!safeName) return res.status(400).json({ error: 'invalid newName' });
2122
+ const srcPath = path.join(NOTES_DIR, name);
2123
+ const destPath = path.join(NOTES_DIR, safeName);
2124
+ if (!srcPath.startsWith(NOTES_DIR) || !destPath.startsWith(NOTES_DIR)) return res.status(400).json({ error: 'invalid path' });
2125
+ try {
2126
+ if (!fs.existsSync(srcPath)) return res.status(404).json({ error: 'folder not found' });
2127
+ if (fs.existsSync(destPath)) return res.status(409).json({ error: 'folder already exists' });
2128
+ fs.renameSync(srcPath, destPath);
2129
+ res.json({ name: safeName });
2130
+ } catch (e) { res.status(500).json({ error: e.message }); }
2131
+ });
2132
+
2133
+ app.delete('/api/notes/folders/:name', (req, res) => {
2134
+ ensureNotesDir();
2135
+ const { name } = req.params;
2136
+ const { action } = req.query; // 'delete' | 'orphan'
2137
+ if (!name) return res.status(400).json({ error: 'name required' });
2138
+ if (!['delete', 'orphan'].includes(action)) return res.status(400).json({ error: 'action must be delete or orphan' });
2139
+ const folderPath = path.join(NOTES_DIR, name);
2140
+ if (!folderPath.startsWith(NOTES_DIR + path.sep) && folderPath !== NOTES_DIR) return res.status(400).json({ error: 'invalid name' });
2141
+ try {
2142
+ if (!fs.existsSync(folderPath)) return res.status(404).json({ error: 'folder not found' });
2143
+ const files = fs.readdirSync(folderPath).filter(f => f.endsWith('.md'));
2144
+ if (action === 'orphan') {
2145
+ for (const f of files) {
2146
+ let dest = path.join(NOTES_DIR, f);
2147
+ if (fs.existsSync(dest)) dest = path.join(NOTES_DIR, f.slice(0, -3) + '-orphaned.md');
2148
+ fs.renameSync(path.join(folderPath, f), dest);
2149
+ }
2150
+ } else {
2151
+ for (const f of files) fs.unlinkSync(path.join(folderPath, f));
2152
+ }
2153
+ try { fs.rmdirSync(folderPath); } catch {}
2154
+ res.json({ ok: true, action, count: files.length });
2155
+ } catch (e) { res.status(500).json({ error: e.message }); }
2156
+ });
2157
+
2158
+ function rewriteNoteTags(notepath, updatedTags) {
2159
+ const filePath = path.join(NOTES_DIR, notepath);
2160
+ const existing = parseNoteFile(notepath);
2161
+ const sessionLine = existing.session ? `\nsession: ${existing.session}` : '';
2162
+ const tagsLine = updatedTags.length > 0 ? `\ntags: [${updatedTags.join(', ')}]` : '';
2163
+ const pinnedLine = existing.pinned ? `\npinned: true` : '';
2164
+ const raw = `---\ntitle: ${existing.title}\ndate: ${existing.date}${sessionLine}${tagsLine}${pinnedLine}\n---\n\n${existing.content}`;
2165
+ fs.writeFileSync(filePath, raw, 'utf8');
2166
+ }
2167
+
2168
+ app.patch('/api/notes/tags/:name/rename', (req, res) => {
2169
+ const { name } = req.params;
2170
+ const { newName } = req.body;
2171
+ if (!name || !newName) return res.status(400).json({ error: 'name and newName required' });
2172
+ const safe = newName.trim().replace(/[^a-zA-Z0-9_-]/g, '-').replace(/^-+|-+$/g, '').slice(0, 40);
2173
+ if (!safe) return res.status(400).json({ error: 'invalid newName' });
2174
+ ensureNotesDir();
2175
+ try {
2176
+ const affected = scanNotes().filter(n => (n.tags || []).includes(name));
2177
+ for (const n of affected) {
2178
+ const tags = n.tags.map(t => t === name ? safe : t);
2179
+ rewriteNoteTags(n.path, tags);
2180
+ }
2181
+ res.json({ renamed: name, to: safe, count: affected.length });
2182
+ } catch (e) { res.status(500).json({ error: e.message }); }
2183
+ });
2184
+
2185
+ app.delete('/api/notes/tags/:name', (req, res) => {
2186
+ const { name } = req.params;
2187
+ if (!name) return res.status(400).json({ error: 'name required' });
2188
+ ensureNotesDir();
2189
+ try {
2190
+ const affected = scanNotes().filter(n => (n.tags || []).includes(name));
2191
+ for (const n of affected) {
2192
+ const tags = n.tags.filter(t => t !== name);
2193
+ rewriteNoteTags(n.path, tags);
2194
+ }
2195
+ res.json({ deleted: name, count: affected.length });
2196
+ } catch (e) { res.status(500).json({ error: e.message }); }
2197
+ });
2198
+
2006
2199
  app.get('/api/notes', (req, res) => {
2007
2200
  ensureNotesDir();
2008
2201
  try {
2009
- const files = fs.readdirSync(NOTES_DIR).filter(f => f.endsWith('.md'));
2010
- const notes = files.map(f => parseNoteFile(f)).sort((a, b) => b.modified.localeCompare(a.modified));
2202
+ const notes = scanNotes().sort((a, b) => b.modified.localeCompare(a.modified));
2011
2203
  res.json(notes);
2012
2204
  } catch (e) { res.status(500).json({ error: e.message }); }
2013
2205
  });
2014
2206
 
2015
2207
  app.post('/api/notes', (req, res) => {
2016
2208
  ensureNotesDir();
2017
- const { title, content, session } = req.body;
2209
+ const { title, content, session, tags, folder } = req.body;
2018
2210
  if (!title) return res.status(400).json({ error: 'title required' });
2019
2211
  const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 40) || 'note';
2020
2212
  const date = new Date().toISOString();
2021
2213
  const datePrefix = date.slice(0, 10);
2214
+ const safeFolder = folder ? folder.trim().replace(/[^a-zA-Z0-9_/-]/g, '-').replace(/^\/+|\/+$/g, '') : '';
2215
+ const noteDir = safeFolder ? path.join(NOTES_DIR, safeFolder) : NOTES_DIR;
2216
+ if (!noteDir.startsWith(NOTES_DIR)) return res.status(400).json({ error: 'invalid folder' });
2217
+ if (!fs.existsSync(noteDir)) fs.mkdirSync(noteDir, { recursive: true });
2022
2218
  let filename = `${datePrefix}-${slug}.md`;
2023
- // avoid collisions
2024
2219
  let i = 1;
2025
- while (fs.existsSync(path.join(NOTES_DIR, filename))) { filename = `${datePrefix}-${slug}-${i++}.md`; }
2220
+ while (fs.existsSync(path.join(noteDir, filename))) { filename = `${datePrefix}-${slug}-${i++}.md`; }
2221
+ const notepath = safeFolder ? `${safeFolder}/${filename}` : filename;
2026
2222
  const sessionLine = session ? `\nsession: ${session}` : '';
2027
- const raw = `---\ntitle: ${title}\ndate: ${date}${sessionLine}\n---\n\n${content || ''}`;
2028
- fs.writeFileSync(path.join(NOTES_DIR, filename), raw, 'utf8');
2029
- res.json(parseNoteFile(filename));
2223
+ const tagsArr = Array.isArray(tags) ? tags.filter(Boolean) : [];
2224
+ const tagsLine = tagsArr.length > 0 ? `\ntags: [${tagsArr.join(', ')}]` : '';
2225
+ const raw = `---\ntitle: ${title}\ndate: ${date}${sessionLine}${tagsLine}\n---\n\n${content || ''}`;
2226
+ fs.writeFileSync(path.join(NOTES_DIR, notepath), raw, 'utf8');
2227
+ res.json(parseNoteFile(notepath));
2228
+ });
2229
+
2230
+ app.patch('/api/notes/*/move', (req, res) => {
2231
+ const notepath = req.params[0];
2232
+ if (!notepath || !notepath.endsWith('.md')) return res.status(400).json({ error: 'invalid filename' });
2233
+ const srcPath = path.join(NOTES_DIR, notepath);
2234
+ if (!srcPath.startsWith(NOTES_DIR)) return res.status(400).json({ error: 'invalid path' });
2235
+ const { folder } = req.body;
2236
+ const filename = path.basename(notepath);
2237
+ const destDir = folder ? path.join(NOTES_DIR, folder) : NOTES_DIR;
2238
+ const destPath = path.join(destDir, filename);
2239
+ if (!destPath.startsWith(NOTES_DIR)) return res.status(400).json({ error: 'invalid destination' });
2240
+ try {
2241
+ if (!fs.existsSync(srcPath)) return res.status(404).json({ error: 'note not found' });
2242
+ if (destPath === srcPath) return res.json(parseNoteFile(notepath));
2243
+ if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
2244
+ fs.renameSync(srcPath, destPath);
2245
+ const newRelPath = folder ? `${folder}/${filename}` : filename;
2246
+ res.json(parseNoteFile(newRelPath));
2247
+ } catch (e) { res.status(500).json({ error: e.message }); }
2030
2248
  });
2031
2249
 
2032
- app.put('/api/notes/:filename', (req, res) => {
2033
- const filename = path.basename(req.params.filename);
2034
- if (!filename.endsWith('.md')) return res.status(400).json({ error: 'invalid filename' });
2035
- const filePath = path.join(NOTES_DIR, filename);
2250
+ app.put('/api/notes/*', (req, res) => {
2251
+ const notepath = req.params[0];
2252
+ if (!notepath || !notepath.endsWith('.md')) return res.status(400).json({ error: 'invalid filename' });
2253
+ const filePath = path.join(NOTES_DIR, notepath);
2036
2254
  if (!filePath.startsWith(NOTES_DIR)) return res.status(400).json({ error: 'invalid path' });
2037
- const { title, content } = req.body;
2255
+ const { title, content, tags, pinned } = req.body;
2038
2256
  try {
2039
- const existing = parseNoteFile(filename);
2257
+ const existing = parseNoteFile(notepath);
2258
+ const resolvedTags = Array.isArray(tags) ? tags : existing.tags;
2259
+ const resolvedPinned = pinned !== undefined ? pinned : existing.pinned;
2040
2260
  const sessionLine = existing.session ? `\nsession: ${existing.session}` : '';
2041
- const raw = `---\ntitle: ${title || existing.title}\ndate: ${existing.date}${sessionLine}\n---\n\n${content ?? existing.content}`;
2261
+ const tagsLine = resolvedTags.length > 0 ? `\ntags: [${resolvedTags.join(', ')}]` : '';
2262
+ const pinnedLine = resolvedPinned ? `\npinned: true` : '';
2263
+ const raw = `---\ntitle: ${title || existing.title}\ndate: ${existing.date}${sessionLine}${tagsLine}${pinnedLine}\n---\n\n${content ?? existing.content}`;
2042
2264
  fs.writeFileSync(filePath, raw, 'utf8');
2043
- res.json(parseNoteFile(filename));
2265
+ res.json(parseNoteFile(notepath));
2044
2266
  } catch (e) { res.status(500).json({ error: e.message }); }
2045
2267
  });
2046
2268
 
2047
- app.delete('/api/notes/:filename', (req, res) => {
2048
- const filename = path.basename(req.params.filename);
2049
- if (!filename.endsWith('.md')) return res.status(400).json({ error: 'invalid filename' });
2050
- const filePath = path.join(NOTES_DIR, filename);
2269
+ app.delete('/api/notes/*', (req, res) => {
2270
+ const notepath = req.params[0];
2271
+ if (!notepath || !notepath.endsWith('.md')) return res.status(400).json({ error: 'invalid filename' });
2272
+ const filePath = path.join(NOTES_DIR, notepath);
2051
2273
  if (!filePath.startsWith(NOTES_DIR)) return res.status(400).json({ error: 'invalid path' });
2052
2274
  try {
2053
2275
  if (fs.existsSync(filePath)) fs.unlinkSync(filePath);