claude-starter 1.1.1 → 1.3.0

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.
Files changed (3) hide show
  1. package/README.md +49 -6
  2. package/index.js +497 -61
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  <p align="center">
2
2
  <img src="https://img.shields.io/badge/%F0%9F%9A%80-Claude_Starter-7aa2f7?style=for-the-badge&labelColor=1a1b26" alt="Claude Starter" />
3
3
  <br/>
4
+ <img src="https://img.shields.io/npm/v/claude-starter?style=flat-square&color=f7768e&logo=npm" alt="npm" />
4
5
  <img src="https://img.shields.io/badge/node-%3E%3D18-9ece6a?style=flat-square&logo=node.js&logoColor=white" alt="Node.js" />
5
6
  <img src="https://img.shields.io/badge/license-MIT-bb9af7?style=flat-square" alt="MIT License" />
6
7
  <img src="https://img.shields.io/github/v/release/Bojun-Vvibe/claude-starter?style=flat-square&color=7dcfff" alt="Release" />
@@ -15,7 +16,7 @@
15
16
  </p>
16
17
 
17
18
  <p align="center">
18
- <code>git clone</code>&nbsp;&nbsp;→&nbsp;&nbsp;<code>npm link</code>&nbsp;&nbsp;→&nbsp;&nbsp;<code>claude-starter</code>
19
+ <code>npm install -g claude-starter</code>&nbsp;&nbsp;→&nbsp;&nbsp;<code>claude-starter</code>
19
20
  </p>
20
21
 
21
22
  <p align="center">
@@ -73,11 +74,19 @@ claude-starter
73
74
  | 📋 | **对话预览** | 右侧面板展示完整元数据和对话历史 |
74
75
  | 🔀 | **多种排序** | 时间 / 大小 / 消息数 / 项目 |
75
76
  | 📎 | **复制 ID** | `c` 一键复制到剪贴板 |
77
+ | 🔒 | **权限模式** | `m` 设置权限模式,`d` 一键 danger 模式恢复 |
78
+ | 🗑️ | **删除会话** | `x` 删除不需要的会话 |
76
79
  | 🧠 | **智能 CLI** | 自动检测 `mai-claude` / `claude` |
77
- | 🔒 | **完全本地** | 不联网,不上传,不追踪 |
80
+ | 🔐 | **完全本地** | 不联网,不上传,不追踪 |
78
81
 
79
82
  ## 安装
80
83
 
84
+ ```bash
85
+ npm install -g claude-starter
86
+ ```
87
+
88
+ 或者从源码安装:
89
+
81
90
  ```bash
82
91
  git clone https://github.com/Bojun-Vvibe/claude-starter.git
83
92
  cd claude-starter
@@ -87,6 +96,16 @@ npm link
87
96
 
88
97
  然后运行 `claude-starter`,就这么简单。
89
98
 
99
+ ## CLI 参数
100
+
101
+ ```bash
102
+ claude-starter # 启动交互式 TUI
103
+ claude-starter --list [N] # 打印最近 N 个会话(默认 30)
104
+ claude-starter --version # 显示版本号
105
+ claude-starter --update # 检查并更新到最新版本
106
+ claude-starter --help # 显示帮助信息
107
+ ```
108
+
90
109
  ## 快捷键
91
110
 
92
111
  | 按键 | 功能 |
@@ -94,12 +113,15 @@ npm link
94
113
  | `↑` `↓` | 上下导航 |
95
114
  | `Enter` | 新建 / 恢复对话 |
96
115
  | `n` | 直接新建 |
116
+ | `d` | Danger 模式恢复(bypassPermissions) |
117
+ | `m` | 权限模式选择器 |
97
118
  | `/` | 搜索 |
98
119
  | `Backspace` | 删除搜索字符,删空自动退出 |
99
120
  | `Esc` | 清空搜索 |
100
121
  | `p` | 按项目过滤 |
101
- | `s` | 切换排序 |
122
+ | `s` | 切换排序(时间/大小/消息数/项目) |
102
123
  | `c` | 复制 Session ID |
124
+ | `x` / `Delete` | 删除会话 |
103
125
  | `Home` / `End` | 跳到顶 / 底 |
104
126
  | `Ctrl-D` / `Ctrl-U` | 翻页 |
105
127
  | `q` / `Ctrl-C` | 退出 |
@@ -151,17 +173,25 @@ Searches across **everything** — project names, Git branches, conversation con
151
173
  |---|---|---|
152
174
  | 🎨 | **Beautiful TUI** | Tokyo Night color scheme, split-pane layout, feels native in your terminal |
153
175
  | ✨ | **New Session** | Launch a fresh conversation in one keystroke |
154
- | 🔍 | **Instant Search** | Fuzzy search across everything, no Enter needed |
176
+ | 🔍 | **Instant Search** | Fuzzy search across everything |
155
177
  | 📂 | **Project Filter** | Press `p` to filter by project |
156
178
  | ⚡ | **One-Key Resume** | Arrow, Enter, you're back in the conversation |
157
179
  | 📋 | **Session Preview** | Full metadata + conversation history in the right panel |
158
180
  | 🔀 | **Sort Modes** | Sort by time, size, messages, or project |
159
181
  | 📎 | **Copy ID** | Press `c` to copy session ID |
182
+ | 🔒 | **Permission Modes** | Press `m` to configure, `d` for quick danger-mode resume |
183
+ | 🗑️ | **Delete Sessions** | Press `x` to remove unwanted sessions |
160
184
  | 🧠 | **Smart CLI** | Auto-detects `mai-claude` vs `claude` |
161
- | 🔒 | **100% Local** | No network, no telemetry, no data leaves your machine |
185
+ | 🔐 | **100% Local** | No network, no telemetry, no data leaves your machine |
162
186
 
163
187
  ## Install
164
188
 
189
+ ```bash
190
+ npm install -g claude-starter
191
+ ```
192
+
193
+ Or install from source:
194
+
165
195
  ```bash
166
196
  git clone https://github.com/Bojun-Vvibe/claude-starter.git
167
197
  cd claude-starter
@@ -175,6 +205,16 @@ Then run:
175
205
  claude-starter
176
206
  ```
177
207
 
208
+ ## CLI Options
209
+
210
+ ```bash
211
+ claude-starter # Launch interactive TUI
212
+ claude-starter --list [N] # Print latest N sessions (default: 30)
213
+ claude-starter --version # Show version
214
+ claude-starter --update # Update to the latest version
215
+ claude-starter --help # Show help
216
+ ```
217
+
178
218
  ## Keyboard Shortcuts
179
219
 
180
220
  | Key | Action |
@@ -182,12 +222,15 @@ claude-starter
182
222
  | `↑` `↓` | Navigate sessions |
183
223
  | `Enter` | Start new / resume selected session |
184
224
  | `n` | New session |
225
+ | `d` | Resume with bypassPermissions (danger mode) |
226
+ | `m` | Permission mode picker |
185
227
  | `/` | Search |
186
228
  | `Backspace` | Edit search, auto-exit when empty |
187
229
  | `Esc` | Clear filter |
188
230
  | `p` | Filter by project |
189
- | `s` | Cycle sort mode |
231
+ | `s` | Cycle sort mode (time/size/messages/project) |
190
232
  | `c` | Copy session ID |
233
+ | `x` / `Delete` | Delete session |
191
234
  | `Home` / `End` | Jump to first / last |
192
235
  | `Ctrl-D` / `Ctrl-U` | Page down / up |
193
236
  | `q` / `Ctrl-C` | Quit |
package/index.js CHANGED
@@ -9,6 +9,8 @@
9
9
  * claude-starter # Launch interactive TUI
10
10
  * claude-starter --list # Print sessions as a table (no TUI)
11
11
  * claude-starter --list N # Print the latest N sessions
12
+ * claude-starter --version # Show version
13
+ * claude-starter --update # Update to the latest version
12
14
  *
13
15
  * Keyboard shortcuts (TUI mode):
14
16
  * ↑/↓ Navigate sessions
@@ -18,9 +20,12 @@
18
20
  * p Filter by project (popup)
19
21
  * s Cycle sort: time → size → messages → project
20
22
  * n Start new session
23
+ * d Resume with bypassPermissions (danger mode)
24
+ * m Permission mode picker
21
25
  * Home / End Jump to top / bottom
22
26
  * Ctrl-D/U Page down / up
23
27
  * c Copy session ID to clipboard
28
+ * x / Delete Delete selected session
24
29
  * q / Ctrl-C Quit
25
30
  */
26
31
 
@@ -32,37 +37,72 @@ const os = require('os');
32
37
 
33
38
  // ─── CLI Detection ──────────────────────────────────────────────────────────
34
39
  // Detect whether `mai-claude` is available (binary, alias, or function).
35
- // We check inside an interactive shell so aliases defined in .bashrc/.zshrc
36
- // are visible. Falls back to plain `claude`.
40
+ // First checks PATH directly, then sources shell config non-interactively
41
+ // to resolve aliases. Falls back to plain `claude`.
42
+ //
43
+ // NOTE: We deliberately avoid `shell -i` (interactive mode) because it
44
+ // triggers SIGTTOU in terminals like Warp that strictly manage TTY process
45
+ // groups, causing `suspended (tty output)`.
37
46
  //
38
47
  // Returns { name, cmd } where:
39
48
  // name = display label ("mai-claude" or "claude")
40
49
  // cmd = the actual command string to spawn (resolves aliases)
41
50
 
42
51
  function detectCLI() {
52
+ // Strategy:
53
+ // 1. First try non-interactive lookup (safe for all terminals including Warp)
54
+ // 2. Only fall back to interactive shell if needed for alias resolution
55
+ //
56
+ // IMPORTANT: avoid `shell -i` (interactive mode) — it can trigger SIGTTOU
57
+ // in terminals like Warp that strictly manage TTY process groups, causing
58
+ // the process to be suspended with "suspended (tty output)".
59
+
43
60
  const shell = process.env.SHELL || '/bin/sh';
61
+
62
+ // 1) Non-interactive: check if mai-claude exists as a binary on PATH
44
63
  try {
45
- const raw = execSync(`${shell} -ic "command -v mai-claude" 2>/dev/null`, {
64
+ const binPath = execSync('command -v mai-claude 2>/dev/null', {
46
65
  stdio: ['pipe', 'pipe', 'pipe'],
47
66
  timeout: 3000,
67
+ shell: true,
48
68
  }).toString().trim();
69
+ if (binPath) {
70
+ return { name: 'mai-claude', cmd: 'mai-claude' };
71
+ }
72
+ } catch { /* not found as binary, continue */ }
49
73
 
50
- // Interactive shells may print extra lines (e.g. "Restored session: …").
51
- // The relevant output is the last line(s) containing the alias or path.
52
- const lines = raw.split('\n');
53
- const aliasLine = lines.find(l => l.startsWith('alias ')) || lines[lines.length - 1];
54
-
55
- // `command -v` for an alias returns: alias mai-claude='actual command'
56
- // Extract the real command from inside the quotes if it's an alias.
57
- const aliasMatch = aliasLine.match(/^alias [^=]+=(?:'(.+)'|"(.+)")$/s);
58
- if (aliasMatch) {
59
- return { name: 'mai-claude', cmd: aliasMatch[1] || aliasMatch[2] };
74
+ // 2) Source shell config non-interactively to resolve aliases/functions.
75
+ // This avoids `-i` which would try to claim the TTY and risk SIGTTOU.
76
+ try {
77
+ const isZsh = shell.endsWith('/zsh');
78
+ const rcFile = isZsh
79
+ ? path.join(os.homedir(), '.zshrc')
80
+ : path.join(os.homedir(), '.bashrc');
81
+
82
+ if (fs.existsSync(rcFile)) {
83
+ const raw = execSync(
84
+ `${shell} -c 'source "${rcFile}" 2>/dev/null; command -v mai-claude 2>/dev/null'`,
85
+ {
86
+ stdio: ['pipe', 'pipe', 'pipe'],
87
+ timeout: 3000,
88
+ env: { ...process.env, PS1: '', PROMPT: '', NO_TTY: '1' },
89
+ },
90
+ ).toString().trim();
91
+
92
+ if (raw) {
93
+ const lines = raw.split('\n');
94
+ const aliasLine = lines.find(l => l.startsWith('alias ')) || lines[lines.length - 1];
95
+
96
+ const aliasMatch = aliasLine.match(/^alias [^=]+=(?:'(.+)'|"(.+)")$/s);
97
+ if (aliasMatch) {
98
+ return { name: 'mai-claude', cmd: aliasMatch[1] || aliasMatch[2] };
99
+ }
100
+ return { name: 'mai-claude', cmd: 'mai-claude' };
101
+ }
60
102
  }
61
- // Otherwise it's a binary/function path use the name directly
62
- return { name: 'mai-claude', cmd: 'mai-claude' };
63
- } catch {
64
- return { name: 'claude', cmd: 'claude' };
65
- }
103
+ } catch { /* alias resolution failed, fall back to claude */ }
104
+
105
+ return { name: 'claude', cmd: 'claude' };
66
106
  }
67
107
 
68
108
  const CLI = detectCLI();
@@ -76,15 +116,86 @@ const PROJECT_COLORS = [
76
116
  // ─── Paths ───────────────────────────────────────────────────────────────────
77
117
  const CLAUDE_DIR = path.join(os.homedir(), '.claude');
78
118
  const PROJECTS_DIR = path.join(CLAUDE_DIR, 'projects');
119
+ const META_FILE = path.join(CLAUDE_DIR, 'claude-starter-meta.json');
120
+
121
+ // ─── Session Meta ────────────────────────────────────────────────────
122
+ // Stores user-defined metadata for sessions in a simple JSON file.
123
+
124
+ function loadMeta() {
125
+ try {
126
+ if (fs.existsSync(META_FILE)) {
127
+ return JSON.parse(fs.readFileSync(META_FILE, 'utf-8'));
128
+ }
129
+ } catch (e) { /* corrupt file, start fresh */ }
130
+ return { sessions: {} };
131
+ }
132
+
133
+ const PERMISSION_MODES = ['default', 'bypassPermissions', 'acceptEdits', 'dontAsk', 'plan', 'auto'];
134
+
135
+ function saveMeta(meta) {
136
+ try {
137
+ fs.writeFileSync(META_FILE, JSON.stringify(meta, null, 2), 'utf-8');
138
+ } catch (e) { /* silently fail */ }
139
+ }
140
+
141
+ function getSessionMeta(meta, sessionId) {
142
+ return meta.sessions[sessionId] || {};
143
+ }
144
+
145
+ function getEffectivePermissionMode(meta, session) {
146
+ // Priority: per-session override > session's original mode from JSONL > global default
147
+ const sm = meta.sessions[session.sessionId];
148
+ if (sm && sm.permissionMode) return sm.permissionMode;
149
+ if (session.permissionMode) return session.permissionMode;
150
+ if (meta.defaultPermissionMode) return meta.defaultPermissionMode;
151
+ return '';
152
+ }
153
+
154
+ function setSessionPermissionMode(meta, sessionId, mode) {
155
+ if (!meta.sessions[sessionId]) meta.sessions[sessionId] = {};
156
+ meta.sessions[sessionId].permissionMode = mode || undefined;
157
+ if (!mode) delete meta.sessions[sessionId].permissionMode;
158
+ saveMeta(meta);
159
+ }
160
+
161
+ function setGlobalPermissionMode(meta, mode) {
162
+ meta.defaultPermissionMode = mode || undefined;
163
+ if (!mode) delete meta.defaultPermissionMode;
164
+ saveMeta(meta);
165
+ }
166
+
79
167
 
80
168
  // ─── Data Layer ──────────────────────────────────────────────────────────────
81
169
 
82
170
  function getProjectDisplayName(dirName) {
83
- return dirName
84
- .replace(/-Users-[^-]+-Desktop-MSProject-/, '')
85
- .replace(/-Users-[^-]+-Desktop-/, '')
86
- .replace(/-Users-[^-]+/, '~')
87
- .replace(/^-/, '') || '~';
171
+ // Claude stores projects as path with `-` separators, e.g.:
172
+ // -Users-bob-Desktop-MSProject-my-app
173
+ // -Users-bob-Projects-Router-Maestro
174
+ // -Users-bob-Desktop-GraphConnector
175
+ // -Users-bob
176
+ //
177
+ // Strategy: strip the user home prefix, then take the last meaningful path segment.
178
+ // This gives clean names like "my-app", "Router-Maestro", "GraphConnector".
179
+
180
+ // Remove leading -Users-<username> prefix
181
+ let name = dirName.replace(/^-Users-[^-]+/, '');
182
+
183
+ // If nothing left, it was just the home dir
184
+ if (!name || name === '-') return '~';
185
+
186
+ // Remove leading dash
187
+ name = name.replace(/^-/, '');
188
+
189
+ // Get the last path segment (split by common directory markers)
190
+ // e.g. "Desktop-MSProject-my-app" → "my-app"
191
+ // "Desktop-GraphConnector" → "GraphConnector"
192
+ // "Projects-Router-Maestro" → "Router-Maestro"
193
+ const knownPrefixes = /^(Desktop|Documents|Projects|Downloads|dev|src|code|repos|work|home)(?:-|$)/i;
194
+ while (knownPrefixes.test(name)) {
195
+ name = name.replace(/^[^-]+-?/, '');
196
+ }
197
+
198
+ return name || dirName.split('-').pop() || '~';
88
199
  }
89
200
 
90
201
  function loadSessionQuick(filePath, projectName) {
@@ -107,9 +218,10 @@ function loadSessionQuick(filePath, projectName) {
107
218
  const tailStr = tailBuf.toString('utf-8');
108
219
 
109
220
  let firstTs = null, lastTs = null;
110
- let version = '', gitBranch = '', cwd = '';
221
+ let version = '', gitBranch = '', cwd = '', permissionMode = '';
111
222
  let firstUserMsg = '';
112
223
  let userMsgCount = 0;
224
+ let customTitle = '';
113
225
 
114
226
  const headLines = headStr.split('\n').filter(Boolean);
115
227
  for (const line of headLines) {
@@ -121,6 +233,8 @@ function loadSessionQuick(filePath, projectName) {
121
233
  if (!version && d.version) version = d.version;
122
234
  if (!gitBranch && d.gitBranch) gitBranch = d.gitBranch;
123
235
  if (!cwd && d.cwd) cwd = d.cwd;
236
+ if (!permissionMode && d.permissionMode) permissionMode = d.permissionMode;
237
+ if (d.type === 'custom-title' && d.customTitle) customTitle = d.customTitle;
124
238
  if (d.type === 'user') {
125
239
  userMsgCount++;
126
240
  if (!firstUserMsg) firstUserMsg = extractUserText(d);
@@ -135,6 +249,7 @@ function loadSessionQuick(filePath, projectName) {
135
249
  const d = JSON.parse(line);
136
250
  if (d.timestamp) lastTs = d.timestamp;
137
251
  if (d.type === 'user') userMsgCount++;
252
+ if (d.type === 'custom-title' && d.customTitle) customTitle = d.customTitle;
138
253
  } catch (e) { /* partial line */ }
139
254
  }
140
255
  }
@@ -157,6 +272,7 @@ function loadSessionQuick(filePath, projectName) {
157
272
  return {
158
273
  sessionId, project: projectName,
159
274
  topic: topic || '(no user messages)',
275
+ customTitle, permissionMode,
160
276
  firstTs, lastTs, version, gitBranch, cwd,
161
277
  fileSize: stat.size, duration: durationStr,
162
278
  estimatedMessages, filePath, _detailLoaded: false,
@@ -189,6 +305,7 @@ function loadSessionDetail(session) {
189
305
  for (const line of lines) {
190
306
  try {
191
307
  const d = JSON.parse(line);
308
+ if (d.type === 'custom-title' && d.customTitle) session.customTitle = d.customTitle;
192
309
  if (d.type === 'user') {
193
310
  totalMessages++;
194
311
  const text = extractUserText(d);
@@ -242,7 +359,11 @@ function loadAllSessions() {
242
359
  for (const file of files) {
243
360
  try {
244
361
  const session = loadSessionQuick(path.join(projPath, file), projectName);
245
- if (session.firstTs) sessions.push(session);
362
+ // Skip sessions without timestamps, without real user messages, or warmup sessions
363
+ if (session.firstTs
364
+ && session.topic !== '(no user messages)'
365
+ && !/^warmup$/i.test(session.topic.trim())
366
+ ) sessions.push(session);
246
367
  } catch (e) { /* skip */ }
247
368
  }
248
369
  }
@@ -312,6 +433,7 @@ function runListMode(limit) {
312
433
 
313
434
  function createApp() {
314
435
  const allSessions = loadAllSessions();
436
+ const meta = loadMeta();
315
437
  let filteredSessions = [...allSessions];
316
438
  let selectedIndex = -1; // -1 = "New Session", 0+ = session index
317
439
  let filterText = '';
@@ -324,12 +446,17 @@ function createApp() {
324
446
 
325
447
  // ─── Screen ────────────────────────────────────────────────────────────
326
448
  const screen = blessed.screen({
327
- smartCSR: true,
449
+ smartCSR: false,
450
+ fastCSR: false,
328
451
  title: 'Claude Starter',
329
452
  fullUnicode: true,
330
453
  autoPadding: true,
454
+ dockBorders: true,
331
455
  });
332
456
 
457
+ // Force screen-level fill color so no terminal bg leaks through
458
+ screen.style = { bg: 234 }; // 234 = xterm color closest to #1a1b26
459
+
333
460
  // ─── Header ────────────────────────────────────────────────────────────
334
461
  const header = blessed.box({
335
462
  parent: screen, top: 0, left: 0, width: '100%', height: 3,
@@ -337,17 +464,20 @@ function createApp() {
337
464
  });
338
465
 
339
466
  function updateHeader() {
340
- const title = '{bold}{#7aa2f7-fg}🚀 Claude Starter{/}';
467
+ const title = '{bold}{#7aa2f7-fg}Claude Starter{/}';
341
468
  const count = `{#9ece6a-fg}${filteredSessions.length}{/}{#565f89-fg}/${allSessions.length} sessions{/}`;
342
469
  const proj = `{#bb9af7-fg}${uniqueProjects.length}{/}{#565f89-fg} projects{/}`;
343
- const sort = `{#73daca-fg}↕${sortMode}{/}`;
470
+ const sort = `{#73daca-fg}[${sortMode}]{/}`;
344
471
  const search = isSearchMode
345
472
  ? `{#e0af68-fg}/ ${filterText}▌{/}`
346
473
  : (filterText ? `{#e0af68-fg}/ ${filterText}{/}` : '');
347
- header.setContent(`\n ${title} {#414868-fg}│{/} ${count} {#414868-fg}│{/} ${proj} {#414868-fg}│{/} ${sort}${search ? ` {#414868-fg}│{/} ${search}` : ''}`);
474
+ let parts = [title, count, proj];
475
+ parts.push(sort);
476
+ if (search) parts.push(search);
477
+ header.setContent(`\n ${parts.join(' {#414868-fg}│{/} ')}`);
348
478
  }
349
479
 
350
- blessed.line({ parent: screen, top: 3, left: 0, width: '100%', orientation: 'horizontal', style: { fg: '#414868' } });
480
+ blessed.line({ parent: screen, top: 3, left: 0, width: '100%', orientation: 'horizontal', style: { fg: '#414868', bg: '#1a1b26' } });
351
481
 
352
482
  // ─── Left Panel: blessed.list for correct scroll tracking ──────────────
353
483
  const listPanel = blessed.list({
@@ -368,7 +498,7 @@ function createApp() {
368
498
  interactive: true,
369
499
  });
370
500
 
371
- blessed.line({ parent: screen, top: 4, left: '50%', height: '100%-7', orientation: 'vertical', style: { fg: '#414868' } });
501
+ blessed.line({ parent: screen, top: 4, left: '50%', height: '100%-7', orientation: 'vertical', style: { fg: '#414868', bg: '#1a1b26' } });
372
502
 
373
503
  // ─── Right Panel ───────────────────────────────────────────────────────
374
504
  const detailPanel = blessed.box({
@@ -380,7 +510,7 @@ function createApp() {
380
510
  mouse: true,
381
511
  });
382
512
 
383
- blessed.line({ parent: screen, bottom: 2, left: 0, width: '100%', orientation: 'horizontal', style: { fg: '#414868' } });
513
+ blessed.line({ parent: screen, bottom: 2, left: 0, width: '100%', orientation: 'horizontal', style: { fg: '#414868', bg: '#1a1b26' } });
384
514
 
385
515
  // ─── Footer ────────────────────────────────────────────────────────────
386
516
  const footer = blessed.box({
@@ -390,13 +520,15 @@ function createApp() {
390
520
 
391
521
  function updateFooter() {
392
522
  const keys = [
393
- '{#7aa2f7-fg}{bold}↵{/} {#565f89-fg}Start/Resume{/}',
394
523
  '{#7aa2f7-fg}{bold}n{/} {#565f89-fg}New{/}',
524
+ '{#7aa2f7-fg}{bold}↵{/} {#565f89-fg}Resume{/}',
525
+ '{#7aa2f7-fg}{bold}m{/} {#565f89-fg}Mode{/}',
526
+ '{#f7768e-fg}{bold}d{/} {#565f89-fg}Danger{/}',
395
527
  '{#7aa2f7-fg}{bold}/{/} {#565f89-fg}Search{/}',
396
- '{#7aa2f7-fg}{bold}↑/↓{/} {#565f89-fg}Nav{/}',
397
528
  '{#7aa2f7-fg}{bold}p{/} {#565f89-fg}Project{/}',
398
529
  '{#7aa2f7-fg}{bold}s{/} {#565f89-fg}Sort{/}',
399
530
  '{#7aa2f7-fg}{bold}c{/} {#565f89-fg}Copy ID{/}',
531
+ '{#f7768e-fg}{bold}x{/} {#565f89-fg}Delete{/}',
400
532
  '{#7aa2f7-fg}{bold}q{/} {#565f89-fg}Quit{/}',
401
533
  ];
402
534
  footer.setContent(`\n ${keys.join(' {#414868-fg}│{/} ')}`);
@@ -420,7 +552,7 @@ function createApp() {
420
552
  const branch = session.gitBranch
421
553
  ? `{#73daca-fg}${session.gitBranch.substring(0, 25)}{/}`
422
554
  : '';
423
- const dur = session.duration ? `{#565f89-fg}⏱${session.duration}{/}` : '';
555
+ const dur = session.duration ? `{#565f89-fg}${session.duration}{/}` : '';
424
556
 
425
557
  // Compose a multi-line string for each list item.
426
558
  // blessed.list renders each item as a single row, so we pack info densely.
@@ -441,23 +573,32 @@ function createApp() {
441
573
 
442
574
  // ─── Populate list ─────────────────────────────────────────────────────
443
575
  // Index 0 = "New Session", index 1+ = sessions
444
- const NEW_SESSION_LABEL = ' {#9ece6a-fg}{bold} New Conversation{/}';
576
+ const NEW_SESSION_LABEL = ' {#9ece6a-fg}{bold}+ New Conversation{/}';
445
577
 
446
578
  function refreshList() {
447
579
  const listW = Math.floor((screen.width || 100) / 2) - 2;
448
580
 
449
581
  const sessionItems = filteredSessions.map((session) => {
450
582
  const color = getProjectColor(session.project, projectColorMap);
451
- const proj = `{${color}-fg}${session.project.substring(0, 13).padEnd(13)}{/}`;
583
+ const eMode = getEffectivePermissionMode(meta, session);
584
+ const modeIcon = (eMode === 'bypassPermissions') ? '{#f7768e-fg}!{/}' : ' ';
585
+ const proj = `{${color}-fg}${session.project.substring(0, 12).padEnd(12)}{/}`;
452
586
  const time = `{#e0af68-fg}${formatTimestamp(session.lastTs).padEnd(16)}{/}`;
453
- const msgs = `{#7aa2f7-fg}${String(session.estimatedMessages).padStart(4)}{/}{#565f89-fg}m{/}`;
454
587
 
455
- const fixedLen = 13 + 1 + 16 + 1 + 5 + 2 + 3;
456
- const topicMaxLen = Math.max(15, listW - fixedLen);
457
- let topic = session.topic;
588
+ const fixedLen = 1 + 12 + 1 + 16 + 1 + 3;
589
+ const topicMaxLen = Math.max(10, listW - fixedLen);
590
+ let topic = session.customTitle || session.topic;
591
+
458
592
  if (topic.length > topicMaxLen) topic = topic.substring(0, topicMaxLen) + '…';
459
593
 
460
- return ` ${proj} ${time} ${msgs} {#a9b1d6-fg}${esc(topic)}{/}`;
594
+ let label = `${modeIcon}${proj} ${time} `;
595
+ if (session.customTitle) {
596
+ label += `{#73daca-fg}{bold}${esc(topic)}{/}`;
597
+ } else {
598
+ label += `{#a9b1d6-fg}${esc(topic)}{/}`;
599
+ }
600
+
601
+ return label;
461
602
  });
462
603
 
463
604
  const items = [NEW_SESSION_LABEL, ...sessionItems];
@@ -471,14 +612,19 @@ function createApp() {
471
612
  function renderDetail() {
472
613
  if (selectedIndex === -1) {
473
614
  const cli = CLI.name;
615
+ const defaultMode = meta.defaultPermissionMode || '';
616
+ const modeFlag = (defaultMode && defaultMode !== 'default') ? ` --permission-mode ${defaultMode}` : '';
474
617
  let c = '';
475
- c += `\n {#9ece6a-fg}{bold}Start a New Conversation{/}\n`;
618
+ c += `\n {#9ece6a-fg}{bold}Start a New Conversation{/}\n`;
476
619
  c += ` {#414868-fg}${'─'.repeat(44)}{/}\n\n`;
477
620
  c += ` {#a9b1d6-fg}Open a fresh Claude session and start{/}\n`;
478
621
  c += ` {#a9b1d6-fg}coding from scratch.{/}\n\n`;
479
622
  c += ` {#565f89-fg}Working Dir{/} {#7dcfff-fg}${process.cwd()}{/}\n`;
480
623
  c += ` {#565f89-fg}CLI{/} {#73daca-fg}${cli}{/}\n`;
481
- c += ` {#565f89-fg}Command{/} {#565f89-fg}${cli}{/}\n\n`;
624
+ if (defaultMode && defaultMode !== 'default') {
625
+ c += ` {#565f89-fg}Mode{/} {#f7768e-fg}${defaultMode}{/}\n`;
626
+ }
627
+ c += ` {#565f89-fg}Command{/} {#565f89-fg}${cli}${modeFlag}{/}\n\n`;
482
628
  c += ` {#414868-fg}${'─'.repeat(44)}{/}\n`;
483
629
  c += ` {#9ece6a-fg}{bold}↵ Enter{/}{#9ece6a-fg} or {/}{#9ece6a-fg}{bold}n{/}{#9ece6a-fg} to launch{/}\n`;
484
630
  detailPanel.setContent(c);
@@ -498,7 +644,11 @@ function createApp() {
498
644
  let c = '';
499
645
  const sep = ` {#414868-fg}${'─'.repeat(44)}{/}`;
500
646
 
647
+ // Title
501
648
  c += `\n {${color}-fg}{bold}█ ${session.project}{/}\n`;
649
+ if (session.customTitle) {
650
+ c += ` {#73daca-fg}{bold}${esc(session.customTitle)}{/}\n`;
651
+ }
502
652
  c += sep + '\n\n';
503
653
 
504
654
  const fields = [
@@ -513,6 +663,12 @@ function createApp() {
513
663
  if (session.version) fields.push(['Claude', `{#565f89-fg}v${session.version}{/}`]);
514
664
  if (session.cwd) fields.push(['Directory', `{#565f89-fg}${session.cwd}{/}`]);
515
665
 
666
+ const effectiveMode = getEffectivePermissionMode(meta, session);
667
+ if (effectiveMode && effectiveMode !== 'default') {
668
+ const modeColor = effectiveMode === 'bypassPermissions' ? '#f7768e' : '#e0af68';
669
+ fields.push(['Mode', `{${modeColor}-fg}${effectiveMode}{/}`]);
670
+ }
671
+
516
672
  for (const [label, value] of fields) {
517
673
  c += ` {#565f89-fg}${label.padEnd(12)}{/} ${value}\n`;
518
674
  }
@@ -524,7 +680,7 @@ function createApp() {
524
680
  if (session.toolsUsed.length > 10) c += ` {#565f89-fg}+${session.toolsUsed.length - 10} more{/}\n`;
525
681
  }
526
682
 
527
- c += `\n {#bb9af7-fg}{bold}💬 Conversation{/}\n`;
683
+ c += `\n {#bb9af7-fg}{bold}Conversation{/}\n`;
528
684
  c += sep + '\n';
529
685
 
530
686
  const msgs = (session.userMessages || []).slice(0, 10);
@@ -536,11 +692,11 @@ function createApp() {
536
692
  msgs.forEach((msg, i) => {
537
693
  const clean = esc(msg.replace(/\n/g, ' ').trim());
538
694
  const trunc = clean.length > 80 ? clean.substring(0, 80) + '…' : clean;
539
- c += `\n {#7aa2f7-fg}{bold}You {/} ${trunc}\n`;
695
+ c += `\n {#7aa2f7-fg}{bold}You >{/} ${trunc}\n`;
540
696
  if (assists[i]) {
541
697
  const aClean = esc(assists[i].replace(/\n/g, ' ').trim());
542
698
  const aTrunc = aClean.length > 80 ? aClean.substring(0, 80) + '…' : aClean;
543
- c += ` {#9ece6a-fg}Claude {/} {#565f89-fg}${aTrunc}{/}\n`;
699
+ c += ` {#9ece6a-fg}Claude >{/} {#565f89-fg}${aTrunc}{/}\n`;
544
700
  }
545
701
  });
546
702
  }
@@ -570,8 +726,11 @@ function createApp() {
570
726
  } else {
571
727
  const terms = filterText.toLowerCase().split(/\s+/);
572
728
  filteredSessions = allSessions.filter(s => {
573
- const haystack = [s.project, s.topic, s.gitBranch || '', s.sessionId, ...(s.userMessages || [])].join(' ').toLowerCase();
574
- return terms.every(t => haystack.includes(t));
729
+ const haystack = [s.project, s.topic, s.customTitle || '', s.gitBranch || '', s.sessionId, ...(s.userMessages || [])].join(' ').toLowerCase();
730
+
731
+ return terms.every(t => {
732
+ return haystack.includes(t);
733
+ });
575
734
  });
576
735
  }
577
736
  selectedIndex = Math.min(selectedIndex, Math.max(-1, filteredSessions.length - 1));
@@ -670,14 +829,17 @@ function createApp() {
670
829
  }
671
830
 
672
831
  screen.key(['down'], () => {
832
+ if (popupOpen) return;
673
833
  if (isSearchMode) { isSearchMode = false; updateHeader(); screen.render(); }
674
834
  moveSelection(1);
675
835
  });
676
836
  screen.key(['up'], () => {
837
+ if (popupOpen) return;
677
838
  if (isSearchMode) { isSearchMode = false; updateHeader(); screen.render(); }
678
839
  moveSelection(-1);
679
840
  });
680
841
  screen.key(['home'], () => {
842
+ if (popupOpen) return;
681
843
  if (isSearchMode) { isSearchMode = false; }
682
844
  selectedIndex = -1;
683
845
  suppressSelectEvent = true; listPanel.select(0); suppressSelectEvent = false;
@@ -685,6 +847,7 @@ function createApp() {
685
847
  renderDetail(); updateHeader(); screen.render();
686
848
  });
687
849
  screen.key(['end'], () => {
850
+ if (popupOpen) return;
688
851
  if (isSearchMode) { isSearchMode = false; }
689
852
  selectedIndex = Math.max(0, filteredSessions.length - 1);
690
853
  suppressSelectEvent = true; listPanel.select(selectedIndex + 1); suppressSelectEvent = false;
@@ -692,10 +855,12 @@ function createApp() {
692
855
  renderDetail(); updateHeader(); screen.render();
693
856
  });
694
857
  screen.key(['pagedown', 'C-d'], () => {
858
+ if (popupOpen) return;
695
859
  if (isSearchMode) { isSearchMode = false; updateHeader(); screen.render(); }
696
860
  moveSelection(Math.floor((listPanel.height || 20) / 2));
697
861
  });
698
862
  screen.key(['pageup', 'C-u'], () => {
863
+ if (popupOpen) return;
699
864
  if (isSearchMode) { isSearchMode = false; updateHeader(); screen.render(); }
700
865
  moveSelection(-Math.floor((listPanel.height || 20) / 2));
701
866
  });
@@ -725,35 +890,46 @@ function createApp() {
725
890
  // ─── Resume Session ─────────────────────────────────────────────────────
726
891
  // Auto-detect: use mai-claude if available, otherwise fall back to claude
727
892
 
728
- function resumeSession(session) {
893
+ function resumeSession(session, modeOverride) {
894
+ process.stdout.write('\x1b[0m');
729
895
  screen.destroy();
730
896
 
731
897
  const label = CLI.name;
898
+ const mode = modeOverride || getEffectivePermissionMode(meta, session);
899
+ const modeFlag = (mode && mode !== 'default') ? ` --permission-mode ${mode}` : '';
732
900
 
733
901
  console.log(`\n\x1b[36m⚡ Resuming conversation with ${label}\x1b[0m`);
734
902
  console.log(`\x1b[90m Session: ${session.sessionId}\x1b[0m`);
735
- console.log(`\x1b[90m Project: ${session.project} │ Branch: ${session.gitBranch || 'N/A'} │ Messages: ${session.estimatedMessages}\x1b[0m\n`);
903
+ console.log(`\x1b[90m Project: ${session.project} │ Branch: ${session.gitBranch || 'N/A'} │ Messages: ${session.estimatedMessages}\x1b[0m`);
904
+ if (mode && mode !== 'default') console.log(`\x1b[33m Mode: ${mode}\x1b[0m`);
905
+ console.log('');
736
906
 
737
907
  const child = spawn(
738
- `${CLI.cmd} --resume ${session.sessionId}`,
908
+ `${CLI.cmd} --resume ${session.sessionId}${modeFlag}`,
739
909
  { stdio: 'inherit', cwd: session.cwd || process.cwd(), shell: true },
740
910
  );
741
911
  child.on('error', (err) => {
742
912
  console.error(`\x1b[31mFailed to resume: ${err.message}\x1b[0m`);
743
- console.log(`\x1b[33mManual: ${label} --resume ${session.sessionId}\x1b[0m`);
913
+ console.log(`\x1b[33mManual: ${label} --resume ${session.sessionId}${modeFlag}\x1b[0m`);
744
914
  process.exit(1);
745
915
  });
746
916
  child.on('exit', (code) => process.exit(code || 0));
747
917
  }
748
918
 
749
919
  function startNewSession() {
920
+ process.stdout.write('\x1b[0m');
750
921
  screen.destroy();
751
922
 
752
923
  const label = CLI.name;
924
+ const mode = meta.defaultPermissionMode || '';
925
+ const modeFlag = (mode && mode !== 'default') ? ` --permission-mode ${mode}` : '';
753
926
 
754
- console.log(`\n\x1b[36m✨ Starting new conversation with ${label}\x1b[0m\n`);
927
+ console.log(`\n\x1b[36m✨ Starting new conversation with ${label}\x1b[0m`);
928
+ if (mode && mode !== 'default') console.log(`\x1b[33m Mode: ${mode}\x1b[0m`);
929
+ console.log('');
755
930
 
756
- const child = spawn(CLI.cmd, { stdio: 'inherit', cwd: process.cwd(), shell: true });
931
+ const cmd = modeFlag ? `${CLI.cmd}${modeFlag}` : CLI.cmd;
932
+ const child = spawn(cmd, { stdio: 'inherit', cwd: process.cwd(), shell: true });
757
933
  child.on('error', (err) => {
758
934
  console.error(`\x1b[31mFailed to start: ${err.message}\x1b[0m`);
759
935
  process.exit(1);
@@ -789,13 +965,222 @@ function createApp() {
789
965
  } catch (e) { /* silently fail */ }
790
966
  });
791
967
 
968
+
969
+ // ─── Permission Mode Picker ──────────────────────────────────────────────
970
+
971
+ function showResumeConfirm(session) {
972
+ // Delay to avoid the Enter key from mode picker leaking into this popup
973
+ setTimeout(() => {
974
+ const mode = getEffectivePermissionMode(meta, session);
975
+ const modeLabel = (mode && mode !== 'default') ? `{#bb9af7-fg}${mode}{/}` : '{#565f89-fg}default{/}';
976
+ const confirmPopup = blessed.box({
977
+ parent: screen, top: 'center', left: 'center',
978
+ width: 44, height: 7,
979
+ label: ' {bold}{#9ece6a-fg}Resume?{/} ',
980
+ tags: true, border: { type: 'line' },
981
+ style: {
982
+ border: { fg: '#9ece6a' }, bg: '#24283b', fg: '#a9b1d6',
983
+ label: { fg: '#9ece6a' },
984
+ },
985
+ content: `\n Mode: ${modeLabel}\n\n {#9ece6a-fg}{bold}Enter{/}{#a9b1d6-fg} Resume {/}{#565f89-fg}Esc{/}{#a9b1d6-fg} Cancel{/}`,
986
+ });
987
+ popupOpen = true;
988
+ confirmPopup.focus();
989
+ screen.render();
990
+
991
+ confirmPopup.key(['enter', 'return'], () => {
992
+ confirmPopup.destroy();
993
+ popupOpen = false;
994
+ resumeSession(session);
995
+ });
996
+ confirmPopup.key(['escape', 'q'], () => {
997
+ confirmPopup.destroy();
998
+ popupOpen = false;
999
+ renderAll();
1000
+ });
1001
+ }, 50);
1002
+ }
1003
+
1004
+ function showPermissionModePicker(session) {
1005
+ const currentSessionMode = (meta.sessions[session.sessionId] && meta.sessions[session.sessionId].permissionMode) || '';
1006
+ const currentGlobalMode = meta.defaultPermissionMode || '';
1007
+ const effectiveMode = getEffectivePermissionMode(meta, session);
1008
+
1009
+ const items = [
1010
+ ' {#bb9af7-fg}{bold}── Session Override ──{/}',
1011
+ ...PERMISSION_MODES.map(m => {
1012
+ const checked = currentSessionMode === m ? '{#9ece6a-fg}✓{/}' : ' ';
1013
+ const label = m === 'default' ? 'default (none)' : m;
1014
+ return ` ${checked} {#a9b1d6-fg}${label}{/}`;
1015
+ }),
1016
+ ' {#7aa2f7-fg}{bold}Clear session override{/}',
1017
+ '',
1018
+ ' {#bb9af7-fg}{bold}── Global Default ──{/}',
1019
+ ...PERMISSION_MODES.map(m => {
1020
+ const checked = currentGlobalMode === m ? '{#9ece6a-fg}✓{/}' : ' ';
1021
+ const label = m === 'default' ? 'default (none)' : m;
1022
+ return ` ${checked} {#a9b1d6-fg}${label}{/}`;
1023
+ }),
1024
+ ' {#7aa2f7-fg}{bold}Clear global default{/}',
1025
+ ];
1026
+
1027
+ const popup = blessed.list({
1028
+ parent: screen, top: 'center', left: 'center',
1029
+ width: 42,
1030
+ height: Math.min(items.length + 4, 24),
1031
+ label: ' {bold}{#bb9af7-fg}Permission Mode{/} ',
1032
+ tags: true, border: { type: 'line' },
1033
+ style: {
1034
+ border: { fg: '#bb9af7' }, bg: '#24283b', fg: '#a9b1d6',
1035
+ selected: { bg: '#3d59a1', fg: 'white', bold: true },
1036
+ label: { fg: '#bb9af7' },
1037
+ },
1038
+ items: items, keys: true, vi: true, mouse: true,
1039
+ });
1040
+ popupOpen = true;
1041
+ popup.focus(); screen.render();
1042
+
1043
+ // Section header indices (0-indexed)
1044
+ const sessionHeaderIdx = 0;
1045
+ const sessionClearIdx = PERMISSION_MODES.length + 1;
1046
+ const spacerIdx = sessionClearIdx + 1;
1047
+ const globalHeaderIdx = spacerIdx + 1;
1048
+ const globalClearIdx = globalHeaderIdx + PERMISSION_MODES.length + 1;
1049
+
1050
+ popup.on('select', (item, index) => {
1051
+ // Skip headers and spacer
1052
+ if (index === sessionHeaderIdx || index === globalHeaderIdx || index === spacerIdx) return;
1053
+
1054
+ if (index === sessionClearIdx) {
1055
+ // Clear session override
1056
+ setSessionPermissionMode(meta, session.sessionId, '');
1057
+ popup.destroy(); popupOpen = false; renderAll();
1058
+ showResumeConfirm(session);
1059
+ return;
1060
+ }
1061
+
1062
+ if (index === globalClearIdx) {
1063
+ // Clear global default
1064
+ setGlobalPermissionMode(meta, '');
1065
+ footer.setContent(`\n {#9ece6a-fg}{bold}> Global default mode cleared{/}`);
1066
+ popup.destroy(); popupOpen = false; renderAll();
1067
+ setTimeout(() => { updateFooter(); screen.render(); }, 1500);
1068
+ return;
1069
+ }
1070
+
1071
+ // Session mode selection (indices 1 to PERMISSION_MODES.length)
1072
+ if (index > sessionHeaderIdx && index <= sessionClearIdx - 1) {
1073
+ const mode = PERMISSION_MODES[index - 1];
1074
+ setSessionPermissionMode(meta, session.sessionId, mode === 'default' ? '' : mode);
1075
+ popup.destroy(); popupOpen = false; renderAll();
1076
+ showResumeConfirm(session);
1077
+ return;
1078
+ }
1079
+
1080
+ // Global mode selection
1081
+ if (index > globalHeaderIdx && index <= globalClearIdx - 1) {
1082
+ const mode = PERMISSION_MODES[index - globalHeaderIdx - 1];
1083
+ setGlobalPermissionMode(meta, mode === 'default' ? '' : mode);
1084
+ footer.setContent(`\n {#9ece6a-fg}{bold}> Global default:{/} {#bb9af7-fg}${mode}{/}`);
1085
+ popup.destroy(); popupOpen = false; renderAll();
1086
+ setTimeout(() => { updateFooter(); screen.render(); }, 1500);
1087
+ return;
1088
+ }
1089
+ });
1090
+
1091
+ popup.key(['escape', 'q'], () => {
1092
+ popup.destroy();
1093
+ popupOpen = false;
1094
+ renderAll();
1095
+ });
1096
+ }
1097
+
1098
+ // ─── Quick dangerous resume (d key) ────────────────────────────────────
1099
+ screen.key(['d'], () => {
1100
+ if (isSearchMode || popupOpen) return;
1101
+ if (selectedIndex < 0 || selectedIndex >= filteredSessions.length) return;
1102
+ resumeSession(filteredSessions[selectedIndex], 'bypassPermissions');
1103
+ });
1104
+
1105
+ // ─── Permission mode picker (m key) ───────────────────────────────────
1106
+ screen.key(['m'], () => {
1107
+ if (isSearchMode || popupOpen) return;
1108
+ if (selectedIndex < 0 || selectedIndex >= filteredSessions.length) return;
1109
+ showPermissionModePicker(filteredSessions[selectedIndex]);
1110
+ });
1111
+
1112
+ // ─── Delete Session ───────────────────────────────────────────────────
1113
+ function deleteSession(session) {
1114
+ try {
1115
+ // Delete the .jsonl file
1116
+ if (fs.existsSync(session.filePath)) {
1117
+ fs.unlinkSync(session.filePath);
1118
+ }
1119
+ // Clean up meta entry
1120
+ if (meta.sessions[session.sessionId]) {
1121
+ delete meta.sessions[session.sessionId];
1122
+ saveMeta(meta);
1123
+ }
1124
+ // Remove from in-memory arrays
1125
+ const allIdx = allSessions.indexOf(session);
1126
+ if (allIdx !== -1) allSessions.splice(allIdx, 1);
1127
+ const filtIdx = filteredSessions.indexOf(session);
1128
+ if (filtIdx !== -1) filteredSessions.splice(filtIdx, 1);
1129
+ // Adjust selection
1130
+ if (selectedIndex >= filteredSessions.length) {
1131
+ selectedIndex = Math.max(-1, filteredSessions.length - 1);
1132
+ }
1133
+ } catch (e) { /* silently fail */ }
1134
+ }
1135
+
1136
+ function showDeleteConfirm(session) {
1137
+ const topic = (session.customTitle || session.topic || '').substring(0, 30);
1138
+ const confirmPopup = blessed.box({
1139
+ parent: screen, top: 'center', left: 'center',
1140
+ width: 50, height: 9,
1141
+ label: ' {bold}{#f7768e-fg}Delete Session?{/} ',
1142
+ tags: true, border: { type: 'line' },
1143
+ style: {
1144
+ border: { fg: '#f7768e' }, bg: '#24283b', fg: '#a9b1d6',
1145
+ label: { fg: '#f7768e' },
1146
+ },
1147
+ content:
1148
+ `\n {#a9b1d6-fg}${esc(topic)}{/}\n`
1149
+ + ` {#565f89-fg}${session.sessionId}{/}\n\n`
1150
+ + ` {#f7768e-fg}{bold}y{/}{#a9b1d6-fg} Delete {/}{#565f89-fg}n / Esc{/}{#a9b1d6-fg} Cancel{/}`,
1151
+ });
1152
+ popupOpen = true;
1153
+ confirmPopup.focus();
1154
+ screen.render();
1155
+
1156
+ confirmPopup.key(['y'], () => {
1157
+ confirmPopup.destroy();
1158
+ popupOpen = false;
1159
+ deleteSession(session);
1160
+ footer.setContent(`\n {#f7768e-fg}{bold}✗ Deleted:{/} {#565f89-fg}${session.sessionId}{/}`);
1161
+ renderAll();
1162
+ setTimeout(() => { updateFooter(); screen.render(); }, 1500);
1163
+ });
1164
+ confirmPopup.key(['n', 'escape', 'q'], () => {
1165
+ confirmPopup.destroy();
1166
+ popupOpen = false;
1167
+ screen.render();
1168
+ });
1169
+ }
1170
+
1171
+ screen.key(['x', 'delete'], () => {
1172
+ if (isSearchMode || popupOpen) return;
1173
+ if (selectedIndex < 0 || selectedIndex >= filteredSessions.length) return;
1174
+ showDeleteConfirm(filteredSessions[selectedIndex]);
1175
+ });
1176
+
792
1177
  screen.key(['s'], () => { if (!isSearchMode) cycleSort(); });
793
1178
  screen.key(['p'], () => { if (!isSearchMode) showProjectPicker(); });
794
1179
  screen.key(['escape'], () => {
795
1180
  if (isSearchMode) { isSearchMode = false; filterText = ''; applyFilter(); return; }
796
1181
  filterText = ''; selectedIndex = -1; applyFilter();
797
1182
  });
798
- screen.key(['q', 'C-c'], () => { screen.destroy(); process.exit(0); });
1183
+ screen.key(['q', 'C-c'], () => { process.stdout.write('\x1b[0m'); screen.destroy(); process.exit(0); });
799
1184
 
800
1185
  // Remove blessed's built-in wheel handlers (they call select which changes selection)
801
1186
  listPanel.removeAllListeners('element wheeldown');
@@ -844,25 +1229,76 @@ function createApp() {
844
1229
 
845
1230
  // ─── Entry Point ─────────────────────────────────────────────────────────────
846
1231
 
1232
+ const PKG = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf-8'));
1233
+
847
1234
  const args = process.argv.slice(2);
848
1235
 
1236
+ if (args.includes('--version') || args.includes('-v') || args.includes('-V')) {
1237
+ console.log(`claude-starter v${PKG.version}`);
1238
+ process.exit(0);
1239
+ }
1240
+
1241
+ if (args.includes('--update') || args.includes('-u')) {
1242
+ const C = {
1243
+ reset: '\x1b[0m', dim: '\x1b[2m', bold: '\x1b[1m',
1244
+ cyan: '\x1b[36m', yellow: '\x1b[33m', green: '\x1b[32m',
1245
+ red: '\x1b[31m',
1246
+ };
1247
+ console.log(`\n${C.cyan}🔄 Checking for updates…${C.reset}\n`);
1248
+
1249
+ try {
1250
+ const latest = execSync('npm view claude-starter version 2>/dev/null', {
1251
+ stdio: ['pipe', 'pipe', 'pipe'],
1252
+ timeout: 10000,
1253
+ }).toString().trim();
1254
+
1255
+ if (latest === PKG.version) {
1256
+ console.log(`${C.green}✓ Already on the latest version (v${PKG.version})${C.reset}\n`);
1257
+ process.exit(0);
1258
+ }
1259
+
1260
+ console.log(`${C.yellow} Current: v${PKG.version}${C.reset}`);
1261
+ console.log(`${C.green} Latest: v${latest}${C.reset}\n`);
1262
+ console.log(`${C.cyan}📦 Updating…${C.reset}\n`);
1263
+
1264
+ try {
1265
+ execSync('npm install -g claude-starter@latest', { stdio: 'inherit', timeout: 60000 });
1266
+ console.log(`\n${C.green}${C.bold}✓ Updated to v${latest}${C.reset}\n`);
1267
+ } catch (e) {
1268
+ console.error(`\n${C.red}✗ Update failed. Try manually:${C.reset}`);
1269
+ console.log(`${C.yellow} npm install -g claude-starter@latest${C.reset}\n`);
1270
+ process.exit(1);
1271
+ }
1272
+ } catch (e) {
1273
+ console.error(`${C.red}✗ Could not check for updates (network error or npm not found)${C.reset}\n`);
1274
+ process.exit(1);
1275
+ }
1276
+
1277
+ process.exit(0);
1278
+ }
1279
+
849
1280
  if (args.includes('--help') || args.includes('-h')) {
850
1281
  console.log(`
851
- \x1b[36m🚀 Claude Starter\x1b[0m
1282
+ \x1b[36m🚀 Claude Starter\x1b[0m \x1b[2mv${PKG.version}\x1b[0m
852
1283
 
853
1284
  Usage:
854
- claude-starter Launch interactive TUI
855
- claude-starter --list [N] Print latest N sessions (default: 30)
856
- claude-starter --help Show this help
1285
+ claude-starter Launch interactive TUI
1286
+ claude-starter --list [N] Print latest N sessions (default: 30)
1287
+ claude-starter --version Show version
1288
+ claude-starter --update Update to the latest version
1289
+ claude-starter --help Show this help
857
1290
 
858
1291
  TUI Keyboard Shortcuts:
859
1292
  ↑/↓ Navigate sessions
860
1293
  Enter Start new / resume selected session
861
1294
  n Start new session
1295
+ d Resume with bypassPermissions (danger mode)
1296
+ m Permission mode picker
862
1297
  / Search (fuzzy filter)
863
1298
  p Filter by project
864
- s Cycle sort mode
1299
+ s Cycle sort mode (time/size/messages/project)
865
1300
  c Copy session ID
1301
+ x / Delete Delete selected session
866
1302
  Home / End Jump to top / bottom
867
1303
  Ctrl-D/U Page down / up
868
1304
  Esc Clear filter
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-starter",
3
- "version": "1.1.1",
3
+ "version": "1.3.0",
4
4
  "description": "A beautiful terminal UI for managing Claude Code sessions — start new or resume past conversations",
5
5
  "main": "index.js",
6
6
  "bin": {