@qqq123456789/codex-doctor 0.6.0 → 0.8.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 CHANGED
@@ -25,7 +25,13 @@ Codex CLI 迭代很快,社区里反复出现的问题高度集中:**登录
25
25
 
26
26
  ## 快速开始:先跑一次环境自检
27
27
 
28
- 遇到任何异常,先跑自检脚本收集环境信息:
28
+ 最简单的方式(工具已发布 npm):
29
+
30
+ ```bash
31
+ npx -y @qqq123456789/codex-doctor
32
+ ```
33
+
34
+ 也可以用脚本方式(无需 Node):
29
35
 
30
36
  ```powershell
31
37
  # Windows PowerShell
@@ -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
- | `update` | 查询仓库最新发布与更新方式 | 只读 |
34
+ | `sessions [-n 10] [--dir 关键字]` | 浏览历史会话:时间、工作目录、来源、首条提问预览;`--dir` 按目录过滤,找回「上次那个对话」 | 只读 |
35
+ | `update` | 查询 npm 上工具的最新版本与更新方式 | 只读 |
35
36
 
36
37
  ## 设计原则
37
38
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qqq123456789/codex-doctor",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Codex CLI 维护与排障工具:环境自检、会话/日志归档、配置与凭据备份恢复、版本追踪(零依赖)",
5
5
  "keywords": [
6
6
  "codex",
package/tool/cli.mjs CHANGED
@@ -3,11 +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 } from './sessions.mjs';
6
+ import { listSessions, searchSessions, readTranscript } from './sessions.mjs';
7
7
  import { CODEX_DIR } from './util.mjs';
8
8
  import path from 'node:path';
9
9
 
10
- const VERSION = '0.6.0';
10
+ const VERSION = '0.8.0';
11
11
 
12
12
  const HELP = `codex-doctor v${VERSION} — Codex CLI 维护与排障工具(零依赖)
13
13
 
@@ -28,6 +28,8 @@ const HELP = `codex-doctor v${VERSION} — Codex CLI 维护与排障工具(零
28
28
  archive delete <名称|--all> 删除归档(需 --yes 或交互确认)
29
29
  versions [-n N] 查看 openai/codex 最近 N 个版本(默认 10)
30
30
  sessions [-n N] [--dir 关键字] 浏览历史会话:时间、目录、来源、首条提问预览
31
+ [--search 关键词] [--deep] 按关键词搜索会话(--deep 全文扫描)
32
+ --show [--search 关键词] [--pick N] [--full] 查看会话完整对话
31
33
  update 查询 npm 最新版本与更新方式
32
34
  help 显示本帮助
33
35
 
@@ -45,6 +47,11 @@ function parseFlags(args) {
45
47
  else if (a === '--days') flags.days = Number(args[++i]);
46
48
  else if (a === '-n' || a === '--limit') flags.limit = Number(args[++i]);
47
49
  else if (a === '--dir') flags.dir = args[++i];
50
+ else if (a === '--search') flags.search = args[++i];
51
+ else if (a === '--deep') flags.deep = true;
52
+ else if (a === '--show') flags.show = true;
53
+ else if (a === '--pick') flags.pick = Number(args[++i]);
54
+ else if (a === '--full') flags.full = true;
48
55
  else if (a === '--out') flags.out = args[++i];
49
56
  else rest.push(a);
50
57
  }
@@ -131,6 +138,49 @@ async function main() {
131
138
  break;
132
139
  }
133
140
  case 'sessions': {
141
+ if (flags.show) {
142
+ // 展示会话完整对话:默认最近一次;--search 按关键词定位;--pick N 选第 N 个命中
143
+ const items = flags.search
144
+ ? searchSessions({ keyword: flags.search, limit: 20, deep: true })
145
+ : listSessions({ limit: 20 });
146
+ if (items.length === 0) {
147
+ console.log(flags.search ? `没有找到包含「${flags.search}」的会话` : '~/.codex/sessions 里没有会话记录');
148
+ break;
149
+ }
150
+ const pick = Math.min(Math.max(Number(flags.pick) || 1, 1), items.length);
151
+ const item = items[pick - 1];
152
+ console.log(`== 会话 ${item.date} @ ${item.dirName} ==`);
153
+ console.log(`文件: ${item.file}\n`);
154
+ const transcript = readTranscript(item.file, { maxLen: flags.full ? Infinity : 400 });
155
+ if (!transcript || transcript.length === 0) {
156
+ console.log('(该会话没有可展示的对话消息)');
157
+ break;
158
+ }
159
+ for (const msg of transcript) {
160
+ const who = msg.role === 'user' ? '[用户]' : '[Codex]';
161
+ console.log(`${who} ${msg.text}\n`);
162
+ }
163
+ console.log(`(共 ${transcript.length} 条消息${flags.full ? '' : ',单条超 400 字已截断,--full 查看全文'})`);
164
+ break;
165
+ }
166
+ if (flags.search) {
167
+ const items = searchSessions({
168
+ keyword: flags.search,
169
+ limit: Number.isFinite(flags.limit) ? flags.limit : 10,
170
+ deep: flags.deep === true,
171
+ });
172
+ if (items.length === 0) {
173
+ console.log(
174
+ `没有找到包含「${flags.search}」的会话${flags.deep ? '' : '(默认只搜每个文件开头部分,可加 --deep 全文搜索)'}`
175
+ );
176
+ break;
177
+ }
178
+ console.log('时间 工作目录 匹配片段');
179
+ for (const it of items) {
180
+ console.log(`${it.date.padEnd(18)} ${it.dirName.padEnd(20).slice(0, 20)} ${it.snippet}`);
181
+ }
182
+ break;
183
+ }
134
184
  const items = listSessions({
135
185
  limit: Number.isFinite(flags.limit) ? flags.limit : 10,
136
186
  cwdFilter: flags.dir || null,
package/tool/sessions.mjs CHANGED
@@ -61,6 +61,7 @@ export function listSessions({ limit = 10, cwdFilter = null } = {}) {
61
61
  const { meta, preview } = extract(readHead(f));
62
62
  if (!meta) continue;
63
63
  items.push({
64
+ file: f,
64
65
  date: meta.date || st.mtime.toISOString().slice(0, 16).replace('T', ' '),
65
66
  dirName: meta.cwd ? path.basename(meta.cwd) : '?',
66
67
  cwd: meta.cwd,
@@ -79,3 +80,77 @@ export function listSessions({ limit = 10, cwdFilter = null } = {}) {
79
80
  : items;
80
81
  return filtered.slice(0, Math.max(Number(limit) || 10, 1));
81
82
  }
83
+
84
+ // 关键词搜索:默认读每个文件前 256KB(浅搜),--deep 全文扫描
85
+ export function searchSessions({ keyword, limit = 10, deep = false } = {}) {
86
+ const root = path.join(CODEX_DIR, 'sessions');
87
+ if (!exists(root) || !keyword) return [];
88
+ const kw = String(keyword).toLowerCase();
89
+ const files = walkFiles(root)
90
+ .filter((f) => f.endsWith('.jsonl'))
91
+ .map((f) => ({ f, mtimeMs: fs.statSync(f).mtimeMs }))
92
+ .sort((a, b) => b.mtimeMs - a.mtimeMs);
93
+
94
+ const results = [];
95
+ for (const { f, mtimeMs } of files) {
96
+ let content;
97
+ try {
98
+ content = deep ? fs.readFileSync(f, 'utf8') : readHead(f, 262144);
99
+ } catch {
100
+ continue;
101
+ }
102
+ const at = content.toLowerCase().indexOf(kw);
103
+ if (at === -1) continue;
104
+
105
+ // 定位匹配行,尽量抽出对话文本片段
106
+ const lineEnd = content.indexOf('\n', at) === -1 ? content.length : content.indexOf('\n', at);
107
+ const line = content.slice(content.lastIndexOf('\n', at) + 1, lineEnd);
108
+ let snippet = '';
109
+ try {
110
+ const j = JSON.parse(line);
111
+ const text =
112
+ (j.payload?.content || []).find((c) => c.text)?.text || j.payload?.text || line;
113
+ const t = text.toLowerCase().indexOf(kw);
114
+ snippet = (t >= 0 ? text.slice(Math.max(0, t - 30), t + 70) : text.slice(0, 90)).replace(/\s+/g, ' ').trim();
115
+ } catch {
116
+ snippet = line.slice(0, 90).replace(/\s+/g, ' ').trim();
117
+ }
118
+
119
+ const { meta } = extract(content);
120
+ results.push({
121
+ file: f,
122
+ date: (meta?.date) || new Date(mtimeMs).toISOString().slice(0, 16).replace('T', ' '),
123
+ dirName: meta?.cwd ? path.basename(meta.cwd) : '?',
124
+ snippet: snippet || '(匹配在元数据中)',
125
+ mtimeMs,
126
+ });
127
+ if (results.length >= Math.max(Number(limit) || 10, 1)) break;
128
+ }
129
+ return results;
130
+ }
131
+
132
+ // 读取单个会话的完整对话(user / assistant 消息,跳过环境包装与工具输出)
133
+ export function readTranscript(file, { maxLen = 400 } = {}) {
134
+ if (!exists(file)) return null;
135
+ const content = fs.readFileSync(file, 'utf8');
136
+ const out = [];
137
+ for (const line of content.split('\n')) {
138
+ if (!line.trim()) continue;
139
+ let j;
140
+ try {
141
+ j = JSON.parse(line);
142
+ } catch {
143
+ continue;
144
+ }
145
+ if (j.type !== 'response_item' || j.payload?.type !== 'message') continue;
146
+ const role = j.payload.role;
147
+ if (role !== 'user' && role !== 'assistant') continue;
148
+ let text = (j.payload.content || []).map((c) => c.text || '').filter(Boolean).join('\n');
149
+ if (!text) continue;
150
+ if (role === 'user' && (text.startsWith('<environment_context>') || text.startsWith('<user_instructions>'))) continue;
151
+ if (role === 'user' && text.startsWith('<permissions')) continue;
152
+ if (text.length > maxLen) text = text.slice(0, maxLen) + '…';
153
+ out.push({ role, text });
154
+ }
155
+ return out;
156
+ }