iivo-sub 0.1.1 → 0.1.2

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
@@ -26,11 +26,15 @@ The main menu includes:
26
26
  - Quick configuration
27
27
  - Backup configuration
28
28
  - Restore backup
29
+ - Backup chat records
30
+ - Restore chat records
29
31
  - Clear cache and backups
30
32
  - Exit
31
33
 
32
34
  Backup configuration lets you choose Codex, Claude Code, Hermes, OpenClaw, or all targets. You can name the backup yourself; the default name is `<date-time>_<target>`.
33
35
 
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
+
34
38
  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.
35
39
 
36
40
  Existing config files are backed up under `~/.iivo-sub/backups/` before being replaced.
package/bin/iivo-sub.js CHANGED
@@ -5,7 +5,7 @@ import os from 'node:os'
5
5
  import path from 'node:path'
6
6
  import readline from 'node:readline'
7
7
 
8
- const VERSION = '0.1.1'
8
+ const VERSION = '0.1.2'
9
9
  const APP_DIR = path.join(os.homedir(), '.iivo-sub')
10
10
  const CACHE_FILE = path.join(APP_DIR, 'cache.json')
11
11
  const SCRIPTED_INPUT = !process.stdin.isTTY ? fs.readFileSync(0, 'utf8').split(/\r?\n/) : null
@@ -40,6 +40,8 @@ const MAIN_MENU = [
40
40
  { label: '快速配置', value: 'quick' },
41
41
  { label: '备份配置', value: 'backup' },
42
42
  { label: '恢复备份', value: 'restore' },
43
+ { label: '备份聊天记录', value: 'backup-chats' },
44
+ { label: '还原聊天记录', value: 'restore-chats' },
43
45
  { label: '清空缓存及备份', value: 'clear-cache' },
44
46
  { label: '退出', value: 'exit' }
45
47
  ]
@@ -52,6 +54,14 @@ const BACKUP_TARGETS = [
52
54
  { label: '全部备份', value: 'all' }
53
55
  ]
54
56
 
57
+ const CHAT_BACKUP_TARGETS = [
58
+ { label: 'Codex 聊天记录', value: 'codex' },
59
+ { label: 'Claude Code 聊天记录', value: 'claude-code' },
60
+ { label: 'Hermes 聊天记录', value: 'hermes' },
61
+ { label: 'OpenClaw 聊天记录', value: 'openclaw' },
62
+ { label: '全部聊天记录', value: 'all' }
63
+ ]
64
+
55
65
  const RESTORE_CONFIRM_OPTIONS = [
56
66
  { label: '确认恢复', value: 'yes' },
57
67
  { label: '取消', value: 'no' }
@@ -260,6 +270,10 @@ function fileExists(filePath) {
260
270
  return fs.existsSync(filePath) && fs.statSync(filePath).isFile()
261
271
  }
262
272
 
273
+ function pathExists(targetPath) {
274
+ return fs.existsSync(targetPath)
275
+ }
276
+
263
277
  function configTargets() {
264
278
  return {
265
279
  codex: [
@@ -301,6 +315,38 @@ function filesForBackupTarget(target) {
301
315
  return targets[target] ?? []
302
316
  }
303
317
 
318
+ function chatTargets() {
319
+ return {
320
+ codex: [
321
+ path.join(os.homedir(), '.codex', 'sessions'),
322
+ path.join(os.homedir(), '.codex', 'history.json')
323
+ ],
324
+ 'claude-code': [
325
+ path.join(os.homedir(), '.claude', 'projects'),
326
+ path.join(os.homedir(), '.claude', 'todos'),
327
+ path.join(os.homedir(), '.claude', 'shell-snapshots')
328
+ ],
329
+ hermes: [
330
+ path.join(os.homedir(), '.hermes', 'sessions'),
331
+ path.join(os.homedir(), '.hermes', 'chats'),
332
+ path.join(os.homedir(), '.hermes', 'history.json')
333
+ ],
334
+ openclaw: [
335
+ path.join(os.homedir(), '.openclaw', 'sessions'),
336
+ path.join(os.homedir(), '.openclaw', 'chats'),
337
+ path.join(os.homedir(), '.openclaw', 'history.json')
338
+ ]
339
+ }
340
+ }
341
+
342
+ function pathsForChatBackupTarget(target) {
343
+ const targets = chatTargets()
344
+ if (target === 'all') {
345
+ return Object.values(targets).flat()
346
+ }
347
+ return targets[target] ?? []
348
+ }
349
+
304
350
  function uniqueFiles(files) {
305
351
  return [...new Set(files)]
306
352
  }
@@ -309,6 +355,10 @@ function backupTargetLabel(value) {
309
355
  return BACKUP_TARGETS.find((item) => item.value === value)?.label ?? value
310
356
  }
311
357
 
358
+ function chatBackupTargetLabel(value) {
359
+ return CHAT_BACKUP_TARGETS.find((item) => item.value === value)?.label ?? value
360
+ }
361
+
312
362
  function timestampForName() {
313
363
  const now = new Date()
314
364
  const pad = (value) => String(value).padStart(2, '0')
@@ -349,6 +399,19 @@ function copyConfigToBackup(filePath, backupRoot) {
349
399
  }
350
400
  }
351
401
 
402
+ function copyPathToBackup(sourcePath, backupRoot) {
403
+ const stat = fs.statSync(sourcePath)
404
+ const backupFile = toBackupFileName(sourcePath)
405
+ const backupPath = path.join(backupRoot, 'files', backupFile)
406
+ ensureDir(path.dirname(backupPath))
407
+ fs.cpSync(sourcePath, backupPath, { recursive: true, force: true })
408
+ return {
409
+ source: sourcePath,
410
+ backup: path.relative(backupRoot, backupPath),
411
+ type: stat.isDirectory() ? 'directory' : 'file'
412
+ }
413
+ }
414
+
352
415
  function loadBackupManifest(dir) {
353
416
  const manifestPath = path.join(dir, 'manifest.json')
354
417
  if (!fileExists(manifestPath)) return null
@@ -746,6 +809,7 @@ async function backupConfig() {
746
809
  ensureDir(backupRoot)
747
810
  const files = existing.map((filePath) => copyConfigToBackup(filePath, backupRoot))
748
811
  const manifest = {
812
+ kind: 'config',
749
813
  name,
750
814
  target,
751
815
  target_label: backupTargetLabel(target),
@@ -769,27 +833,105 @@ async function backupConfig() {
769
833
  await pause()
770
834
  }
771
835
 
836
+ async function backupChatRecords() {
837
+ clear()
838
+ banner()
839
+ const target = await select('请选择聊天记录备份目标:', CHAT_BACKUP_TARGETS)
840
+ if (target === '__exit__') return '__exit__'
841
+ if (!target) return
842
+
843
+ const defaultName = `${timestampForName()}_chats_${target}`
844
+ clear()
845
+ banner()
846
+ const rawName = await question('请输入聊天记录备份名称 [Esc 返回上一步]', { defaultValue: defaultName })
847
+ if (rawName === '__exit__') return '__exit__'
848
+ if (rawName === null) return backupChatRecords()
849
+
850
+ const name = safeBackupName(rawName) || defaultName
851
+ const backupRoot = path.join(APP_DIR, 'backups', name)
852
+ if (fs.existsSync(backupRoot)) {
853
+ console.log(c('red', `备份名称已存在: ${name}`))
854
+ await pause()
855
+ return backupChatRecords()
856
+ }
857
+
858
+ const candidates = uniqueFiles(pathsForChatBackupTarget(target))
859
+ const existing = candidates.filter(pathExists)
860
+ if (existing.length === 0) {
861
+ console.log(c('yellow', `没有找到可备份的 ${chatBackupTargetLabel(target)}。`))
862
+ console.log()
863
+ console.log(c('dim', '已检查常见目录:'))
864
+ for (const item of candidates) {
865
+ console.log(c('dim', `- ${item}`))
866
+ }
867
+ await pause()
868
+ return
869
+ }
870
+
871
+ ensureDir(backupRoot)
872
+ const files = existing.map((sourcePath) => copyPathToBackup(sourcePath, backupRoot))
873
+ const manifest = {
874
+ kind: 'chat',
875
+ name,
876
+ target,
877
+ target_label: chatBackupTargetLabel(target),
878
+ created_at: new Date().toISOString(),
879
+ files
880
+ }
881
+ fs.writeFileSync(path.join(backupRoot, 'manifest.json'), json(manifest), 'utf8')
882
+
883
+ clear()
884
+ banner()
885
+ console.log(c('green', '聊天记录备份完成。'))
886
+ console.log()
887
+ console.log(`备份名称: ${name}`)
888
+ console.log(`备份目标: ${chatBackupTargetLabel(target)}`)
889
+ console.log(`备份目录: ${backupRoot}`)
890
+ console.log()
891
+ console.log(c('cyan', '已备份路径:'))
892
+ for (const file of files) {
893
+ console.log(`- ${file.source}`)
894
+ }
895
+ await pause()
896
+ }
897
+
898
+ function backupEntryMatchesKind(entryName, backupDir, kind) {
899
+ const manifest = loadBackupManifest(path.join(backupDir, entryName))
900
+ if (!manifest) return kind === 'config'
901
+ if (kind === 'config') return !manifest.kind || manifest.kind === 'config'
902
+ return manifest.kind === kind
903
+ }
904
+
772
905
  async function restoreBackup() {
906
+ return restoreBackupByKind('config', '请选择要查看的配置备份:', '暂无配置备份。')
907
+ }
908
+
909
+ async function restoreChatRecords() {
910
+ return restoreBackupByKind('chat', '请选择要还原的聊天记录备份:', '暂无聊天记录备份。')
911
+ }
912
+
913
+ async function restoreBackupByKind(kind, title, emptyMessage) {
773
914
  clear()
774
915
  banner()
775
916
  const backupDir = path.join(APP_DIR, 'backups')
776
917
  if (!fs.existsSync(backupDir)) {
777
- console.log(c('yellow', '暂无备份。'))
918
+ console.log(c('yellow', emptyMessage))
778
919
  return pause()
779
920
  }
780
921
 
781
922
  const entries = fs.readdirSync(backupDir, { withFileTypes: true })
782
923
  .filter((entry) => entry.isDirectory())
783
924
  .map((entry) => entry.name)
925
+ .filter((name) => backupEntryMatchesKind(name, backupDir, kind))
784
926
  .sort()
785
927
  .reverse()
786
928
 
787
929
  if (entries.length === 0) {
788
- console.log(c('yellow', '暂无备份。'))
930
+ console.log(c('yellow', emptyMessage))
789
931
  return pause()
790
932
  }
791
933
 
792
- const selected = await select('请选择要查看的备份:', [
934
+ const selected = await select(title, [
793
935
  ...entries.map((name) => ({ label: name, value: name })),
794
936
  { label: '返回', value: null }
795
937
  ])
@@ -816,13 +958,16 @@ async function restoreBackup() {
816
958
  console.log(`备份目标: ${manifest.target_label || manifest.target || '未知'}`)
817
959
  console.log(`创建时间: ${manifest.created_at || '未知'}`)
818
960
  console.log()
819
- console.log(c('cyan', '将恢复以下文件:'))
961
+ console.log(c('cyan', kind === 'chat' ? '将还原以下聊天记录路径:' : '将恢复以下文件:'))
820
962
  for (const file of manifest.files || []) {
821
963
  console.log(`- ${file.source}`)
822
964
  }
823
965
  console.log()
824
966
 
825
- const confirm = await select('确认恢复这个备份吗?当前同名配置会先自动备份。', RESTORE_CONFIRM_OPTIONS)
967
+ const confirmText = kind === 'chat'
968
+ ? '确认还原这个聊天记录备份吗?当前同名聊天记录会先自动备份。'
969
+ : '确认恢复这个备份吗?当前同名配置会先自动备份。'
970
+ const confirm = await select(confirmText, RESTORE_CONFIRM_OPTIONS)
826
971
  if (confirm === '__exit__') return '__exit__'
827
972
  if (confirm !== 'yes') return
828
973
 
@@ -832,18 +977,21 @@ async function restoreBackup() {
832
977
  for (const file of manifest.files || []) {
833
978
  const source = file.source
834
979
  const backupPath = path.join(selectedDir, file.backup)
835
- if (!source || !fileExists(backupPath)) continue
836
- if (fileExists(source)) {
837
- copyConfigToBackup(source, safetyBackup)
980
+ if (!source || !pathExists(backupPath)) continue
981
+ if (pathExists(source)) {
982
+ copyPathToBackup(source, safetyBackup)
838
983
  }
839
984
  ensureDir(path.dirname(source))
840
- fs.copyFileSync(backupPath, source)
985
+ if (pathExists(source)) {
986
+ deletePath(source)
987
+ }
988
+ fs.cpSync(backupPath, source, { recursive: true, force: true })
841
989
  restored.push(source)
842
990
  }
843
991
 
844
992
  clear()
845
993
  banner()
846
- console.log(c('green', '恢复完成。'))
994
+ console.log(c('green', kind === 'chat' ? '聊天记录还原完成。' : '恢复完成。'))
847
995
  console.log()
848
996
  for (const file of restored) {
849
997
  console.log(`- ${file}`)
@@ -858,7 +1006,7 @@ async function clearCacheAndBackups() {
858
1006
  banner()
859
1007
  console.log(c('yellow', '将删除以下 iivo-sub 数据:'))
860
1008
  console.log(`- API Key 缓存: ${CACHE_FILE}`)
861
- console.log(`- 备份目录: ${path.join(APP_DIR, 'backups')}`)
1009
+ console.log(`- 备份目录: ${path.join(APP_DIR, 'backups')} ${c('dim', '(包含配置备份和聊天记录备份)')}`)
862
1010
  console.log(`- 临时配置脚本: ${path.join(APP_DIR, '*.env / *.ps1 / *.cmd')}`)
863
1011
  console.log(`- 最近配置摘要: ${path.join(APP_DIR, 'config.json')}`)
864
1012
  console.log()
@@ -925,6 +1073,14 @@ async function main() {
925
1073
  const result = await restoreBackup()
926
1074
  if (result === '__exit__') break
927
1075
  }
1076
+ if (action === 'backup-chats') {
1077
+ const result = await backupChatRecords()
1078
+ if (result === '__exit__') break
1079
+ }
1080
+ if (action === 'restore-chats') {
1081
+ const result = await restoreChatRecords()
1082
+ if (result === '__exit__') break
1083
+ }
928
1084
  if (action === 'clear-cache') {
929
1085
  const result = await clearCacheAndBackups()
930
1086
  if (result === '__exit__') break
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iivo-sub",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "IIVO AI gateway quick configuration CLI",
5
5
  "type": "module",
6
6
  "bin": {