neuro-cli 4.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 (175) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +510 -0
  3. package/dist/advisor/advisor.d.ts +50 -0
  4. package/dist/advisor/advisor.js +178 -0
  5. package/dist/agents/base.d.ts +62 -0
  6. package/dist/agents/base.js +215 -0
  7. package/dist/agents/orchestrator.d.ts +46 -0
  8. package/dist/agents/orchestrator.js +192 -0
  9. package/dist/agents/team.d.ts +51 -0
  10. package/dist/agents/team.js +210 -0
  11. package/dist/api/models.d.ts +23 -0
  12. package/dist/api/models.js +514 -0
  13. package/dist/api/ollama.d.ts +153 -0
  14. package/dist/api/ollama.js +751 -0
  15. package/dist/api/openrouter.d.ts +55 -0
  16. package/dist/api/openrouter.js +223 -0
  17. package/dist/commands/commands.d.ts +43 -0
  18. package/dist/commands/commands.js +308 -0
  19. package/dist/config/config.d.ts +8 -0
  20. package/dist/config/config.js +311 -0
  21. package/dist/context/compaction.d.ts +54 -0
  22. package/dist/context/compaction.js +251 -0
  23. package/dist/context/custom-agents.d.ts +107 -0
  24. package/dist/context/custom-agents.js +397 -0
  25. package/dist/context/custom-tools.d.ts +120 -0
  26. package/dist/context/custom-tools.js +564 -0
  27. package/dist/context/git-checkpoint.d.ts +52 -0
  28. package/dist/context/git-checkpoint.js +240 -0
  29. package/dist/context/neuro-md.d.ts +48 -0
  30. package/dist/context/neuro-md.js +202 -0
  31. package/dist/context/neuroignore.d.ts +102 -0
  32. package/dist/context/neuroignore.js +441 -0
  33. package/dist/context/repo-map.d.ts +38 -0
  34. package/dist/context/repo-map.js +220 -0
  35. package/dist/context/skill-standard.d.ts +262 -0
  36. package/dist/context/skill-standard.js +1156 -0
  37. package/dist/context/skill-system.d.ts +75 -0
  38. package/dist/context/skill-system.js +578 -0
  39. package/dist/context/tree-sitter.d.ts +99 -0
  40. package/dist/context/tree-sitter.js +1956 -0
  41. package/dist/core/acp.d.ts +325 -0
  42. package/dist/core/acp.js +1498 -0
  43. package/dist/core/api-server.d.ts +143 -0
  44. package/dist/core/api-server.js +550 -0
  45. package/dist/core/approval.d.ts +81 -0
  46. package/dist/core/approval.js +432 -0
  47. package/dist/core/auto-compact.d.ts +127 -0
  48. package/dist/core/auto-compact.js +436 -0
  49. package/dist/core/auto-mode.d.ts +232 -0
  50. package/dist/core/auto-mode.js +831 -0
  51. package/dist/core/background-session.d.ts +166 -0
  52. package/dist/core/background-session.js +696 -0
  53. package/dist/core/cicd.d.ts +257 -0
  54. package/dist/core/cicd.js +1443 -0
  55. package/dist/core/cloud-sync.d.ts +156 -0
  56. package/dist/core/cloud-sync.js +582 -0
  57. package/dist/core/code-review.d.ts +132 -0
  58. package/dist/core/code-review.js +1191 -0
  59. package/dist/core/completion.d.ts +49 -0
  60. package/dist/core/completion.js +384 -0
  61. package/dist/core/context.d.ts +38 -0
  62. package/dist/core/context.js +144 -0
  63. package/dist/core/diff-preview.d.ts +35 -0
  64. package/dist/core/diff-preview.js +173 -0
  65. package/dist/core/doom-loop.d.ts +51 -0
  66. package/dist/core/doom-loop.js +179 -0
  67. package/dist/core/engine.d.ts +183 -0
  68. package/dist/core/engine.js +942 -0
  69. package/dist/core/extended-thinking.d.ts +103 -0
  70. package/dist/core/extended-thinking.js +269 -0
  71. package/dist/core/fallback.d.ts +54 -0
  72. package/dist/core/fallback.js +104 -0
  73. package/dist/core/git-worktree.d.ts +139 -0
  74. package/dist/core/git-worktree.js +614 -0
  75. package/dist/core/headless.d.ts +30 -0
  76. package/dist/core/headless.js +101 -0
  77. package/dist/core/i18n.d.ts +83 -0
  78. package/dist/core/i18n.js +739 -0
  79. package/dist/core/linting.d.ts +129 -0
  80. package/dist/core/linting.js +699 -0
  81. package/dist/core/model-router.d.ts +109 -0
  82. package/dist/core/model-router.js +581 -0
  83. package/dist/core/multi-model.d.ts +243 -0
  84. package/dist/core/multi-model.js +1099 -0
  85. package/dist/core/multi-session.d.ts +144 -0
  86. package/dist/core/multi-session.js +442 -0
  87. package/dist/core/multimodal.d.ts +125 -0
  88. package/dist/core/multimodal.js +286 -0
  89. package/dist/core/observability.d.ts +93 -0
  90. package/dist/core/observability.js +737 -0
  91. package/dist/core/os-sandbox.d.ts +122 -0
  92. package/dist/core/os-sandbox.js +1193 -0
  93. package/dist/core/outcome-grading.d.ts +228 -0
  94. package/dist/core/outcome-grading.js +1123 -0
  95. package/dist/core/output-styles.d.ts +57 -0
  96. package/dist/core/output-styles.js +382 -0
  97. package/dist/core/parallel-agents.d.ts +183 -0
  98. package/dist/core/parallel-agents.js +563 -0
  99. package/dist/core/plugin-bundle.d.ts +236 -0
  100. package/dist/core/plugin-bundle.js +887 -0
  101. package/dist/core/plugin-sdk.d.ts +139 -0
  102. package/dist/core/plugin-sdk.js +273 -0
  103. package/dist/core/prompt-cache.d.ts +163 -0
  104. package/dist/core/prompt-cache.js +599 -0
  105. package/dist/core/sandbox.d.ts +127 -0
  106. package/dist/core/sandbox.js +369 -0
  107. package/dist/core/scheduled-tasks.d.ts +151 -0
  108. package/dist/core/scheduled-tasks.js +502 -0
  109. package/dist/core/security-scanner.d.ts +160 -0
  110. package/dist/core/security-scanner.js +1494 -0
  111. package/dist/core/session.d.ts +83 -0
  112. package/dist/core/session.js +269 -0
  113. package/dist/core/shell-completion.d.ts +51 -0
  114. package/dist/core/shell-completion.js +674 -0
  115. package/dist/core/smart-monitor.d.ts +146 -0
  116. package/dist/core/smart-monitor.js +1199 -0
  117. package/dist/core/spec-driven.d.ts +233 -0
  118. package/dist/core/spec-driven.js +1485 -0
  119. package/dist/core/spending-warnings.d.ts +123 -0
  120. package/dist/core/spending-warnings.js +456 -0
  121. package/dist/core/sub-agent.d.ts +298 -0
  122. package/dist/core/sub-agent.js +1023 -0
  123. package/dist/core/telemetry.d.ts +157 -0
  124. package/dist/core/telemetry.js +412 -0
  125. package/dist/core/terminal-ux.d.ts +134 -0
  126. package/dist/core/terminal-ux.js +649 -0
  127. package/dist/core/testing.d.ts +146 -0
  128. package/dist/core/testing.js +1200 -0
  129. package/dist/core/types.d.ts +439 -0
  130. package/dist/core/types.js +6 -0
  131. package/dist/core/undo-redo.d.ts +112 -0
  132. package/dist/core/undo-redo.js +290 -0
  133. package/dist/core/updater.d.ts +159 -0
  134. package/dist/core/updater.js +608 -0
  135. package/dist/core/vim-mode.d.ts +151 -0
  136. package/dist/core/vim-mode.js +771 -0
  137. package/dist/core/voice.d.ts +137 -0
  138. package/dist/core/voice.js +538 -0
  139. package/dist/core/web-dashboard.d.ts +109 -0
  140. package/dist/core/web-dashboard.js +484 -0
  141. package/dist/hooks/hooks.d.ts +74 -0
  142. package/dist/hooks/hooks.js +160 -0
  143. package/dist/hooks/llm-evaluator.d.ts +165 -0
  144. package/dist/hooks/llm-evaluator.js +560 -0
  145. package/dist/index.d.ts +3 -0
  146. package/dist/index.js +1186 -0
  147. package/dist/lsp/lsp-manager.d.ts +63 -0
  148. package/dist/lsp/lsp-manager.js +351 -0
  149. package/dist/mcp/client.d.ts +133 -0
  150. package/dist/mcp/client.js +684 -0
  151. package/dist/mcp/mcp-apps.d.ts +70 -0
  152. package/dist/mcp/mcp-apps.js +1007 -0
  153. package/dist/tools/bash.d.ts +5 -0
  154. package/dist/tools/bash.js +195 -0
  155. package/dist/tools/browser.d.ts +92 -0
  156. package/dist/tools/browser.js +1570 -0
  157. package/dist/tools/extended.d.ts +6 -0
  158. package/dist/tools/extended.js +191 -0
  159. package/dist/tools/file.d.ts +10 -0
  160. package/dist/tools/file.js +382 -0
  161. package/dist/tools/github.d.ts +389 -0
  162. package/dist/tools/github.js +759 -0
  163. package/dist/tools/index.d.ts +9 -0
  164. package/dist/tools/index.js +40 -0
  165. package/dist/tools/memory.d.ts +6 -0
  166. package/dist/tools/memory.js +197 -0
  167. package/dist/tools/registry.d.ts +29 -0
  168. package/dist/tools/registry.js +64 -0
  169. package/dist/tools/web.d.ts +6 -0
  170. package/dist/tools/web.js +150 -0
  171. package/dist/ui/renderer.d.ts +97 -0
  172. package/dist/ui/renderer.js +279 -0
  173. package/dist/ui/theme.d.ts +27 -0
  174. package/dist/ui/theme.js +106 -0
  175. package/package.json +83 -0
@@ -0,0 +1,582 @@
1
+ // ============================================================
2
+ // NeuroCLI - Cloud Sync
3
+ // Sync sessions to GitHub Gist as free storage backend
4
+ // Import/export sessions, conflict resolution
5
+ // /sync push/pull commands
6
+ // ============================================================
7
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from 'fs';
8
+ import { join } from 'path';
9
+ import { homedir } from 'os';
10
+ import chalk from 'chalk';
11
+ import { createHash } from 'crypto';
12
+ // -----------------------------------------------------------
13
+ // Default config
14
+ // -----------------------------------------------------------
15
+ const SYNC_CONFIG_PATH = join(homedir(), '.neuro', 'sync-config.json');
16
+ const SYNC_METADATA_PATH = join(homedir(), '.neuro', 'sync-metadata.json');
17
+ function defaultConfig() {
18
+ return {
19
+ enabled: false,
20
+ backend: 'gist',
21
+ githubToken: process.env.GITHUB_TOKEN || '',
22
+ gistId: '',
23
+ autoSync: false,
24
+ syncIntervalMs: 0,
25
+ maxCloudSessions: 50,
26
+ includeContent: true,
27
+ localSyncDir: join(homedir(), '.neuro', 'cloud-sync'),
28
+ };
29
+ }
30
+ // -----------------------------------------------------------
31
+ // CloudSync
32
+ // -----------------------------------------------------------
33
+ export class CloudSync {
34
+ config;
35
+ syncMetadata = new Map();
36
+ isSyncing = false;
37
+ lastSyncAt = 0;
38
+ syncTimer = null;
39
+ constructor(config) {
40
+ this.config = { ...defaultConfig(), ...config };
41
+ this.loadConfig();
42
+ this.loadSyncMetadata();
43
+ this.ensureLocalSyncDir();
44
+ if (this.config.autoSync && this.config.syncIntervalMs > 0) {
45
+ this.startAutoSync();
46
+ }
47
+ }
48
+ // ----------------------------------------------------------
49
+ // Public API
50
+ // ----------------------------------------------------------
51
+ /**
52
+ * Check if cloud sync is enabled
53
+ */
54
+ isEnabled() {
55
+ return this.config.enabled;
56
+ }
57
+ /**
58
+ * Enable cloud sync
59
+ */
60
+ enable() {
61
+ if (!this.config.githubToken && this.config.backend === 'gist') {
62
+ console.log(chalk.yellow('GitHub token not set. Set GITHUB_TOKEN env var or use /sync config.'));
63
+ console.log(chalk.gray(' export GITHUB_TOKEN=ghp_your_token_here'));
64
+ }
65
+ this.config.enabled = true;
66
+ this.saveConfig();
67
+ console.log(chalk.green('Cloud sync enabled.'));
68
+ }
69
+ /**
70
+ * Disable cloud sync
71
+ */
72
+ disable() {
73
+ this.config.enabled = false;
74
+ this.stopAutoSync();
75
+ this.saveConfig();
76
+ console.log(chalk.gray('Cloud sync disabled.'));
77
+ }
78
+ /**
79
+ * Toggle cloud sync
80
+ */
81
+ toggle() {
82
+ if (this.config.enabled)
83
+ this.disable();
84
+ else
85
+ this.enable();
86
+ return this.config.enabled;
87
+ }
88
+ /**
89
+ * Push local sessions to cloud
90
+ */
91
+ async push() {
92
+ if (!this.config.enabled) {
93
+ return this.errorResult('Cloud sync is disabled');
94
+ }
95
+ if (this.isSyncing) {
96
+ return this.errorResult('Sync already in progress');
97
+ }
98
+ this.isSyncing = true;
99
+ const startTime = Date.now();
100
+ let pushed = 0;
101
+ const errors = [];
102
+ try {
103
+ const localSessions = this.getLocalSessions();
104
+ for (const session of localSessions) {
105
+ try {
106
+ const checksum = this.computeChecksum(JSON.stringify(session));
107
+ const existing = this.syncMetadata.get(session.id);
108
+ // Skip if already synced and unchanged
109
+ if (existing && existing.checksum === checksum)
110
+ continue;
111
+ await this.pushSession(session, checksum);
112
+ pushed++;
113
+ this.syncMetadata.set(session.id, {
114
+ sessionId: session.id,
115
+ lastSyncedAt: Date.now(),
116
+ checksum,
117
+ version: (existing?.version || 0) + 1,
118
+ source: 'cloud',
119
+ });
120
+ }
121
+ catch (error) {
122
+ errors.push(`Failed to push session ${session.id}: ${error instanceof Error ? error.message : String(error)}`);
123
+ }
124
+ }
125
+ this.persistSyncMetadata();
126
+ this.lastSyncAt = Date.now();
127
+ }
128
+ finally {
129
+ this.isSyncing = false;
130
+ }
131
+ return {
132
+ pushed,
133
+ pulled: 0,
134
+ conflicts: [],
135
+ errors,
136
+ duration: Date.now() - startTime,
137
+ };
138
+ }
139
+ /**
140
+ * Pull sessions from cloud
141
+ */
142
+ async pull() {
143
+ if (!this.config.enabled) {
144
+ return this.errorResult('Cloud sync is disabled');
145
+ }
146
+ if (this.isSyncing) {
147
+ return this.errorResult('Sync already in progress');
148
+ }
149
+ this.isSyncing = true;
150
+ const startTime = Date.now();
151
+ let pulled = 0;
152
+ const conflicts = [];
153
+ const errors = [];
154
+ try {
155
+ const cloudSessions = await this.fetchCloudSessions();
156
+ for (const cloudSession of cloudSessions) {
157
+ try {
158
+ const localMeta = this.syncMetadata.get(cloudSession.id);
159
+ const localSession = this.getLocalSession(cloudSession.id);
160
+ // Check for conflicts
161
+ if (localSession && localMeta) {
162
+ const localChecksum = this.computeChecksum(JSON.stringify(localSession));
163
+ const cloudChecksum = cloudSession.checksum;
164
+ if (localChecksum !== cloudChecksum && localMeta.source === 'local') {
165
+ // Conflict: both modified
166
+ conflicts.push({
167
+ sessionId: cloudSession.id,
168
+ localVersion: localMeta.version,
169
+ cloudVersion: cloudSession.version,
170
+ localChecksum,
171
+ cloudChecksum,
172
+ localUpdatedAt: localSession.updatedAt,
173
+ cloudUpdatedAt: cloudSession.updatedAt,
174
+ resolution: 'pending',
175
+ });
176
+ continue;
177
+ }
178
+ }
179
+ // No conflict - pull
180
+ this.saveLocalSession(cloudSession);
181
+ pulled++;
182
+ this.syncMetadata.set(cloudSession.id, {
183
+ sessionId: cloudSession.id,
184
+ lastSyncedAt: Date.now(),
185
+ checksum: cloudSession.checksum,
186
+ version: cloudSession.version,
187
+ source: 'cloud',
188
+ });
189
+ }
190
+ catch (error) {
191
+ errors.push(`Failed to pull session ${cloudSession.id}: ${error instanceof Error ? error.message : String(error)}`);
192
+ }
193
+ }
194
+ this.persistSyncMetadata();
195
+ this.lastSyncAt = Date.now();
196
+ }
197
+ finally {
198
+ this.isSyncing = false;
199
+ }
200
+ return {
201
+ pushed: 0,
202
+ pulled,
203
+ conflicts,
204
+ errors,
205
+ duration: Date.now() - startTime,
206
+ };
207
+ }
208
+ /**
209
+ * Full sync (push + pull)
210
+ */
211
+ async sync() {
212
+ const pushResult = await this.push();
213
+ const pullResult = await this.pull();
214
+ return {
215
+ pushed: pushResult.pushed,
216
+ pulled: pullResult.pulled,
217
+ conflicts: [...pushResult.conflicts, ...pullResult.conflicts],
218
+ errors: [...pushResult.errors, ...pullResult.errors],
219
+ duration: pushResult.duration + pullResult.duration,
220
+ };
221
+ }
222
+ /**
223
+ * Resolve a sync conflict
224
+ */
225
+ resolveConflict(sessionId, resolution) {
226
+ // Implementation for conflict resolution
227
+ switch (resolution) {
228
+ case 'local':
229
+ // Push local version to cloud
230
+ console.log(chalk.green(`Conflict resolved: keeping local version for ${sessionId}`));
231
+ return true;
232
+ case 'cloud':
233
+ // Pull cloud version to local
234
+ console.log(chalk.green(`Conflict resolved: using cloud version for ${sessionId}`));
235
+ return true;
236
+ case 'merge':
237
+ console.log(chalk.yellow(`Merge conflict resolution not yet implemented for ${sessionId}`));
238
+ return false;
239
+ default:
240
+ return false;
241
+ }
242
+ }
243
+ /**
244
+ * Export sessions to a local file
245
+ */
246
+ exportSessions(filePath) {
247
+ const sessions = this.getLocalSessions();
248
+ const exportData = {
249
+ version: '3.0.0',
250
+ exportedAt: Date.now(),
251
+ sessions,
252
+ };
253
+ const exportPath = filePath || join(this.config.localSyncDir, `export-${Date.now()}.json`);
254
+ writeFileSync(exportPath, JSON.stringify(exportData, null, 2), 'utf-8');
255
+ console.log(chalk.green(`Exported ${sessions.length} session(s) to ${exportPath}`));
256
+ return exportPath;
257
+ }
258
+ /**
259
+ * Import sessions from a file
260
+ */
261
+ importSessions(filePath) {
262
+ try {
263
+ const raw = readFileSync(filePath, 'utf-8');
264
+ const data = JSON.parse(raw);
265
+ if (!Array.isArray(data.sessions)) {
266
+ console.log(chalk.red('Invalid import file: no sessions array found.'));
267
+ return 0;
268
+ }
269
+ let imported = 0;
270
+ for (const session of data.sessions) {
271
+ try {
272
+ this.saveLocalSession(session);
273
+ imported++;
274
+ }
275
+ catch (error) {
276
+ console.log(chalk.yellow(`Failed to import session ${session.id}: ${error instanceof Error ? error.message : String(error)}`));
277
+ }
278
+ }
279
+ console.log(chalk.green(`Imported ${imported} session(s) from ${filePath}`));
280
+ return imported;
281
+ }
282
+ catch (error) {
283
+ console.log(chalk.red(`Failed to import: ${error instanceof Error ? error.message : String(error)}`));
284
+ return 0;
285
+ }
286
+ }
287
+ /**
288
+ * Get sync status
289
+ */
290
+ getStatus() {
291
+ return {
292
+ enabled: this.config.enabled,
293
+ backend: this.config.backend,
294
+ lastSyncAt: this.lastSyncAt,
295
+ syncedSessions: this.syncMetadata.size,
296
+ isSyncing: this.isSyncing,
297
+ gistConfigured: this.config.backend === 'gist' ? !!this.config.gistId : true,
298
+ };
299
+ }
300
+ /**
301
+ * Set GitHub token
302
+ */
303
+ setGitHubToken(token) {
304
+ this.config.githubToken = token;
305
+ this.saveConfig();
306
+ console.log(chalk.green('GitHub token updated.'));
307
+ }
308
+ /**
309
+ * Get/set gist ID
310
+ */
311
+ setGistId(gistId) {
312
+ this.config.gistId = gistId;
313
+ this.saveConfig();
314
+ console.log(chalk.green(`Gist ID set to: ${gistId}`));
315
+ }
316
+ /**
317
+ * Get config
318
+ */
319
+ getConfig() {
320
+ return { ...this.config };
321
+ }
322
+ /**
323
+ * Print sync status
324
+ */
325
+ printStatus() {
326
+ const status = this.getStatus();
327
+ console.log('');
328
+ console.log(chalk.bold('--- NeuroCLI Cloud Sync ---'));
329
+ console.log(` Enabled: ${status.enabled ? chalk.green('yes') : chalk.gray('no')}`);
330
+ console.log(` Backend: ${chalk.cyan(status.backend)}`);
331
+ console.log(` Synced sessions: ${status.syncedSessions}`);
332
+ console.log(` Last sync: ${status.lastSyncAt > 0 ? new Date(status.lastSyncAt).toLocaleString() : chalk.gray('never')}`);
333
+ console.log(` Currently syncing: ${status.isSyncing ? chalk.yellow('yes') : chalk.gray('no')}`);
334
+ if (this.config.backend === 'gist') {
335
+ console.log(` GitHub token: ${this.config.githubToken ? chalk.green('configured') : chalk.red('not set')}`);
336
+ console.log(` Gist ID: ${this.config.gistId || chalk.gray('(auto-create on first push)')}`);
337
+ }
338
+ console.log(` Auto-sync: ${this.config.autoSync ? chalk.green('on') : chalk.gray('off')}`);
339
+ console.log('');
340
+ }
341
+ // ----------------------------------------------------------
342
+ // Private helpers
343
+ // ----------------------------------------------------------
344
+ startAutoSync() {
345
+ if (this.syncTimer)
346
+ clearInterval(this.syncTimer);
347
+ this.syncTimer = setInterval(() => {
348
+ this.sync().catch(() => { });
349
+ }, this.config.syncIntervalMs);
350
+ }
351
+ stopAutoSync() {
352
+ if (this.syncTimer) {
353
+ clearInterval(this.syncTimer);
354
+ this.syncTimer = null;
355
+ }
356
+ }
357
+ getLocalSessions() {
358
+ const sessionDir = join(homedir(), '.neuro', 'sessions');
359
+ const sessions = [];
360
+ try {
361
+ if (!existsSync(sessionDir))
362
+ return sessions;
363
+ const files = readdirSync(sessionDir).filter(f => f.endsWith('.json'));
364
+ for (const file of files) {
365
+ try {
366
+ const raw = readFileSync(join(sessionDir, file), 'utf-8');
367
+ const data = JSON.parse(raw);
368
+ sessions.push(this.toCloudSession(data));
369
+ }
370
+ catch { /* skip invalid */ }
371
+ }
372
+ }
373
+ catch { /* ignore */ }
374
+ return sessions;
375
+ }
376
+ getLocalSession(id) {
377
+ try {
378
+ const sessionPath = join(homedir(), '.neuro', 'sessions', `${id}.json`);
379
+ if (!existsSync(sessionPath))
380
+ return null;
381
+ const raw = readFileSync(sessionPath, 'utf-8');
382
+ return this.toCloudSession(JSON.parse(raw));
383
+ }
384
+ catch {
385
+ return null;
386
+ }
387
+ }
388
+ toCloudSession(data) {
389
+ return {
390
+ id: data.id || 'unknown',
391
+ createdAt: data.createdAt || 0,
392
+ updatedAt: data.updatedAt || Date.now(),
393
+ model: data.model || 'unknown',
394
+ messageCount: data.messages?.length || 0,
395
+ totalCost: data.totalCost || 0,
396
+ description: data.description,
397
+ tags: data.tags || [],
398
+ messages: this.config.includeContent ? data.messages : undefined,
399
+ checksum: this.computeChecksum(JSON.stringify(data)),
400
+ version: 1,
401
+ };
402
+ }
403
+ saveLocalSession(session) {
404
+ const sessionDir = join(homedir(), '.neuro', 'sessions');
405
+ if (!existsSync(sessionDir))
406
+ mkdirSync(sessionDir, { recursive: true });
407
+ const sessionPath = join(sessionDir, `${session.id}.json`);
408
+ const data = {
409
+ id: session.id,
410
+ createdAt: session.createdAt,
411
+ updatedAt: session.updatedAt,
412
+ model: session.model,
413
+ messages: session.messages || [],
414
+ totalCost: session.totalCost,
415
+ description: session.description,
416
+ tags: session.tags,
417
+ };
418
+ writeFileSync(sessionPath, JSON.stringify(data, null, 2), 'utf-8');
419
+ }
420
+ async pushSession(session, checksum) {
421
+ if (this.config.backend === 'gist') {
422
+ await this.pushToGist(session, checksum);
423
+ }
424
+ else {
425
+ this.pushToLocal(session, checksum);
426
+ }
427
+ }
428
+ async pushToGist(session, _checksum) {
429
+ if (!this.config.githubToken) {
430
+ throw new Error('GitHub token not configured');
431
+ }
432
+ const filename = `session-${session.id}.json`;
433
+ const content = JSON.stringify(session, null, 2);
434
+ const gistData = {
435
+ description: `NeuroCLI Session - ${session.id}`,
436
+ public: false,
437
+ files: {
438
+ [filename]: { content },
439
+ },
440
+ };
441
+ const url = this.config.gistId
442
+ ? `https://api.github.com/gists/${this.config.gistId}`
443
+ : 'https://api.github.com/gists';
444
+ const method = this.config.gistId ? 'PATCH' : 'POST';
445
+ try {
446
+ const response = await fetch(url, {
447
+ method,
448
+ headers: {
449
+ 'Authorization': `Bearer ${this.config.githubToken}`,
450
+ 'Content-Type': 'application/json',
451
+ 'Accept': 'application/vnd.github.v3+json',
452
+ 'User-Agent': 'NeuroCLI',
453
+ },
454
+ body: JSON.stringify(gistData),
455
+ });
456
+ if (!response.ok) {
457
+ const text = await response.text();
458
+ throw new Error(`GitHub API error: ${response.status} - ${text}`);
459
+ }
460
+ const result = await response.json();
461
+ if (!this.config.gistId) {
462
+ this.config.gistId = result.id;
463
+ this.saveConfig();
464
+ }
465
+ }
466
+ catch (error) {
467
+ throw new Error(`Gist push failed: ${error instanceof Error ? error.message : String(error)}`);
468
+ }
469
+ }
470
+ pushToLocal(session, _checksum) {
471
+ const dir = this.config.localSyncDir;
472
+ if (!existsSync(dir))
473
+ mkdirSync(dir, { recursive: true });
474
+ const filePath = join(dir, `session-${session.id}.json`);
475
+ writeFileSync(filePath, JSON.stringify(session, null, 2), 'utf-8');
476
+ }
477
+ async fetchCloudSessions() {
478
+ if (this.config.backend === 'gist') {
479
+ return this.fetchFromGist();
480
+ }
481
+ return this.fetchFromLocal();
482
+ }
483
+ async fetchFromGist() {
484
+ if (!this.config.githubToken || !this.config.gistId) {
485
+ return [];
486
+ }
487
+ try {
488
+ const response = await fetch(`https://api.github.com/gists/${this.config.gistId}`, {
489
+ headers: {
490
+ 'Authorization': `Bearer ${this.config.githubToken}`,
491
+ 'Accept': 'application/vnd.github.v3+json',
492
+ 'User-Agent': 'NeuroCLI',
493
+ },
494
+ });
495
+ if (!response.ok)
496
+ return [];
497
+ const gist = await response.json();
498
+ const sessions = [];
499
+ for (const [name, file] of Object.entries(gist.files)) {
500
+ if (!name.startsWith('session-'))
501
+ continue;
502
+ try {
503
+ const session = JSON.parse(file.content);
504
+ sessions.push(session);
505
+ }
506
+ catch { /* skip invalid */ }
507
+ }
508
+ return sessions.slice(0, this.config.maxCloudSessions);
509
+ }
510
+ catch {
511
+ return [];
512
+ }
513
+ }
514
+ fetchFromLocal() {
515
+ const dir = this.config.localSyncDir;
516
+ if (!existsSync(dir))
517
+ return [];
518
+ const sessions = [];
519
+ try {
520
+ const files = readdirSync(dir).filter(f => f.startsWith('session-') && f.endsWith('.json'));
521
+ for (const file of files) {
522
+ try {
523
+ const raw = readFileSync(join(dir, file), 'utf-8');
524
+ sessions.push(JSON.parse(raw));
525
+ }
526
+ catch { /* skip */ }
527
+ }
528
+ }
529
+ catch { /* ignore */ }
530
+ return sessions.slice(0, this.config.maxCloudSessions);
531
+ }
532
+ computeChecksum(data) {
533
+ return createHash('sha256').update(data).digest('hex').slice(0, 16);
534
+ }
535
+ errorResult(message) {
536
+ return { pushed: 0, pulled: 0, conflicts: [], errors: [message], duration: 0 };
537
+ }
538
+ ensureLocalSyncDir() {
539
+ if (!existsSync(this.config.localSyncDir)) {
540
+ mkdirSync(this.config.localSyncDir, { recursive: true });
541
+ }
542
+ }
543
+ saveConfig() {
544
+ try {
545
+ const dir = join(SYNC_CONFIG_PATH, '..');
546
+ if (!existsSync(dir))
547
+ mkdirSync(dir, { recursive: true });
548
+ writeFileSync(SYNC_CONFIG_PATH, JSON.stringify(this.config, null, 2), 'utf-8');
549
+ }
550
+ catch { /* Silently fail */ }
551
+ }
552
+ loadConfig() {
553
+ try {
554
+ if (existsSync(SYNC_CONFIG_PATH)) {
555
+ const raw = readFileSync(SYNC_CONFIG_PATH, 'utf-8');
556
+ const saved = JSON.parse(raw);
557
+ this.config = { ...this.config, ...saved };
558
+ }
559
+ }
560
+ catch { /* Silently fail */ }
561
+ }
562
+ loadSyncMetadata() {
563
+ try {
564
+ if (existsSync(SYNC_METADATA_PATH)) {
565
+ const raw = readFileSync(SYNC_METADATA_PATH, 'utf-8');
566
+ const entries = JSON.parse(raw);
567
+ this.syncMetadata = new Map(entries);
568
+ }
569
+ }
570
+ catch { /* Silently fail */ }
571
+ }
572
+ persistSyncMetadata() {
573
+ try {
574
+ const dir = join(SYNC_METADATA_PATH, '..');
575
+ if (!existsSync(dir))
576
+ mkdirSync(dir, { recursive: true });
577
+ writeFileSync(SYNC_METADATA_PATH, JSON.stringify(Array.from(this.syncMetadata.entries()), null, 2), 'utf-8');
578
+ }
579
+ catch { /* Silently fail */ }
580
+ }
581
+ }
582
+ //# sourceMappingURL=cloud-sync.js.map