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 +21 -0
- package/README.en-US.md +125 -0
- package/README.md +125 -0
- package/SECURITY.md +23 -0
- package/claude-history-viewer.png +0 -0
- package/index.html +27 -0
- package/package.json +44 -0
- package/postcss.config.js +6 -0
- package/server/export.js +73 -0
- package/server/index.js +190 -0
- package/server/open-browser.js +37 -0
- package/server/parser-cursor.js +125 -0
- package/server/parser.js +208 -0
- package/server/scanner-cursor.js +177 -0
- package/server/scanner.js +285 -0
- package/server/search-utils.js +93 -0
- package/src/App.vue +338 -0
- package/src/components/ChatView.vue +353 -0
- package/src/components/MarkdownRenderer.vue +63 -0
- package/src/components/MessageBubble.vue +105 -0
- package/src/components/RawJsonView.vue +116 -0
- package/src/components/SessionItem.vue +48 -0
- package/src/components/SessionSidebar.vue +202 -0
- package/src/components/SettingsMenu.vue +63 -0
- package/src/components/ThemeToggle.vue +47 -0
- package/src/components/ToolCallBlock.vue +43 -0
- package/src/components/ToolResultBlock.vue +46 -0
- package/src/composables/useLocale.js +68 -0
- package/src/composables/useTheme.js +49 -0
- package/src/i18n/messages.js +135 -0
- package/src/main.js +7 -0
- package/src/styles/main.css +151 -0
- package/src/utils/format.js +37 -0
- package/src/utils/highlight.js +135 -0
- package/src/utils/hljs-theme.js +22 -0
- package/src/utils/markdown.js +59 -0
- package/tailwind.config.js +44 -0
- package/vite.config.js +16 -0
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.
|
package/README.en-US.md
ADDED
|
@@ -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
|
+
[](https://github.com/wonderomg/claude-history-viewer/stargazers)
|
|
10
|
+

|
|
11
|
+

|
|
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
|
+
[](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
|
+
[](https://github.com/wonderomg/claude-history-viewer/stargazers)
|
|
10
|
+

|
|
11
|
+

|
|
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
|
+
[](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
|
+
}
|
package/server/export.js
ADDED
|
@@ -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
|
+
}
|
package/server/index.js
ADDED
|
@@ -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
|
+
}
|