@qqq123456789/codex-doctor 0.5.0 → 0.7.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/README.md +7 -1
- package/docs/13-codex-doctor.en.md +2 -1
- package/docs/13-codex-doctor.md +2 -1
- package/package.json +1 -1
- package/tool/cli.mjs +47 -2
- package/tool/sessions.mjs +128 -0
package/README.md
CHANGED
|
@@ -31,7 +31,8 @@ node codex-troubleshooting/tool/cli.mjs --help
|
|
|
31
31
|
| `archive list` | List archive folders with sizes | read-only |
|
|
32
32
|
| `archive delete <name\|--all>` | Delete archives (interactive confirm by default, `--yes` to skip) | medium (deletes, requires confirm) |
|
|
33
33
|
| `versions [-n 10]` | List the latest openai/codex releases (prerelease flagged) | read-only |
|
|
34
|
-
| `
|
|
34
|
+
| `sessions [-n 10] [--dir keyword]` | Browse past sessions: time, workdir, source, first-prompt preview; `--dir` filters by directory — find "that conversation" | read-only |
|
|
35
|
+
| `update` | Check the latest npm version and how to update | read-only |
|
|
35
36
|
|
|
36
37
|
## Design principles
|
|
37
38
|
|
package/docs/13-codex-doctor.md
CHANGED
|
@@ -31,7 +31,8 @@ node codex-troubleshooting/tool/cli.mjs --help
|
|
|
31
31
|
| `archive list` | 查看归档目录与体积 | 只读 |
|
|
32
32
|
| `archive delete <名称\|--all>` | 删除归档(默认需交互确认,`--yes` 跳过) | 中(删除,需确认) |
|
|
33
33
|
| `versions [-n 10]` | 查看 openai/codex 最近 N 个版本(含预发布标记) | 只读 |
|
|
34
|
-
| `
|
|
34
|
+
| `sessions [-n 10] [--dir 关键字]` | 浏览历史会话:时间、工作目录、来源、首条提问预览;`--dir` 按目录过滤,找回「上次那个对话」 | 只读 |
|
|
35
|
+
| `update` | 查询 npm 上工具的最新版本与更新方式 | 只读 |
|
|
35
36
|
|
|
36
37
|
## 设计原则
|
|
37
38
|
|
package/package.json
CHANGED
package/tool/cli.mjs
CHANGED
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
import { collectChecks, summarize, renderHuman } from './checks.mjs';
|
|
4
4
|
import { cleanTarget } from './clean.mjs';
|
|
5
5
|
import { backupConfig, restoreBackup, resetAuth, listVersions, listArchives, deleteArchive, checkUpdate } from './ops.mjs';
|
|
6
|
+
import { listSessions, searchSessions } from './sessions.mjs';
|
|
7
|
+
import { CODEX_DIR } from './util.mjs';
|
|
8
|
+
import path from 'node:path';
|
|
6
9
|
|
|
7
|
-
const VERSION = '0.
|
|
10
|
+
const VERSION = '0.7.0';
|
|
8
11
|
|
|
9
12
|
const HELP = `codex-doctor v${VERSION} — Codex CLI 维护与排障工具(零依赖)
|
|
10
13
|
|
|
@@ -24,7 +27,9 @@ const HELP = `codex-doctor v${VERSION} — Codex CLI 维护与排障工具(零
|
|
|
24
27
|
archive list 查看归档目录与体积
|
|
25
28
|
archive delete <名称|--all> 删除归档(需 --yes 或交互确认)
|
|
26
29
|
versions [-n N] 查看 openai/codex 最近 N 个版本(默认 10)
|
|
27
|
-
|
|
30
|
+
sessions [-n N] [--dir 关键字] 浏览历史会话:时间、目录、来源、首条提问预览
|
|
31
|
+
[--search 关键词] [--deep] 按关键词搜索会话(--deep 全文扫描)
|
|
32
|
+
update 查询 npm 最新版本与更新方式
|
|
28
33
|
help 显示本帮助
|
|
29
34
|
|
|
30
35
|
全局: --yes 跳过交互确认(非 TTY 环境必须显式提供)。文档: docs/13-codex-doctor.md`;
|
|
@@ -40,6 +45,9 @@ function parseFlags(args) {
|
|
|
40
45
|
else if (a === '--strict') flags.strict = true;
|
|
41
46
|
else if (a === '--days') flags.days = Number(args[++i]);
|
|
42
47
|
else if (a === '-n' || a === '--limit') flags.limit = Number(args[++i]);
|
|
48
|
+
else if (a === '--dir') flags.dir = args[++i];
|
|
49
|
+
else if (a === '--search') flags.search = args[++i];
|
|
50
|
+
else if (a === '--deep') flags.deep = true;
|
|
43
51
|
else if (a === '--out') flags.out = args[++i];
|
|
44
52
|
else rest.push(a);
|
|
45
53
|
}
|
|
@@ -125,6 +133,43 @@ async function main() {
|
|
|
125
133
|
process.exitCode = 1;
|
|
126
134
|
break;
|
|
127
135
|
}
|
|
136
|
+
case 'sessions': {
|
|
137
|
+
if (flags.search) {
|
|
138
|
+
const items = searchSessions({
|
|
139
|
+
keyword: flags.search,
|
|
140
|
+
limit: Number.isFinite(flags.limit) ? flags.limit : 10,
|
|
141
|
+
deep: flags.deep === true,
|
|
142
|
+
});
|
|
143
|
+
if (items.length === 0) {
|
|
144
|
+
console.log(
|
|
145
|
+
`没有找到包含「${flags.search}」的会话${flags.deep ? '' : '(默认只搜每个文件开头部分,可加 --deep 全文搜索)'}`
|
|
146
|
+
);
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
console.log('时间 工作目录 匹配片段');
|
|
150
|
+
for (const it of items) {
|
|
151
|
+
console.log(`${it.date.padEnd(18)} ${it.dirName.padEnd(20).slice(0, 20)} ${it.snippet}`);
|
|
152
|
+
}
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
const items = listSessions({
|
|
156
|
+
limit: Number.isFinite(flags.limit) ? flags.limit : 10,
|
|
157
|
+
cwdFilter: flags.dir || null,
|
|
158
|
+
});
|
|
159
|
+
if (items.length === 0) {
|
|
160
|
+
console.log(flags.dir ? `没有匹配「${flags.dir}」的会话记录` : '~/.codex/sessions 里没有会话记录');
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
console.log('时间 工作目录 来源 首条提问');
|
|
164
|
+
for (const it of items) {
|
|
165
|
+
const origin = (it.originator || '?') + (it.subagent ? '(子代理)' : '');
|
|
166
|
+
console.log(
|
|
167
|
+
`${it.date.padEnd(18)} ${it.dirName.padEnd(20).slice(0, 20)} ${origin.padEnd(16).slice(0, 16)} ${it.preview || ''}`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
console.log(`\n会话文件位于 ${path.join(CODEX_DIR, 'sessions')}(-n 条数 / --dir 按目录关键字过滤)`);
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
128
173
|
case 'update': {
|
|
129
174
|
const r = await checkUpdate(VERSION);
|
|
130
175
|
print(r.lines);
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// sessions:浏览 ~/.codex/sessions 下的历史会话(只读)
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { CODEX_DIR, exists, walkFiles } from './util.mjs';
|
|
5
|
+
|
|
6
|
+
// 只读每个文件头部 64KB:meta 在第 1 行,真实提问通常也在最前面
|
|
7
|
+
function readHead(file, bytes = 65536) {
|
|
8
|
+
const fd = fs.openSync(file, 'r');
|
|
9
|
+
try {
|
|
10
|
+
const buf = Buffer.alloc(bytes);
|
|
11
|
+
const n = fs.readSync(fd, buf, 0, bytes, 0);
|
|
12
|
+
return buf.subarray(0, n).toString('utf8');
|
|
13
|
+
} finally {
|
|
14
|
+
fs.closeSync(fd);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function extract(head) {
|
|
19
|
+
let meta = null;
|
|
20
|
+
let preview = null;
|
|
21
|
+
for (const line of head.split('\n')) {
|
|
22
|
+
if (!line.trim()) continue;
|
|
23
|
+
let j;
|
|
24
|
+
try {
|
|
25
|
+
j = JSON.parse(line);
|
|
26
|
+
} catch {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (!meta && j.type === 'session_meta' && j.payload) {
|
|
30
|
+
meta = {
|
|
31
|
+
date: (j.payload.timestamp || '').slice(0, 16).replace('T', ' '),
|
|
32
|
+
cwd: j.payload.cwd || '',
|
|
33
|
+
originator: j.payload.originator || '',
|
|
34
|
+
subagent: Boolean(j.payload.source?.subagent),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (
|
|
38
|
+
j.type === 'response_item' &&
|
|
39
|
+
j.payload?.type === 'message' &&
|
|
40
|
+
j.payload?.role === 'user'
|
|
41
|
+
) {
|
|
42
|
+
const text = (j.payload.content || []).find((c) => c.text)?.text || '';
|
|
43
|
+
// 跳过环境上下文/项目指令包装,找真实提问
|
|
44
|
+
if (!text.startsWith('<environment_context>') && !text.startsWith('<user_instructions>')) {
|
|
45
|
+
preview = text.replace(/\s+/g, ' ').trim().slice(0, 70);
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return { meta, preview };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function listSessions({ limit = 10, cwdFilter = null } = {}) {
|
|
54
|
+
const root = path.join(CODEX_DIR, 'sessions');
|
|
55
|
+
if (!exists(root)) return [];
|
|
56
|
+
const items = [];
|
|
57
|
+
for (const f of walkFiles(root)) {
|
|
58
|
+
if (!f.endsWith('.jsonl')) continue;
|
|
59
|
+
try {
|
|
60
|
+
const st = fs.statSync(f);
|
|
61
|
+
const { meta, preview } = extract(readHead(f));
|
|
62
|
+
if (!meta) continue;
|
|
63
|
+
items.push({
|
|
64
|
+
date: meta.date || st.mtime.toISOString().slice(0, 16).replace('T', ' '),
|
|
65
|
+
dirName: meta.cwd ? path.basename(meta.cwd) : '?',
|
|
66
|
+
cwd: meta.cwd,
|
|
67
|
+
originator: meta.originator || '',
|
|
68
|
+
subagent: meta.subagent,
|
|
69
|
+
preview,
|
|
70
|
+
mtimeMs: st.mtimeMs,
|
|
71
|
+
});
|
|
72
|
+
} catch {
|
|
73
|
+
/* 单个文件损坏不影响整体列表 */
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
items.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
77
|
+
const filtered = cwdFilter
|
|
78
|
+
? items.filter((it) => it.cwd.toLowerCase().includes(String(cwdFilter).toLowerCase()))
|
|
79
|
+
: items;
|
|
80
|
+
return filtered.slice(0, Math.max(Number(limit) || 10, 1));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 关键词搜索:默认读每个文件前 256KB(浅搜),--deep 全文扫描
|
|
84
|
+
export function searchSessions({ keyword, limit = 10, deep = false } = {}) {
|
|
85
|
+
const root = path.join(CODEX_DIR, 'sessions');
|
|
86
|
+
if (!exists(root) || !keyword) return [];
|
|
87
|
+
const kw = String(keyword).toLowerCase();
|
|
88
|
+
const files = walkFiles(root)
|
|
89
|
+
.filter((f) => f.endsWith('.jsonl'))
|
|
90
|
+
.map((f) => ({ f, mtimeMs: fs.statSync(f).mtimeMs }))
|
|
91
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
92
|
+
|
|
93
|
+
const results = [];
|
|
94
|
+
for (const { f, mtimeMs } of files) {
|
|
95
|
+
let content;
|
|
96
|
+
try {
|
|
97
|
+
content = deep ? fs.readFileSync(f, 'utf8') : readHead(f, 262144);
|
|
98
|
+
} catch {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const at = content.toLowerCase().indexOf(kw);
|
|
102
|
+
if (at === -1) continue;
|
|
103
|
+
|
|
104
|
+
// 定位匹配行,尽量抽出对话文本片段
|
|
105
|
+
const lineEnd = content.indexOf('\n', at) === -1 ? content.length : content.indexOf('\n', at);
|
|
106
|
+
const line = content.slice(content.lastIndexOf('\n', at) + 1, lineEnd);
|
|
107
|
+
let snippet = '';
|
|
108
|
+
try {
|
|
109
|
+
const j = JSON.parse(line);
|
|
110
|
+
const text =
|
|
111
|
+
(j.payload?.content || []).find((c) => c.text)?.text || j.payload?.text || line;
|
|
112
|
+
const t = text.toLowerCase().indexOf(kw);
|
|
113
|
+
snippet = (t >= 0 ? text.slice(Math.max(0, t - 30), t + 70) : text.slice(0, 90)).replace(/\s+/g, ' ').trim();
|
|
114
|
+
} catch {
|
|
115
|
+
snippet = line.slice(0, 90).replace(/\s+/g, ' ').trim();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const { meta } = extract(content);
|
|
119
|
+
results.push({
|
|
120
|
+
date: (meta?.date) || new Date(mtimeMs).toISOString().slice(0, 16).replace('T', ' '),
|
|
121
|
+
dirName: meta?.cwd ? path.basename(meta.cwd) : '?',
|
|
122
|
+
snippet: snippet || '(匹配在元数据中)',
|
|
123
|
+
mtimeMs,
|
|
124
|
+
});
|
|
125
|
+
if (results.length >= Math.max(Number(limit) || 10, 1)) break;
|
|
126
|
+
}
|
|
127
|
+
return results;
|
|
128
|
+
}
|