claude-home 1.6.0 → 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,6 +17,8 @@ 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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-home",
3
- "version": "1.6.0",
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
@@ -496,6 +496,8 @@
496
496
  }
497
497
  .note-folder-row:hover { background: var(--canvas); }
498
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; }
499
501
  .note-folder-icon { font-size: 8px; color: var(--ink-3); }
500
502
  .note-folder-name { flex: 1; font-weight: 500; }
501
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; }
@@ -2450,7 +2452,7 @@
2450
2452
  <button class="export-drop-item" @click="copySessionMd(); exportDropOpen=false">Copy as Markdown</button>
2451
2453
  <button class="export-drop-item" @click="shareSession(); exportDropOpen=false" :disabled="!!shareMsg"><span x-text="shareMsg || 'Publish to Gist'"></span></button>
2452
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>
2453
- <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>
2454
2456
  <div style="height:1px;background:var(--rule);margin:4px 0"></div>
2455
2457
  <button class="export-drop-item" style="color:var(--red)" @click="deleteSession();exportDropOpen=false" x-show="!deletingSession">Delete session</button>
2456
2458
  </div>
@@ -2966,6 +2968,10 @@
2966
2968
  <span style="font-size:10px;color:var(--ink-3);padding:0 2px;opacity:0;transition:opacity .15s" class="note-folder-rename-btn"
2967
2969
  @click.stop="noteRenamingFolder=f;noteRenameFolderDraft=f" title="Rename">✎</span>
2968
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>
2969
2975
  <template x-if="noteRenamingFolder === f">
2970
2976
  <button class="btn btn-sm btn-primary" style="padding:1px 7px;font-size:11px" @click.stop="renameFolderConfirm(f)">OK</button>
2971
2977
  </template>
@@ -3068,14 +3074,24 @@
3068
3074
  <div style="padding:24px;max-width:380px">
3069
3075
  <div style="font-size:13px;font-weight:600;margin-bottom:6px;color:var(--ink)">Your personal notepad</div>
3070
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>
3071
- <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>
3072
- <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">
3073
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>
3074
- <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>
3075
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>
3076
- <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">
3077
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>
3078
- <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>
3079
3095
  </div>
3080
3096
  </div>
3081
3097
  </template>
@@ -4684,6 +4700,24 @@
4684
4700
 
4685
4701
  </div>
4686
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
+
4687
4721
  <!-- New Memory modal -->
4688
4722
  <template x-if="memoryNewModal">
4689
4723
  <div class="memory-new-modal" @click.self="memoryNewModal=false">
@@ -4779,6 +4813,7 @@
4779
4813
  selectedPlan: null,
4780
4814
  plansSearch: '',
4781
4815
  deletingSession: false,
4816
+ snapshottingSession: false,
4782
4817
  exportDropOpen: false,
4783
4818
  exportMsg: '',
4784
4819
  cleanMode: true,
@@ -4819,6 +4854,7 @@
4819
4854
  noteFolders: [],
4820
4855
  noteRenamingFolder: null,
4821
4856
  noteRenameFolderDraft: '',
4857
+ deletingFolder: null,
4822
4858
  noteTagMenu: null,
4823
4859
  noteTagRenaming: false,
4824
4860
  noteTagRenameDraft: '',
@@ -5489,6 +5525,29 @@
5489
5525
  this.noteRenameFolderDraft = '';
5490
5526
  },
5491
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
+
5492
5551
  noteAllTags() {
5493
5552
  const counts = {};
5494
5553
  const scoped = this.personalNotes.filter(n => (n.folder ?? '') === this.noteFolderPath);
@@ -6234,21 +6293,23 @@
6234
6293
  window.location.hash = `#/note/${updated.path}`;
6235
6294
  },
6236
6295
 
6237
- async saveNoteFromSession() {
6238
- const title = this.selectedSession?.firstPrompt?.slice(0, 60) || 'Session note';
6239
- const sessionId = this.selectedSession?.sessionId || '';
6240
- const note = await fetch('/api/notes', {
6241
- method: 'POST',
6242
- headers: { 'Content-Type': 'application/json' },
6243
- body: JSON.stringify({ title, content: '', session: sessionId }),
6244
- }).then(r => r.json());
6245
- this.personalNotes.unshift(note);
6246
- this.selectedNote = note;
6247
- this.noteEditing = true;
6248
- this.noteTitleDraft = note.title;
6249
- this.noteBodyDraft = note.content;
6250
- this.view = 'notes';
6251
- this.selectedSession = null;
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
+ }
6252
6313
  },
6253
6314
 
6254
6315
  async loadToday() {
package/server.js CHANGED
@@ -1799,6 +1799,58 @@ app.get('/api/sessions/:project/:sessionId/export', async (req, res) => {
1799
1799
  } catch (e) { res.status(500).json({ error: e.message }); }
1800
1800
  });
1801
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
+
1802
1854
  // ─── App settings (claude-home specific, not Claude's settings.json) ─────────
1803
1855
  const APP_SETTINGS_FILE = path.join(DATA_DIR, 'app-settings.json');
1804
1856
 
@@ -2078,6 +2130,31 @@ app.patch('/api/notes/folders/:name/rename', (req, res) => {
2078
2130
  } catch (e) { res.status(500).json({ error: e.message }); }
2079
2131
  });
2080
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
+
2081
2158
  function rewriteNoteTags(notepath, updatedTags) {
2082
2159
  const filePath = path.join(NOTES_DIR, notepath);
2083
2160
  const existing = parseNoteFile(notepath);