@liuyoumi/codex-history 0.1.4 → 0.3.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/CHANGELOG.md +12 -0
- package/README.en.md +9 -8
- package/README.md +9 -8
- package/dist/cli.js +157 -63
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0 - 2026-05-29
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Allow `purge` to accept multiple thread ids or unique short id prefixes in one guarded batch.
|
|
8
|
+
|
|
9
|
+
## 0.2.0 - 2026-05-29
|
|
10
|
+
|
|
11
|
+
### Removed
|
|
12
|
+
|
|
13
|
+
- Remove the public `--json` output mode so the CLI stays focused on human-reviewed cleanup workflows.
|
|
14
|
+
|
|
3
15
|
## 0.1.4 - 2026-05-29
|
|
4
16
|
|
|
5
17
|
### Added
|
package/README.en.md
CHANGED
|
@@ -51,6 +51,7 @@ codex-history doctor
|
|
|
51
51
|
codex-history list
|
|
52
52
|
codex-history list --grep "Astro"
|
|
53
53
|
codex-history purge 019e6885
|
|
54
|
+
codex-history purge 019e6885 019e6874
|
|
54
55
|
codex-history purge-orphans
|
|
55
56
|
```
|
|
56
57
|
|
|
@@ -75,7 +76,7 @@ Type 019e6885 to confirm:
|
|
|
75
76
|
| `codex-history doctor` | Check whether the local Codex data layout is supported. |
|
|
76
77
|
| `codex-history list` | List local conversations. |
|
|
77
78
|
| `codex-history list --grep <keyword>` | Filter conversations by title, id, or cwd. |
|
|
78
|
-
| `codex-history purge <id
|
|
79
|
+
| `codex-history purge <id...>` | Remove one or more resolved local conversations after confirmation. |
|
|
79
80
|
| `codex-history purge-orphans` | Remove orphaned local data after confirmation. |
|
|
80
81
|
|
|
81
82
|
### `doctor`
|
|
@@ -129,19 +130,23 @@ When `list` runs in an interactive terminal without `--limit`, output is sent th
|
|
|
129
130
|
|
|
130
131
|
### `purge`
|
|
131
132
|
|
|
132
|
-
Remove one local Codex
|
|
133
|
+
Remove one or more local Codex conversations by full id or unique short id prefix.
|
|
133
134
|
|
|
134
135
|
```bash
|
|
135
136
|
codex-history purge 019e6885
|
|
137
|
+
codex-history purge 019e6885 019e6874
|
|
136
138
|
```
|
|
137
139
|
|
|
140
|
+
When multiple targets are provided, the tool resolves all targets and prints a batch plan before deletion. If any id is missing, ambiguous, or active, the whole batch is refused before mutation. Interactive batch purge requires typing `purge-selected`.
|
|
141
|
+
|
|
138
142
|
For scripts or non-interactive shells, use `--force`:
|
|
139
143
|
|
|
140
144
|
```bash
|
|
141
145
|
codex-history purge 019e6885 --force
|
|
146
|
+
codex-history purge 019e6885 019e6874 --force
|
|
142
147
|
```
|
|
143
148
|
|
|
144
|
-
`--force` skips only
|
|
149
|
+
`--force` skips only interactive confirmation. It still keeps schema validation, active-thread protection, and post-purge verification.
|
|
145
150
|
|
|
146
151
|
### `purge-orphans`
|
|
147
152
|
|
|
@@ -159,7 +164,7 @@ For scripts or non-interactive shells, use `--force`:
|
|
|
159
164
|
codex-history purge-orphans --force
|
|
160
165
|
```
|
|
161
166
|
|
|
162
|
-
`--force` skips only interactive confirmation. It still keeps schema validation, active-thread protection, and post-purge verification.
|
|
167
|
+
`--force` skips only interactive confirmation. It still keeps schema validation, active-thread protection, and post-purge verification.
|
|
163
168
|
|
|
164
169
|
Space reporting is an estimate. After SQLite rows are deleted, database files may not shrink until Codex or another SQLite maintenance step runs vacuum.
|
|
165
170
|
|
|
@@ -167,13 +172,9 @@ Space reporting is an estimate. After SQLite rows are deleted, database files ma
|
|
|
167
172
|
|
|
168
173
|
```bash
|
|
169
174
|
codex-history --codex-home /path/to/.codex list
|
|
170
|
-
codex-history --json list --grep "Astro"
|
|
171
|
-
codex-history --json purge 019e6885 --force
|
|
172
175
|
```
|
|
173
176
|
|
|
174
177
|
- `--codex-home` defaults to `~/.codex`.
|
|
175
|
-
- `--json` prints machine-readable output. For `purge`, JSON output requires `--force` because interactive confirmation is text-only.
|
|
176
|
-
- `purge-orphans` does not support JSON output.
|
|
177
178
|
- Color is enabled only in interactive terminals and respects `NO_COLOR`.
|
|
178
179
|
|
|
179
180
|
## Safety
|
package/README.md
CHANGED
|
@@ -51,6 +51,7 @@ codex-history doctor
|
|
|
51
51
|
codex-history list
|
|
52
52
|
codex-history list --grep "Astro"
|
|
53
53
|
codex-history purge 019e6885
|
|
54
|
+
codex-history purge 019e6885 019e6874
|
|
54
55
|
codex-history purge-orphans
|
|
55
56
|
```
|
|
56
57
|
|
|
@@ -75,7 +76,7 @@ Type 019e6885 to confirm:
|
|
|
75
76
|
| `codex-history doctor` | 检查当前本地 Codex 数据结构是否受支持。 |
|
|
76
77
|
| `codex-history list` | 列出本地对话。 |
|
|
77
78
|
| `codex-history list --grep <keyword>` | 按标题、id 或 cwd 过滤对话。 |
|
|
78
|
-
| `codex-history purge <id
|
|
79
|
+
| `codex-history purge <id...>` | 确认后删除一条或多条解析到的本地对话。 |
|
|
79
80
|
| `codex-history purge-orphans` | 确认后清理本地孤儿数据。 |
|
|
80
81
|
|
|
81
82
|
### `doctor`
|
|
@@ -129,19 +130,23 @@ codex-history list --grep "Astro 博客"
|
|
|
129
130
|
|
|
130
131
|
### `purge`
|
|
131
132
|
|
|
132
|
-
通过完整 id 或唯一短 id
|
|
133
|
+
通过完整 id 或唯一短 id 前缀删除一条或多条本地 Codex 对话。
|
|
133
134
|
|
|
134
135
|
```bash
|
|
135
136
|
codex-history purge 019e6885
|
|
137
|
+
codex-history purge 019e6885 019e6874
|
|
136
138
|
```
|
|
137
139
|
|
|
140
|
+
删除多个目标时,工具会先解析全部目标并展示批量计划;只要任意一个 id 不存在、不唯一,或命中 active thread,整个批量操作都会拒绝执行,不会部分删除。交互式批量删除需要输入 `purge-selected` 确认。
|
|
141
|
+
|
|
138
142
|
脚本或非交互环境可以使用 `--force`:
|
|
139
143
|
|
|
140
144
|
```bash
|
|
141
145
|
codex-history purge 019e6885 --force
|
|
146
|
+
codex-history purge 019e6885 019e6874 --force
|
|
142
147
|
```
|
|
143
148
|
|
|
144
|
-
`--force`
|
|
149
|
+
`--force` 只跳过交互确认,不会跳过数据结构校验、active thread 保护和删除后的验证。
|
|
145
150
|
|
|
146
151
|
### `purge-orphans`
|
|
147
152
|
|
|
@@ -159,7 +164,7 @@ codex-history purge-orphans
|
|
|
159
164
|
codex-history purge-orphans --force
|
|
160
165
|
```
|
|
161
166
|
|
|
162
|
-
`--force` 只跳过交互确认,不会跳过数据结构校验、active thread
|
|
167
|
+
`--force` 只跳过交互确认,不会跳过数据结构校验、active thread 保护和删除后的验证。
|
|
163
168
|
|
|
164
169
|
空间统计是估算值。SQLite 删除记录后,数据库文件可能不会立刻缩小,直到 Codex 或其他 SQLite 维护步骤执行 vacuum。
|
|
165
170
|
|
|
@@ -167,13 +172,9 @@ codex-history purge-orphans --force
|
|
|
167
172
|
|
|
168
173
|
```bash
|
|
169
174
|
codex-history --codex-home /path/to/.codex list
|
|
170
|
-
codex-history --json list --grep "Astro"
|
|
171
|
-
codex-history --json purge 019e6885 --force
|
|
172
175
|
```
|
|
173
176
|
|
|
174
177
|
- `--codex-home` 默认是 `~/.codex`。
|
|
175
|
-
- `--json` 输出机器可读的 JSON。`purge` 使用 JSON 输出时必须加 `--force`,因为交互确认只适合文本模式。
|
|
176
|
-
- `purge-orphans` 不支持 JSON 输出。
|
|
177
178
|
- 颜色只会在交互式终端中启用,并遵守 `NO_COLOR`。
|
|
178
179
|
|
|
179
180
|
## 安全机制
|
package/dist/cli.js
CHANGED
|
@@ -837,15 +837,47 @@ function findShellSnapshots(shellSnapshotsDir, threadId) {
|
|
|
837
837
|
}
|
|
838
838
|
|
|
839
839
|
// src/commands/purge.ts
|
|
840
|
-
function
|
|
840
|
+
function planBatchPurgeCommand(paths, threadIds) {
|
|
841
841
|
validateSupportedDataModel(paths);
|
|
842
|
-
const
|
|
843
|
-
|
|
842
|
+
const plans = [];
|
|
843
|
+
const seenTargets = /* @__PURE__ */ new Set();
|
|
844
|
+
const duplicateInputs = [];
|
|
845
|
+
for (const threadId of threadIds) {
|
|
846
|
+
const target = resolvePurgeTarget(paths, threadId);
|
|
847
|
+
if (seenTargets.has(target.id)) {
|
|
848
|
+
duplicateInputs.push({ input: threadId, targetId: target.id });
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
seenTargets.add(target.id);
|
|
852
|
+
plans.push(buildPurgePlan(paths, target));
|
|
853
|
+
}
|
|
854
|
+
return {
|
|
855
|
+
mode: "planned",
|
|
856
|
+
requestedCount: threadIds.length,
|
|
857
|
+
plans,
|
|
858
|
+
duplicateInputs
|
|
859
|
+
};
|
|
844
860
|
}
|
|
845
861
|
function executePurgePlanCommand(paths, plan) {
|
|
846
862
|
validateSupportedDataModel(paths);
|
|
847
863
|
return executePurge(paths, plan);
|
|
848
864
|
}
|
|
865
|
+
function executeBatchPurgePlanCommand(paths, plan) {
|
|
866
|
+
validateSupportedDataModel(paths);
|
|
867
|
+
const activeThreadChecks = plan.plans.flatMap((item) => assertThreadIsNotActive(item.target));
|
|
868
|
+
const purgeReports = plan.plans.map((item) => executePurge(paths, item, { skipActiveThreadCheck: true }));
|
|
869
|
+
const remainingReferences = purgeReports.flatMap((report) => report.verification.remainingReferences);
|
|
870
|
+
return {
|
|
871
|
+
mode: "executed",
|
|
872
|
+
plan,
|
|
873
|
+
activeThreadChecks,
|
|
874
|
+
purgeReports,
|
|
875
|
+
verification: {
|
|
876
|
+
success: remainingReferences.length === 0,
|
|
877
|
+
remainingReferences
|
|
878
|
+
}
|
|
879
|
+
};
|
|
880
|
+
}
|
|
849
881
|
|
|
850
882
|
// src/core/orphans.ts
|
|
851
883
|
import { existsSync as existsSync10 } from "fs";
|
|
@@ -1049,11 +1081,7 @@ function executePurgeOrphansPlanCommand(paths, plan) {
|
|
|
1049
1081
|
}
|
|
1050
1082
|
|
|
1051
1083
|
// src/core/output.ts
|
|
1052
|
-
function printOutput(value
|
|
1053
|
-
if (mode === "json") {
|
|
1054
|
-
console.log(JSON.stringify(value, null, 2));
|
|
1055
|
-
return;
|
|
1056
|
-
}
|
|
1084
|
+
function printOutput(value) {
|
|
1057
1085
|
if (typeof value === "string") {
|
|
1058
1086
|
if (value.length === 0) {
|
|
1059
1087
|
return;
|
|
@@ -1077,7 +1105,7 @@ function shortId(id) {
|
|
|
1077
1105
|
var TITLE_MAX_LENGTH = 80;
|
|
1078
1106
|
var packageVersion = JSON.parse(readFileSync5(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
1079
1107
|
var program = new Command();
|
|
1080
|
-
program.name("codex-history").description("Inspect and safely purge local Codex conversation history.").version(packageVersion).option("--codex-home <path>", "Path to Codex home directory", "~/.codex")
|
|
1108
|
+
program.name("codex-history").description("Inspect and safely purge local Codex conversation history.").version(packageVersion).option("--codex-home <path>", "Path to Codex home directory", "~/.codex");
|
|
1081
1109
|
program.command("doctor").description("Check whether the local Codex data model is supported.").action(
|
|
1082
1110
|
() => runCommand(() => {
|
|
1083
1111
|
const report = doctorCommand(currentPaths());
|
|
@@ -1102,25 +1130,26 @@ program.command("list").description("List local Codex conversations.").option("-
|
|
|
1102
1130
|
)
|
|
1103
1131
|
)
|
|
1104
1132
|
);
|
|
1105
|
-
program.command("purge").argument("<
|
|
1106
|
-
(
|
|
1133
|
+
program.command("purge").argument("<threadIds...>", "Codex thread id(s) or unique short id prefix(es) to purge").option("--force", "Skip interactive confirmation").description("Purge local Codex conversation(s) after target confirmation.").action(
|
|
1134
|
+
(threadIds, options) => runCommand(async () => {
|
|
1107
1135
|
const paths = currentPaths();
|
|
1108
|
-
const plan =
|
|
1136
|
+
const plan = planBatchPurgeCommand(paths, threadIds);
|
|
1109
1137
|
const force = Boolean(options.force);
|
|
1110
|
-
|
|
1111
|
-
|
|
1138
|
+
const useBatchConfirmation = plan.requestedCount > 1;
|
|
1139
|
+
if (plan.plans.length === 1 && !useBatchConfirmation) {
|
|
1140
|
+
if (!force) {
|
|
1141
|
+
await confirmPurge(plan.plans[0]);
|
|
1142
|
+
}
|
|
1143
|
+
return formatPurgeResult(executePurgePlanCommand(paths, plan.plans[0]));
|
|
1112
1144
|
}
|
|
1113
1145
|
if (!force) {
|
|
1114
|
-
await
|
|
1146
|
+
await confirmBatchPurge(plan);
|
|
1115
1147
|
}
|
|
1116
|
-
return
|
|
1148
|
+
return formatBatchPurgeResult(executeBatchPurgePlanCommand(paths, plan));
|
|
1117
1149
|
})
|
|
1118
1150
|
);
|
|
1119
1151
|
program.command("purge-orphans").option("--force", "Skip interactive confirmation").description("Purge orphaned local Codex data after target confirmation.").action(
|
|
1120
1152
|
(options) => runCommand(async () => {
|
|
1121
|
-
if (currentOutputModeIsJson()) {
|
|
1122
|
-
throw new UsageError("purge-orphans does not support JSON output.");
|
|
1123
|
-
}
|
|
1124
1153
|
const paths = currentPaths();
|
|
1125
1154
|
const plan = planPurgeOrphansCommand(paths);
|
|
1126
1155
|
const force = Boolean(options.force);
|
|
@@ -1134,23 +1163,16 @@ program.command("purge-orphans").option("--force", "Skip interactive confirmatio
|
|
|
1134
1163
|
})
|
|
1135
1164
|
);
|
|
1136
1165
|
await program.parseAsync();
|
|
1137
|
-
function currentOutputMode() {
|
|
1138
|
-
return program.opts().json ? "json" : "text";
|
|
1139
|
-
}
|
|
1140
1166
|
function currentPaths() {
|
|
1141
1167
|
return resolvePaths(program.opts().codexHome);
|
|
1142
1168
|
}
|
|
1143
1169
|
async function runCommand(produce) {
|
|
1144
1170
|
try {
|
|
1145
|
-
printOutput(await produce()
|
|
1171
|
+
printOutput(await produce());
|
|
1146
1172
|
} catch (error) {
|
|
1147
1173
|
const exitCode = error instanceof CodexHistoryError ? error.exitCode : 1;
|
|
1148
1174
|
const message = error instanceof Error ? error.message : String(error);
|
|
1149
|
-
|
|
1150
|
-
console.error(JSON.stringify({ error: message, exitCode }, null, 2));
|
|
1151
|
-
} else {
|
|
1152
|
-
console.error(`${colorizeError("red", "Error:")} ${message}`);
|
|
1153
|
-
}
|
|
1175
|
+
console.error(`${colorizeError("red", "Error:")} ${message}`);
|
|
1154
1176
|
process.exitCode = exitCode;
|
|
1155
1177
|
}
|
|
1156
1178
|
}
|
|
@@ -1168,10 +1190,7 @@ function parsePretty(value) {
|
|
|
1168
1190
|
throw new Error(`Expected pretty format oneline, medium, or full; got: ${value}`);
|
|
1169
1191
|
}
|
|
1170
1192
|
function shouldUsePager(options) {
|
|
1171
|
-
return Boolean(options.limit === void 0 && process.stdout.isTTY
|
|
1172
|
-
}
|
|
1173
|
-
function currentOutputModeIsJson() {
|
|
1174
|
-
return currentOutputMode() === "json";
|
|
1193
|
+
return Boolean(options.limit === void 0 && process.stdout.isTTY);
|
|
1175
1194
|
}
|
|
1176
1195
|
async function confirmPurge(plan) {
|
|
1177
1196
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
@@ -1192,6 +1211,25 @@ async function confirmPurge(plan) {
|
|
|
1192
1211
|
readline.close();
|
|
1193
1212
|
}
|
|
1194
1213
|
}
|
|
1214
|
+
async function confirmBatchPurge(plan) {
|
|
1215
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
1216
|
+
throw new SafetyRefusalError("Batch purge requires an interactive terminal. Use --force to skip confirmation.");
|
|
1217
|
+
}
|
|
1218
|
+
const expected = "purge-selected";
|
|
1219
|
+
process.stdout.write(formatBatchPurgeConfirmation(plan));
|
|
1220
|
+
const readline = createInterface({
|
|
1221
|
+
input: process.stdin,
|
|
1222
|
+
output: process.stdout
|
|
1223
|
+
});
|
|
1224
|
+
try {
|
|
1225
|
+
const answer = await readline.question(`Type ${colorize("yellow", expected)} to confirm: `);
|
|
1226
|
+
if (answer.trim() !== expected) {
|
|
1227
|
+
throw new SafetyRefusalError("Confirmation did not match. No local Codex data was modified.");
|
|
1228
|
+
}
|
|
1229
|
+
} finally {
|
|
1230
|
+
readline.close();
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1195
1233
|
async function confirmPurgeOrphans(plan) {
|
|
1196
1234
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
1197
1235
|
throw new SafetyRefusalError("purge-orphans requires an interactive terminal. Use --force to skip confirmation.");
|
|
@@ -1212,9 +1250,6 @@ async function confirmPurgeOrphans(plan) {
|
|
|
1212
1250
|
}
|
|
1213
1251
|
}
|
|
1214
1252
|
function formatDoctor(report) {
|
|
1215
|
-
if (currentOutputMode() === "json") {
|
|
1216
|
-
return report;
|
|
1217
|
-
}
|
|
1218
1253
|
const lines = [
|
|
1219
1254
|
`${colorize("dim", "Codex home:")} ${colorize("dim", report.codexHome)}`,
|
|
1220
1255
|
`${colorize("dim", "Supported:")} ${report.supported ? colorize("green", "yes") : colorize("red", "no")}`,
|
|
@@ -1226,9 +1261,6 @@ function formatDoctor(report) {
|
|
|
1226
1261
|
return lines.join("\n");
|
|
1227
1262
|
}
|
|
1228
1263
|
function formatThreads(threads, pretty = "oneline", usePager = false) {
|
|
1229
|
-
if (currentOutputMode() === "json") {
|
|
1230
|
-
return { count: threads.length, threads: threads.map(toPublicThread) };
|
|
1231
|
-
}
|
|
1232
1264
|
if (threads.length === 0) {
|
|
1233
1265
|
return "No Codex conversations found.";
|
|
1234
1266
|
}
|
|
@@ -1274,9 +1306,6 @@ function pageText(text) {
|
|
|
1274
1306
|
return "";
|
|
1275
1307
|
}
|
|
1276
1308
|
function formatPurgeResult(result) {
|
|
1277
|
-
if (currentOutputMode() === "json") {
|
|
1278
|
-
return sanitizePurgeResult(result);
|
|
1279
|
-
}
|
|
1280
1309
|
const lines = [
|
|
1281
1310
|
colorize("green", "Purge executed."),
|
|
1282
1311
|
"",
|
|
@@ -1306,6 +1335,38 @@ function formatPurgeResult(result) {
|
|
|
1306
1335
|
}
|
|
1307
1336
|
return lines.join("\n");
|
|
1308
1337
|
}
|
|
1338
|
+
function formatBatchPurgeResult(result) {
|
|
1339
|
+
const sqliteChanges = aggregateBatchSqliteChanges(result);
|
|
1340
|
+
const jsonChanged = result.purgeReports.flatMap((report) => report.json).filter((change) => change.changed).length;
|
|
1341
|
+
const filesDeleted = result.purgeReports.flatMap((report) => report.files).filter((change) => change.deleted).length;
|
|
1342
|
+
const lines = [
|
|
1343
|
+
colorize("green", "Batch purge executed."),
|
|
1344
|
+
"",
|
|
1345
|
+
"Summary:",
|
|
1346
|
+
`- Requested targets: ${result.plan.requestedCount}`,
|
|
1347
|
+
`- Unique targets purged: ${result.plan.plans.length}`,
|
|
1348
|
+
`- Duplicate inputs skipped: ${result.plan.duplicateInputs.length}`,
|
|
1349
|
+
`- SQLite row changes: ${sqliteChanges.reduce((sum, change) => sum + change.rows, 0)}`,
|
|
1350
|
+
`- JSON/JSONL file changes: ${jsonChanged}`,
|
|
1351
|
+
`- Files deleted: ${filesDeleted}`,
|
|
1352
|
+
"",
|
|
1353
|
+
"SQLite changes:",
|
|
1354
|
+
...formatSqliteRows(sqliteChanges),
|
|
1355
|
+
"",
|
|
1356
|
+
`Verification: ${result.verification.success ? colorize("green", "passed") : colorize("red", "failed")}`
|
|
1357
|
+
];
|
|
1358
|
+
if (result.verification.remainingReferences.length > 0) {
|
|
1359
|
+
lines.push(
|
|
1360
|
+
"",
|
|
1361
|
+
"Remaining references:",
|
|
1362
|
+
...result.verification.remainingReferences.map(
|
|
1363
|
+
(reference) => `- ${reference.store}: ${reference.path} (${reference.detail})`
|
|
1364
|
+
)
|
|
1365
|
+
);
|
|
1366
|
+
process.exitCode = 1;
|
|
1367
|
+
}
|
|
1368
|
+
return lines.join("\n");
|
|
1369
|
+
}
|
|
1309
1370
|
function formatPurgeOrphansPlan(plan) {
|
|
1310
1371
|
if (!hasPurgeOrphansWork(plan)) {
|
|
1311
1372
|
return "No orphaned local Codex data found.";
|
|
@@ -1359,6 +1420,22 @@ function formatPurgeConfirmation(plan) {
|
|
|
1359
1420
|
""
|
|
1360
1421
|
].join("\n");
|
|
1361
1422
|
}
|
|
1423
|
+
function formatBatchPurgeConfirmation(plan) {
|
|
1424
|
+
return [
|
|
1425
|
+
"About to purge selected local Codex conversations:",
|
|
1426
|
+
"",
|
|
1427
|
+
`Requested targets: ${plan.requestedCount}`,
|
|
1428
|
+
`Unique targets: ${plan.plans.length}`,
|
|
1429
|
+
`Duplicate inputs skipped: ${plan.duplicateInputs.length}`,
|
|
1430
|
+
"",
|
|
1431
|
+
"SQLite row changes:",
|
|
1432
|
+
...formatSqliteRows(summarizePlannedSqliteRows(plan.plans)),
|
|
1433
|
+
"",
|
|
1434
|
+
...formatBatchPurgeExamples(plan),
|
|
1435
|
+
colorize("dim", "This cannot be undone."),
|
|
1436
|
+
""
|
|
1437
|
+
].join("\n");
|
|
1438
|
+
}
|
|
1362
1439
|
function formatPurgeOrphansConfirmation(plan) {
|
|
1363
1440
|
return [
|
|
1364
1441
|
"About to purge orphaned local Codex data:",
|
|
@@ -1402,6 +1479,44 @@ function formatOrphanExamples(plan) {
|
|
|
1402
1479
|
}
|
|
1403
1480
|
return lines;
|
|
1404
1481
|
}
|
|
1482
|
+
function formatBatchPurgeExamples(plan) {
|
|
1483
|
+
const examples = plan.plans.slice(0, 5).map((item) => {
|
|
1484
|
+
const target = item.target;
|
|
1485
|
+
return `- ${shortId(target.id)} ${displayTitle(target.title)} ${colorize("dim", target.cwd)}`;
|
|
1486
|
+
});
|
|
1487
|
+
if (examples.length === 0) {
|
|
1488
|
+
return [];
|
|
1489
|
+
}
|
|
1490
|
+
const lines = ["Targets:", ...examples, ""];
|
|
1491
|
+
if (plan.plans.length > examples.length) {
|
|
1492
|
+
lines.push(`Showing ${examples.length} of ${plan.plans.length} target(s).`, "");
|
|
1493
|
+
}
|
|
1494
|
+
return lines;
|
|
1495
|
+
}
|
|
1496
|
+
function summarizePlannedSqliteRows(plans) {
|
|
1497
|
+
const rows = /* @__PURE__ */ new Map();
|
|
1498
|
+
for (const plan of plans) {
|
|
1499
|
+
for (const store of plan.stores) {
|
|
1500
|
+
if (store.action !== "delete_rows" && store.store !== "state_db.agent_job_items") {
|
|
1501
|
+
continue;
|
|
1502
|
+
}
|
|
1503
|
+
if (!store.count || store.count <= 0) {
|
|
1504
|
+
continue;
|
|
1505
|
+
}
|
|
1506
|
+
rows.set(store.store, (rows.get(store.store) ?? 0) + store.count);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
return [...rows.entries()].map(([store, count]) => ({ store, rows: count })).sort((a, b) => a.store.localeCompare(b.store));
|
|
1510
|
+
}
|
|
1511
|
+
function aggregateBatchSqliteChanges(result) {
|
|
1512
|
+
const changes = /* @__PURE__ */ new Map();
|
|
1513
|
+
for (const report of result.purgeReports) {
|
|
1514
|
+
for (const change of report.sqlite) {
|
|
1515
|
+
changes.set(change.store, (changes.get(change.store) ?? 0) + change.changedRows);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
return [...changes.entries()].filter(([, rows]) => rows > 0).map(([store, rows]) => ({ store, rows })).sort((a, b) => a.store.localeCompare(b.store));
|
|
1519
|
+
}
|
|
1405
1520
|
function aggregateOrphanSqliteChanges(result) {
|
|
1406
1521
|
const changes = /* @__PURE__ */ new Map();
|
|
1407
1522
|
for (const report of result.purgeReports) {
|
|
@@ -1473,7 +1588,7 @@ function errorColorsEnabled() {
|
|
|
1473
1588
|
return streamColorsEnabled(process.stderr);
|
|
1474
1589
|
}
|
|
1475
1590
|
function streamColorsEnabled(stream) {
|
|
1476
|
-
return !
|
|
1591
|
+
return !("NO_COLOR" in process.env) && Boolean(stream.isTTY);
|
|
1477
1592
|
}
|
|
1478
1593
|
function displayTitle(title) {
|
|
1479
1594
|
const normalized = title.trim().replaceAll(/\s+/g, " ");
|
|
@@ -1485,24 +1600,3 @@ function displayTitle(title) {
|
|
|
1485
1600
|
}
|
|
1486
1601
|
return `${normalized.slice(0, TITLE_MAX_LENGTH - 3)}...`;
|
|
1487
1602
|
}
|
|
1488
|
-
function toPublicThread(thread) {
|
|
1489
|
-
return {
|
|
1490
|
-
id: thread.id,
|
|
1491
|
-
title: displayTitle(thread.title),
|
|
1492
|
-
titleTruncated: thread.title.trim().replaceAll(/\s+/g, " ").length > TITLE_MAX_LENGTH,
|
|
1493
|
-
rolloutPath: thread.rolloutPath,
|
|
1494
|
-
createdAtMs: thread.createdAtMs,
|
|
1495
|
-
updatedAtMs: thread.updatedAtMs,
|
|
1496
|
-
cwd: thread.cwd,
|
|
1497
|
-
archived: thread.archived
|
|
1498
|
-
};
|
|
1499
|
-
}
|
|
1500
|
-
function sanitizePurgeResult(result) {
|
|
1501
|
-
return {
|
|
1502
|
-
...result,
|
|
1503
|
-
plan: {
|
|
1504
|
-
...result.plan,
|
|
1505
|
-
target: toPublicThread(result.plan.target)
|
|
1506
|
-
}
|
|
1507
|
-
};
|
|
1508
|
-
}
|