@muyichengshayu/promptx 0.2.12 → 0.2.14
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/CHANGELOG.md +12 -0
- package/apps/server/src/agentSessionDiscovery.js +180 -7
- package/apps/server/src/agentSessionDiscovery.test.js +386 -0
- package/apps/web/dist/assets/{CodexSessionManagerDialog-BN4cweyO.js → CodexSessionManagerDialog-DD2XtFAl.js} +1 -1
- package/apps/web/dist/assets/{TaskDiffReviewDialog-B8r9DD0G.js → TaskDiffReviewDialog-CGLpQQ97.js} +1 -1
- package/apps/web/dist/assets/{WorkbenchSettingsDialog-CjNLZ7j9.js → WorkbenchSettingsDialog-DxU55X3l.js} +1 -1
- package/apps/web/dist/assets/{WorkbenchView-DlttkjeG.css → WorkbenchView-D1oxqNr4.css} +1 -1
- package/apps/web/dist/assets/{WorkbenchView-pIUuQsCk.js → WorkbenchView-DnrN_wUG.js} +5 -5
- package/apps/web/dist/assets/index-BLxPFky8.js +2 -0
- package/apps/web/dist/index.html +1 -1
- package/package.json +1 -1
- package/scripts/relay-service.mjs +1 -1
- package/apps/web/dist/assets/index-Ch8uSQYT.js +0 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.14
|
|
4
|
+
|
|
5
|
+
- 修复 `Claude Code` 历史 session 在项目路径包含 `.` 时无法匹配的问题:候选发现会优先读取 jsonl 中的真实 `cwd`,并在缺失时用当前工作目录正向编码匹配 Claude projects 目录,避免不可逆 decode 导致 `/Users/foo/.claude` 被误判为 `/Users/foo//claude`。
|
|
6
|
+
- 增强 Claude session 路径匹配的兼容性:支持尾斜杠、Windows 盘符大小写与反斜杠、symlink / realpath 等价路径,并优先扫描当前项目对应目录,避免全局历史文件过多时被扫描上限漏掉。
|
|
7
|
+
- 优化 Claude jsonl 读取策略:大文件现在会安全读取文件头部用于提取 `cwd` 和会话预览,不再因为超过 256KB 就完全跳过。
|
|
8
|
+
- 重构 `Stone Dark` 主题为高对比度冷暖配色,提升工作台、弹窗、列表和状态色在暗色环境下的辨识度。
|
|
9
|
+
|
|
10
|
+
## 0.2.13
|
|
11
|
+
|
|
12
|
+
- 修复 `promptx relay start/restart` 启动失败并提示 `logDir is not defined` 的问题,Relay 后台进程现在会正确拿到日志目录。
|
|
13
|
+
- 修复任务列表点击标题进入编辑时标题文字上移、任务卡片高度抖动的问题,标题浏览态与编辑态保持一致行高。
|
|
14
|
+
|
|
3
15
|
## 0.2.12
|
|
4
16
|
|
|
5
17
|
- 修复全局 npm 安装后 `promptx start` 启动失败的问题:server / runner 运行时代码不再通过 workspace 裸包名加载 `@promptx/shared`,改为使用发布包内可解析的相对路径,避免 `ERR_MODULE_NOT_FOUND`。
|
|
@@ -39,6 +39,32 @@ function safeReadFile(filePath = '', maxBytes = MAX_DAT_FILE_SIZE) {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
function safeReadFileHead(filePath = '', maxBytes = 256 * 1024) {
|
|
43
|
+
const stat = safeStat(filePath)
|
|
44
|
+
if (!stat?.isFile()) {
|
|
45
|
+
return ''
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const bytesToRead = Math.min(Math.max(1, Number(maxBytes) || 1), stat.size)
|
|
49
|
+
const buffer = Buffer.allocUnsafe(bytesToRead)
|
|
50
|
+
let fd = null
|
|
51
|
+
try {
|
|
52
|
+
fd = fs.openSync(filePath, 'r')
|
|
53
|
+
const bytesRead = fs.readSync(fd, buffer, 0, bytesToRead, 0)
|
|
54
|
+
return buffer.subarray(0, bytesRead).toString('utf8')
|
|
55
|
+
} catch {
|
|
56
|
+
return ''
|
|
57
|
+
} finally {
|
|
58
|
+
if (fd !== null) {
|
|
59
|
+
try {
|
|
60
|
+
fs.closeSync(fd)
|
|
61
|
+
} catch {
|
|
62
|
+
// ignore close errors for best-effort discovery
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
42
68
|
function parseJson(value) {
|
|
43
69
|
const text = normalizeText(value)
|
|
44
70
|
if (!text) {
|
|
@@ -288,7 +314,7 @@ function extractMessageText(value, depth = 0) {
|
|
|
288
314
|
}
|
|
289
315
|
|
|
290
316
|
function readJsonlPreview(filePath = '') {
|
|
291
|
-
const content =
|
|
317
|
+
const content = safeReadFileHead(filePath, 256 * 1024)
|
|
292
318
|
if (!content) {
|
|
293
319
|
return ''
|
|
294
320
|
}
|
|
@@ -314,6 +340,134 @@ function readJsonlPreview(filePath = '') {
|
|
|
314
340
|
return ''
|
|
315
341
|
}
|
|
316
342
|
|
|
343
|
+
function readClaudeJsonlCwd(filePath = '') {
|
|
344
|
+
const content = safeReadFileHead(filePath, 256 * 1024)
|
|
345
|
+
if (!content) {
|
|
346
|
+
return ''
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const lines = content.replace(/\r\n/g, '\n').split('\n')
|
|
350
|
+
for (const line of lines) {
|
|
351
|
+
const event = parseJson(line)
|
|
352
|
+
if (!event || typeof event !== 'object') {
|
|
353
|
+
continue
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const cwd = normalizeText(event.cwd)
|
|
357
|
+
if (cwd) {
|
|
358
|
+
return cwd
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const message = event.message
|
|
362
|
+
if (message && typeof message === 'object') {
|
|
363
|
+
const msgCwd = normalizeText(message.cwd)
|
|
364
|
+
if (msgCwd) {
|
|
365
|
+
return msgCwd
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return ''
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function normalizeClaudeProjectPathInput(cwd = '') {
|
|
374
|
+
const value = normalizeText(cwd)
|
|
375
|
+
if (!value) {
|
|
376
|
+
return ''
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const normalized = value.replace(/\\/g, '/')
|
|
380
|
+
if (normalized.length > 1 && !/^[A-Za-z]:\/$/i.test(normalized)) {
|
|
381
|
+
return normalized.replace(/\/+$/, '')
|
|
382
|
+
}
|
|
383
|
+
return normalized
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function getClaudeProjectPathInputs(cwd = '') {
|
|
387
|
+
const primary = normalizeClaudeProjectPathInput(cwd)
|
|
388
|
+
if (!primary) {
|
|
389
|
+
return []
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const values = [primary]
|
|
393
|
+
try {
|
|
394
|
+
const realPath = normalizeClaudeProjectPathInput(fs.realpathSync.native(primary))
|
|
395
|
+
if (realPath && !values.includes(realPath)) {
|
|
396
|
+
values.push(realPath)
|
|
397
|
+
}
|
|
398
|
+
} catch {
|
|
399
|
+
// Some candidate paths may not exist locally, especially cross-platform paths.
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return values
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function encodeClaudeProjectPath(cwd = '') {
|
|
406
|
+
const value = normalizeClaudeProjectPathInput(cwd)
|
|
407
|
+
if (!value) {
|
|
408
|
+
return ''
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return value.replace(/[/:.]/g, '-')
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function getClaudeProjectKeysForCwd(cwd = '') {
|
|
415
|
+
const keys = []
|
|
416
|
+
getClaudeProjectPathInputs(cwd).forEach((targetPath) => {
|
|
417
|
+
const key = encodeClaudeProjectPath(targetPath)
|
|
418
|
+
if (!key || keys.includes(key)) {
|
|
419
|
+
return
|
|
420
|
+
}
|
|
421
|
+
keys.push(key)
|
|
422
|
+
|
|
423
|
+
if (/^[A-Za-z]--/.test(key)) {
|
|
424
|
+
const upperDriveKey = `${key[0].toUpperCase()}${key.slice(1)}`
|
|
425
|
+
const lowerDriveKey = `${key[0].toLowerCase()}${key.slice(1)}`
|
|
426
|
+
;[upperDriveKey, lowerDriveKey].forEach((driveKey) => {
|
|
427
|
+
if (!keys.includes(driveKey)) {
|
|
428
|
+
keys.push(driveKey)
|
|
429
|
+
}
|
|
430
|
+
})
|
|
431
|
+
}
|
|
432
|
+
})
|
|
433
|
+
return keys
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function inferClaudeProjectCwd(projectKey = '', options = {}) {
|
|
437
|
+
const key = normalizeText(projectKey)
|
|
438
|
+
const targetPaths = getClaudeProjectPathInputs(options.cwd)
|
|
439
|
+
if (!key || !targetPaths.length) {
|
|
440
|
+
return ''
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const matched = targetPaths.some((targetPath) => {
|
|
444
|
+
const encodedTarget = encodeClaudeProjectPath(targetPath)
|
|
445
|
+
const isWindowsPath = /^[A-Za-z]:\//.test(targetPath)
|
|
446
|
+
return isWindowsPath
|
|
447
|
+
? encodedTarget.toLowerCase() === key.toLowerCase()
|
|
448
|
+
: encodedTarget === key
|
|
449
|
+
})
|
|
450
|
+
return matched ? targetPaths[0] : ''
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function normalizeClaudeDiscoveredCwd(cwd = '', options = {}) {
|
|
454
|
+
const discoveredCwd = normalizeClaudeProjectPathInput(cwd)
|
|
455
|
+
if (!discoveredCwd) {
|
|
456
|
+
return ''
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const targetPaths = getClaudeProjectPathInputs(options.cwd)
|
|
460
|
+
if (!targetPaths.length) {
|
|
461
|
+
return discoveredCwd
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const discoveredComparable = normalizeComparablePath(discoveredCwd)
|
|
465
|
+
const isTargetEquivalent = targetPaths.some((targetPath) => (
|
|
466
|
+
normalizeComparablePath(targetPath) === discoveredComparable
|
|
467
|
+
))
|
|
468
|
+
return isTargetEquivalent ? targetPaths[0] : discoveredCwd
|
|
469
|
+
}
|
|
470
|
+
|
|
317
471
|
export function decodeClaudeProjectPath(projectKey = '') {
|
|
318
472
|
const key = normalizeText(projectKey)
|
|
319
473
|
if (!key) {
|
|
@@ -338,6 +492,7 @@ export function listKnownClaudeCodeSessions(options = {}) {
|
|
|
338
492
|
const transcriptDir = path.join(claudeHome, 'transcripts')
|
|
339
493
|
const projectsDir = path.join(claudeHome, 'projects')
|
|
340
494
|
const items = []
|
|
495
|
+
const seenProjectFiles = new Set()
|
|
341
496
|
|
|
342
497
|
collectFiles(transcriptDir, {
|
|
343
498
|
maxDepth: 0,
|
|
@@ -355,15 +510,19 @@ export function listKnownClaudeCodeSessions(options = {}) {
|
|
|
355
510
|
})
|
|
356
511
|
})
|
|
357
512
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
513
|
+
function addClaudeProjectFile(filePath) {
|
|
514
|
+
const fileKey = path.resolve(filePath)
|
|
515
|
+
if (seenProjectFiles.has(fileKey)) {
|
|
516
|
+
return
|
|
517
|
+
}
|
|
518
|
+
seenProjectFiles.add(fileKey)
|
|
519
|
+
|
|
363
520
|
const stat = safeStat(filePath)
|
|
364
521
|
const relativeParts = path.relative(projectsDir, filePath).split(path.sep).filter(Boolean)
|
|
365
522
|
const projectKey = relativeParts[0] || ''
|
|
366
|
-
const cwd =
|
|
523
|
+
const cwd = normalizeClaudeDiscoveredCwd(readClaudeJsonlCwd(filePath), options)
|
|
524
|
+
|| inferClaudeProjectCwd(projectKey, options)
|
|
525
|
+
|| decodeClaudeProjectPath(projectKey)
|
|
367
526
|
const id = path.basename(filePath, '.jsonl')
|
|
368
527
|
items.push({
|
|
369
528
|
id,
|
|
@@ -373,8 +532,22 @@ export function listKnownClaudeCodeSessions(options = {}) {
|
|
|
373
532
|
updatedAt: stat?.mtime,
|
|
374
533
|
source: 'claude_projects',
|
|
375
534
|
})
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
getClaudeProjectKeysForCwd(options.cwd).forEach((targetProjectKey) => {
|
|
538
|
+
collectFiles(path.join(projectsDir, targetProjectKey), {
|
|
539
|
+
maxDepth: 2,
|
|
540
|
+
maxFiles: MAX_SCAN_FILES,
|
|
541
|
+
match: (filePath) => filePath.endsWith('.jsonl'),
|
|
542
|
+
}).forEach(addClaudeProjectFile)
|
|
376
543
|
})
|
|
377
544
|
|
|
545
|
+
collectFiles(projectsDir, {
|
|
546
|
+
maxDepth: 3,
|
|
547
|
+
maxFiles: MAX_SCAN_FILES,
|
|
548
|
+
match: (filePath) => filePath.endsWith('.jsonl'),
|
|
549
|
+
}).forEach(addClaudeProjectFile)
|
|
550
|
+
|
|
378
551
|
return sortAndLimitCandidates(items, options)
|
|
379
552
|
}
|
|
380
553
|
|
|
@@ -70,6 +70,392 @@ test('listKnownClaudeCodeSessions merges transcripts and project files', () => {
|
|
|
70
70
|
}
|
|
71
71
|
})
|
|
72
72
|
|
|
73
|
+
test('listKnownClaudeCodeSessions reads real cwd from session file when project key contains dots', () => {
|
|
74
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-claude-dot-discovery-'))
|
|
75
|
+
const claudeHome = path.join(tempRoot, '.claude')
|
|
76
|
+
const projectsDir = path.join(claudeHome, 'projects', '-Users-bravf--claude')
|
|
77
|
+
|
|
78
|
+
fs.mkdirSync(projectsDir, { recursive: true })
|
|
79
|
+
|
|
80
|
+
const projectPath = path.join(projectsDir, 'ses_dot.jsonl')
|
|
81
|
+
fs.writeFileSync(
|
|
82
|
+
projectPath,
|
|
83
|
+
`${JSON.stringify({ type: 'user', message: { text: '看下配置' }, cwd: '/Users/bravf/.claude' })}
|
|
84
|
+
`
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
const now = new Date('2026-04-13T08:00:00.000Z')
|
|
88
|
+
fs.utimesSync(projectPath, now, now)
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const items = listKnownClaudeCodeSessions({
|
|
92
|
+
claudeHome,
|
|
93
|
+
limit: 10,
|
|
94
|
+
cwd: '/Users/bravf/.claude',
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
assert.equal(items.length, 1)
|
|
98
|
+
assert.deepEqual(
|
|
99
|
+
{
|
|
100
|
+
id: items[0].id,
|
|
101
|
+
cwd: items[0].cwd,
|
|
102
|
+
matchedCwd: items[0].matchedCwd,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: 'ses_dot',
|
|
106
|
+
cwd: '/Users/bravf/.claude',
|
|
107
|
+
matchedCwd: true,
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
} finally {
|
|
111
|
+
fs.rmSync(tempRoot, { recursive: true, force: true })
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('listKnownClaudeCodeSessions reads cwd from large Claude jsonl files', () => {
|
|
116
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-claude-large-discovery-'))
|
|
117
|
+
const claudeHome = path.join(tempRoot, '.claude')
|
|
118
|
+
const projectsDir = path.join(claudeHome, 'projects', '-Users-bravf--config')
|
|
119
|
+
|
|
120
|
+
fs.mkdirSync(projectsDir, { recursive: true })
|
|
121
|
+
|
|
122
|
+
const projectPath = path.join(projectsDir, 'ses_large.jsonl')
|
|
123
|
+
const firstLine = `${JSON.stringify({ type: 'user', message: { text: '读取大文件历史' }, cwd: '/Users/bravf/.config' })}\n`
|
|
124
|
+
fs.writeFileSync(projectPath, `${firstLine}${'x'.repeat(300 * 1024)}\n`)
|
|
125
|
+
|
|
126
|
+
const now = new Date('2026-04-13T08:00:00.000Z')
|
|
127
|
+
fs.utimesSync(projectPath, now, now)
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const items = listKnownClaudeCodeSessions({
|
|
131
|
+
claudeHome,
|
|
132
|
+
limit: 10,
|
|
133
|
+
cwd: '/Users/bravf/.config',
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
assert.equal(items.length, 1)
|
|
137
|
+
assert.deepEqual(
|
|
138
|
+
{
|
|
139
|
+
id: items[0].id,
|
|
140
|
+
cwd: items[0].cwd,
|
|
141
|
+
matchedCwd: items[0].matchedCwd,
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
id: 'ses_large',
|
|
145
|
+
cwd: '/Users/bravf/.config',
|
|
146
|
+
matchedCwd: true,
|
|
147
|
+
}
|
|
148
|
+
)
|
|
149
|
+
assert.match(items[0].label, /读取大文件历史|config/i)
|
|
150
|
+
} finally {
|
|
151
|
+
fs.rmSync(tempRoot, { recursive: true, force: true })
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
test('listKnownClaudeCodeSessions matches encoded project key when session file has no cwd', () => {
|
|
156
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-claude-encoded-cwd-discovery-'))
|
|
157
|
+
const claudeHome = path.join(tempRoot, '.claude')
|
|
158
|
+
const projectsDir = path.join(claudeHome, 'projects', '-Users-bravf--claude')
|
|
159
|
+
|
|
160
|
+
fs.mkdirSync(projectsDir, { recursive: true })
|
|
161
|
+
|
|
162
|
+
const projectPath = path.join(projectsDir, 'ses_encoded_cwd.jsonl')
|
|
163
|
+
fs.writeFileSync(projectPath, `${JSON.stringify({ type: 'user', message: { text: '继续之前的任务' } })}\n`)
|
|
164
|
+
|
|
165
|
+
const now = new Date('2026-04-13T08:00:00.000Z')
|
|
166
|
+
fs.utimesSync(projectPath, now, now)
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const items = listKnownClaudeCodeSessions({
|
|
170
|
+
claudeHome,
|
|
171
|
+
limit: 10,
|
|
172
|
+
cwd: '/Users/bravf/.claude',
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
assert.equal(items.length, 1)
|
|
176
|
+
assert.deepEqual(
|
|
177
|
+
{
|
|
178
|
+
id: items[0].id,
|
|
179
|
+
cwd: items[0].cwd,
|
|
180
|
+
matchedCwd: items[0].matchedCwd,
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
id: 'ses_encoded_cwd',
|
|
184
|
+
cwd: '/Users/bravf/.claude',
|
|
185
|
+
matchedCwd: true,
|
|
186
|
+
}
|
|
187
|
+
)
|
|
188
|
+
} finally {
|
|
189
|
+
fs.rmSync(tempRoot, { recursive: true, force: true })
|
|
190
|
+
}
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
test('listKnownClaudeCodeSessions matches encoded project key with trailing cwd slash', () => {
|
|
194
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-claude-trailing-cwd-discovery-'))
|
|
195
|
+
const claudeHome = path.join(tempRoot, '.claude')
|
|
196
|
+
const projectsDir = path.join(claudeHome, 'projects', '-Users-bravf--claude')
|
|
197
|
+
|
|
198
|
+
fs.mkdirSync(projectsDir, { recursive: true })
|
|
199
|
+
|
|
200
|
+
const projectPath = path.join(projectsDir, 'ses_trailing_cwd.jsonl')
|
|
201
|
+
fs.writeFileSync(projectPath, `${JSON.stringify({ type: 'user', message: { text: '继续之前的任务' } })}\n`)
|
|
202
|
+
|
|
203
|
+
const now = new Date('2026-04-13T08:00:00.000Z')
|
|
204
|
+
fs.utimesSync(projectPath, now, now)
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const items = listKnownClaudeCodeSessions({
|
|
208
|
+
claudeHome,
|
|
209
|
+
limit: 10,
|
|
210
|
+
cwd: '/Users/bravf/.claude/',
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
assert.equal(items.length, 1)
|
|
214
|
+
assert.deepEqual(
|
|
215
|
+
{
|
|
216
|
+
id: items[0].id,
|
|
217
|
+
cwd: items[0].cwd,
|
|
218
|
+
matchedCwd: items[0].matchedCwd,
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
id: 'ses_trailing_cwd',
|
|
222
|
+
cwd: '/Users/bravf/.claude',
|
|
223
|
+
matchedCwd: true,
|
|
224
|
+
}
|
|
225
|
+
)
|
|
226
|
+
} finally {
|
|
227
|
+
fs.rmSync(tempRoot, { recursive: true, force: true })
|
|
228
|
+
}
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
test('listKnownClaudeCodeSessions scans target project before global file limit', () => {
|
|
232
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-claude-target-first-discovery-'))
|
|
233
|
+
const claudeHome = path.join(tempRoot, '.claude')
|
|
234
|
+
const projectsRoot = path.join(claudeHome, 'projects')
|
|
235
|
+
const targetDir = path.join(projectsRoot, '-zzzz--claude')
|
|
236
|
+
|
|
237
|
+
fs.mkdirSync(targetDir, { recursive: true })
|
|
238
|
+
|
|
239
|
+
for (let index = 0; index < 805; index += 1) {
|
|
240
|
+
const fillerDir = path.join(projectsRoot, `-Users-bravf-code-filler-${String(index).padStart(3, '0')}`)
|
|
241
|
+
fs.mkdirSync(fillerDir, { recursive: true })
|
|
242
|
+
fs.writeFileSync(
|
|
243
|
+
path.join(fillerDir, `ses_filler_${index}.jsonl`),
|
|
244
|
+
`${JSON.stringify({ type: 'user', message: { text: `无关历史 ${index}` } })}\n`
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const projectPath = path.join(targetDir, 'ses_target_first.jsonl')
|
|
249
|
+
fs.writeFileSync(projectPath, `${JSON.stringify({ type: 'user', message: { text: '目标项目历史' } })}\n`)
|
|
250
|
+
|
|
251
|
+
const now = new Date('2026-04-13T08:00:00.000Z')
|
|
252
|
+
fs.utimesSync(projectPath, now, now)
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
const items = listKnownClaudeCodeSessions({
|
|
256
|
+
claudeHome,
|
|
257
|
+
limit: 10,
|
|
258
|
+
cwd: '/zzzz/.claude',
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
assert.equal(items[0]?.id, 'ses_target_first')
|
|
262
|
+
assert.equal(items[0]?.cwd, '/zzzz/.claude')
|
|
263
|
+
assert.equal(items[0]?.matchedCwd, true)
|
|
264
|
+
} finally {
|
|
265
|
+
fs.rmSync(tempRoot, { recursive: true, force: true })
|
|
266
|
+
}
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
test('listKnownClaudeCodeSessions matches realpath project key for symlink cwd', () => {
|
|
270
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-claude-realpath-discovery-'))
|
|
271
|
+
const claudeHome = path.join(tempRoot, '.claude')
|
|
272
|
+
const realCwd = path.join(tempRoot, 'real-workspace')
|
|
273
|
+
const linkCwd = path.join(tempRoot, 'link-workspace')
|
|
274
|
+
|
|
275
|
+
fs.mkdirSync(realCwd, { recursive: true })
|
|
276
|
+
fs.symlinkSync(realCwd, linkCwd, 'dir')
|
|
277
|
+
|
|
278
|
+
const realProjectKey = fs.realpathSync.native(realCwd).replace(/[/:.]/g, '-')
|
|
279
|
+
const projectsDir = path.join(claudeHome, 'projects', realProjectKey)
|
|
280
|
+
fs.mkdirSync(projectsDir, { recursive: true })
|
|
281
|
+
|
|
282
|
+
const projectPath = path.join(projectsDir, 'ses_realpath_cwd.jsonl')
|
|
283
|
+
fs.writeFileSync(projectPath, `${JSON.stringify({ type: 'user', message: { text: 'symlink 历史' } })}\n`)
|
|
284
|
+
|
|
285
|
+
const now = new Date('2026-04-13T08:00:00.000Z')
|
|
286
|
+
fs.utimesSync(projectPath, now, now)
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
const items = listKnownClaudeCodeSessions({
|
|
290
|
+
claudeHome,
|
|
291
|
+
limit: 10,
|
|
292
|
+
cwd: linkCwd,
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
assert.equal(items[0]?.id, 'ses_realpath_cwd')
|
|
296
|
+
assert.equal(items[0]?.cwd, linkCwd)
|
|
297
|
+
assert.equal(items[0]?.matchedCwd, true)
|
|
298
|
+
} finally {
|
|
299
|
+
fs.rmSync(tempRoot, { recursive: true, force: true })
|
|
300
|
+
}
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
test('listKnownClaudeCodeSessions treats jsonl realpath cwd as matching symlink cwd', () => {
|
|
304
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-claude-jsonl-realpath-discovery-'))
|
|
305
|
+
const claudeHome = path.join(tempRoot, '.claude')
|
|
306
|
+
const realCwd = path.join(tempRoot, 'real-workspace')
|
|
307
|
+
const linkCwd = path.join(tempRoot, 'link-workspace')
|
|
308
|
+
|
|
309
|
+
fs.mkdirSync(realCwd, { recursive: true })
|
|
310
|
+
fs.symlinkSync(realCwd, linkCwd, 'dir')
|
|
311
|
+
|
|
312
|
+
const nativeRealCwd = fs.realpathSync.native(realCwd)
|
|
313
|
+
const realProjectKey = nativeRealCwd.replace(/[/:.]/g, '-')
|
|
314
|
+
const projectsDir = path.join(claudeHome, 'projects', realProjectKey)
|
|
315
|
+
fs.mkdirSync(projectsDir, { recursive: true })
|
|
316
|
+
|
|
317
|
+
const projectPath = path.join(projectsDir, 'ses_jsonl_realpath_cwd.jsonl')
|
|
318
|
+
fs.writeFileSync(
|
|
319
|
+
projectPath,
|
|
320
|
+
`${JSON.stringify({ type: 'user', message: { text: 'jsonl realpath 历史' }, cwd: nativeRealCwd })}\n`
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
const now = new Date('2026-04-13T08:00:00.000Z')
|
|
324
|
+
fs.utimesSync(projectPath, now, now)
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
const items = listKnownClaudeCodeSessions({
|
|
328
|
+
claudeHome,
|
|
329
|
+
limit: 10,
|
|
330
|
+
cwd: linkCwd,
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
assert.equal(items[0]?.id, 'ses_jsonl_realpath_cwd')
|
|
334
|
+
assert.equal(items[0]?.cwd, linkCwd)
|
|
335
|
+
assert.equal(items[0]?.matchedCwd, true)
|
|
336
|
+
} finally {
|
|
337
|
+
fs.rmSync(tempRoot, { recursive: true, force: true })
|
|
338
|
+
}
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
test('listKnownClaudeCodeSessions matches Windows encoded project key case-insensitively', () => {
|
|
342
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-claude-windows-cwd-discovery-'))
|
|
343
|
+
const claudeHome = path.join(tempRoot, '.claude')
|
|
344
|
+
const projectsDir = path.join(claudeHome, 'projects', 'C--Users-bravf--claude')
|
|
345
|
+
|
|
346
|
+
fs.mkdirSync(projectsDir, { recursive: true })
|
|
347
|
+
|
|
348
|
+
const projectPath = path.join(projectsDir, 'ses_windows_cwd.jsonl')
|
|
349
|
+
fs.writeFileSync(projectPath, `${JSON.stringify({ type: 'user', message: { text: '继续 Windows 任务' } })}\n`)
|
|
350
|
+
|
|
351
|
+
const now = new Date('2026-04-13T08:00:00.000Z')
|
|
352
|
+
fs.utimesSync(projectPath, now, now)
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
const items = listKnownClaudeCodeSessions({
|
|
356
|
+
claudeHome,
|
|
357
|
+
limit: 10,
|
|
358
|
+
cwd: 'c:\\Users\\bravf\\.claude\\',
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
assert.equal(items.length, 1)
|
|
362
|
+
assert.deepEqual(
|
|
363
|
+
{
|
|
364
|
+
id: items[0].id,
|
|
365
|
+
cwd: items[0].cwd,
|
|
366
|
+
matchedCwd: items[0].matchedCwd,
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
id: 'ses_windows_cwd',
|
|
370
|
+
cwd: 'c:/Users/bravf/.claude',
|
|
371
|
+
matchedCwd: true,
|
|
372
|
+
}
|
|
373
|
+
)
|
|
374
|
+
} finally {
|
|
375
|
+
fs.rmSync(tempRoot, { recursive: true, force: true })
|
|
376
|
+
}
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
test('listKnownClaudeCodeSessions scans uppercase Windows project key before global file limit', () => {
|
|
380
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-claude-windows-target-first-discovery-'))
|
|
381
|
+
const claudeHome = path.join(tempRoot, '.claude')
|
|
382
|
+
const projectsRoot = path.join(claudeHome, 'projects')
|
|
383
|
+
const targetDir = path.join(projectsRoot, 'C--Users-bravf--claude')
|
|
384
|
+
|
|
385
|
+
fs.mkdirSync(targetDir, { recursive: true })
|
|
386
|
+
|
|
387
|
+
for (let index = 0; index < 805; index += 1) {
|
|
388
|
+
const fillerDir = path.join(projectsRoot, `A--Users-bravf-code-filler-${String(index).padStart(3, '0')}`)
|
|
389
|
+
fs.mkdirSync(fillerDir, { recursive: true })
|
|
390
|
+
fs.writeFileSync(
|
|
391
|
+
path.join(fillerDir, `ses_windows_filler_${index}.jsonl`),
|
|
392
|
+
`${JSON.stringify({ type: 'user', message: { text: `无关 Windows 历史 ${index}` } })}\n`
|
|
393
|
+
)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const projectPath = path.join(targetDir, 'ses_windows_target_first.jsonl')
|
|
397
|
+
fs.writeFileSync(projectPath, `${JSON.stringify({ type: 'user', message: { text: 'Windows 目标项目历史' } })}\n`)
|
|
398
|
+
|
|
399
|
+
const now = new Date('2026-04-13T08:00:00.000Z')
|
|
400
|
+
fs.utimesSync(projectPath, now, now)
|
|
401
|
+
|
|
402
|
+
try {
|
|
403
|
+
const items = listKnownClaudeCodeSessions({
|
|
404
|
+
claudeHome,
|
|
405
|
+
limit: 10,
|
|
406
|
+
cwd: 'c:\\Users\\bravf\\.claude\\',
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
assert.equal(items[0]?.id, 'ses_windows_target_first')
|
|
410
|
+
assert.equal(items[0]?.cwd, 'c:/Users/bravf/.claude')
|
|
411
|
+
assert.equal(items[0]?.matchedCwd, true)
|
|
412
|
+
} finally {
|
|
413
|
+
fs.rmSync(tempRoot, { recursive: true, force: true })
|
|
414
|
+
}
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
test('listKnownClaudeCodeSessions reads cwd from message.cwd when top-level cwd is missing', () => {
|
|
418
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-claude-msgcwd-discovery-'))
|
|
419
|
+
const claudeHome = path.join(tempRoot, '.claude')
|
|
420
|
+
const projectsDir = path.join(claudeHome, 'projects', '-Users-bravf--config')
|
|
421
|
+
|
|
422
|
+
fs.mkdirSync(projectsDir, { recursive: true })
|
|
423
|
+
|
|
424
|
+
const projectPath = path.join(projectsDir, 'ses_msgcwd.jsonl')
|
|
425
|
+
fs.writeFileSync(
|
|
426
|
+
projectPath,
|
|
427
|
+
`${JSON.stringify({ type: 'user', message: { text: '编辑配置', cwd: '/Users/bravf/.config' } })}
|
|
428
|
+
`
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
const now = new Date('2026-04-13T08:00:00.000Z')
|
|
432
|
+
fs.utimesSync(projectPath, now, now)
|
|
433
|
+
|
|
434
|
+
try {
|
|
435
|
+
const items = listKnownClaudeCodeSessions({
|
|
436
|
+
claudeHome,
|
|
437
|
+
limit: 10,
|
|
438
|
+
cwd: '/Users/bravf/.config',
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
assert.equal(items.length, 1)
|
|
442
|
+
assert.deepEqual(
|
|
443
|
+
{
|
|
444
|
+
id: items[0].id,
|
|
445
|
+
cwd: items[0].cwd,
|
|
446
|
+
matchedCwd: items[0].matchedCwd,
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
id: 'ses_msgcwd',
|
|
450
|
+
cwd: '/Users/bravf/.config',
|
|
451
|
+
matchedCwd: true,
|
|
452
|
+
}
|
|
453
|
+
)
|
|
454
|
+
} finally {
|
|
455
|
+
fs.rmSync(tempRoot, { recursive: true, force: true })
|
|
456
|
+
}
|
|
457
|
+
})
|
|
458
|
+
|
|
73
459
|
test('listKnownOpenCodeSessions discovers sessions from desktop dat files', () => {
|
|
74
460
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-opencode-discovery-'))
|
|
75
461
|
const dataDir = path.join(tempRoot, 'ai.opencode.desktop')
|