claudecode-history-viewer 1.0.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 wonderomg
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,125 @@
1
+ <div align="center">
2
+
3
+ # claude-history-viewer
4
+
5
+ **A lightweight, local web viewer for Claude Code and Cursor conversation history.**
6
+
7
+ Browse, search, and export sessions from `~/.claude` and `~/.cursor` — **100% offline**, no cloud, no telemetry.
8
+
9
+ [![Stars](https://img.shields.io/github/stars/wonderomg/claude-history-viewer?style=flat&color=yellow)](https://github.com/wonderomg/claude-history-viewer/stargazers)
10
+ ![Node](https://img.shields.io/badge/Node.js-18%2B-339933?logo=node.js&logoColor=white)
11
+ ![License](https://img.shields.io/badge/License-MIT-blue.svg)
12
+
13
+ **Languages**: [中文 (简体)](README.md) | [English](README.en-US.md)
14
+
15
+ </div>
16
+
17
+ <p align="center">
18
+ <img width="80%" alt="History" src="https://raw.githubusercontent.com/wonderomg/claude-history-viewer/master/claude-history-viewer.png" />
19
+ </p>
20
+
21
+ ## Quick Start
22
+
23
+ Requires **Node.js 18+**. Optional: existing `~/.claude` / `~/.cursor` history on your machine.
24
+
25
+ ```bash
26
+ git clone https://github.com/wonderomg/claude-history-viewer.git
27
+ cd claude-history-viewer
28
+ npm install
29
+ npm run build && npm start
30
+ ```
31
+
32
+ Open **http://localhost:3747**
33
+
34
+ Auto-open on start; disable: `NO_OPEN_BROWSER=1 npm start`.
35
+
36
+ For development (hot reload): `npm run dev` → http://localhost:5173
37
+
38
+ ---
39
+
40
+ ## Disclaimer
41
+
42
+ Independent open-source project, **not affiliated with Anthropic or Cursor**. Trademark names belong to their owners. Read-only access to local history files only.
43
+
44
+ ---
45
+
46
+ ## Features
47
+
48
+ | Feature | Description |
49
+ |---------|-------------|
50
+ | Dual source | Sidebar **All / Claude Code / Cursor**; search follows source |
51
+ | Sessions | Filter by project, expandable sub-agents |
52
+ | Chat UI | User / Markdown / Thinking / tool calls & results |
53
+ | Search | Global + in-session (highlight, prev/next, Enter to jump) |
54
+ | Extras | Raw JSONL, Markdown export, light/dark theme, EN/中文 UI |
55
+
56
+ ---
57
+
58
+ ## Data paths
59
+
60
+ | Source | Path | Content |
61
+ |--------|------|---------|
62
+ | Claude Code | `~/.claude/sessions/*.json` | Session metadata |
63
+ | Claude Code | `~/.claude/projects/{slug}/{sessionId}.jsonl` | Main conversation |
64
+ | Claude Code | `.../{sessionId}/subagents/*.jsonl` | Sub-agent |
65
+ | Cursor | `~/.cursor/projects/{slug}/agent-transcripts/{id}/{id}.jsonl` | Agent transcript |
66
+ | Cursor | `.../subagents/*.jsonl` | Sub-agent |
67
+
68
+ Project slugs like `-Users-you-code-project` are decoded to readable paths in the UI.
69
+
70
+ ---
71
+
72
+ ## Configuration
73
+
74
+ | Variable | Default | Description |
75
+ |----------|---------|-------------|
76
+ | `HOST` | `127.0.0.1` | API bind address |
77
+ | `PORT` | `3747` | API / production static server |
78
+ | `VITE_PORT` | `5173` | Vite dev port |
79
+ | `NO_OPEN_BROWSER` | — | `1` to skip opening browser |
80
+
81
+ ---
82
+
83
+ ## Local API
84
+
85
+ `GET /api/health` · `GET /api/sessions?source=` · `GET /api/sessions/:id` · `GET /api/sessions/:id/search?q=` · `GET /api/sessions/:id/raw` · `GET /api/sessions/:id/export` · `GET /api/search?q=&source=`
86
+
87
+ ---
88
+
89
+ ## Privacy & security
90
+
91
+ - Reads `~/.claude` and `~/.cursor` only; nothing uploaded
92
+ - Chats may contain secrets; the viewer shows them as-is
93
+ - No auth; bound to localhost by default — do not expose to untrusted networks
94
+
95
+ ---
96
+
97
+ ## FAQ
98
+
99
+ | Issue | Fix |
100
+ |-------|-----|
101
+ | Cannot connect to backend | Run `npm run build && npm start`; ensure port `3747` is free |
102
+ | Empty session list | Ensure history dirs exist and tools have written data |
103
+ | Search/highlight off | Press **Enter** in in-session search, or use ◀ ▶ after render |
104
+
105
+ ---
106
+
107
+ ## Limitations
108
+
109
+ Claude Code and Cursor only; web UI; no token analytics, live file watch, or session edit/delete.
110
+
111
+ ---
112
+
113
+ ## License
114
+
115
+ [MIT License](LICENSE)
116
+
117
+ ---
118
+
119
+ <div align="center">
120
+
121
+ If this project helps you, please give it a star!
122
+
123
+ [![Star History Chart](https://api.star-history.com/svg?repos=wonderomg/claude-history-viewer&type=Date)](https://star-history.com/#wonderomg/claude-history-viewer&Date)
124
+
125
+ </div>
package/README.md ADDED
@@ -0,0 +1,125 @@
1
+ <div align="center">
2
+
3
+ # claude-history-viewer
4
+
5
+ **轻量级本地 Web 查看器,用于浏览 Claude Code 与 Cursor 会话历史。**
6
+
7
+ 从 `~/.claude` 与 `~/.cursor` 读取对话记录,支持浏览、搜索与导出 — **完全离线**,无云端、无遥测。
8
+
9
+ [![Stars](https://img.shields.io/github/stars/wonderomg/claude-history-viewer?style=flat&color=yellow)](https://github.com/wonderomg/claude-history-viewer/stargazers)
10
+ ![Node](https://img.shields.io/badge/Node.js-18%2B-339933?logo=node.js&logoColor=white)
11
+ ![License](https://img.shields.io/badge/License-MIT-blue.svg)
12
+
13
+ **语言**: [中文 (简体)](README.md) | [English](README.en-US.md)
14
+
15
+ </div>
16
+
17
+ <p align="center">
18
+ <img width="80%" alt="History" src="https://raw.githubusercontent.com/wonderomg/claude-history-viewer/master/claude-history-viewer.png" />
19
+ </p>
20
+
21
+ ## 快速开始
22
+
23
+ 需要 **Node.js 18+**。本机可选已有 `~/.claude` / `~/.cursor` 历史数据。
24
+
25
+ ```bash
26
+ git clone https://github.com/wonderomg/claude-history-viewer.git
27
+ cd claude-history-viewer
28
+ npm install
29
+ npm run build && npm start
30
+ ```
31
+
32
+ 浏览器访问 **http://localhost:3747**
33
+
34
+ 启动后会尝试自动打开;禁用:`NO_OPEN_BROWSER=1 npm start`。
35
+
36
+ 开发调试(热更新):`npm run dev` → http://localhost:5173
37
+
38
+ ---
39
+
40
+ ## 免责声明
41
+
42
+ 独立开源项目,与 **Anthropic**、**Cursor** 无官方关系;相关名称为各自商标。仅只读本机历史文件。
43
+
44
+ ---
45
+
46
+ ## 功能
47
+
48
+ | 功能 | 说明 |
49
+ |------|------|
50
+ | 双来源 | 侧栏 **全部 / Claude Code / Cursor**,搜索随来源过滤 |
51
+ | 会话列表 | 按项目筛选、Sub-agent 树形展开 |
52
+ | 对话渲染 | 用户 / Markdown / Thinking / Tool Call & Result |
53
+ | 搜索 | 跨会话 + 会话内(高亮、`上一处/下一处`、回车定位) |
54
+ | 其他 | 原始 JSONL、导出 Markdown、深浅主题、中/英文界面 |
55
+
56
+ ---
57
+
58
+ ## 数据路径
59
+
60
+ | 来源 | 路径 | 内容 |
61
+ |------|------|------|
62
+ | Claude Code | `~/.claude/sessions/*.json` | 会话元数据 |
63
+ | Claude Code | `~/.claude/projects/{slug}/{sessionId}.jsonl` | 主对话 |
64
+ | Claude Code | `.../{sessionId}/subagents/*.jsonl` | Sub-agent |
65
+ | Cursor | `~/.cursor/projects/{slug}/agent-transcripts/{id}/{id}.jsonl` | Agent 对话 |
66
+ | Cursor | `.../subagents/*.jsonl` | Sub-agent |
67
+
68
+ 项目 slug(如 `-Users-you-code-project`)会在界面中还原为可读路径。
69
+
70
+ ---
71
+
72
+ ## 配置
73
+
74
+ | 变量 | 默认 | 说明 |
75
+ |------|------|------|
76
+ | `HOST` | `127.0.0.1` | API 监听地址 |
77
+ | `PORT` | `3747` | API / 生产静态服务端口 |
78
+ | `VITE_PORT` | `5173` | Vite 开发端口 |
79
+ | `NO_OPEN_BROWSER` | — | `1` 禁用自动打开浏览器 |
80
+
81
+ ---
82
+
83
+ ## 本地 API
84
+
85
+ `GET /api/health` · `GET /api/sessions?source=` · `GET /api/sessions/:id` · `GET /api/sessions/:id/search?q=` · `GET /api/sessions/:id/raw` · `GET /api/sessions/:id/export` · `GET /api/search?q=&source=`
86
+
87
+ ---
88
+
89
+ ## 隐私与安全
90
+
91
+ - 仅读取 `~/.claude`、`~/.cursor`,不上传云端
92
+ - 对话中可能含密钥与内部信息,界面会如实展示
93
+ - API 无鉴权,默认仅本机;勿暴露到不可信网络
94
+
95
+ ---
96
+
97
+ ## 常见问题
98
+
99
+ | 问题 | 处理 |
100
+ |------|------|
101
+ | 无法连接后端 | 确认已执行 `npm run build && npm start`,端口 `3747` 未被占用 |
102
+ | 列表为空 | 确认对应目录存在且已有工具产生的历史 |
103
+ | 搜索/高亮不准 | 会话内搜索按 **回车** 或等渲染后用 ◀ ▶ |
104
+
105
+ ---
106
+
107
+ ## 局限性
108
+
109
+ 仅支持 Claude Code 与 Cursor;Web 界面;无 Token 统计、实时监听、会话增删改。
110
+
111
+ ---
112
+
113
+ ## 许可证
114
+
115
+ [MIT License](LICENSE)
116
+
117
+ ---
118
+
119
+ <div align="center">
120
+
121
+ 如果这个项目对您有帮助,请给它一个星标!
122
+
123
+ [![Star History Chart](https://api.star-history.com/svg?repos=wonderomg/claude-history-viewer&type=Date)](https://star-history.com/#wonderomg/claude-history-viewer&Date)
124
+
125
+ </div>
package/SECURITY.md ADDED
@@ -0,0 +1,23 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ Security fixes are provided for the latest release on the default branch.
6
+
7
+ ## Reporting a Vulnerability
8
+
9
+ Please **do not** open a public GitHub issue for security vulnerabilities.
10
+
11
+ Report security issues via [GitHub Security Advisories](https://github.com/wonderomg/claude-history-viewer/security/advisories/new) or by opening a private report through the repository maintainer.
12
+
13
+ Include:
14
+
15
+ - A description of the issue and potential impact
16
+ - Steps to reproduce
17
+ - Your environment (OS, Node.js version)
18
+
19
+ We aim to acknowledge reports within a reasonable timeframe.
20
+
21
+ ## Scope Notes
22
+
23
+ This application reads local conversation files under `~/.claude` and `~/.cursor`. It is intended for **local use only**. Do not expose the API port to untrusted networks without authentication and a reverse proxy.
Binary file
package/index.html ADDED
@@ -0,0 +1,27 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN" class="dark">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>claude-history-viewer</title>
7
+ <script>
8
+ (function () {
9
+ var theme = localStorage.getItem('claude-history-viewer-theme');
10
+ if (theme === 'light' || theme === 'dark') {
11
+ document.documentElement.classList.remove('dark', 'light');
12
+ document.documentElement.classList.add(theme);
13
+ document.documentElement.dataset.theme = theme;
14
+ }
15
+ var locale = localStorage.getItem('claude-history-viewer-locale');
16
+ if (locale === 'zh' || locale === 'en') {
17
+ document.documentElement.lang = locale === 'zh' ? 'zh-CN' : 'en';
18
+ document.documentElement.dataset.locale = locale;
19
+ }
20
+ })();
21
+ </script>
22
+ </head>
23
+ <body>
24
+ <div id="app"></div>
25
+ <script type="module" src="/src/main.js"></script>
26
+ </body>
27
+ </html>
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "claudecode-history-viewer",
3
+ "version": "1.0.0",
4
+ "description": "Local web viewer for Claude Code and Cursor conversation history",
5
+ "license": "MIT",
6
+ "author": "wonderomg",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/wonderomg/claude-history-viewer.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/wonderomg/claude-history-viewer/issues"
13
+ },
14
+ "homepage": "https://github.com/wonderomg/claude-history-viewer#readme",
15
+ "keywords": [
16
+ "claude",
17
+ "cursor",
18
+ "conversation-history",
19
+ "jsonl",
20
+ "local",
21
+ "viewer"
22
+ ],
23
+ "type": "module",
24
+ "scripts": {
25
+ "dev": "concurrently -n server,web -c blue,green \"node server/index.js\" \"vite\"",
26
+ "build": "vite build",
27
+ "preview": "vite preview",
28
+ "start": "NODE_ENV=production node server/index.js"
29
+ },
30
+ "dependencies": {
31
+ "express": "^4.21.2",
32
+ "highlight.js": "^11.11.1",
33
+ "markdown-it": "^14.1.0",
34
+ "vue": "^3.5.13"
35
+ },
36
+ "devDependencies": {
37
+ "@vitejs/plugin-vue": "^5.2.1",
38
+ "autoprefixer": "^10.4.20",
39
+ "concurrently": "^9.1.2",
40
+ "postcss": "^8.4.49",
41
+ "tailwindcss": "^3.4.17",
42
+ "vite": "^6.0.7"
43
+ }
44
+ }
@@ -0,0 +1,6 @@
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * 将解析后的消息列表导出为 Markdown
3
+ */
4
+
5
+ export function messagesToMarkdown(session, meta, messages) {
6
+ const lines = []
7
+ const title = meta?.title || session?.title || 'Claude Code Session'
8
+ lines.push(`# ${title}`)
9
+ lines.push('')
10
+ lines.push(`- **Session ID**: \`${session?.id || ''}\``)
11
+ if (session?.parentSessionId) {
12
+ lines.push(`- **类型**: Sub-agent(父会话 \`${session.parentSessionId}\`)`)
13
+ }
14
+ if (session?.agentType) lines.push(`- **Agent 类型**: ${session.agentType}`)
15
+ if (session?.agentDescription) lines.push(`- **描述**: ${session.agentDescription}`)
16
+ lines.push(`- **项目**: \`${session?.projectPath || ''}\``)
17
+ lines.push(`- **工作目录**: \`${session?.cwd || ''}\``)
18
+ if (session?.updatedAt) {
19
+ lines.push(`- **更新时间**: ${new Date(session.updatedAt).toLocaleString('zh-CN')}`)
20
+ }
21
+ lines.push('')
22
+ lines.push('---')
23
+ lines.push('')
24
+
25
+ for (const msg of messages) {
26
+ if (msg.role === 'user') {
27
+ lines.push('## User')
28
+ lines.push('')
29
+ lines.push(msg.text || '')
30
+ lines.push('')
31
+ } else if (msg.role === 'assistant') {
32
+ lines.push('## Assistant')
33
+ if (msg.model) lines.push(`*model: ${msg.model}*`)
34
+ lines.push('')
35
+ if (msg.thinking) {
36
+ lines.push('<details>')
37
+ lines.push('<summary>Thinking</summary>')
38
+ lines.push('')
39
+ lines.push(msg.thinking)
40
+ lines.push('')
41
+ lines.push('</details>')
42
+ lines.push('')
43
+ }
44
+ if (msg.toolUses?.length) {
45
+ for (const t of msg.toolUses) {
46
+ lines.push(`### Tool: \`${t.name}\``)
47
+ lines.push('')
48
+ lines.push('```json')
49
+ lines.push(JSON.stringify(t.input, null, 2))
50
+ lines.push('```')
51
+ lines.push('')
52
+ }
53
+ }
54
+ if (msg.text) {
55
+ lines.push(msg.text)
56
+ lines.push('')
57
+ }
58
+ } else if (msg.role === 'tool_result') {
59
+ const label = msg.isError ? 'Tool Error' : 'Tool Result'
60
+ lines.push(`## ${label}`)
61
+ lines.push('')
62
+ lines.push('```')
63
+ lines.push(msg.content || '')
64
+ lines.push('```')
65
+ lines.push('')
66
+ } else if (msg.role === 'system' && msg.subtype === 'turn_duration') {
67
+ lines.push(`*— 本轮耗时 ${Math.round((msg.durationMs || 0) / 1000)}s —*`)
68
+ lines.push('')
69
+ }
70
+ }
71
+
72
+ return lines.join('\n')
73
+ }
@@ -0,0 +1,190 @@
1
+ import express from 'express'
2
+ import path from 'path'
3
+ import { fileURLToPath } from 'url'
4
+ import {
5
+ scanAllSessions,
6
+ findSessionById,
7
+ readRawTranscript,
8
+ getClaudeDir,
9
+ decodeProjectSlug,
10
+ } from './scanner.js'
11
+ import { getCursorDir } from './scanner-cursor.js'
12
+ import { parseTranscript, searchInTranscript } from './parser.js'
13
+ import { parseCursorTranscript, searchInCursorTranscript } from './parser-cursor.js'
14
+ import { messagesToMarkdown } from './export.js'
15
+ import { openBrowser } from './open-browser.js'
16
+
17
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
18
+ const PORT = process.env.PORT || 3747
19
+ const HOST = process.env.HOST || '127.0.0.1'
20
+ const isProd = process.env.NODE_ENV === 'production'
21
+
22
+ const app = express()
23
+ app.use(express.json())
24
+
25
+ function parseSessionFile(session) {
26
+ if (session.source === 'cursor') {
27
+ return parseCursorTranscript(session.filePath)
28
+ }
29
+ return parseTranscript(session.filePath)
30
+ }
31
+
32
+ function searchSessionFile(session, query) {
33
+ if (session.source === 'cursor') {
34
+ return searchInCursorTranscript(session.filePath, query)
35
+ }
36
+ return searchInTranscript(session.filePath, query)
37
+ }
38
+
39
+ app.get('/api/health', (_req, res) => {
40
+ res.json({ ok: true, claudeDir: getClaudeDir(), cursorDir: getCursorDir() })
41
+ })
42
+
43
+ app.get('/api/sessions', (req, res) => {
44
+ try {
45
+ const source = req.query.source || 'all'
46
+ const sessions = scanAllSessions({ source })
47
+ const projects = [...new Set(sessions.map((s) => s.projectSlug))].map((slug) => ({
48
+ slug,
49
+ path: sessions.find((s) => s.projectSlug === slug)?.projectPath || slug,
50
+ count: sessions.filter((s) => s.projectSlug === slug).length,
51
+ }))
52
+ res.json({
53
+ sessions,
54
+ projects,
55
+ claudeDir: getClaudeDir(),
56
+ cursorDir: getCursorDir(),
57
+ })
58
+ } catch (err) {
59
+ console.error(err)
60
+ res.status(500).json({ error: err.message })
61
+ }
62
+ })
63
+
64
+ app.get('/api/sessions/:id', (req, res) => {
65
+ try {
66
+ const session = findSessionById(req.params.id)
67
+ if (!session) {
68
+ return res.status(404).json({ error: 'Session not found' })
69
+ }
70
+ const parsed = parseSessionFile(session)
71
+ const subagents =
72
+ session.kind === 'main'
73
+ ? scanAllSessions({ source: session.source }).filter(
74
+ (s) => s.kind === 'subagent' && s.parentSessionId === session.id
75
+ )
76
+ : []
77
+ res.json({
78
+ session,
79
+ meta: parsed.meta,
80
+ messages: parsed.messages,
81
+ subagents,
82
+ })
83
+ } catch (err) {
84
+ console.error(err)
85
+ res.status(500).json({ error: err.message })
86
+ }
87
+ })
88
+
89
+ app.get('/api/sessions/:id/raw', (req, res) => {
90
+ try {
91
+ const session = findSessionById(req.params.id)
92
+ if (!session) {
93
+ return res.status(404).json({ error: 'Session not found' })
94
+ }
95
+ const raw = readRawTranscript(session.filePath)
96
+ res.json({ session: { id: session.id, filePath: session.filePath }, ...raw })
97
+ } catch (err) {
98
+ console.error(err)
99
+ res.status(500).json({ error: err.message })
100
+ }
101
+ })
102
+
103
+ app.get('/api/sessions/:id/export', (req, res) => {
104
+ try {
105
+ const session = findSessionById(req.params.id)
106
+ if (!session) {
107
+ return res.status(404).json({ error: 'Session not found' })
108
+ }
109
+ const parsed = parseSessionFile(session)
110
+ const md = messagesToMarkdown(session, parsed.meta, parsed.messages)
111
+ const filename = `${session.title.replace(/[^\w\u4e00-\u9fa5-]+/g, '_').slice(0, 40)}.md`
112
+ res.setHeader('Content-Type', 'text/markdown; charset=utf-8')
113
+ res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(filename)}"`)
114
+ res.send(md)
115
+ } catch (err) {
116
+ console.error(err)
117
+ res.status(500).json({ error: err.message })
118
+ }
119
+ })
120
+
121
+ app.get('/api/sessions/:id/search', (req, res) => {
122
+ try {
123
+ const q = req.query.q || ''
124
+ const session = findSessionById(req.params.id)
125
+ if (!session) {
126
+ return res.status(404).json({ error: 'Session not found' })
127
+ }
128
+ const result = searchSessionFile(session, q)
129
+ res.json({ query: q, hits: result.hits, matches: result.matches, total: result.total })
130
+ } catch (err) {
131
+ console.error(err)
132
+ res.status(500).json({ error: err.message })
133
+ }
134
+ })
135
+
136
+ app.get('/api/search', (req, res) => {
137
+ try {
138
+ const q = (req.query.q || '').trim()
139
+ if (!q) return res.json({ query: q, results: [] })
140
+
141
+ const source = req.query.source || 'all'
142
+ const sessions = scanAllSessions({ source })
143
+ const results = []
144
+
145
+ for (const session of sessions) {
146
+ const result = searchSessionFile(session, q)
147
+ if (result.total > 0) {
148
+ results.push({
149
+ sessionId: session.id,
150
+ title: session.title,
151
+ projectPath: session.projectPath,
152
+ source: session.source,
153
+ kind: session.kind,
154
+ parentSessionId: session.parentSessionId,
155
+ hits: result.hits,
156
+ total: result.total,
157
+ })
158
+ }
159
+ }
160
+
161
+ res.json({ query: q, results })
162
+ } catch (err) {
163
+ console.error(err)
164
+ res.status(500).json({ error: err.message })
165
+ }
166
+ })
167
+
168
+ if (isProd) {
169
+ const dist = path.join(__dirname, '..', 'dist')
170
+ app.use(express.static(dist))
171
+ app.get('*', (_req, res) => {
172
+ res.sendFile(path.join(dist, 'index.html'))
173
+ })
174
+ }
175
+
176
+ const DEV_WEB_PORT = process.env.VITE_PORT || 5173
177
+
178
+ app.listen(PORT, HOST, () => {
179
+ const apiUrl = `http://${HOST === '0.0.0.0' ? 'localhost' : HOST}:${PORT}`
180
+ const browseUrl = isProd ? apiUrl : `http://localhost:${DEV_WEB_PORT}`
181
+
182
+ console.log(`Claude History Viewer API → ${apiUrl}`)
183
+ console.log(`Reading from ${getClaudeDir()}`)
184
+ if (!isProd) {
185
+ console.log(`Dev frontend → ${browseUrl} (proxy /api → :${PORT})`)
186
+ }
187
+
188
+ const delay = isProd ? 300 : 800
189
+ setTimeout(() => openBrowser(browseUrl), delay)
190
+ })
@@ -0,0 +1,37 @@
1
+ import { spawn } from 'child_process'
2
+
3
+ /**
4
+ * 启动后自动打开浏览器(macOS / Windows / Linux)
5
+ * 设置环境变量 NO_OPEN_BROWSER=1 可禁用
6
+ */
7
+ export function openBrowser(url) {
8
+ if (process.env.NO_OPEN_BROWSER === '1' || process.env.NO_OPEN_BROWSER === 'true') {
9
+ return
10
+ }
11
+
12
+ let command
13
+ let args
14
+
15
+ if (process.platform === 'darwin') {
16
+ command = 'open'
17
+ args = [url]
18
+ } else if (process.platform === 'win32') {
19
+ command = 'cmd'
20
+ args = ['/c', 'start', '', url]
21
+ } else {
22
+ command = 'xdg-open'
23
+ args = [url]
24
+ }
25
+
26
+ try {
27
+ const child = spawn(command, args, {
28
+ detached: true,
29
+ stdio: 'ignore',
30
+ windowsHide: true,
31
+ })
32
+ child.unref()
33
+ } catch (err) {
34
+ console.warn(`[open-browser] 无法自动打开浏览器: ${err.message}`)
35
+ console.warn(`[open-browser] 请手动访问: ${url}`)
36
+ }
37
+ }