@jhihjian/claude-daemon 1.1.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/lib/logger.ts ADDED
@@ -0,0 +1,140 @@
1
+ /**
2
+ * logger.ts
3
+ * 统一的日志系统
4
+ *
5
+ * 功能:
6
+ * - 分级日志(DEBUG、INFO、WARN、ERROR)
7
+ * - 可配置日志级别
8
+ * - 结构化日志输出
9
+ * - 性能友好(不阻塞)
10
+ */
11
+
12
+ export enum LogLevel {
13
+ DEBUG = 0,
14
+ INFO = 1,
15
+ WARN = 2,
16
+ ERROR = 3,
17
+ SILENT = 4,
18
+ }
19
+
20
+ interface LoggerOptions {
21
+ level?: LogLevel;
22
+ prefix?: string;
23
+ enableColors?: boolean;
24
+ }
25
+
26
+ class Logger {
27
+ private level: LogLevel;
28
+ private prefix: string;
29
+ private enableColors: boolean;
30
+
31
+ constructor(options: LoggerOptions = {}) {
32
+ // 从环境变量读取日志级别
33
+ const envLevel = process.env.SESSION_LOG_LEVEL?.toUpperCase();
34
+ this.level = envLevel ? LogLevel[envLevel as keyof typeof LogLevel] || LogLevel.INFO : (options.level ?? LogLevel.INFO);
35
+ this.prefix = options.prefix || '';
36
+ this.enableColors = options.enableColors ?? true;
37
+ }
38
+
39
+ private shouldLog(level: LogLevel): boolean {
40
+ return level >= this.level;
41
+ }
42
+
43
+ private formatMessage(level: string, message: string, data?: any): string {
44
+ const timestamp = new Date().toISOString();
45
+ const prefix = this.prefix ? `[${this.prefix}] ` : '';
46
+
47
+ let output = `${timestamp} ${prefix}${level}: ${message}`;
48
+
49
+ if (data !== undefined) {
50
+ if (typeof data === 'object' && data !== null) {
51
+ output += '\n' + JSON.stringify(data, null, 2);
52
+ } else {
53
+ output += ` ${data}`;
54
+ }
55
+ }
56
+
57
+ return output;
58
+ }
59
+
60
+ private colorize(text: string, colorCode: number): string {
61
+ if (!this.enableColors) return text;
62
+ return `\x1b[${colorCode}m${text}\x1b[0m`;
63
+ }
64
+
65
+ debug(message: string, data?: any): void {
66
+ if (!this.shouldLog(LogLevel.DEBUG)) return;
67
+
68
+ const formatted = this.formatMessage('DEBUG', message, data);
69
+ console.error(this.colorize(formatted, 90)); // Gray
70
+ }
71
+
72
+ info(message: string, data?: any): void {
73
+ if (!this.shouldLog(LogLevel.INFO)) return;
74
+
75
+ const formatted = this.formatMessage('INFO', message, data);
76
+ console.error(this.colorize(formatted, 36)); // Cyan
77
+ }
78
+
79
+ warn(message: string, data?: any): void {
80
+ if (!this.shouldLog(LogLevel.WARN)) return;
81
+
82
+ const formatted = this.formatMessage('WARN', message, data);
83
+ console.error(this.colorize(formatted, 33)); // Yellow
84
+ }
85
+
86
+ error(message: string, data?: any): void {
87
+ if (!this.shouldLog(LogLevel.ERROR)) return;
88
+
89
+ const formatted = this.formatMessage('ERROR', message, data);
90
+ console.error(this.colorize(formatted, 31)); // Red
91
+ }
92
+
93
+ /**
94
+ * 记录性能指标
95
+ */
96
+ perf(operation: string, startTime: number): void {
97
+ if (!this.shouldLog(LogLevel.DEBUG)) return;
98
+
99
+ const duration = Date.now() - startTime;
100
+ this.debug(`Performance: ${operation}`, { duration_ms: duration });
101
+ }
102
+
103
+ /**
104
+ * 创建子 Logger(带新前缀)
105
+ */
106
+ child(prefix: string): Logger {
107
+ const childPrefix = this.prefix ? `${this.prefix}:${prefix}` : prefix;
108
+ return new Logger({
109
+ level: this.level,
110
+ prefix: childPrefix,
111
+ enableColors: this.enableColors,
112
+ });
113
+ }
114
+
115
+ /**
116
+ * 设置日志级别
117
+ */
118
+ setLevel(level: LogLevel): void {
119
+ this.level = level;
120
+ }
121
+ }
122
+
123
+ /**
124
+ * 默认 logger 实例
125
+ */
126
+ export const logger = new Logger();
127
+
128
+ /**
129
+ * 创建带前缀的 logger
130
+ */
131
+ export function createLogger(prefix: string, options?: Omit<LoggerOptions, 'prefix'>): Logger {
132
+ return new Logger({ ...options, prefix });
133
+ }
134
+
135
+ /**
136
+ * 用于 Hook 的快速 logger 创建函数
137
+ */
138
+ export function createHookLogger(hookName: string): Logger {
139
+ return createLogger(hookName, { level: LogLevel.INFO });
140
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@jhihjian/claude-daemon",
3
+ "version": "1.1.0",
4
+ "description": "Claude Code 会话历史记录系统 - 自动记录、分类和分析 Claude Code 会话",
5
+ "keywords": [
6
+ "claude",
7
+ "claude-code",
8
+ "session-history",
9
+ "hooks",
10
+ "ai",
11
+ "typescript",
12
+ "bun"
13
+ ],
14
+ "author": "JhihJian <JhihJian@users.noreply.github.com>",
15
+ "license": "MIT",
16
+ "homepage": "https://github.com/JhihJian/claude-daemon#readme",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/JhihJian/claude-daemon.git"
20
+ },
21
+ "bugs": {
22
+ "url": "https://github.com/JhihJian/claude-daemon/issues"
23
+ },
24
+ "bin": {
25
+ "claude-daemon": "./bin/cli.js"
26
+ },
27
+ "files": [
28
+ "hooks/",
29
+ "lib/",
30
+ "tools/",
31
+ "bin/",
32
+ "install.sh",
33
+ "README.md",
34
+ "QUICKSTART.md"
35
+ ],
36
+ "scripts": {
37
+ "postinstall": "echo '\n🎉 @jhihjian/claude-daemon 安装成功!\n\n使用方法:\n npx @jhihjian/claude-daemon install\n\n或查看文档:\n https://github.com/JhihJian/claude-daemon\n'"
38
+ },
39
+ "engines": {
40
+ "node": ">=16.0.0"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ }
45
+ }
@@ -0,0 +1,262 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * SessionQuery.ts
4
+ * 会话查询工具
5
+ *
6
+ * 提供灵活的会话查询接口,支持按类型、目录、时间范围查询
7
+ */
8
+
9
+ import { readFileSync, readdirSync, existsSync } from 'fs';
10
+ import { join } from 'path';
11
+ import { homedir } from 'os';
12
+
13
+ // ============================================================================
14
+ // 类型定义
15
+ // ============================================================================
16
+
17
+ type SessionType =
18
+ | 'coding'
19
+ | 'debugging'
20
+ | 'research'
21
+ | 'writing'
22
+ | 'git'
23
+ | 'refactoring'
24
+ | 'mixed';
25
+
26
+ interface SessionSummary {
27
+ session_id: string;
28
+ timestamp: string;
29
+ working_directory: string;
30
+ git_repo: string | null;
31
+ git_branch: string | null;
32
+ hostname: string;
33
+ user: string;
34
+ platform: string;
35
+ session_type: SessionType;
36
+ duration_seconds: number;
37
+ total_tools: number;
38
+ success_rate: number;
39
+ files_modified: string[];
40
+ tool_usage: Record<string, number>;
41
+ summary_text: string;
42
+ conversation?: {
43
+ user_messages: string[];
44
+ assistant_responses: string[];
45
+ message_count: number;
46
+ };
47
+ }
48
+
49
+ interface QueryOptions {
50
+ // 按类型过滤
51
+ type?: SessionType;
52
+
53
+ // 按目录过滤
54
+ directory?: string;
55
+
56
+ // 按时间范围过滤
57
+ start_date?: string; // ISO 8601
58
+ end_date?: string;
59
+
60
+ // 限制结果数量
61
+ limit?: number;
62
+
63
+ // 排序
64
+ sort_by?: 'timestamp' | 'duration' | 'tools';
65
+ sort_order?: 'asc' | 'desc';
66
+ }
67
+
68
+ // ============================================================================
69
+ // SessionQuery 类
70
+ // ============================================================================
71
+
72
+ export class SessionQuery {
73
+ private paiDir: string;
74
+
75
+ constructor(paiDir?: string) {
76
+ this.paiDir = paiDir || process.env.PAI_DIR || join(homedir(), '.claude');
77
+ }
78
+
79
+ /**
80
+ * 按类型查询会话
81
+ */
82
+ queryByType(type: SessionType, options?: QueryOptions): SessionSummary[] {
83
+ const typeDir = join(this.paiDir, 'SESSIONS/analysis/by-type', type);
84
+ const indexFile = join(typeDir, 'sessions.json');
85
+
86
+ if (!existsSync(indexFile)) {
87
+ return [];
88
+ }
89
+
90
+ const sessions = JSON.parse(readFileSync(indexFile, 'utf-8'));
91
+ return this.applyFilters(sessions, options);
92
+ }
93
+
94
+ /**
95
+ * 按目录查询会话
96
+ */
97
+ queryByDirectory(directory: string, options?: QueryOptions): SessionSummary[] {
98
+ const dirHash = this.encodePath(directory);
99
+ const dirIndexDir = join(this.paiDir, 'SESSIONS/analysis/by-directory', dirHash);
100
+ const indexFile = join(dirIndexDir, 'sessions.json');
101
+
102
+ if (!existsSync(indexFile)) {
103
+ return [];
104
+ }
105
+
106
+ const sessions = JSON.parse(readFileSync(indexFile, 'utf-8'));
107
+ return this.applyFilters(sessions, options);
108
+ }
109
+
110
+ /**
111
+ * 按主机名查询会话
112
+ */
113
+ queryByHostname(hostname: string, options?: QueryOptions): SessionSummary[] {
114
+ const allSessions = this.getRecentSessions(1000); // 获取所有会话
115
+ const filtered = allSessions.filter(s => s.hostname === hostname);
116
+ return this.applyFilters(filtered, options);
117
+ }
118
+
119
+ /**
120
+ * 按时间范围查询会话
121
+ */
122
+ queryByTimeRange(startDate: string, endDate: string, options?: QueryOptions): SessionSummary[] {
123
+ const allSessions: SessionSummary[] = [];
124
+
125
+ // 遍历所有月份目录
126
+ const summariesDir = join(this.paiDir, 'SESSIONS/analysis/summaries');
127
+
128
+ if (!existsSync(summariesDir)) {
129
+ return [];
130
+ }
131
+
132
+ const months = readdirSync(summariesDir);
133
+
134
+ for (const month of months) {
135
+ const monthDir = join(summariesDir, month);
136
+
137
+ if (!existsSync(monthDir)) continue;
138
+
139
+ const files = readdirSync(monthDir);
140
+
141
+ for (const file of files) {
142
+ if (file.startsWith('summary-') && file.endsWith('.json')) {
143
+ const summaryPath = join(monthDir, file);
144
+ const summary = JSON.parse(readFileSync(summaryPath, 'utf-8'));
145
+
146
+ // 时间范围过滤
147
+ if (summary.timestamp >= startDate && summary.timestamp <= endDate) {
148
+ allSessions.push(summary);
149
+ }
150
+ }
151
+ }
152
+ }
153
+
154
+ return this.applyFilters(allSessions, options);
155
+ }
156
+
157
+ /**
158
+ * 获取最近的会话
159
+ */
160
+ getRecentSessions(limit: number = 10): SessionSummary[] {
161
+ const now = new Date().toISOString();
162
+ const monthAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
163
+
164
+ return this.queryByTimeRange(monthAgo, now, {
165
+ limit,
166
+ sort_by: 'timestamp',
167
+ sort_order: 'desc',
168
+ });
169
+ }
170
+
171
+ /**
172
+ * 应用过滤和排序
173
+ */
174
+ private applyFilters(
175
+ sessions: SessionSummary[],
176
+ options?: QueryOptions
177
+ ): SessionSummary[] {
178
+ let filtered = [...sessions];
179
+
180
+ // 时间范围过滤
181
+ if (options?.start_date) {
182
+ filtered = filtered.filter(s => s.timestamp >= options.start_date!);
183
+ }
184
+ if (options?.end_date) {
185
+ filtered = filtered.filter(s => s.timestamp <= options.end_date!);
186
+ }
187
+
188
+ // 排序
189
+ if (options?.sort_by) {
190
+ filtered.sort((a, b) => {
191
+ const aVal = a[options.sort_by!] as any;
192
+ const bVal = b[options.sort_by!] as any;
193
+ const order = options.sort_order === 'asc' ? 1 : -1;
194
+ return (aVal > bVal ? 1 : -1) * order;
195
+ });
196
+ }
197
+
198
+ // 限制数量
199
+ if (options?.limit) {
200
+ filtered = filtered.slice(0, options.limit);
201
+ }
202
+
203
+ return filtered;
204
+ }
205
+
206
+ /**
207
+ * 编码路径为 Base64
208
+ */
209
+ private encodePath(path: string): string {
210
+ return Buffer.from(path)
211
+ .toString('base64')
212
+ .replace(/\//g, '_')
213
+ .replace(/\+/g, '-')
214
+ .replace(/=/g, '');
215
+ }
216
+ }
217
+
218
+ // ============================================================================
219
+ // 导出单例
220
+ // ============================================================================
221
+
222
+ export const sessionQuery = new SessionQuery();
223
+
224
+ // ============================================================================
225
+ // CLI 入口(如果直接运行)
226
+ // ============================================================================
227
+
228
+ if (import.meta.main) {
229
+ const command = process.argv[2];
230
+ const arg = process.argv[3];
231
+
232
+ switch (command) {
233
+ case 'type':
234
+ const sessions = sessionQuery.queryByType(arg as SessionType, { limit: 20 });
235
+ console.log(JSON.stringify(sessions, null, 2));
236
+ break;
237
+
238
+ case 'dir':
239
+ const dirSessions = sessionQuery.queryByDirectory(arg);
240
+ console.log(JSON.stringify(dirSessions, null, 2));
241
+ break;
242
+
243
+ case 'host':
244
+ case 'hostname':
245
+ const hostSessions = sessionQuery.queryByHostname(arg, { limit: 20 });
246
+ console.log(JSON.stringify(hostSessions, null, 2));
247
+ break;
248
+
249
+ case 'recent':
250
+ const limit = arg ? parseInt(arg, 10) : 10;
251
+ const recentSessions = sessionQuery.getRecentSessions(limit);
252
+ console.log(JSON.stringify(recentSessions, null, 2));
253
+ break;
254
+
255
+ default:
256
+ console.log('Usage:');
257
+ console.log(' bun SessionQuery.ts type <type>');
258
+ console.log(' bun SessionQuery.ts dir <directory>');
259
+ console.log(' bun SessionQuery.ts host <hostname>');
260
+ console.log(' bun SessionQuery.ts recent [limit]');
261
+ }
262
+ }
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * SessionStats.ts
4
+ * 会话统计分析工具
5
+ *
6
+ * 提供统计分析和可视化数据
7
+ */
8
+
9
+ import { readFileSync, readdirSync, existsSync } from 'fs';
10
+ import { join } from 'path';
11
+ import { homedir } from 'os';
12
+
13
+ // ============================================================================
14
+ // 类型定义
15
+ // ============================================================================
16
+
17
+ type SessionType = 'coding' | 'debugging' | 'research' | 'writing' | 'git' | 'refactoring' | 'mixed';
18
+
19
+ interface Stats {
20
+ total_sessions: number;
21
+ total_duration_hours: number;
22
+ avg_session_duration_minutes: number;
23
+ by_type: Record<string, number>;
24
+ by_directory: Record<string, number>;
25
+ }
26
+
27
+ // ============================================================================
28
+ // SessionStats 类
29
+ // ============================================================================
30
+
31
+ export class SessionStats {
32
+ private paiDir: string;
33
+
34
+ constructor(paiDir?: string) {
35
+ this.paiDir = paiDir || process.env.PAI_DIR || join(homedir(), '.claude');
36
+ }
37
+
38
+ /**
39
+ * 获取全局统计
40
+ */
41
+ getGlobalStats(): Stats {
42
+ const metadataFile = join(this.paiDir, 'SESSIONS/index/metadata.json');
43
+
44
+ if (!existsSync(metadataFile)) {
45
+ return this.emptyStats();
46
+ }
47
+
48
+ const metadata = JSON.parse(readFileSync(metadataFile, 'utf-8'));
49
+
50
+ return {
51
+ total_sessions: metadata.total_sessions || 0,
52
+ total_duration_hours: 0, // 需要遍历所有会话计算
53
+ avg_session_duration_minutes: 0,
54
+ by_type: metadata.sessions_by_type || {},
55
+ by_directory: metadata.sessions_by_directory || {},
56
+ };
57
+ }
58
+
59
+ /**
60
+ * 获取类型分布
61
+ */
62
+ getTypeDistribution(): Record<SessionType, number> {
63
+ const metadataFile = join(this.paiDir, 'SESSIONS/index/metadata.json');
64
+
65
+ if (!existsSync(metadataFile)) {
66
+ return {} as Record<SessionType, number>;
67
+ }
68
+
69
+ const metadata = JSON.parse(readFileSync(metadataFile, 'utf-8'));
70
+ return metadata.sessions_by_type || {};
71
+ }
72
+
73
+ /**
74
+ * 获取最活跃的目录
75
+ */
76
+ getTopDirectories(limit: number = 10): Array<{ directory: string; count: number }> {
77
+ const metadataFile = join(this.paiDir, 'SESSIONS/index/metadata.json');
78
+
79
+ if (!existsSync(metadataFile)) {
80
+ return [];
81
+ }
82
+
83
+ const metadata = JSON.parse(readFileSync(metadataFile, 'utf-8'));
84
+ const byDir = metadata.sessions_by_directory || {};
85
+
86
+ return Object.entries(byDir)
87
+ .map(([directory, count]) => ({ directory, count: count as number }))
88
+ .sort((a, b) => b.count - a.count)
89
+ .slice(0, limit);
90
+ }
91
+
92
+ private emptyStats(): Stats {
93
+ return {
94
+ total_sessions: 0,
95
+ total_duration_hours: 0,
96
+ avg_session_duration_minutes: 0,
97
+ by_type: {},
98
+ by_directory: {},
99
+ };
100
+ }
101
+ }
102
+
103
+ // ============================================================================
104
+ // 导出单例
105
+ // ============================================================================
106
+
107
+ export const sessionStats = new SessionStats();
108
+
109
+ // ============================================================================
110
+ // CLI 入口
111
+ // ============================================================================
112
+
113
+ if (import.meta.main) {
114
+ const command = process.argv[2];
115
+
116
+ switch (command) {
117
+ case 'global':
118
+ const stats = sessionStats.getGlobalStats();
119
+ console.log(JSON.stringify(stats, null, 2));
120
+ break;
121
+
122
+ case 'types':
123
+ const types = sessionStats.getTypeDistribution();
124
+ console.log(JSON.stringify(types, null, 2));
125
+ break;
126
+
127
+ case 'dirs':
128
+ const limit = process.argv[3] ? parseInt(process.argv[3], 10) : 10;
129
+ const dirs = sessionStats.getTopDirectories(limit);
130
+ console.log(JSON.stringify(dirs, null, 2));
131
+ break;
132
+
133
+ default:
134
+ console.log('Usage:');
135
+ console.log(' bun SessionStats.ts global');
136
+ console.log(' bun SessionStats.ts types');
137
+ console.log(' bun SessionStats.ts dirs [limit]');
138
+ }
139
+ }
@@ -0,0 +1,80 @@
1
+ #!/bin/bash
2
+ # 显示会话对话内容的友好脚本
3
+
4
+ SESSION_ID=$1
5
+
6
+ if [ -z "$SESSION_ID" ]; then
7
+ echo "用法: $0 <session_id>"
8
+ echo ""
9
+ echo "示例:"
10
+ echo " $0 04291516-d83b-4436-86c2-138eb01a1bf4"
11
+ echo ""
12
+ echo "或者查看最近的会话:"
13
+ echo " ~/.bun/bin/bun /data/app/claude-history/tools/SessionQuery.ts recent 1 | jq -r '.[0].session_id' | xargs $0"
14
+ exit 1
15
+ fi
16
+
17
+ # 查找summary文件
18
+ SUMMARY_FILE=$(find ~/.claude/SESSIONS/analysis/summaries -name "summary-${SESSION_ID}.json" 2>/dev/null | head -1)
19
+
20
+ if [ -z "$SUMMARY_FILE" ]; then
21
+ echo "错误: 找不到会话 $SESSION_ID"
22
+ exit 1
23
+ fi
24
+
25
+ # 提取并显示信息
26
+ echo "========================================"
27
+ echo "会话详情"
28
+ echo "========================================"
29
+ echo ""
30
+
31
+ # 基本信息
32
+ echo "📋 会话ID: $SESSION_ID"
33
+ echo "🖥️ 主机: $(jq -r '.hostname' "$SUMMARY_FILE") ($(jq -r '.user' "$SUMMARY_FILE")@$(jq -r '.platform' "$SUMMARY_FILE"))"
34
+ echo "📁 工作目录: $(jq -r '.working_directory' "$SUMMARY_FILE")"
35
+ echo "📅 时间: $(jq -r '.timestamp' "$SUMMARY_FILE")"
36
+ echo "🏷️ 类型: $(jq -r '.session_type' "$SUMMARY_FILE")"
37
+ echo ""
38
+
39
+ # 对话内容
40
+ echo "========================================"
41
+ echo "💬 对话内容"
42
+ echo "========================================"
43
+ echo ""
44
+
45
+ # 提取用户消息和助手回复
46
+ USER_MSGS=$(jq -r '.conversation.user_messages[]?' "$SUMMARY_FILE" 2>/dev/null)
47
+ ASST_MSGS=$(jq -r '.conversation.assistant_responses[]?' "$SUMMARY_FILE" 2>/dev/null)
48
+
49
+ if [ -z "$USER_MSGS" ]; then
50
+ echo "⚠️ 没有记录对话内容"
51
+ else
52
+ # 交替显示用户和助手消息
53
+ jq -r '.conversation |
54
+ .user_messages as $users |
55
+ .assistant_responses as $assts |
56
+ range(0; [$users, $assts] | map(length) | max) |
57
+ (
58
+ if $users[.] then "👤 用户: \($users[.])\n" else "" end,
59
+ if $assts[.] then "🤖 Claude: \($assts[.])\n" else "" end
60
+ )' "$SUMMARY_FILE"
61
+ fi
62
+
63
+ echo ""
64
+ echo "========================================"
65
+ echo "🔧 工具使用"
66
+ echo "========================================"
67
+ echo ""
68
+
69
+ TOOL_COUNT=$(jq -r '.total_tools' "$SUMMARY_FILE")
70
+ if [ "$TOOL_COUNT" -eq 0 ]; then
71
+ echo "没有使用工具"
72
+ else
73
+ echo "总计: $TOOL_COUNT 次工具调用"
74
+ echo ""
75
+ jq -r '.tool_usage | to_entries[] | " - \(.key): \(.value) 次"' "$SUMMARY_FILE"
76
+ echo ""
77
+ echo "成功率: $(jq -r '.success_rate' "$SUMMARY_FILE")%"
78
+ fi
79
+
80
+ echo ""