iivo-sub 0.1.4 → 0.1.5
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 +1 -1
- package/bin/iivo-sub.js +42 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ 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.
|
|
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`.
|
|
39
39
|
|
|
40
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.
|
|
41
41
|
|
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.5'
|
|
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
|
|
@@ -424,7 +424,7 @@ function copyPathToBackup(sourcePath, backupRoot) {
|
|
|
424
424
|
type: 'sqlite'
|
|
425
425
|
}
|
|
426
426
|
}
|
|
427
|
-
fs.cpSync(sourcePath, backupPath, { recursive: true, force: true })
|
|
427
|
+
fs.cpSync(sourcePath, backupPath, { recursive: true, force: true, preserveTimestamps: true })
|
|
428
428
|
return {
|
|
429
429
|
source: sourcePath,
|
|
430
430
|
backup: path.relative(backupRoot, backupPath),
|
|
@@ -441,7 +441,7 @@ function findSqlite3() {
|
|
|
441
441
|
path.join(os.homedir(), 'Android', 'Sdk', 'platform-tools', 'sqlite3')
|
|
442
442
|
].filter(Boolean)
|
|
443
443
|
for (const candidate of candidates) {
|
|
444
|
-
const result = spawnSync(candidate, ['-version'], { encoding: 'utf8' })
|
|
444
|
+
const result = spawnSync(candidate, ['-version'], { encoding: 'utf8', timeout: 5000 })
|
|
445
445
|
if (result.status === 0) return candidate
|
|
446
446
|
}
|
|
447
447
|
return ''
|
|
@@ -452,11 +452,27 @@ function sqliteBackup(sourcePath, backupPath) {
|
|
|
452
452
|
if (!sqlite3) return false
|
|
453
453
|
ensureDir(path.dirname(backupPath))
|
|
454
454
|
const result = spawnSync(sqlite3, [sourcePath, `.backup '${backupPath.replaceAll("'", "''")}'`], {
|
|
455
|
-
encoding: 'utf8'
|
|
455
|
+
encoding: 'utf8',
|
|
456
|
+
timeout: 30000
|
|
456
457
|
})
|
|
457
458
|
return result.status === 0 && fileExists(backupPath)
|
|
458
459
|
}
|
|
459
460
|
|
|
461
|
+
function runSqliteScript(sqlite3, dbPath, script) {
|
|
462
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'iivo-sub-sqlite-'))
|
|
463
|
+
const scriptPath = path.join(tempDir, 'script.sql')
|
|
464
|
+
try {
|
|
465
|
+
fs.writeFileSync(scriptPath, script, 'utf8')
|
|
466
|
+
return spawnSync(sqlite3, [dbPath, `.read ${scriptPath}`], {
|
|
467
|
+
encoding: 'utf8',
|
|
468
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
469
|
+
timeout: 30000
|
|
470
|
+
})
|
|
471
|
+
} finally {
|
|
472
|
+
deletePath(tempDir)
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
460
476
|
function deleteSqliteSidecars(sqlitePath) {
|
|
461
477
|
for (const suffix of ['-wal', '-shm']) {
|
|
462
478
|
const sidecar = `${sqlitePath}${suffix}`
|
|
@@ -520,11 +536,12 @@ function titleFromCodexSession(filePath) {
|
|
|
520
536
|
|
|
521
537
|
function codexSessionSummary(filePath) {
|
|
522
538
|
const stat = fs.statSync(filePath)
|
|
539
|
+
const fileCreatedAt = stat.birthtimeMs > 0 ? stat.birthtimeMs : stat.mtimeMs
|
|
523
540
|
const id = codexSessionIDFromPath(filePath)
|
|
524
541
|
const summary = {
|
|
525
542
|
id,
|
|
526
543
|
rollout_path: filePath,
|
|
527
|
-
created_at:
|
|
544
|
+
created_at: fileCreatedAt,
|
|
528
545
|
updated_at: stat.mtimeMs,
|
|
529
546
|
source: 'cli',
|
|
530
547
|
model_provider: 'openai',
|
|
@@ -540,6 +557,8 @@ function codexSessionSummary(filePath) {
|
|
|
540
557
|
}
|
|
541
558
|
|
|
542
559
|
try {
|
|
560
|
+
let firstEventAt = 0
|
|
561
|
+
let latestEventAt = 0
|
|
543
562
|
const content = fs.readFileSync(filePath, 'utf8')
|
|
544
563
|
for (const line of content.split(/\r?\n/)) {
|
|
545
564
|
if (!line.trim()) continue
|
|
@@ -552,8 +571,8 @@ function codexSessionSummary(filePath) {
|
|
|
552
571
|
if (typeof item.timestamp === 'string') {
|
|
553
572
|
const ts = Date.parse(item.timestamp)
|
|
554
573
|
if (Number.isFinite(ts)) {
|
|
555
|
-
|
|
556
|
-
if (
|
|
574
|
+
if (!firstEventAt || ts < firstEventAt) firstEventAt = ts
|
|
575
|
+
if (ts > latestEventAt) latestEventAt = ts
|
|
557
576
|
}
|
|
558
577
|
}
|
|
559
578
|
const payload = item?.payload
|
|
@@ -583,6 +602,15 @@ function codexSessionSummary(filePath) {
|
|
|
583
602
|
if (text) summary.first_user_message = truncateTitle(text)
|
|
584
603
|
}
|
|
585
604
|
}
|
|
605
|
+
if (firstEventAt && (!summary.created_at || firstEventAt < summary.created_at)) {
|
|
606
|
+
summary.created_at = firstEventAt
|
|
607
|
+
}
|
|
608
|
+
if (latestEventAt) {
|
|
609
|
+
summary.updated_at = latestEventAt
|
|
610
|
+
}
|
|
611
|
+
if (summary.created_at > summary.updated_at) {
|
|
612
|
+
summary.created_at = summary.updated_at
|
|
613
|
+
}
|
|
586
614
|
} catch {
|
|
587
615
|
// Keep filesystem-derived fallback fields.
|
|
588
616
|
}
|
|
@@ -675,7 +703,10 @@ function rebuildCodexThreadsState() {
|
|
|
675
703
|
|
|
676
704
|
let total = 0
|
|
677
705
|
for (const dbPath of stateDbs) {
|
|
678
|
-
const tableCheck = spawnSync(sqlite3, [dbPath, "select name from sqlite_master where type='table' and name='threads';"], {
|
|
706
|
+
const tableCheck = spawnSync(sqlite3, [dbPath, "select name from sqlite_master where type='table' and name='threads';"], {
|
|
707
|
+
encoding: 'utf8',
|
|
708
|
+
timeout: 10000
|
|
709
|
+
})
|
|
679
710
|
if (tableCheck.status !== 0 || !tableCheck.stdout.includes('threads')) continue
|
|
680
711
|
|
|
681
712
|
if (fileExists(dbPath)) {
|
|
@@ -704,11 +735,7 @@ ${createdMs}, ${updatedMs}, ${row.thread_source ? sqlString(row.thread_source) :
|
|
|
704
735
|
}
|
|
705
736
|
statements.push('COMMIT;')
|
|
706
737
|
|
|
707
|
-
const result =
|
|
708
|
-
input: statements.join('\n'),
|
|
709
|
-
encoding: 'utf8',
|
|
710
|
-
maxBuffer: 10 * 1024 * 1024
|
|
711
|
-
})
|
|
738
|
+
const result = runSqliteScript(sqlite3, dbPath, statements.join('\n'))
|
|
712
739
|
if (result.status === 0) total += rows.length
|
|
713
740
|
}
|
|
714
741
|
return { rebuilt: total, reason: total > 0 ? '' : 'threads table not rebuilt' }
|
|
@@ -1294,7 +1321,7 @@ async function restoreBackupByKind(kind, title, emptyMessage) {
|
|
|
1294
1321
|
if (kind === 'chat' && source.endsWith('.sqlite')) {
|
|
1295
1322
|
deleteSqliteSidecars(source)
|
|
1296
1323
|
}
|
|
1297
|
-
fs.cpSync(backupPath, source, { recursive: true, force: true })
|
|
1324
|
+
fs.cpSync(backupPath, source, { recursive: true, force: true, preserveTimestamps: true })
|
|
1298
1325
|
if (kind === 'chat' && source.endsWith('.sqlite')) {
|
|
1299
1326
|
deleteSqliteSidecars(source)
|
|
1300
1327
|
}
|
|
@@ -1309,7 +1336,7 @@ async function restoreBackupByKind(kind, title, emptyMessage) {
|
|
|
1309
1336
|
|
|
1310
1337
|
let rebuiltCodexIndexCount = 0
|
|
1311
1338
|
let rebuiltCodexThreads = { rebuilt: 0, reason: '' }
|
|
1312
|
-
if (restoredCodexSessions
|
|
1339
|
+
if (restoredCodexSessions) {
|
|
1313
1340
|
rebuiltCodexIndexCount = rebuildCodexSessionIndex()
|
|
1314
1341
|
}
|
|
1315
1342
|
if (kind === 'chat' && restoredCodexSessions) {
|