iivo-sub 0.1.4 → 0.1.6
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 +54 -19
- 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.6'
|
|
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}`
|
|
@@ -502,14 +518,18 @@ function titleFromCodexSession(filePath) {
|
|
|
502
518
|
}
|
|
503
519
|
const payload = item?.payload
|
|
504
520
|
if (payload?.type === 'user_message' && payload.message) {
|
|
505
|
-
|
|
521
|
+
const title = truncateTitle(payload.message)
|
|
522
|
+
if (title) return title
|
|
506
523
|
}
|
|
507
524
|
if (payload?.type === 'message' && payload.role === 'user' && Array.isArray(payload.content)) {
|
|
508
525
|
const text = payload.content
|
|
509
526
|
.map((part) => part?.text || part?.input_text || '')
|
|
510
527
|
.filter(Boolean)
|
|
511
528
|
.join(' ')
|
|
512
|
-
if (text)
|
|
529
|
+
if (text) {
|
|
530
|
+
const title = truncateTitle(text)
|
|
531
|
+
if (title) return title
|
|
532
|
+
}
|
|
513
533
|
}
|
|
514
534
|
}
|
|
515
535
|
} catch {
|
|
@@ -520,11 +540,12 @@ function titleFromCodexSession(filePath) {
|
|
|
520
540
|
|
|
521
541
|
function codexSessionSummary(filePath) {
|
|
522
542
|
const stat = fs.statSync(filePath)
|
|
543
|
+
const fileCreatedAt = stat.birthtimeMs > 0 ? stat.birthtimeMs : stat.mtimeMs
|
|
523
544
|
const id = codexSessionIDFromPath(filePath)
|
|
524
545
|
const summary = {
|
|
525
546
|
id,
|
|
526
547
|
rollout_path: filePath,
|
|
527
|
-
created_at:
|
|
548
|
+
created_at: fileCreatedAt,
|
|
528
549
|
updated_at: stat.mtimeMs,
|
|
529
550
|
source: 'cli',
|
|
530
551
|
model_provider: 'openai',
|
|
@@ -540,6 +561,8 @@ function codexSessionSummary(filePath) {
|
|
|
540
561
|
}
|
|
541
562
|
|
|
542
563
|
try {
|
|
564
|
+
let firstEventAt = 0
|
|
565
|
+
let latestEventAt = 0
|
|
543
566
|
const content = fs.readFileSync(filePath, 'utf8')
|
|
544
567
|
for (const line of content.split(/\r?\n/)) {
|
|
545
568
|
if (!line.trim()) continue
|
|
@@ -552,8 +575,8 @@ function codexSessionSummary(filePath) {
|
|
|
552
575
|
if (typeof item.timestamp === 'string') {
|
|
553
576
|
const ts = Date.parse(item.timestamp)
|
|
554
577
|
if (Number.isFinite(ts)) {
|
|
555
|
-
|
|
556
|
-
if (
|
|
578
|
+
if (!firstEventAt || ts < firstEventAt) firstEventAt = ts
|
|
579
|
+
if (ts > latestEventAt) latestEventAt = ts
|
|
557
580
|
}
|
|
558
581
|
}
|
|
559
582
|
const payload = item?.payload
|
|
@@ -573,16 +596,29 @@ function codexSessionSummary(filePath) {
|
|
|
573
596
|
if (payload.approval_policy) summary.approval_mode = String(payload.approval_policy)
|
|
574
597
|
}
|
|
575
598
|
if (!summary.first_user_message && payload?.type === 'user_message' && payload.message) {
|
|
576
|
-
|
|
599
|
+
const title = truncateTitle(payload.message)
|
|
600
|
+
if (title) summary.first_user_message = title
|
|
577
601
|
}
|
|
578
602
|
if (!summary.first_user_message && payload?.type === 'message' && payload.role === 'user' && Array.isArray(payload.content)) {
|
|
579
603
|
const text = payload.content
|
|
580
604
|
.map((part) => part?.text || part?.input_text || '')
|
|
581
605
|
.filter(Boolean)
|
|
582
606
|
.join(' ')
|
|
583
|
-
if (text)
|
|
607
|
+
if (text) {
|
|
608
|
+
const title = truncateTitle(text)
|
|
609
|
+
if (title) summary.first_user_message = title
|
|
610
|
+
}
|
|
584
611
|
}
|
|
585
612
|
}
|
|
613
|
+
if (firstEventAt && (!summary.created_at || firstEventAt < summary.created_at)) {
|
|
614
|
+
summary.created_at = firstEventAt
|
|
615
|
+
}
|
|
616
|
+
if (latestEventAt) {
|
|
617
|
+
summary.updated_at = latestEventAt
|
|
618
|
+
}
|
|
619
|
+
if (summary.created_at > summary.updated_at) {
|
|
620
|
+
summary.created_at = summary.updated_at
|
|
621
|
+
}
|
|
586
622
|
} catch {
|
|
587
623
|
// Keep filesystem-derived fallback fields.
|
|
588
624
|
}
|
|
@@ -675,7 +711,10 @@ function rebuildCodexThreadsState() {
|
|
|
675
711
|
|
|
676
712
|
let total = 0
|
|
677
713
|
for (const dbPath of stateDbs) {
|
|
678
|
-
const tableCheck = spawnSync(sqlite3, [dbPath, "select name from sqlite_master where type='table' and name='threads';"], {
|
|
714
|
+
const tableCheck = spawnSync(sqlite3, [dbPath, "select name from sqlite_master where type='table' and name='threads';"], {
|
|
715
|
+
encoding: 'utf8',
|
|
716
|
+
timeout: 10000
|
|
717
|
+
})
|
|
679
718
|
if (tableCheck.status !== 0 || !tableCheck.stdout.includes('threads')) continue
|
|
680
719
|
|
|
681
720
|
if (fileExists(dbPath)) {
|
|
@@ -704,11 +743,7 @@ ${createdMs}, ${updatedMs}, ${row.thread_source ? sqlString(row.thread_source) :
|
|
|
704
743
|
}
|
|
705
744
|
statements.push('COMMIT;')
|
|
706
745
|
|
|
707
|
-
const result =
|
|
708
|
-
input: statements.join('\n'),
|
|
709
|
-
encoding: 'utf8',
|
|
710
|
-
maxBuffer: 10 * 1024 * 1024
|
|
711
|
-
})
|
|
746
|
+
const result = runSqliteScript(sqlite3, dbPath, statements.join('\n'))
|
|
712
747
|
if (result.status === 0) total += rows.length
|
|
713
748
|
}
|
|
714
749
|
return { rebuilt: total, reason: total > 0 ? '' : 'threads table not rebuilt' }
|
|
@@ -1294,7 +1329,7 @@ async function restoreBackupByKind(kind, title, emptyMessage) {
|
|
|
1294
1329
|
if (kind === 'chat' && source.endsWith('.sqlite')) {
|
|
1295
1330
|
deleteSqliteSidecars(source)
|
|
1296
1331
|
}
|
|
1297
|
-
fs.cpSync(backupPath, source, { recursive: true, force: true })
|
|
1332
|
+
fs.cpSync(backupPath, source, { recursive: true, force: true, preserveTimestamps: true })
|
|
1298
1333
|
if (kind === 'chat' && source.endsWith('.sqlite')) {
|
|
1299
1334
|
deleteSqliteSidecars(source)
|
|
1300
1335
|
}
|
|
@@ -1309,7 +1344,7 @@ async function restoreBackupByKind(kind, title, emptyMessage) {
|
|
|
1309
1344
|
|
|
1310
1345
|
let rebuiltCodexIndexCount = 0
|
|
1311
1346
|
let rebuiltCodexThreads = { rebuilt: 0, reason: '' }
|
|
1312
|
-
if (restoredCodexSessions
|
|
1347
|
+
if (restoredCodexSessions) {
|
|
1313
1348
|
rebuiltCodexIndexCount = rebuildCodexSessionIndex()
|
|
1314
1349
|
}
|
|
1315
1350
|
if (kind === 'chat' && restoredCodexSessions) {
|