iivo-sub 0.1.5 → 0.1.7
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 +3 -1
- package/bin/iivo-sub.js +55 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,7 +35,9 @@ 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. Restore rebuilds the recent-task index from restored session files and also rebuilds the Codex SQLite `threads` task list when possible, because recent Codex builds read the task list from `~/.codex/state_*.sqlite`.
|
|
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. Restore rebuilds the recent-task index from restored session files and also rebuilds the Codex SQLite `threads` task list plus `backfill_state` when possible, because recent Codex builds read the task list from `~/.codex/state_*.sqlite`.
|
|
39
|
+
|
|
40
|
+
After restoring Codex chats in VS Code, run `Developer: Reload Window` or fully restart the VS Code/Codex window. The Codex extension keeps an in-process task-list cache, so the sidebar may continue to show the old small list until the window reloads.
|
|
39
41
|
|
|
40
42
|
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.
|
|
41
43
|
|
package/bin/iivo-sub.js
CHANGED
|
@@ -6,7 +6,7 @@ import path from 'node:path'
|
|
|
6
6
|
import readline from 'node:readline'
|
|
7
7
|
import { spawnSync } from 'node:child_process'
|
|
8
8
|
|
|
9
|
-
const VERSION = '0.1.
|
|
9
|
+
const VERSION = '0.1.7'
|
|
10
10
|
const APP_DIR = path.join(os.homedir(), '.iivo-sub')
|
|
11
11
|
const CACHE_FILE = path.join(APP_DIR, 'cache.json')
|
|
12
12
|
const SCRIPTED_INPUT = !process.stdin.isTTY ? fs.readFileSync(0, 'utf8').split(/\r?\n/) : null
|
|
@@ -518,14 +518,18 @@ function titleFromCodexSession(filePath) {
|
|
|
518
518
|
}
|
|
519
519
|
const payload = item?.payload
|
|
520
520
|
if (payload?.type === 'user_message' && payload.message) {
|
|
521
|
-
|
|
521
|
+
const title = truncateTitle(payload.message)
|
|
522
|
+
if (title) return title
|
|
522
523
|
}
|
|
523
524
|
if (payload?.type === 'message' && payload.role === 'user' && Array.isArray(payload.content)) {
|
|
524
525
|
const text = payload.content
|
|
525
526
|
.map((part) => part?.text || part?.input_text || '')
|
|
526
527
|
.filter(Boolean)
|
|
527
528
|
.join(' ')
|
|
528
|
-
if (text)
|
|
529
|
+
if (text) {
|
|
530
|
+
const title = truncateTitle(text)
|
|
531
|
+
if (title) return title
|
|
532
|
+
}
|
|
529
533
|
}
|
|
530
534
|
}
|
|
531
535
|
} catch {
|
|
@@ -592,14 +596,18 @@ function codexSessionSummary(filePath) {
|
|
|
592
596
|
if (payload.approval_policy) summary.approval_mode = String(payload.approval_policy)
|
|
593
597
|
}
|
|
594
598
|
if (!summary.first_user_message && payload?.type === 'user_message' && payload.message) {
|
|
595
|
-
|
|
599
|
+
const title = truncateTitle(payload.message)
|
|
600
|
+
if (title) summary.first_user_message = title
|
|
596
601
|
}
|
|
597
602
|
if (!summary.first_user_message && payload?.type === 'message' && payload.role === 'user' && Array.isArray(payload.content)) {
|
|
598
603
|
const text = payload.content
|
|
599
604
|
.map((part) => part?.text || part?.input_text || '')
|
|
600
605
|
.filter(Boolean)
|
|
601
606
|
.join(' ')
|
|
602
|
-
if (text)
|
|
607
|
+
if (text) {
|
|
608
|
+
const title = truncateTitle(text)
|
|
609
|
+
if (title) summary.first_user_message = title
|
|
610
|
+
}
|
|
603
611
|
}
|
|
604
612
|
}
|
|
605
613
|
if (firstEventAt && (!summary.created_at || firstEventAt < summary.created_at)) {
|
|
@@ -655,6 +663,10 @@ function listCodexSessionFiles(dir) {
|
|
|
655
663
|
return out
|
|
656
664
|
}
|
|
657
665
|
|
|
666
|
+
function codexRelativePath(filePath) {
|
|
667
|
+
return path.relative(path.join(os.homedir(), '.codex'), filePath).split(path.sep).join('/')
|
|
668
|
+
}
|
|
669
|
+
|
|
658
670
|
function rebuildCodexSessionIndex() {
|
|
659
671
|
const sessionsDir = path.join(os.homedir(), '.codex', 'sessions')
|
|
660
672
|
const indexPath = path.join(os.homedir(), '.codex', 'session_index.jsonl')
|
|
@@ -687,6 +699,25 @@ function sqlString(value) {
|
|
|
687
699
|
return `'${String(value ?? '').replaceAll("'", "''")}'`
|
|
688
700
|
}
|
|
689
701
|
|
|
702
|
+
function updateCodexBackfillState(sqlite3, dbPath, watermark) {
|
|
703
|
+
if (!watermark) return false
|
|
704
|
+
const tableCheck = spawnSync(sqlite3, [dbPath, "select name from sqlite_master where type='table' and name='backfill_state';"], {
|
|
705
|
+
encoding: 'utf8',
|
|
706
|
+
timeout: 10000
|
|
707
|
+
})
|
|
708
|
+
if (tableCheck.status !== 0 || !tableCheck.stdout.includes('backfill_state')) return false
|
|
709
|
+
|
|
710
|
+
const completedAt = Math.trunc(Date.now() / 1000)
|
|
711
|
+
const script = [
|
|
712
|
+
'BEGIN;',
|
|
713
|
+
'DELETE FROM backfill_state WHERE id = 1;',
|
|
714
|
+
`INSERT INTO backfill_state (id, status, last_watermark, last_success_at, updated_at) VALUES (1, 'complete', ${sqlString(watermark)}, ${completedAt}, ${completedAt});`,
|
|
715
|
+
'COMMIT;'
|
|
716
|
+
].join('\n')
|
|
717
|
+
const result = runSqliteScript(sqlite3, dbPath, script)
|
|
718
|
+
return result.status === 0
|
|
719
|
+
}
|
|
720
|
+
|
|
690
721
|
function rebuildCodexThreadsState() {
|
|
691
722
|
const sqlite3 = findSqlite3()
|
|
692
723
|
if (!sqlite3) return { rebuilt: 0, reason: 'sqlite3 not found' }
|
|
@@ -701,7 +732,14 @@ function rebuildCodexThreadsState() {
|
|
|
701
732
|
const rows = files.map(codexSessionSummary).filter((row) => row.id)
|
|
702
733
|
if (rows.length === 0) return { rebuilt: 0, reason: 'no session ids' }
|
|
703
734
|
|
|
735
|
+
const watermark = files
|
|
736
|
+
.map(codexRelativePath)
|
|
737
|
+
.filter((value) => value && !value.startsWith('..'))
|
|
738
|
+
.sort()
|
|
739
|
+
.at(-1) || ''
|
|
740
|
+
|
|
704
741
|
let total = 0
|
|
742
|
+
let backfillUpdated = 0
|
|
705
743
|
for (const dbPath of stateDbs) {
|
|
706
744
|
const tableCheck = spawnSync(sqlite3, [dbPath, "select name from sqlite_master where type='table' and name='threads';"], {
|
|
707
745
|
encoding: 'utf8',
|
|
@@ -736,9 +774,12 @@ ${createdMs}, ${updatedMs}, ${row.thread_source ? sqlString(row.thread_source) :
|
|
|
736
774
|
statements.push('COMMIT;')
|
|
737
775
|
|
|
738
776
|
const result = runSqliteScript(sqlite3, dbPath, statements.join('\n'))
|
|
739
|
-
if (result.status === 0)
|
|
777
|
+
if (result.status === 0) {
|
|
778
|
+
total += rows.length
|
|
779
|
+
if (updateCodexBackfillState(sqlite3, dbPath, watermark)) backfillUpdated += 1
|
|
780
|
+
}
|
|
740
781
|
}
|
|
741
|
-
return { rebuilt: total, reason: total > 0 ? '' : 'threads table not rebuilt' }
|
|
782
|
+
return { rebuilt: total, backfillUpdated, reason: total > 0 ? '' : 'threads table not rebuilt' }
|
|
742
783
|
}
|
|
743
784
|
|
|
744
785
|
function loadBackupManifest(dir) {
|
|
@@ -1357,9 +1398,16 @@ async function restoreBackupByKind(kind, title, emptyMessage) {
|
|
|
1357
1398
|
}
|
|
1358
1399
|
if (rebuiltCodexThreads.rebuilt > 0) {
|
|
1359
1400
|
console.log(c('cyan', `已重建 Codex sqlite 任务列表: ${rebuiltCodexThreads.rebuilt} 条`))
|
|
1401
|
+
if (rebuiltCodexThreads.backfillUpdated > 0) {
|
|
1402
|
+
console.log(c('cyan', `已同步 Codex sqlite 回填状态: ${rebuiltCodexThreads.backfillUpdated} 个数据库`))
|
|
1403
|
+
}
|
|
1360
1404
|
} else if (rebuiltCodexThreads.reason) {
|
|
1361
1405
|
console.log(c('yellow', `未重建 Codex sqlite 任务列表: ${rebuiltCodexThreads.reason}`))
|
|
1362
1406
|
}
|
|
1407
|
+
if (kind === 'chat' && restoredCodexSessions) {
|
|
1408
|
+
console.log()
|
|
1409
|
+
console.log(c('yellow', '重要: Codex VS Code 插件会缓存任务列表。还原后请执行 "Developer: Reload Window",或完全退出并重开 VS Code/Codex 窗口,否则侧边栏可能仍显示还原前的少量记录。'))
|
|
1410
|
+
}
|
|
1363
1411
|
console.log()
|
|
1364
1412
|
console.log(c('yellow', `恢复前安全备份目录: ${safetyBackup}`))
|
|
1365
1413
|
await pause()
|