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.
- package/LICENSE +21 -0
- package/README.md +510 -0
- package/dist/advisor/advisor.d.ts +50 -0
- package/dist/advisor/advisor.js +178 -0
- package/dist/agents/base.d.ts +62 -0
- package/dist/agents/base.js +215 -0
- package/dist/agents/orchestrator.d.ts +46 -0
- package/dist/agents/orchestrator.js +192 -0
- package/dist/agents/team.d.ts +51 -0
- package/dist/agents/team.js +210 -0
- package/dist/api/models.d.ts +23 -0
- package/dist/api/models.js +514 -0
- package/dist/api/ollama.d.ts +153 -0
- package/dist/api/ollama.js +751 -0
- package/dist/api/openrouter.d.ts +55 -0
- package/dist/api/openrouter.js +223 -0
- package/dist/commands/commands.d.ts +43 -0
- package/dist/commands/commands.js +308 -0
- package/dist/config/config.d.ts +8 -0
- package/dist/config/config.js +311 -0
- package/dist/context/compaction.d.ts +54 -0
- package/dist/context/compaction.js +251 -0
- package/dist/context/custom-agents.d.ts +107 -0
- package/dist/context/custom-agents.js +397 -0
- package/dist/context/custom-tools.d.ts +120 -0
- package/dist/context/custom-tools.js +564 -0
- package/dist/context/git-checkpoint.d.ts +52 -0
- package/dist/context/git-checkpoint.js +240 -0
- package/dist/context/neuro-md.d.ts +48 -0
- package/dist/context/neuro-md.js +202 -0
- package/dist/context/neuroignore.d.ts +102 -0
- package/dist/context/neuroignore.js +441 -0
- package/dist/context/repo-map.d.ts +38 -0
- package/dist/context/repo-map.js +220 -0
- package/dist/context/skill-standard.d.ts +262 -0
- package/dist/context/skill-standard.js +1156 -0
- package/dist/context/skill-system.d.ts +75 -0
- package/dist/context/skill-system.js +578 -0
- package/dist/context/tree-sitter.d.ts +99 -0
- package/dist/context/tree-sitter.js +1956 -0
- package/dist/core/acp.d.ts +325 -0
- package/dist/core/acp.js +1498 -0
- package/dist/core/api-server.d.ts +143 -0
- package/dist/core/api-server.js +550 -0
- package/dist/core/approval.d.ts +81 -0
- package/dist/core/approval.js +432 -0
- package/dist/core/auto-compact.d.ts +127 -0
- package/dist/core/auto-compact.js +436 -0
- package/dist/core/auto-mode.d.ts +232 -0
- package/dist/core/auto-mode.js +831 -0
- package/dist/core/background-session.d.ts +166 -0
- package/dist/core/background-session.js +696 -0
- package/dist/core/cicd.d.ts +257 -0
- package/dist/core/cicd.js +1443 -0
- package/dist/core/cloud-sync.d.ts +156 -0
- package/dist/core/cloud-sync.js +582 -0
- package/dist/core/code-review.d.ts +132 -0
- package/dist/core/code-review.js +1191 -0
- package/dist/core/completion.d.ts +49 -0
- package/dist/core/completion.js +384 -0
- package/dist/core/context.d.ts +38 -0
- package/dist/core/context.js +144 -0
- package/dist/core/diff-preview.d.ts +35 -0
- package/dist/core/diff-preview.js +173 -0
- package/dist/core/doom-loop.d.ts +51 -0
- package/dist/core/doom-loop.js +179 -0
- package/dist/core/engine.d.ts +183 -0
- package/dist/core/engine.js +942 -0
- package/dist/core/extended-thinking.d.ts +103 -0
- package/dist/core/extended-thinking.js +269 -0
- package/dist/core/fallback.d.ts +54 -0
- package/dist/core/fallback.js +104 -0
- package/dist/core/git-worktree.d.ts +139 -0
- package/dist/core/git-worktree.js +614 -0
- package/dist/core/headless.d.ts +30 -0
- package/dist/core/headless.js +101 -0
- package/dist/core/i18n.d.ts +83 -0
- package/dist/core/i18n.js +739 -0
- package/dist/core/linting.d.ts +129 -0
- package/dist/core/linting.js +699 -0
- package/dist/core/model-router.d.ts +109 -0
- package/dist/core/model-router.js +581 -0
- package/dist/core/multi-model.d.ts +243 -0
- package/dist/core/multi-model.js +1099 -0
- package/dist/core/multi-session.d.ts +144 -0
- package/dist/core/multi-session.js +442 -0
- package/dist/core/multimodal.d.ts +125 -0
- package/dist/core/multimodal.js +286 -0
- package/dist/core/observability.d.ts +93 -0
- package/dist/core/observability.js +737 -0
- package/dist/core/os-sandbox.d.ts +122 -0
- package/dist/core/os-sandbox.js +1193 -0
- package/dist/core/outcome-grading.d.ts +228 -0
- package/dist/core/outcome-grading.js +1123 -0
- package/dist/core/output-styles.d.ts +57 -0
- package/dist/core/output-styles.js +382 -0
- package/dist/core/parallel-agents.d.ts +183 -0
- package/dist/core/parallel-agents.js +563 -0
- package/dist/core/plugin-bundle.d.ts +236 -0
- package/dist/core/plugin-bundle.js +887 -0
- package/dist/core/plugin-sdk.d.ts +139 -0
- package/dist/core/plugin-sdk.js +273 -0
- package/dist/core/prompt-cache.d.ts +163 -0
- package/dist/core/prompt-cache.js +599 -0
- package/dist/core/sandbox.d.ts +127 -0
- package/dist/core/sandbox.js +369 -0
- package/dist/core/scheduled-tasks.d.ts +151 -0
- package/dist/core/scheduled-tasks.js +502 -0
- package/dist/core/security-scanner.d.ts +160 -0
- package/dist/core/security-scanner.js +1494 -0
- package/dist/core/session.d.ts +83 -0
- package/dist/core/session.js +269 -0
- package/dist/core/shell-completion.d.ts +51 -0
- package/dist/core/shell-completion.js +674 -0
- package/dist/core/smart-monitor.d.ts +146 -0
- package/dist/core/smart-monitor.js +1199 -0
- package/dist/core/spec-driven.d.ts +233 -0
- package/dist/core/spec-driven.js +1485 -0
- package/dist/core/spending-warnings.d.ts +123 -0
- package/dist/core/spending-warnings.js +456 -0
- package/dist/core/sub-agent.d.ts +298 -0
- package/dist/core/sub-agent.js +1023 -0
- package/dist/core/telemetry.d.ts +157 -0
- package/dist/core/telemetry.js +412 -0
- package/dist/core/terminal-ux.d.ts +134 -0
- package/dist/core/terminal-ux.js +649 -0
- package/dist/core/testing.d.ts +146 -0
- package/dist/core/testing.js +1200 -0
- package/dist/core/types.d.ts +439 -0
- package/dist/core/types.js +6 -0
- package/dist/core/undo-redo.d.ts +112 -0
- package/dist/core/undo-redo.js +290 -0
- package/dist/core/updater.d.ts +159 -0
- package/dist/core/updater.js +608 -0
- package/dist/core/vim-mode.d.ts +151 -0
- package/dist/core/vim-mode.js +771 -0
- package/dist/core/voice.d.ts +137 -0
- package/dist/core/voice.js +538 -0
- package/dist/core/web-dashboard.d.ts +109 -0
- package/dist/core/web-dashboard.js +484 -0
- package/dist/hooks/hooks.d.ts +74 -0
- package/dist/hooks/hooks.js +160 -0
- package/dist/hooks/llm-evaluator.d.ts +165 -0
- package/dist/hooks/llm-evaluator.js +560 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1186 -0
- package/dist/lsp/lsp-manager.d.ts +63 -0
- package/dist/lsp/lsp-manager.js +351 -0
- package/dist/mcp/client.d.ts +133 -0
- package/dist/mcp/client.js +684 -0
- package/dist/mcp/mcp-apps.d.ts +70 -0
- package/dist/mcp/mcp-apps.js +1007 -0
- package/dist/tools/bash.d.ts +5 -0
- package/dist/tools/bash.js +195 -0
- package/dist/tools/browser.d.ts +92 -0
- package/dist/tools/browser.js +1570 -0
- package/dist/tools/extended.d.ts +6 -0
- package/dist/tools/extended.js +191 -0
- package/dist/tools/file.d.ts +10 -0
- package/dist/tools/file.js +382 -0
- package/dist/tools/github.d.ts +389 -0
- package/dist/tools/github.js +759 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/index.js +40 -0
- package/dist/tools/memory.d.ts +6 -0
- package/dist/tools/memory.js +197 -0
- package/dist/tools/registry.d.ts +29 -0
- package/dist/tools/registry.js +64 -0
- package/dist/tools/web.d.ts +6 -0
- package/dist/tools/web.js +150 -0
- package/dist/ui/renderer.d.ts +97 -0
- package/dist/ui/renderer.js +279 -0
- package/dist/ui/theme.d.ts +27 -0
- package/dist/ui/theme.js +106 -0
- 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
|