@savestate/cli 0.1.0

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.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +256 -0
  3. package/dist/adapters/claude-code.d.ts +81 -0
  4. package/dist/adapters/claude-code.d.ts.map +1 -0
  5. package/dist/adapters/claude-code.js +540 -0
  6. package/dist/adapters/claude-code.js.map +1 -0
  7. package/dist/adapters/clawdbot.d.ts +93 -0
  8. package/dist/adapters/clawdbot.d.ts.map +1 -0
  9. package/dist/adapters/clawdbot.js +673 -0
  10. package/dist/adapters/clawdbot.js.map +1 -0
  11. package/dist/adapters/index.d.ts +9 -0
  12. package/dist/adapters/index.d.ts.map +1 -0
  13. package/dist/adapters/index.js +8 -0
  14. package/dist/adapters/index.js.map +1 -0
  15. package/dist/adapters/interface.d.ts +8 -0
  16. package/dist/adapters/interface.d.ts.map +1 -0
  17. package/dist/adapters/interface.js +8 -0
  18. package/dist/adapters/interface.js.map +1 -0
  19. package/dist/adapters/openai-assistants.d.ts +51 -0
  20. package/dist/adapters/openai-assistants.d.ts.map +1 -0
  21. package/dist/adapters/openai-assistants.js +114 -0
  22. package/dist/adapters/openai-assistants.js.map +1 -0
  23. package/dist/adapters/registry.d.ts +38 -0
  24. package/dist/adapters/registry.d.ts.map +1 -0
  25. package/dist/adapters/registry.js +79 -0
  26. package/dist/adapters/registry.js.map +1 -0
  27. package/dist/cli.d.ts +18 -0
  28. package/dist/cli.d.ts.map +1 -0
  29. package/dist/cli.js +81 -0
  30. package/dist/cli.js.map +1 -0
  31. package/dist/commands/adapters.d.ts +5 -0
  32. package/dist/commands/adapters.d.ts.map +1 -0
  33. package/dist/commands/adapters.js +49 -0
  34. package/dist/commands/adapters.js.map +1 -0
  35. package/dist/commands/config.d.ts +10 -0
  36. package/dist/commands/config.d.ts.map +1 -0
  37. package/dist/commands/config.js +60 -0
  38. package/dist/commands/config.js.map +1 -0
  39. package/dist/commands/diff.d.ts +5 -0
  40. package/dist/commands/diff.d.ts.map +1 -0
  41. package/dist/commands/diff.js +51 -0
  42. package/dist/commands/diff.js.map +1 -0
  43. package/dist/commands/index.d.ts +12 -0
  44. package/dist/commands/index.d.ts.map +1 -0
  45. package/dist/commands/index.js +12 -0
  46. package/dist/commands/index.js.map +1 -0
  47. package/dist/commands/init.d.ts +5 -0
  48. package/dist/commands/init.d.ts.map +1 -0
  49. package/dist/commands/init.js +71 -0
  50. package/dist/commands/init.js.map +1 -0
  51. package/dist/commands/list.d.ts +10 -0
  52. package/dist/commands/list.d.ts.map +1 -0
  53. package/dist/commands/list.js +89 -0
  54. package/dist/commands/list.js.map +1 -0
  55. package/dist/commands/restore.d.ts +11 -0
  56. package/dist/commands/restore.d.ts.map +1 -0
  57. package/dist/commands/restore.js +86 -0
  58. package/dist/commands/restore.js.map +1 -0
  59. package/dist/commands/search.d.ts +11 -0
  60. package/dist/commands/search.d.ts.map +1 -0
  61. package/dist/commands/search.js +50 -0
  62. package/dist/commands/search.js.map +1 -0
  63. package/dist/commands/snapshot.d.ts +12 -0
  64. package/dist/commands/snapshot.d.ts.map +1 -0
  65. package/dist/commands/snapshot.js +84 -0
  66. package/dist/commands/snapshot.js.map +1 -0
  67. package/dist/config.d.ts +44 -0
  68. package/dist/config.d.ts.map +1 -0
  69. package/dist/config.js +83 -0
  70. package/dist/config.js.map +1 -0
  71. package/dist/encryption.d.ts +40 -0
  72. package/dist/encryption.d.ts.map +1 -0
  73. package/dist/encryption.js +120 -0
  74. package/dist/encryption.js.map +1 -0
  75. package/dist/format.d.ts +47 -0
  76. package/dist/format.d.ts.map +1 -0
  77. package/dist/format.js +198 -0
  78. package/dist/format.js.map +1 -0
  79. package/dist/index-file.d.ts +42 -0
  80. package/dist/index-file.d.ts.map +1 -0
  81. package/dist/index-file.js +68 -0
  82. package/dist/index-file.js.map +1 -0
  83. package/dist/index.d.ts +15 -0
  84. package/dist/index.d.ts.map +1 -0
  85. package/dist/index.js +22 -0
  86. package/dist/index.js.map +1 -0
  87. package/dist/passphrase.d.ts +23 -0
  88. package/dist/passphrase.d.ts.map +1 -0
  89. package/dist/passphrase.js +82 -0
  90. package/dist/passphrase.js.map +1 -0
  91. package/dist/restore.d.ts +46 -0
  92. package/dist/restore.d.ts.map +1 -0
  93. package/dist/restore.js +113 -0
  94. package/dist/restore.js.map +1 -0
  95. package/dist/search.d.ts +35 -0
  96. package/dist/search.d.ts.map +1 -0
  97. package/dist/search.js +59 -0
  98. package/dist/search.js.map +1 -0
  99. package/dist/snapshot.d.ts +43 -0
  100. package/dist/snapshot.d.ts.map +1 -0
  101. package/dist/snapshot.js +95 -0
  102. package/dist/snapshot.js.map +1 -0
  103. package/dist/storage/index.d.ts +7 -0
  104. package/dist/storage/index.d.ts.map +1 -0
  105. package/dist/storage/index.js +6 -0
  106. package/dist/storage/index.js.map +1 -0
  107. package/dist/storage/interface.d.ts +8 -0
  108. package/dist/storage/interface.d.ts.map +1 -0
  109. package/dist/storage/interface.js +8 -0
  110. package/dist/storage/interface.js.map +1 -0
  111. package/dist/storage/local.d.ts +22 -0
  112. package/dist/storage/local.d.ts.map +1 -0
  113. package/dist/storage/local.js +63 -0
  114. package/dist/storage/local.js.map +1 -0
  115. package/dist/storage/resolve.d.ts +11 -0
  116. package/dist/storage/resolve.d.ts.map +1 -0
  117. package/dist/storage/resolve.js +21 -0
  118. package/dist/storage/resolve.js.map +1 -0
  119. package/dist/types.d.ts +273 -0
  120. package/dist/types.d.ts.map +1 -0
  121. package/dist/types.js +8 -0
  122. package/dist/types.js.map +1 -0
  123. package/package.json +61 -0
@@ -0,0 +1,84 @@
1
+ /**
2
+ * savestate snapshot — Capture current AI state to encrypted archive
3
+ */
4
+ import chalk from 'chalk';
5
+ import ora from 'ora';
6
+ import { isInitialized, loadConfig } from '../config.js';
7
+ import { detectAdapter, getAdapter } from '../adapters/registry.js';
8
+ import { createSnapshot } from '../snapshot.js';
9
+ import { resolveStorage } from '../storage/resolve.js';
10
+ import { getPassphrase } from '../passphrase.js';
11
+ export async function snapshotCommand(options) {
12
+ console.log();
13
+ if (!isInitialized()) {
14
+ console.log(chalk.red('✗ SaveState not initialized. Run `savestate init` first.'));
15
+ process.exit(1);
16
+ }
17
+ if (options.schedule) {
18
+ console.log(chalk.cyan(`⏰ Scheduled snapshots: every ${options.schedule}`));
19
+ console.log(chalk.dim(' [Coming soon] Will run as a background daemon.'));
20
+ console.log(chalk.dim(' For now, use cron: */6 * * * * savestate snapshot'));
21
+ console.log();
22
+ return;
23
+ }
24
+ const config = await loadConfig();
25
+ try {
26
+ // Resolve adapter
27
+ let adapter;
28
+ if (options.adapter) {
29
+ adapter = getAdapter(options.adapter);
30
+ if (!adapter) {
31
+ console.log(chalk.red(`✗ Unknown adapter: ${options.adapter}`));
32
+ process.exit(1);
33
+ }
34
+ }
35
+ else if (config.defaultAdapter) {
36
+ adapter = getAdapter(config.defaultAdapter);
37
+ }
38
+ else {
39
+ adapter = await detectAdapter();
40
+ }
41
+ if (!adapter) {
42
+ console.log(chalk.red('✗ No adapter found. Specify one with --adapter or configure a default.'));
43
+ process.exit(1);
44
+ }
45
+ // Get passphrase
46
+ const passphrase = await getPassphrase();
47
+ // Resolve storage backend
48
+ const storage = resolveStorage(config);
49
+ const spinner = ora(`Extracting state via ${adapter.name} adapter...`).start();
50
+ const result = await createSnapshot(adapter, storage, passphrase, {
51
+ label: options.label,
52
+ tags: options.tags?.split(',').map((t) => t.trim()),
53
+ });
54
+ spinner.succeed('Snapshot created!');
55
+ console.log();
56
+ console.log(` ${chalk.dim('ID:')} ${chalk.cyan(result.snapshot.manifest.id)}`);
57
+ console.log(` ${chalk.dim('Adapter:')} ${adapter.name}`);
58
+ if (options.label) {
59
+ console.log(` ${chalk.dim('Label:')} ${options.label}`);
60
+ }
61
+ console.log(` ${chalk.dim('Files:')} ${result.fileCount} files in archive`);
62
+ console.log(` ${chalk.dim('Archive:')} ${formatBytes(result.archiveSize)}`);
63
+ console.log(` ${chalk.dim('Encrypted:')} ${formatBytes(result.encryptedSize)}`);
64
+ console.log(` ${chalk.dim('Storage:')} ${config.storage.type}`);
65
+ console.log(` ${chalk.dim('Status:')} ${chalk.green('✓ Encrypted & stored')}`);
66
+ console.log();
67
+ console.log(chalk.dim(` Restore with: savestate restore ${result.snapshot.manifest.id}`));
68
+ console.log();
69
+ }
70
+ catch (err) {
71
+ console.error();
72
+ console.error(chalk.red('✗ Snapshot failed'));
73
+ console.error(chalk.red(err instanceof Error ? err.message : String(err)));
74
+ process.exit(1);
75
+ }
76
+ }
77
+ function formatBytes(bytes) {
78
+ if (bytes < 1024)
79
+ return `${bytes} B`;
80
+ if (bytes < 1024 * 1024)
81
+ return `${(bytes / 1024).toFixed(1)} KB`;
82
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
83
+ }
84
+ //# sourceMappingURL=snapshot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot.js","sourceRoot":"","sources":["../../src/commands/snapshot.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * SaveState Configuration
3
+ *
4
+ * Manages .savestate/config.json in the current project directory.
5
+ * Also supports global config at ~/.savestate/config.json.
6
+ */
7
+ import type { SaveStateConfig } from './types.js';
8
+ /** Directory name for local SaveState config */
9
+ export declare const SAVESTATE_DIR = ".savestate";
10
+ /** Config filename */
11
+ export declare const CONFIG_FILE = "config.json";
12
+ /** Global SaveState home directory */
13
+ export declare const GLOBAL_SAVESTATE_DIR: string;
14
+ /**
15
+ * Default configuration for new projects.
16
+ */
17
+ export declare function defaultConfig(): SaveStateConfig;
18
+ /**
19
+ * Resolve the local .savestate directory for the current project.
20
+ */
21
+ export declare function localConfigDir(cwd?: string): string;
22
+ /**
23
+ * Resolve the path to the local config file.
24
+ */
25
+ export declare function localConfigPath(cwd?: string): string;
26
+ /**
27
+ * Check if SaveState is initialized in the given directory.
28
+ */
29
+ export declare function isInitialized(cwd?: string): boolean;
30
+ /**
31
+ * Load the SaveState config from the local .savestate/ directory.
32
+ * Falls back to global config if local doesn't exist.
33
+ */
34
+ export declare function loadConfig(cwd?: string): Promise<SaveStateConfig>;
35
+ /**
36
+ * Save the SaveState config to the local .savestate/ directory.
37
+ */
38
+ export declare function saveConfig(config: SaveStateConfig, cwd?: string): Promise<void>;
39
+ /**
40
+ * Initialize SaveState in the given directory.
41
+ * Creates .savestate/ and writes default config.
42
+ */
43
+ export declare function initializeProject(cwd?: string): Promise<SaveStateConfig>;
44
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,gDAAgD;AAChD,eAAO,MAAM,aAAa,eAAe,CAAC;AAE1C,sBAAsB;AACtB,eAAO,MAAM,WAAW,gBAAgB,CAAC;AAEzC,sCAAsC;AACtC,eAAO,MAAM,oBAAoB,QAAgC,CAAC;AAElE;;GAEG;AACH,wBAAgB,aAAa,IAAI,eAAe,CAW/C;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAEnD;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAYvE;AAED;;GAEG;AACH,wBAAsB,UAAU,CAAC,MAAM,EAAE,eAAe,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAKrF;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAI9E"}
package/dist/config.js ADDED
@@ -0,0 +1,83 @@
1
+ /**
2
+ * SaveState Configuration
3
+ *
4
+ * Manages .savestate/config.json in the current project directory.
5
+ * Also supports global config at ~/.savestate/config.json.
6
+ */
7
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
8
+ import { existsSync } from 'node:fs';
9
+ import { join, resolve } from 'node:path';
10
+ import { homedir } from 'node:os';
11
+ /** Directory name for local SaveState config */
12
+ export const SAVESTATE_DIR = '.savestate';
13
+ /** Config filename */
14
+ export const CONFIG_FILE = 'config.json';
15
+ /** Global SaveState home directory */
16
+ export const GLOBAL_SAVESTATE_DIR = join(homedir(), '.savestate');
17
+ /**
18
+ * Default configuration for new projects.
19
+ */
20
+ export function defaultConfig() {
21
+ return {
22
+ version: '0.1.0',
23
+ storage: {
24
+ type: 'local',
25
+ options: {
26
+ path: GLOBAL_SAVESTATE_DIR,
27
+ },
28
+ },
29
+ adapters: [],
30
+ };
31
+ }
32
+ /**
33
+ * Resolve the local .savestate directory for the current project.
34
+ */
35
+ export function localConfigDir(cwd) {
36
+ return join(resolve(cwd ?? process.cwd()), SAVESTATE_DIR);
37
+ }
38
+ /**
39
+ * Resolve the path to the local config file.
40
+ */
41
+ export function localConfigPath(cwd) {
42
+ return join(localConfigDir(cwd), CONFIG_FILE);
43
+ }
44
+ /**
45
+ * Check if SaveState is initialized in the given directory.
46
+ */
47
+ export function isInitialized(cwd) {
48
+ return existsSync(localConfigPath(cwd));
49
+ }
50
+ /**
51
+ * Load the SaveState config from the local .savestate/ directory.
52
+ * Falls back to global config if local doesn't exist.
53
+ */
54
+ export async function loadConfig(cwd) {
55
+ const localPath = localConfigPath(cwd);
56
+ const globalPath = join(GLOBAL_SAVESTATE_DIR, CONFIG_FILE);
57
+ for (const configPath of [localPath, globalPath]) {
58
+ if (existsSync(configPath)) {
59
+ const raw = await readFile(configPath, 'utf-8');
60
+ return JSON.parse(raw);
61
+ }
62
+ }
63
+ return defaultConfig();
64
+ }
65
+ /**
66
+ * Save the SaveState config to the local .savestate/ directory.
67
+ */
68
+ export async function saveConfig(config, cwd) {
69
+ const dir = localConfigDir(cwd);
70
+ await mkdir(dir, { recursive: true });
71
+ const configPath = join(dir, CONFIG_FILE);
72
+ await writeFile(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
73
+ }
74
+ /**
75
+ * Initialize SaveState in the given directory.
76
+ * Creates .savestate/ and writes default config.
77
+ */
78
+ export async function initializeProject(cwd) {
79
+ const config = defaultConfig();
80
+ await saveConfig(config, cwd);
81
+ return config;
82
+ }
83
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAGlC,gDAAgD;AAChD,MAAM,CAAC,MAAM,aAAa,GAAG,YAAY,CAAC;AAE1C,sBAAsB;AACtB,MAAM,CAAC,MAAM,WAAW,GAAG,aAAa,CAAC;AAEzC,sCAAsC;AACtC,MAAM,CAAC,MAAM,oBAAoB,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,YAAY,CAAC,CAAC;AAElE;;GAEG;AACH,MAAM,UAAU,aAAa;IAC3B,OAAO;QACL,OAAO,EAAE,OAAO;QAChB,OAAO,EAAE;YACP,IAAI,EAAE,OAAO;YACb,OAAO,EAAE;gBACP,IAAI,EAAE,oBAAoB;aAC3B;SACF;QACD,QAAQ,EAAE,EAAE;KACb,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,cAAc,CAAC,GAAY;IACzC,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,EAAE,aAAa,CAAC,CAAC;AAC5D,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,eAAe,CAAC,GAAY;IAC1C,OAAO,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,EAAE,WAAW,CAAC,CAAC;AAChD,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,aAAa,CAAC,GAAY;IACxC,OAAO,UAAU,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC,CAAC;AAC1C,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,GAAY;IAC3C,MAAM,SAAS,GAAG,eAAe,CAAC,GAAG,CAAC,CAAC;IACvC,MAAM,UAAU,GAAG,IAAI,CAAC,oBAAoB,EAAE,WAAW,CAAC,CAAC;IAE3D,KAAK,MAAM,UAAU,IAAI,CAAC,SAAS,EAAE,UAAU,CAAC,EAAE,CAAC;QACjD,IAAI,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;YAC3B,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;YAChD,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAoB,CAAC;QAC5C,CAAC;IACH,CAAC;IAED,OAAO,aAAa,EAAE,CAAC;AACzB,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,MAAuB,EAAE,GAAY;IACpE,MAAM,GAAG,GAAG,cAAc,CAAC,GAAG,CAAC,CAAC;IAChC,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACtC,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;IAC1C,MAAM,SAAS,CAAC,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,OAAO,CAAC,CAAC;AAC/E,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,GAAY;IAClD,MAAM,MAAM,GAAG,aAAa,EAAE,CAAC;IAC/B,MAAM,UAAU,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC9B,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * SaveState Encryption Module
3
+ *
4
+ * Provides AES-256-GCM encryption with scrypt key derivation.
5
+ * All data is encrypted before leaving the machine.
6
+ *
7
+ * Key derivation: passphrase → scrypt → 256-bit key
8
+ * Encryption: AES-256-GCM (authenticated encryption)
9
+ * Integrity: Built into GCM auth tag
10
+ */
11
+ /**
12
+ * Encrypt data with a passphrase.
13
+ *
14
+ * Returns a single buffer containing the version header, salt, IV,
15
+ * auth tag, and ciphertext. This buffer is self-contained — everything
16
+ * needed to decrypt (except the passphrase) is included.
17
+ *
18
+ * @param data - Plaintext data to encrypt
19
+ * @param passphrase - User passphrase for key derivation
20
+ * @returns Encrypted buffer with header
21
+ */
22
+ export declare function encrypt(data: Buffer, passphrase: string): Promise<Buffer>;
23
+ /**
24
+ * Decrypt data with a passphrase.
25
+ *
26
+ * Reads the version header, extracts salt/IV/authTag, derives the key,
27
+ * and decrypts. GCM auth tag verification ensures integrity automatically.
28
+ *
29
+ * @param data - Encrypted buffer (as produced by encrypt())
30
+ * @param passphrase - User passphrase for key derivation
31
+ * @returns Decrypted plaintext buffer
32
+ * @throws Error if passphrase is wrong or data is tampered
33
+ */
34
+ export declare function decrypt(data: Buffer, passphrase: string): Promise<Buffer>;
35
+ /**
36
+ * Verify that encrypted data can be decrypted with the given passphrase
37
+ * without actually returning the plaintext.
38
+ */
39
+ export declare function verify(data: Buffer, passphrase: string): Promise<boolean>;
40
+ //# sourceMappingURL=encryption.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encryption.d.ts","sourceRoot":"","sources":["../src/encryption.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAsCH;;;;;;;;;;GAUG;AACH,wBAAsB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAiB/E;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAmC/E;AAED;;;GAGG;AACH,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAO/E"}
@@ -0,0 +1,120 @@
1
+ /**
2
+ * SaveState Encryption Module
3
+ *
4
+ * Provides AES-256-GCM encryption with scrypt key derivation.
5
+ * All data is encrypted before leaving the machine.
6
+ *
7
+ * Key derivation: passphrase → scrypt → 256-bit key
8
+ * Encryption: AES-256-GCM (authenticated encryption)
9
+ * Integrity: Built into GCM auth tag
10
+ */
11
+ import { randomBytes, scryptSync, createCipheriv, createDecipheriv } from 'node:crypto';
12
+ /** Encryption parameters */
13
+ const ALGORITHM = 'aes-256-gcm';
14
+ const KEY_LENGTH = 32; // 256 bits
15
+ const IV_LENGTH = 16; // 128 bits (GCM standard)
16
+ const AUTH_TAG_LENGTH = 16; // 128 bits
17
+ const SALT_LENGTH = 32; // 256 bits
18
+ /**
19
+ * scrypt parameters — intentionally memory-hard to resist brute force.
20
+ * N=2^17 (~128MB), r=8, p=1 — comparable to Argon2id defaults.
21
+ */
22
+ const SCRYPT_N = 131072; // 2^17 — CPU/memory cost
23
+ const SCRYPT_R = 8; // block size
24
+ const SCRYPT_P = 1; // parallelization
25
+ /**
26
+ * Header format: [version(1)] [salt(32)] [iv(16)] [authTag(16)] [ciphertext(...)]
27
+ * Total overhead: 1 + 32 + 16 + 16 = 65 bytes
28
+ */
29
+ const HEADER_VERSION = 0x01;
30
+ const HEADER_OVERHEAD = 1 + SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH;
31
+ /**
32
+ * Derive an encryption key from a passphrase using scrypt.
33
+ */
34
+ function deriveKey(passphrase, salt) {
35
+ return scryptSync(passphrase, salt, KEY_LENGTH, {
36
+ N: SCRYPT_N,
37
+ r: SCRYPT_R,
38
+ p: SCRYPT_P,
39
+ maxmem: 256 * 1024 * 1024, // 256MB max memory
40
+ });
41
+ }
42
+ /**
43
+ * Encrypt data with a passphrase.
44
+ *
45
+ * Returns a single buffer containing the version header, salt, IV,
46
+ * auth tag, and ciphertext. This buffer is self-contained — everything
47
+ * needed to decrypt (except the passphrase) is included.
48
+ *
49
+ * @param data - Plaintext data to encrypt
50
+ * @param passphrase - User passphrase for key derivation
51
+ * @returns Encrypted buffer with header
52
+ */
53
+ export async function encrypt(data, passphrase) {
54
+ const salt = randomBytes(SALT_LENGTH);
55
+ const iv = randomBytes(IV_LENGTH);
56
+ const key = deriveKey(passphrase, salt);
57
+ const cipher = createCipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
58
+ const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
59
+ const authTag = cipher.getAuthTag();
60
+ // Pack: [version][salt][iv][authTag][ciphertext]
61
+ return Buffer.concat([
62
+ Buffer.from([HEADER_VERSION]),
63
+ salt,
64
+ iv,
65
+ authTag,
66
+ encrypted,
67
+ ]);
68
+ }
69
+ /**
70
+ * Decrypt data with a passphrase.
71
+ *
72
+ * Reads the version header, extracts salt/IV/authTag, derives the key,
73
+ * and decrypts. GCM auth tag verification ensures integrity automatically.
74
+ *
75
+ * @param data - Encrypted buffer (as produced by encrypt())
76
+ * @param passphrase - User passphrase for key derivation
77
+ * @returns Decrypted plaintext buffer
78
+ * @throws Error if passphrase is wrong or data is tampered
79
+ */
80
+ export async function decrypt(data, passphrase) {
81
+ if (data.length < HEADER_OVERHEAD) {
82
+ throw new Error('Invalid encrypted data: too short');
83
+ }
84
+ const version = data[0];
85
+ if (version !== HEADER_VERSION) {
86
+ throw new Error(`Unsupported encryption format version: ${version}`);
87
+ }
88
+ let offset = 1;
89
+ const salt = data.subarray(offset, offset + SALT_LENGTH);
90
+ offset += SALT_LENGTH;
91
+ const iv = data.subarray(offset, offset + IV_LENGTH);
92
+ offset += IV_LENGTH;
93
+ const authTag = data.subarray(offset, offset + AUTH_TAG_LENGTH);
94
+ offset += AUTH_TAG_LENGTH;
95
+ const ciphertext = data.subarray(offset);
96
+ const key = deriveKey(passphrase, salt);
97
+ const decipher = createDecipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
98
+ decipher.setAuthTag(authTag);
99
+ try {
100
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
101
+ }
102
+ catch {
103
+ throw new Error('Decryption failed — wrong passphrase or corrupted data. ' +
104
+ 'GCM authentication tag verification failed.');
105
+ }
106
+ }
107
+ /**
108
+ * Verify that encrypted data can be decrypted with the given passphrase
109
+ * without actually returning the plaintext.
110
+ */
111
+ export async function verify(data, passphrase) {
112
+ try {
113
+ await decrypt(data, passphrase);
114
+ return true;
115
+ }
116
+ catch {
117
+ return false;
118
+ }
119
+ }
120
+ //# sourceMappingURL=encryption.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encryption.js","sourceRoot":"","sources":["../src/encryption.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * SaveState Archive Format (SAF)
3
+ *
4
+ * Packing: JSON + files → tar.gz → encrypt → .saf.enc
5
+ * Unpacking: .saf.enc → decrypt → tar.gz → extract
6
+ */
7
+ import type { Snapshot } from './types.js';
8
+ /** File extension for encrypted SaveState archives */
9
+ export declare const SAF_EXTENSION = ".saf.enc";
10
+ /** Current SAF format version */
11
+ export declare const SAF_VERSION = "0.1.0";
12
+ /**
13
+ * Build the directory structure for a snapshot, ready to be tarred.
14
+ * Returns a map of relative paths → content buffers.
15
+ */
16
+ export declare function packSnapshot(snapshot: Snapshot): Map<string, Buffer>;
17
+ /**
18
+ * Unpack a snapshot from extracted archive files.
19
+ */
20
+ export declare function unpackSnapshot(files: Map<string, Buffer>): Snapshot;
21
+ /**
22
+ * Compute SHA-256 checksum of a buffer.
23
+ */
24
+ export declare function computeChecksum(data: Buffer): string;
25
+ /**
26
+ * Generate a snapshot ID from timestamp + random suffix.
27
+ */
28
+ export declare function generateSnapshotId(): string;
29
+ /**
30
+ * Generate a filename for a snapshot archive.
31
+ */
32
+ export declare function snapshotFilename(id: string): string;
33
+ /**
34
+ * Pack a file map into a tar.gz buffer.
35
+ *
36
+ * Creates an in-memory tar archive, gzips it, and returns the buffer.
37
+ * Each entry in the map is a relative path → content buffer.
38
+ */
39
+ export declare function packToArchive(files: Map<string, Buffer>): Buffer;
40
+ /**
41
+ * Unpack a tar.gz buffer into a file map.
42
+ *
43
+ * Decompresses and extracts all files from the archive,
44
+ * returning their paths and content buffers.
45
+ */
46
+ export declare function unpackFromArchive(archive: Buffer): Promise<Map<string, Buffer>>;
47
+ //# sourceMappingURL=format.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAOH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C,sDAAsD;AACtD,eAAO,MAAM,aAAa,aAAa,CAAC;AAExC,iCAAiC;AACjC,eAAO,MAAM,WAAW,UAAU,CAAC;AAEnC;;;GAGG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,QAAQ,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CA+CpE;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,QAAQ,CAmDnE;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAI3C;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAEnD;AAuCD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAGhE;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAoBrF"}
package/dist/format.js ADDED
@@ -0,0 +1,198 @@
1
+ /**
2
+ * SaveState Archive Format (SAF)
3
+ *
4
+ * Packing: JSON + files → tar.gz → encrypt → .saf.enc
5
+ * Unpacking: .saf.enc → decrypt → tar.gz → extract
6
+ */
7
+ import { createHash } from 'node:crypto';
8
+ import { gzipSync, gunzipSync } from 'node:zlib';
9
+ import { Readable } from 'node:stream';
10
+ import { pipeline } from 'node:stream/promises';
11
+ import { Header, Parser } from 'tar';
12
+ /** File extension for encrypted SaveState archives */
13
+ export const SAF_EXTENSION = '.saf.enc';
14
+ /** Current SAF format version */
15
+ export const SAF_VERSION = '0.1.0';
16
+ /**
17
+ * Build the directory structure for a snapshot, ready to be tarred.
18
+ * Returns a map of relative paths → content buffers.
19
+ */
20
+ export function packSnapshot(snapshot) {
21
+ const files = new Map();
22
+ // manifest.json
23
+ files.set('manifest.json', Buffer.from(JSON.stringify(snapshot.manifest, null, 2)));
24
+ // identity/
25
+ if (snapshot.identity.personality) {
26
+ files.set('identity/personality.md', Buffer.from(snapshot.identity.personality));
27
+ }
28
+ if (snapshot.identity.config) {
29
+ files.set('identity/config.json', Buffer.from(JSON.stringify(snapshot.identity.config, null, 2)));
30
+ }
31
+ if (snapshot.identity.tools?.length) {
32
+ files.set('identity/tools.json', Buffer.from(JSON.stringify(snapshot.identity.tools, null, 2)));
33
+ }
34
+ if (snapshot.identity.skills?.length) {
35
+ files.set('identity/skills.json', Buffer.from(JSON.stringify(snapshot.identity.skills, null, 2)));
36
+ }
37
+ if (snapshot.identity.scripts?.length) {
38
+ files.set('identity/scripts.json', Buffer.from(JSON.stringify(snapshot.identity.scripts, null, 2)));
39
+ }
40
+ if (snapshot.identity.extensions?.length) {
41
+ files.set('identity/extensions.json', Buffer.from(JSON.stringify(snapshot.identity.extensions, null, 2)));
42
+ }
43
+ if (snapshot.identity.fileManifest?.length) {
44
+ files.set('identity/file-manifest.json', Buffer.from(JSON.stringify(snapshot.identity.fileManifest, null, 2)));
45
+ }
46
+ if (snapshot.identity.projectMeta && Object.keys(snapshot.identity.projectMeta).length > 0) {
47
+ files.set('identity/project-meta.json', Buffer.from(JSON.stringify(snapshot.identity.projectMeta, null, 2)));
48
+ }
49
+ // memory/
50
+ files.set('memory/core.json', Buffer.from(JSON.stringify(snapshot.memory.core, null, 2)));
51
+ if (snapshot.memory.knowledge.length > 0) {
52
+ files.set('memory/knowledge/index.json', Buffer.from(JSON.stringify(snapshot.memory.knowledge, null, 2)));
53
+ }
54
+ // conversations/
55
+ files.set('conversations/index.json', Buffer.from(JSON.stringify(snapshot.conversations, null, 2)));
56
+ // meta/
57
+ files.set('meta/platform.json', Buffer.from(JSON.stringify(snapshot.platform, null, 2)));
58
+ files.set('meta/snapshot-chain.json', Buffer.from(JSON.stringify(snapshot.chain, null, 2)));
59
+ files.set('meta/restore-hints.json', Buffer.from(JSON.stringify(snapshot.restoreHints, null, 2)));
60
+ return files;
61
+ }
62
+ /**
63
+ * Unpack a snapshot from extracted archive files.
64
+ */
65
+ export function unpackSnapshot(files) {
66
+ const getJson = (path) => {
67
+ const buf = files.get(path);
68
+ if (!buf)
69
+ throw new Error(`Missing required file in archive: ${path}`);
70
+ return JSON.parse(buf.toString('utf-8'));
71
+ };
72
+ const getText = (path) => {
73
+ const buf = files.get(path);
74
+ return buf ? buf.toString('utf-8') : undefined;
75
+ };
76
+ const manifest = getJson('manifest.json');
77
+ const identity = {
78
+ personality: getText('identity/personality.md'),
79
+ config: files.has('identity/config.json')
80
+ ? getJson('identity/config.json')
81
+ : undefined,
82
+ tools: files.has('identity/tools.json')
83
+ ? getJson('identity/tools.json')
84
+ : undefined,
85
+ skills: files.has('identity/skills.json')
86
+ ? getJson('identity/skills.json')
87
+ : undefined,
88
+ scripts: files.has('identity/scripts.json')
89
+ ? getJson('identity/scripts.json')
90
+ : undefined,
91
+ extensions: files.has('identity/extensions.json')
92
+ ? getJson('identity/extensions.json')
93
+ : undefined,
94
+ fileManifest: files.has('identity/file-manifest.json')
95
+ ? getJson('identity/file-manifest.json')
96
+ : undefined,
97
+ projectMeta: files.has('identity/project-meta.json')
98
+ ? getJson('identity/project-meta.json')
99
+ : undefined,
100
+ };
101
+ const memory = {
102
+ core: getJson('memory/core.json'),
103
+ knowledge: files.has('memory/knowledge/index.json')
104
+ ? getJson('memory/knowledge/index.json')
105
+ : [],
106
+ };
107
+ const conversations = getJson('conversations/index.json');
108
+ const platform = getJson('meta/platform.json');
109
+ const chain = getJson('meta/snapshot-chain.json');
110
+ const restoreHints = getJson('meta/restore-hints.json');
111
+ return { manifest, identity, memory, conversations, platform, chain, restoreHints };
112
+ }
113
+ /**
114
+ * Compute SHA-256 checksum of a buffer.
115
+ */
116
+ export function computeChecksum(data) {
117
+ return createHash('sha256').update(data).digest('hex');
118
+ }
119
+ /**
120
+ * Generate a snapshot ID from timestamp + random suffix.
121
+ */
122
+ export function generateSnapshotId() {
123
+ const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
124
+ const rand = Math.random().toString(36).slice(2, 8);
125
+ return `ss-${ts}-${rand}`;
126
+ }
127
+ /**
128
+ * Generate a filename for a snapshot archive.
129
+ */
130
+ export function snapshotFilename(id) {
131
+ return `${id}${SAF_EXTENSION}`;
132
+ }
133
+ /**
134
+ * Build a raw tar archive from in-memory files.
135
+ * Uses tar's Header class for proper POSIX-compliant headers.
136
+ */
137
+ function buildTar(files) {
138
+ const blocks = [];
139
+ for (const [path, data] of files) {
140
+ const headerBuf = Buffer.alloc(512);
141
+ const h = new Header();
142
+ h.path = path;
143
+ h.size = data.length;
144
+ h.type = 'File';
145
+ h.mode = 0o644;
146
+ h.mtime = new Date();
147
+ h.uid = 0;
148
+ h.gid = 0;
149
+ h.uname = 'savestate';
150
+ h.gname = 'savestate';
151
+ h.encode(headerBuf, 0);
152
+ blocks.push(headerBuf);
153
+ blocks.push(data);
154
+ // Pad data to 512-byte boundary
155
+ const remainder = data.length % 512;
156
+ if (remainder > 0) {
157
+ blocks.push(Buffer.alloc(512 - remainder));
158
+ }
159
+ }
160
+ // End-of-archive marker: two 512-byte zero blocks
161
+ blocks.push(Buffer.alloc(1024));
162
+ return Buffer.concat(blocks);
163
+ }
164
+ /**
165
+ * Pack a file map into a tar.gz buffer.
166
+ *
167
+ * Creates an in-memory tar archive, gzips it, and returns the buffer.
168
+ * Each entry in the map is a relative path → content buffer.
169
+ */
170
+ export function packToArchive(files) {
171
+ const tar = buildTar(files);
172
+ return gzipSync(tar);
173
+ }
174
+ /**
175
+ * Unpack a tar.gz buffer into a file map.
176
+ *
177
+ * Decompresses and extracts all files from the archive,
178
+ * returning their paths and content buffers.
179
+ */
180
+ export async function unpackFromArchive(archive) {
181
+ const files = new Map();
182
+ const tar = gunzipSync(archive);
183
+ const parser = new Parser({
184
+ onReadEntry: (entry) => {
185
+ const chunks = [];
186
+ entry.on('data', (chunk) => chunks.push(chunk));
187
+ entry.on('end', () => {
188
+ if (entry.type === 'File') {
189
+ files.set(entry.path, Buffer.concat(chunks));
190
+ }
191
+ });
192
+ },
193
+ });
194
+ const source = Readable.from(tar);
195
+ await pipeline(source, parser);
196
+ return files;
197
+ }
198
+ //# sourceMappingURL=format.js.map