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 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. 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`.
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.4'
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: stat.birthtimeMs > 0 ? stat.birthtimeMs : stat.mtimeMs,
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
- summary.updated_at = Math.max(summary.updated_at, ts)
556
- if (!summary.created_at || summary.created_at > ts) summary.created_at = ts
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';"], { encoding: 'utf8' })
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 = spawnSync(sqlite3, [dbPath], {
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 && !restoredCodexIndex) {
1339
+ if (restoredCodexSessions) {
1313
1340
  rebuiltCodexIndexCount = rebuildCodexSessionIndex()
1314
1341
  }
1315
1342
  if (kind === 'chat' && restoredCodexSessions) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iivo-sub",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "IIVO AI gateway quick configuration CLI",
5
5
  "type": "module",
6
6
  "bin": {