iivo-sub 0.1.2 → 0.1.4
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 +2 -0
- package/bin/iivo-sub.js +339 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,6 +35,8 @@ Backup configuration lets you choose Codex, Claude Code, Hermes, OpenClaw, or al
|
|
|
35
35
|
|
|
36
36
|
Chat record backup lets you back up common local conversation paths for Codex, Claude Code, Hermes, and OpenClaw. Restore creates a safety backup before replacing existing chat record paths.
|
|
37
37
|
|
|
38
|
+
For Codex, chat backup includes sessions, the recent-task index, history, attachments, shell snapshots, and local state files. SQLite state files are backed up with a consistent SQLite snapshot when `sqlite3` is available. Restoring a new backup keeps the original recent-task index; restoring an older Codex chat backup that only contains sessions will rebuild the recent-task index automatically. Restore also rebuilds the Codex SQLite `threads` task list from session files when possible, because recent Codex builds read the task list from `~/.codex/state_*.sqlite`.
|
|
39
|
+
|
|
38
40
|
After you enter an API key once, it is cached in `~/.iivo-sub/cache.json`. Next time the prompt shows a masked key such as `sk-x...abcd`; press Enter to reuse it or type a new key to replace it.
|
|
39
41
|
|
|
40
42
|
Existing config files are backed up under `~/.iivo-sub/backups/` before being replaced.
|
package/bin/iivo-sub.js
CHANGED
|
@@ -4,8 +4,9 @@ import fs from 'node:fs'
|
|
|
4
4
|
import os from 'node:os'
|
|
5
5
|
import path from 'node:path'
|
|
6
6
|
import readline from 'node:readline'
|
|
7
|
+
import { spawnSync } from 'node:child_process'
|
|
7
8
|
|
|
8
|
-
const VERSION = '0.1.
|
|
9
|
+
const VERSION = '0.1.4'
|
|
9
10
|
const APP_DIR = path.join(os.homedir(), '.iivo-sub')
|
|
10
11
|
const CACHE_FILE = path.join(APP_DIR, 'cache.json')
|
|
11
12
|
const SCRIPTED_INPUT = !process.stdin.isTTY ? fs.readFileSync(0, 'utf8').split(/\r?\n/) : null
|
|
@@ -319,7 +320,11 @@ function chatTargets() {
|
|
|
319
320
|
return {
|
|
320
321
|
codex: [
|
|
321
322
|
path.join(os.homedir(), '.codex', 'sessions'),
|
|
322
|
-
path.join(os.homedir(), '.codex', '
|
|
323
|
+
path.join(os.homedir(), '.codex', 'session_index.jsonl'),
|
|
324
|
+
path.join(os.homedir(), '.codex', 'history.jsonl'),
|
|
325
|
+
path.join(os.homedir(), '.codex', 'attachments'),
|
|
326
|
+
path.join(os.homedir(), '.codex', 'shell_snapshots'),
|
|
327
|
+
...matchingHomeFiles('.codex', /^(?:state|goals|logs|memories)_\d+\.sqlite$/)
|
|
323
328
|
],
|
|
324
329
|
'claude-code': [
|
|
325
330
|
path.join(os.homedir(), '.claude', 'projects'),
|
|
@@ -339,6 +344,14 @@ function chatTargets() {
|
|
|
339
344
|
}
|
|
340
345
|
}
|
|
341
346
|
|
|
347
|
+
function matchingHomeFiles(relativeDir, pattern) {
|
|
348
|
+
const dir = path.join(os.homedir(), relativeDir)
|
|
349
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) return []
|
|
350
|
+
return fs.readdirSync(dir)
|
|
351
|
+
.filter((name) => pattern.test(name))
|
|
352
|
+
.map((name) => path.join(dir, name))
|
|
353
|
+
}
|
|
354
|
+
|
|
342
355
|
function pathsForChatBackupTarget(target) {
|
|
343
356
|
const targets = chatTargets()
|
|
344
357
|
if (target === 'all') {
|
|
@@ -404,6 +417,13 @@ function copyPathToBackup(sourcePath, backupRoot) {
|
|
|
404
417
|
const backupFile = toBackupFileName(sourcePath)
|
|
405
418
|
const backupPath = path.join(backupRoot, 'files', backupFile)
|
|
406
419
|
ensureDir(path.dirname(backupPath))
|
|
420
|
+
if (stat.isFile() && sourcePath.endsWith('.sqlite') && sqliteBackup(sourcePath, backupPath)) {
|
|
421
|
+
return {
|
|
422
|
+
source: sourcePath,
|
|
423
|
+
backup: path.relative(backupRoot, backupPath),
|
|
424
|
+
type: 'sqlite'
|
|
425
|
+
}
|
|
426
|
+
}
|
|
407
427
|
fs.cpSync(sourcePath, backupPath, { recursive: true, force: true })
|
|
408
428
|
return {
|
|
409
429
|
source: sourcePath,
|
|
@@ -412,6 +432,288 @@ function copyPathToBackup(sourcePath, backupRoot) {
|
|
|
412
432
|
}
|
|
413
433
|
}
|
|
414
434
|
|
|
435
|
+
function findSqlite3() {
|
|
436
|
+
const candidates = [
|
|
437
|
+
process.env.SQLITE3,
|
|
438
|
+
'sqlite3',
|
|
439
|
+
'/usr/bin/sqlite3',
|
|
440
|
+
'/usr/local/bin/sqlite3',
|
|
441
|
+
path.join(os.homedir(), 'Android', 'Sdk', 'platform-tools', 'sqlite3')
|
|
442
|
+
].filter(Boolean)
|
|
443
|
+
for (const candidate of candidates) {
|
|
444
|
+
const result = spawnSync(candidate, ['-version'], { encoding: 'utf8' })
|
|
445
|
+
if (result.status === 0) return candidate
|
|
446
|
+
}
|
|
447
|
+
return ''
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function sqliteBackup(sourcePath, backupPath) {
|
|
451
|
+
const sqlite3 = findSqlite3()
|
|
452
|
+
if (!sqlite3) return false
|
|
453
|
+
ensureDir(path.dirname(backupPath))
|
|
454
|
+
const result = spawnSync(sqlite3, [sourcePath, `.backup '${backupPath.replaceAll("'", "''")}'`], {
|
|
455
|
+
encoding: 'utf8'
|
|
456
|
+
})
|
|
457
|
+
return result.status === 0 && fileExists(backupPath)
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function deleteSqliteSidecars(sqlitePath) {
|
|
461
|
+
for (const suffix of ['-wal', '-shm']) {
|
|
462
|
+
const sidecar = `${sqlitePath}${suffix}`
|
|
463
|
+
if (pathExists(sidecar)) {
|
|
464
|
+
deletePath(sidecar)
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function codexSessionIDFromPath(filePath) {
|
|
470
|
+
const base = path.basename(filePath, '.jsonl')
|
|
471
|
+
const match = base.match(/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i)
|
|
472
|
+
return match ? match[1] : ''
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function truncateTitle(text) {
|
|
476
|
+
let value = String(text || '')
|
|
477
|
+
const requestMatch = value.match(/(?:##\s*)?My request for Codex:\s*([\s\S]+)/i)
|
|
478
|
+
if (requestMatch) {
|
|
479
|
+
value = requestMatch[1]
|
|
480
|
+
}
|
|
481
|
+
value = value
|
|
482
|
+
.replace(/<environment_context>[\s\S]*?<\/environment_context>/g, ' ')
|
|
483
|
+
.replace(/# Context from my IDE setup:/gi, ' ')
|
|
484
|
+
.replace(/## Open tabs:[\s\S]*?(?=##|$)/gi, ' ')
|
|
485
|
+
.replace(/## Active file:[\s\S]*?(?=##|$)/gi, ' ')
|
|
486
|
+
return value
|
|
487
|
+
.replace(/\s+/g, ' ')
|
|
488
|
+
.trim()
|
|
489
|
+
.slice(0, 80)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function titleFromCodexSession(filePath) {
|
|
493
|
+
try {
|
|
494
|
+
const content = fs.readFileSync(filePath, 'utf8')
|
|
495
|
+
for (const line of content.split(/\r?\n/)) {
|
|
496
|
+
if (!line.trim()) continue
|
|
497
|
+
let item
|
|
498
|
+
try {
|
|
499
|
+
item = JSON.parse(line)
|
|
500
|
+
} catch {
|
|
501
|
+
continue
|
|
502
|
+
}
|
|
503
|
+
const payload = item?.payload
|
|
504
|
+
if (payload?.type === 'user_message' && payload.message) {
|
|
505
|
+
return truncateTitle(payload.message)
|
|
506
|
+
}
|
|
507
|
+
if (payload?.type === 'message' && payload.role === 'user' && Array.isArray(payload.content)) {
|
|
508
|
+
const text = payload.content
|
|
509
|
+
.map((part) => part?.text || part?.input_text || '')
|
|
510
|
+
.filter(Boolean)
|
|
511
|
+
.join(' ')
|
|
512
|
+
if (text) return truncateTitle(text)
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
} catch {
|
|
516
|
+
return ''
|
|
517
|
+
}
|
|
518
|
+
return ''
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function codexSessionSummary(filePath) {
|
|
522
|
+
const stat = fs.statSync(filePath)
|
|
523
|
+
const id = codexSessionIDFromPath(filePath)
|
|
524
|
+
const summary = {
|
|
525
|
+
id,
|
|
526
|
+
rollout_path: filePath,
|
|
527
|
+
created_at: stat.birthtimeMs > 0 ? stat.birthtimeMs : stat.mtimeMs,
|
|
528
|
+
updated_at: stat.mtimeMs,
|
|
529
|
+
source: 'cli',
|
|
530
|
+
model_provider: 'openai',
|
|
531
|
+
cwd: os.homedir(),
|
|
532
|
+
title: '',
|
|
533
|
+
first_user_message: '',
|
|
534
|
+
cli_version: '',
|
|
535
|
+
sandbox_policy: '{"type":"workspace-write"}',
|
|
536
|
+
approval_mode: 'on-request',
|
|
537
|
+
model: '',
|
|
538
|
+
reasoning_effort: '',
|
|
539
|
+
thread_source: ''
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
try {
|
|
543
|
+
const content = fs.readFileSync(filePath, 'utf8')
|
|
544
|
+
for (const line of content.split(/\r?\n/)) {
|
|
545
|
+
if (!line.trim()) continue
|
|
546
|
+
let item
|
|
547
|
+
try {
|
|
548
|
+
item = JSON.parse(line)
|
|
549
|
+
} catch {
|
|
550
|
+
continue
|
|
551
|
+
}
|
|
552
|
+
if (typeof item.timestamp === 'string') {
|
|
553
|
+
const ts = Date.parse(item.timestamp)
|
|
554
|
+
if (Number.isFinite(ts)) {
|
|
555
|
+
summary.updated_at = Math.max(summary.updated_at, ts)
|
|
556
|
+
if (!summary.created_at || summary.created_at > ts) summary.created_at = ts
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
const payload = item?.payload
|
|
560
|
+
if (item?.type === 'session_meta' && payload) {
|
|
561
|
+
if (payload.id && !summary.id) summary.id = String(payload.id)
|
|
562
|
+
if (payload.timestamp) {
|
|
563
|
+
const ts = Date.parse(payload.timestamp)
|
|
564
|
+
if (Number.isFinite(ts)) summary.created_at = ts
|
|
565
|
+
}
|
|
566
|
+
if (payload.cwd) summary.cwd = String(payload.cwd)
|
|
567
|
+
if (payload.source) summary.source = String(payload.source)
|
|
568
|
+
if (payload.model_provider) summary.model_provider = String(payload.model_provider)
|
|
569
|
+
if (payload.cli_version) summary.cli_version = String(payload.cli_version)
|
|
570
|
+
if (payload.model) summary.model = String(payload.model)
|
|
571
|
+
if (payload.reasoning_effort) summary.reasoning_effort = String(payload.reasoning_effort)
|
|
572
|
+
if (payload.sandbox_policy) summary.sandbox_policy = JSON.stringify(payload.sandbox_policy)
|
|
573
|
+
if (payload.approval_policy) summary.approval_mode = String(payload.approval_policy)
|
|
574
|
+
}
|
|
575
|
+
if (!summary.first_user_message && payload?.type === 'user_message' && payload.message) {
|
|
576
|
+
summary.first_user_message = truncateTitle(payload.message)
|
|
577
|
+
}
|
|
578
|
+
if (!summary.first_user_message && payload?.type === 'message' && payload.role === 'user' && Array.isArray(payload.content)) {
|
|
579
|
+
const text = payload.content
|
|
580
|
+
.map((part) => part?.text || part?.input_text || '')
|
|
581
|
+
.filter(Boolean)
|
|
582
|
+
.join(' ')
|
|
583
|
+
if (text) summary.first_user_message = truncateTitle(text)
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
} catch {
|
|
587
|
+
// Keep filesystem-derived fallback fields.
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
summary.title = summary.first_user_message || titleFromCodexSession(filePath) || summary.id || path.basename(filePath, '.jsonl')
|
|
591
|
+
if (!summary.first_user_message) summary.first_user_message = summary.title
|
|
592
|
+
return summary
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function latestTimestampFromCodexSession(filePath) {
|
|
596
|
+
let latest = ''
|
|
597
|
+
try {
|
|
598
|
+
const content = fs.readFileSync(filePath, 'utf8')
|
|
599
|
+
for (const line of content.split(/\r?\n/)) {
|
|
600
|
+
if (!line.trim()) continue
|
|
601
|
+
try {
|
|
602
|
+
const item = JSON.parse(line)
|
|
603
|
+
if (typeof item.timestamp === 'string' && item.timestamp > latest) {
|
|
604
|
+
latest = item.timestamp
|
|
605
|
+
}
|
|
606
|
+
} catch {
|
|
607
|
+
// Ignore malformed lines.
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
} catch {
|
|
611
|
+
return ''
|
|
612
|
+
}
|
|
613
|
+
return latest
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function listCodexSessionFiles(dir) {
|
|
617
|
+
if (!fs.existsSync(dir)) return []
|
|
618
|
+
const out = []
|
|
619
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
620
|
+
const fullPath = path.join(dir, entry.name)
|
|
621
|
+
if (entry.isDirectory()) {
|
|
622
|
+
out.push(...listCodexSessionFiles(fullPath))
|
|
623
|
+
} else if (entry.isFile() && entry.name.endsWith('.jsonl')) {
|
|
624
|
+
out.push(fullPath)
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
return out
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function rebuildCodexSessionIndex() {
|
|
631
|
+
const sessionsDir = path.join(os.homedir(), '.codex', 'sessions')
|
|
632
|
+
const indexPath = path.join(os.homedir(), '.codex', 'session_index.jsonl')
|
|
633
|
+
const files = listCodexSessionFiles(sessionsDir)
|
|
634
|
+
if (files.length === 0) return 0
|
|
635
|
+
|
|
636
|
+
const rows = files.map((filePath) => {
|
|
637
|
+
const id = codexSessionIDFromPath(filePath)
|
|
638
|
+
if (!id) return null
|
|
639
|
+
const title = titleFromCodexSession(filePath) || id
|
|
640
|
+
const stat = fs.statSync(filePath)
|
|
641
|
+
return {
|
|
642
|
+
id,
|
|
643
|
+
thread_name: title,
|
|
644
|
+
updated_at: latestTimestampFromCodexSession(filePath) || stat.mtime.toISOString()
|
|
645
|
+
}
|
|
646
|
+
}).filter(Boolean)
|
|
647
|
+
.sort((a, b) => b.updated_at.localeCompare(a.updated_at))
|
|
648
|
+
|
|
649
|
+
if (rows.length === 0) return 0
|
|
650
|
+
ensureDir(path.dirname(indexPath))
|
|
651
|
+
if (fileExists(indexPath)) {
|
|
652
|
+
fs.copyFileSync(indexPath, `${indexPath}.${timestampForName()}.bak`)
|
|
653
|
+
}
|
|
654
|
+
fs.writeFileSync(indexPath, rows.map((row) => JSON.stringify(row)).join('\n') + '\n', 'utf8')
|
|
655
|
+
return rows.length
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function sqlString(value) {
|
|
659
|
+
return `'${String(value ?? '').replaceAll("'", "''")}'`
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function rebuildCodexThreadsState() {
|
|
663
|
+
const sqlite3 = findSqlite3()
|
|
664
|
+
if (!sqlite3) return { rebuilt: 0, reason: 'sqlite3 not found' }
|
|
665
|
+
|
|
666
|
+
const sessionsDir = path.join(os.homedir(), '.codex', 'sessions')
|
|
667
|
+
const files = listCodexSessionFiles(sessionsDir)
|
|
668
|
+
if (files.length === 0) return { rebuilt: 0, reason: 'no sessions' }
|
|
669
|
+
|
|
670
|
+
const stateDbs = matchingHomeFiles('.codex', /^state_\d+\.sqlite$/)
|
|
671
|
+
if (stateDbs.length === 0) return { rebuilt: 0, reason: 'state sqlite not found' }
|
|
672
|
+
|
|
673
|
+
const rows = files.map(codexSessionSummary).filter((row) => row.id)
|
|
674
|
+
if (rows.length === 0) return { rebuilt: 0, reason: 'no session ids' }
|
|
675
|
+
|
|
676
|
+
let total = 0
|
|
677
|
+
for (const dbPath of stateDbs) {
|
|
678
|
+
const tableCheck = spawnSync(sqlite3, [dbPath, "select name from sqlite_master where type='table' and name='threads';"], { encoding: 'utf8' })
|
|
679
|
+
if (tableCheck.status !== 0 || !tableCheck.stdout.includes('threads')) continue
|
|
680
|
+
|
|
681
|
+
if (fileExists(dbPath)) {
|
|
682
|
+
fs.copyFileSync(dbPath, `${dbPath}.${timestampForName()}.bak`)
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const statements = ['BEGIN;', 'DELETE FROM threads;']
|
|
686
|
+
for (const row of rows) {
|
|
687
|
+
const createdMs = Math.trunc(row.created_at)
|
|
688
|
+
const updatedMs = Math.trunc(row.updated_at)
|
|
689
|
+
const created = Math.trunc(createdMs / 1000)
|
|
690
|
+
const updated = Math.trunc(updatedMs / 1000)
|
|
691
|
+
statements.push(`INSERT OR REPLACE INTO threads (
|
|
692
|
+
id, rollout_path, created_at, updated_at, source, model_provider, cwd, title,
|
|
693
|
+
sandbox_policy, approval_mode, tokens_used, has_user_event, archived,
|
|
694
|
+
cli_version, first_user_message, memory_mode, model, reasoning_effort,
|
|
695
|
+
created_at_ms, updated_at_ms, thread_source, preview
|
|
696
|
+
) VALUES (
|
|
697
|
+
${sqlString(row.id)}, ${sqlString(row.rollout_path)}, ${created}, ${updated},
|
|
698
|
+
${sqlString(row.source)}, ${sqlString(row.model_provider)}, ${sqlString(row.cwd)}, ${sqlString(row.title)},
|
|
699
|
+
${sqlString(row.sandbox_policy)}, ${sqlString(row.approval_mode)}, 0, 1, 0,
|
|
700
|
+
${sqlString(row.cli_version)}, ${sqlString(row.first_user_message)}, 'enabled',
|
|
701
|
+
${row.model ? sqlString(row.model) : 'NULL'}, ${row.reasoning_effort ? sqlString(row.reasoning_effort) : 'NULL'},
|
|
702
|
+
${createdMs}, ${updatedMs}, ${row.thread_source ? sqlString(row.thread_source) : 'NULL'}, ${sqlString(row.title)}
|
|
703
|
+
);`)
|
|
704
|
+
}
|
|
705
|
+
statements.push('COMMIT;')
|
|
706
|
+
|
|
707
|
+
const result = spawnSync(sqlite3, [dbPath], {
|
|
708
|
+
input: statements.join('\n'),
|
|
709
|
+
encoding: 'utf8',
|
|
710
|
+
maxBuffer: 10 * 1024 * 1024
|
|
711
|
+
})
|
|
712
|
+
if (result.status === 0) total += rows.length
|
|
713
|
+
}
|
|
714
|
+
return { rebuilt: total, reason: total > 0 ? '' : 'threads table not rebuilt' }
|
|
715
|
+
}
|
|
716
|
+
|
|
415
717
|
function loadBackupManifest(dir) {
|
|
416
718
|
const manifestPath = path.join(dir, 'manifest.json')
|
|
417
719
|
if (!fileExists(manifestPath)) return null
|
|
@@ -974,6 +1276,10 @@ async function restoreBackupByKind(kind, title, emptyMessage) {
|
|
|
974
1276
|
const safetyBackup = path.join(APP_DIR, 'backups', `${timestampForName()}_pre-restore_${safeBackupName(manifest.name || selected)}`)
|
|
975
1277
|
ensureDir(safetyBackup)
|
|
976
1278
|
const restored = []
|
|
1279
|
+
let restoredCodexSessions = false
|
|
1280
|
+
let restoredCodexIndex = false
|
|
1281
|
+
const codexSessionsPath = path.join(os.homedir(), '.codex', 'sessions')
|
|
1282
|
+
const codexIndexPath = path.join(os.homedir(), '.codex', 'session_index.jsonl')
|
|
977
1283
|
for (const file of manifest.files || []) {
|
|
978
1284
|
const source = file.source
|
|
979
1285
|
const backupPath = path.join(selectedDir, file.backup)
|
|
@@ -985,8 +1291,29 @@ async function restoreBackupByKind(kind, title, emptyMessage) {
|
|
|
985
1291
|
if (pathExists(source)) {
|
|
986
1292
|
deletePath(source)
|
|
987
1293
|
}
|
|
1294
|
+
if (kind === 'chat' && source.endsWith('.sqlite')) {
|
|
1295
|
+
deleteSqliteSidecars(source)
|
|
1296
|
+
}
|
|
988
1297
|
fs.cpSync(backupPath, source, { recursive: true, force: true })
|
|
1298
|
+
if (kind === 'chat' && source.endsWith('.sqlite')) {
|
|
1299
|
+
deleteSqliteSidecars(source)
|
|
1300
|
+
}
|
|
989
1301
|
restored.push(source)
|
|
1302
|
+
if (kind === 'chat' && source === codexSessionsPath) {
|
|
1303
|
+
restoredCodexSessions = true
|
|
1304
|
+
}
|
|
1305
|
+
if (kind === 'chat' && source === codexIndexPath) {
|
|
1306
|
+
restoredCodexIndex = true
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
let rebuiltCodexIndexCount = 0
|
|
1311
|
+
let rebuiltCodexThreads = { rebuilt: 0, reason: '' }
|
|
1312
|
+
if (restoredCodexSessions && !restoredCodexIndex) {
|
|
1313
|
+
rebuiltCodexIndexCount = rebuildCodexSessionIndex()
|
|
1314
|
+
}
|
|
1315
|
+
if (kind === 'chat' && restoredCodexSessions) {
|
|
1316
|
+
rebuiltCodexThreads = rebuildCodexThreadsState()
|
|
990
1317
|
}
|
|
991
1318
|
|
|
992
1319
|
clear()
|
|
@@ -996,6 +1323,16 @@ async function restoreBackupByKind(kind, title, emptyMessage) {
|
|
|
996
1323
|
for (const file of restored) {
|
|
997
1324
|
console.log(`- ${file}`)
|
|
998
1325
|
}
|
|
1326
|
+
if (rebuiltCodexIndexCount > 0) {
|
|
1327
|
+
console.log(c('cyan', `已重建 Codex 任务索引: ${rebuiltCodexIndexCount} 条`))
|
|
1328
|
+
} else if (restoredCodexIndex) {
|
|
1329
|
+
console.log(c('cyan', '已恢复 Codex 任务索引。'))
|
|
1330
|
+
}
|
|
1331
|
+
if (rebuiltCodexThreads.rebuilt > 0) {
|
|
1332
|
+
console.log(c('cyan', `已重建 Codex sqlite 任务列表: ${rebuiltCodexThreads.rebuilt} 条`))
|
|
1333
|
+
} else if (rebuiltCodexThreads.reason) {
|
|
1334
|
+
console.log(c('yellow', `未重建 Codex sqlite 任务列表: ${rebuiltCodexThreads.reason}`))
|
|
1335
|
+
}
|
|
999
1336
|
console.log()
|
|
1000
1337
|
console.log(c('yellow', `恢复前安全备份目录: ${safetyBackup}`))
|
|
1001
1338
|
await pause()
|