@geminilight/mindos 0.1.9 → 0.2.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/bin/cli.js CHANGED
@@ -15,7 +15,13 @@
15
15
  * mindos mcp — start MCP server only
16
16
  * mindos stop — stop running MindOS processes
17
17
  * mindos restart — stop then start
18
+ * mindos open — open Web UI in the default browser
18
19
  * mindos token — show current auth token and MCP config snippet
20
+ * mindos sync — show sync status
21
+ * mindos sync init — configure remote git repo for sync
22
+ * mindos sync now — manual trigger sync
23
+ * mindos sync conflicts — list conflict files
24
+ * mindos sync on|off — enable/disable auto-sync
19
25
  * mindos gateway install — install background service (systemd/launchd)
20
26
  * mindos gateway uninstall — remove background service
21
27
  * mindos gateway start — start the background service
@@ -38,20 +44,21 @@ import { homedir } from 'node:os';
38
44
  import { ROOT, CONFIG_PATH, BUILD_STAMP, LOG_PATH } from './lib/constants.js';
39
45
  import { bold, dim, cyan, green, red, yellow } from './lib/colors.js';
40
46
  import { run } from './lib/utils.js';
41
- import { loadConfig, getStartMode } from './lib/config.js';
47
+ import { loadConfig, getStartMode, isDaemonMode } from './lib/config.js';
42
48
  import { needsBuild, writeBuildStamp, clearBuildLock, cleanNextDir, ensureAppDeps } from './lib/build.js';
43
49
  import { isPortInUse, assertPortFree } from './lib/port.js';
44
50
  import { savePids, clearPids } from './lib/pid.js';
45
51
  import { stopMindos } from './lib/stop.js';
46
52
  import { getPlatform, ensureMindosDir, waitForHttp, runGatewayCommand } from './lib/gateway.js';
47
- import { printStartupInfo } from './lib/startup.js';
53
+ import { printStartupInfo, getLocalIP } from './lib/startup.js';
48
54
  import { spawnMcp } from './lib/mcp-spawn.js';
49
55
  import { mcpInstall } from './lib/mcp-install.js';
56
+ import { initSync, startSyncDaemon, stopSyncDaemon, getSyncStatus, manualSync, listConflicts, setSyncEnabled } from './lib/sync.js';
50
57
 
51
58
  // ── Commands ──────────────────────────────────────────────────────────────────
52
59
 
53
60
  const cmd = process.argv[2];
54
- const isDaemon = process.argv.includes('--daemon');
61
+ const isDaemon = process.argv.includes('--daemon') || (!cmd && isDaemonMode());
55
62
  const isVerbose = process.argv.includes('--verbose');
56
63
  const extra = process.argv.slice(3).filter(a => a !== '--daemon' && a !== '--verbose').join(' ');
57
64
 
@@ -64,22 +71,117 @@ const commands = {
64
71
  init: () => run(`node ${resolve(ROOT, 'scripts/setup.js')}`),
65
72
  setup: () => run(`node ${resolve(ROOT, 'scripts/setup.js')}`),
66
73
 
74
+ // ── open ───────────────────────────────────────────────────────────────────
75
+ open: () => {
76
+ loadConfig();
77
+ const webPort = process.env.MINDOS_WEB_PORT || '3000';
78
+ const url = `http://localhost:${webPort}`;
79
+ let cmd;
80
+ if (process.platform === 'darwin') {
81
+ cmd = 'open';
82
+ } else if (process.platform === 'linux') {
83
+ // WSL detection
84
+ try {
85
+ const uname = execSync('uname -r', { encoding: 'utf-8' });
86
+ cmd = uname.toLowerCase().includes('microsoft') ? 'wslview' : 'xdg-open';
87
+ } catch {
88
+ cmd = 'xdg-open';
89
+ }
90
+ } else {
91
+ cmd = 'start';
92
+ }
93
+ try {
94
+ execSync(`${cmd} ${url}`, { stdio: 'ignore' });
95
+ console.log(`${green('✔')} Opening ${cyan(url)}`);
96
+ } catch {
97
+ console.log(dim(`Could not open browser automatically. Visit: ${cyan(url)}`));
98
+ }
99
+ },
100
+
67
101
  // ── token ──────────────────────────────────────────────────────────────────
68
102
  token: () => {
69
103
  if (!existsSync(CONFIG_PATH)) {
70
104
  console.error(red('No config found. Run `mindos onboard` first.'));
71
105
  process.exit(1);
72
106
  }
73
- let token = '';
74
- try { token = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')).authToken || ''; } catch {}
107
+ let config = {};
108
+ try { config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')); } catch {}
109
+ const token = config.authToken || '';
75
110
  if (!token) {
76
111
  console.log(dim('No auth token set. Run `mindos onboard` to configure one.'));
77
112
  process.exit(0);
78
113
  }
114
+ const mcpPort = config.mcpPort || 8787;
115
+ const localIP = getLocalIP();
116
+
117
+ const localUrl = `http://localhost:${mcpPort}/mcp`;
118
+ const sep = '━'.repeat(40);
119
+
79
120
  console.log(`\n${bold('🔑 Auth token:')} ${cyan(token)}\n`);
80
- console.log(dim('Add to your Agent MCP config:'));
81
- console.log(` "headers": { "Authorization": "Bearer ${cyan(token)}" }\n`);
82
- console.log(dim('Run `mindos onboard` to regenerate.\n'));
121
+
122
+ // Claude Code
123
+ console.log(`${sep}`);
124
+ console.log(`${bold('Claude Code')}`);
125
+ console.log(`${sep}`);
126
+ console.log(dim('一键安装:') + ` mindos mcp install claude-code -g -y`);
127
+ console.log(dim('\n手动配置 (~/.claude.json):'));
128
+ console.log(JSON.stringify({
129
+ mcpServers: {
130
+ mindos: {
131
+ url: localUrl,
132
+ headers: { Authorization: `Bearer ${token}` },
133
+ },
134
+ },
135
+ }, null, 2));
136
+
137
+ // CodeBuddy (Claude Code Internal)
138
+ console.log(`\n${sep}`);
139
+ console.log(`${bold('CodeBuddy (Claude Code Internal)')}`);
140
+ console.log(`${sep}`);
141
+ console.log(dim('一键安装:') + ` mindos mcp install codebuddy -g -y`);
142
+ console.log(dim('\n手动配置 (~/.claude-internal/.claude.json):'));
143
+ console.log(JSON.stringify({
144
+ mcpServers: {
145
+ mindos: {
146
+ url: localUrl,
147
+ headers: { Authorization: `Bearer ${token}` },
148
+ },
149
+ },
150
+ }, null, 2));
151
+
152
+ // Cursor
153
+ console.log(`\n${sep}`);
154
+ console.log(`${bold('Cursor')}`);
155
+ console.log(`${sep}`);
156
+ console.log(dim('一键安装:') + ` mindos mcp install cursor -g -y`);
157
+ console.log(dim('\n手动配置 (~/.cursor/mcp.json):'));
158
+ console.log(JSON.stringify({
159
+ mcpServers: {
160
+ mindos: {
161
+ url: localUrl,
162
+ headers: { Authorization: `Bearer ${token}` },
163
+ },
164
+ },
165
+ }, null, 2));
166
+
167
+ // Remote
168
+ if (localIP) {
169
+ const remoteUrl = `http://${localIP}:${mcpPort}/mcp`;
170
+ console.log(`\n${sep}`);
171
+ console.log(`${bold('Remote (其他设备)')}`);
172
+ console.log(`${sep}`);
173
+ console.log(`URL: ${cyan(remoteUrl)}`);
174
+ console.log(JSON.stringify({
175
+ mcpServers: {
176
+ mindos: {
177
+ url: remoteUrl,
178
+ headers: { Authorization: `Bearer ${token}` },
179
+ },
180
+ },
181
+ }, null, 2));
182
+ }
183
+
184
+ console.log(dim('\nRun `mindos onboard` to regenerate.\n'));
83
185
  },
84
186
 
85
187
  // ── dev ────────────────────────────────────────────────────────────────────
@@ -92,7 +194,12 @@ const commands = {
92
194
  ensureAppDeps();
93
195
  const mcp = spawnMcp(isVerbose);
94
196
  savePids(process.pid, mcp.pid);
95
- process.on('exit', clearPids);
197
+ process.on('exit', () => { stopSyncDaemon(); clearPids(); });
198
+ // Start sync daemon if enabled
199
+ const devMindRoot = process.env.MIND_ROOT;
200
+ if (devMindRoot) {
201
+ startSyncDaemon(devMindRoot).catch(() => {});
202
+ }
96
203
  printStartupInfo(webPort, mcpPort);
97
204
  run(`npx next dev -p ${webPort} ${extra}`, resolve(ROOT, 'app'));
98
205
  },
@@ -119,6 +226,14 @@ const commands = {
119
226
  process.exit(1);
120
227
  }
121
228
  printStartupInfo(webPort, mcpPort);
229
+ // System notification
230
+ try {
231
+ if (process.platform === 'darwin') {
232
+ execSync(`osascript -e 'display notification "http://localhost:${webPort}" with title "MindOS 已就绪"'`, { stdio: 'ignore' });
233
+ } else if (process.platform === 'linux') {
234
+ execSync(`notify-send "MindOS 已就绪" "http://localhost:${webPort}"`, { stdio: 'ignore' });
235
+ }
236
+ } catch { /* notification is best-effort */ }
122
237
  console.log(`${green('✔ MindOS is running as a background service')}`);
123
238
  console.log(dim(' View logs: mindos logs'));
124
239
  console.log(dim(' Stop: mindos gateway stop'));
@@ -140,7 +255,12 @@ const commands = {
140
255
  }
141
256
  const mcp = spawnMcp(isVerbose);
142
257
  savePids(process.pid, mcp.pid);
143
- process.on('exit', clearPids);
258
+ process.on('exit', () => { stopSyncDaemon(); clearPids(); });
259
+ // Start sync daemon if enabled
260
+ const mindRoot = process.env.MIND_ROOT;
261
+ if (mindRoot) {
262
+ startSyncDaemon(mindRoot).catch(() => {});
263
+ }
144
264
  printStartupInfo(webPort, mcpPort);
145
265
  run(`npx next start -p ${webPort} ${extra}`, resolve(ROOT, 'app'));
146
266
  },
@@ -521,6 +641,67 @@ ${bold('Examples:')}
521
641
  ${dim('mindos config set ai.provider openai')}
522
642
  `);
523
643
  },
644
+
645
+ // ── sync ──────────────────────────────────────────────────────────────────
646
+ sync: async () => {
647
+ const sub = process.argv[3];
648
+ loadConfig();
649
+ const mindRoot = process.env.MIND_ROOT;
650
+
651
+ if (sub === 'init') {
652
+ await initSync(mindRoot);
653
+ return;
654
+ }
655
+
656
+ if (sub === 'now') {
657
+ manualSync(mindRoot);
658
+ return;
659
+ }
660
+
661
+ if (sub === 'conflicts') {
662
+ listConflicts(mindRoot);
663
+ return;
664
+ }
665
+
666
+ if (sub === 'on') {
667
+ setSyncEnabled(true);
668
+ return;
669
+ }
670
+
671
+ if (sub === 'off') {
672
+ setSyncEnabled(false);
673
+ stopSyncDaemon();
674
+ return;
675
+ }
676
+
677
+ // default: sync status
678
+ const status = getSyncStatus(mindRoot);
679
+ if (!status.enabled) {
680
+ console.log(`\n${bold('🔄 Sync Status')}`);
681
+ console.log(dim(' Not configured. Run `mindos sync init` to set up.\n'));
682
+ return;
683
+ }
684
+ const ago = status.lastSync
685
+ ? (() => {
686
+ const diff = Date.now() - new Date(status.lastSync).getTime();
687
+ if (diff < 60000) return 'just now';
688
+ if (diff < 3600000) return `${Math.floor(diff / 60000)} minutes ago`;
689
+ return `${Math.floor(diff / 3600000)} hours ago`;
690
+ })()
691
+ : 'never';
692
+
693
+ console.log(`\n${bold('🔄 Sync Status')}`);
694
+ console.log(` ${dim('Provider:')} ${cyan(`${status.provider} (${status.remote})`)}`);
695
+ console.log(` ${dim('Branch:')} ${cyan(status.branch)}`);
696
+ console.log(` ${dim('Last sync:')} ${ago}`);
697
+ console.log(` ${dim('Unpushed:')} ${status.unpushed} commits`);
698
+ console.log(` ${dim('Conflicts:')} ${status.conflicts.length ? yellow(`${status.conflicts.length} file(s)`) : green('none')}`);
699
+ console.log(` ${dim('Auto-sync:')} ${green('● enabled')} ${dim(`(commit: ${status.autoCommitInterval}s, pull: ${status.autoPullInterval / 60}min)`)}`);
700
+ if (status.lastError) {
701
+ console.log(` ${dim('Last error:')} ${red(status.lastError)}`);
702
+ }
703
+ console.log();
704
+ },
524
705
  };
525
706
 
526
707
  // ── Entry ─────────────────────────────────────────────────────────────────────
@@ -545,7 +726,9 @@ ${row('mindos restart', 'Stop then start again')}
545
726
  ${row('mindos build', 'Build the app for production')}
546
727
  ${row('mindos mcp', 'Start MCP server only')}
547
728
  ${row('mindos mcp install [agent]', 'Install MindOS MCP config into Agent (claude-code/cursor/windsurf/…) [-g]')}
729
+ ${row('mindos open', 'Open Web UI in the default browser')}
548
730
  ${row('mindos token', 'Show current auth token and MCP config snippet')}
731
+ ${row('mindos sync', 'Show sync status (init/now/conflicts/on/off)')}
549
732
  ${row('mindos gateway <subcommand>', 'Manage background service (install/uninstall/start/stop/status/logs)')}
550
733
  ${row('mindos doctor', 'Health check (config, ports, build, daemon)')}
551
734
  ${row('mindos update', 'Update MindOS to the latest version')}
package/bin/lib/config.js CHANGED
@@ -40,8 +40,19 @@ export function loadConfig() {
40
40
 
41
41
  export function getStartMode() {
42
42
  try {
43
- return JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')).startMode || 'start';
43
+ const mode = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')).startMode || 'start';
44
+ // 'daemon' is stored in config when user chose background service;
45
+ // CLI maps it to the 'start' command with --daemon flag
46
+ return mode === 'daemon' ? 'start' : mode;
44
47
  } catch {
45
48
  return 'start';
46
49
  }
47
50
  }
51
+
52
+ export function isDaemonMode() {
53
+ try {
54
+ return JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')).startMode === 'daemon';
55
+ } catch {
56
+ return false;
57
+ }
58
+ }
@@ -0,0 +1,367 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ import { CONFIG_PATH, MINDOS_DIR } from './constants.js';
5
+ import { bold, dim, cyan, green, red, yellow } from './colors.js';
6
+
7
+ // ── Config helpers ──────────────────────────────────────────────────────────
8
+
9
+ function loadSyncConfig() {
10
+ try {
11
+ const config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
12
+ return config.sync || {};
13
+ } catch {
14
+ return {};
15
+ }
16
+ }
17
+
18
+ function saveSyncConfig(syncConfig) {
19
+ let config = {};
20
+ try { config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')); } catch {}
21
+ config.sync = syncConfig;
22
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n', 'utf-8');
23
+ }
24
+
25
+ function getMindRoot() {
26
+ try {
27
+ const config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
28
+ return config.mindRoot;
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+
34
+ const SYNC_STATE_PATH = resolve(MINDOS_DIR, 'sync-state.json');
35
+
36
+ function loadSyncState() {
37
+ try {
38
+ return JSON.parse(readFileSync(SYNC_STATE_PATH, 'utf-8'));
39
+ } catch {
40
+ return {};
41
+ }
42
+ }
43
+
44
+ function saveSyncState(state) {
45
+ if (!existsSync(MINDOS_DIR)) mkdirSync(MINDOS_DIR, { recursive: true });
46
+ writeFileSync(SYNC_STATE_PATH, JSON.stringify(state, null, 2) + '\n', 'utf-8');
47
+ }
48
+
49
+ // ── Git helpers ─────────────────────────────────────────────────────────────
50
+
51
+ function isGitRepo(dir) {
52
+ return existsSync(resolve(dir, '.git'));
53
+ }
54
+
55
+ function gitExec(cmd, cwd) {
56
+ return execSync(cmd, { cwd, encoding: 'utf-8', stdio: 'pipe' }).trim();
57
+ }
58
+
59
+ function getRemoteUrl(cwd) {
60
+ try {
61
+ return gitExec('git remote get-url origin', cwd);
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+
67
+ function getBranch(cwd) {
68
+ try {
69
+ return gitExec('git rev-parse --abbrev-ref HEAD', cwd);
70
+ } catch {
71
+ return 'main';
72
+ }
73
+ }
74
+
75
+ function getUnpushedCount(cwd) {
76
+ try {
77
+ return gitExec('git rev-list --count @{u}..HEAD', cwd);
78
+ } catch {
79
+ return '?';
80
+ }
81
+ }
82
+
83
+ // ── Core sync functions ─────────────────────────────────────────────────────
84
+
85
+ function autoCommitAndPush(mindRoot) {
86
+ try {
87
+ execSync('git add -A', { cwd: mindRoot, stdio: 'pipe' });
88
+ const status = gitExec('git status --porcelain', mindRoot);
89
+ if (!status) return;
90
+ const timestamp = new Date().toISOString().replace('T', ' ').slice(0, 19);
91
+ execSync(`git commit -m "auto-sync: ${timestamp}"`, { cwd: mindRoot, stdio: 'pipe' });
92
+ execSync('git push', { cwd: mindRoot, stdio: 'pipe' });
93
+ saveSyncState({ ...loadSyncState(), lastSync: new Date().toISOString(), lastError: null });
94
+ } catch (err) {
95
+ saveSyncState({ ...loadSyncState(), lastError: err.message, lastErrorTime: new Date().toISOString() });
96
+ }
97
+ }
98
+
99
+ function autoPull(mindRoot) {
100
+ try {
101
+ execSync('git pull --rebase --autostash', { cwd: mindRoot, stdio: 'pipe' });
102
+ saveSyncState({ ...loadSyncState(), lastPull: new Date().toISOString() });
103
+ } catch {
104
+ // rebase conflict → abort → merge
105
+ try { execSync('git rebase --abort', { cwd: mindRoot, stdio: 'pipe' }); } catch {}
106
+ try {
107
+ execSync('git pull --no-rebase', { cwd: mindRoot, stdio: 'pipe' });
108
+ saveSyncState({ ...loadSyncState(), lastPull: new Date().toISOString() });
109
+ } catch {
110
+ // merge conflict → keep both versions
111
+ try {
112
+ const conflicts = gitExec('git diff --name-only --diff-filter=U', mindRoot).split('\n').filter(Boolean);
113
+ for (const file of conflicts) {
114
+ try {
115
+ const theirs = execSync(`git show :3:${file}`, { cwd: mindRoot, encoding: 'utf-8' });
116
+ writeFileSync(resolve(mindRoot, file + '.sync-conflict'), theirs, 'utf-8');
117
+ } catch {}
118
+ try { execSync(`git checkout --ours "${file}"`, { cwd: mindRoot, stdio: 'pipe' }); } catch {}
119
+ }
120
+ execSync('git add -A', { cwd: mindRoot, stdio: 'pipe' });
121
+ execSync('git commit -m "auto-sync: resolved conflicts (kept both versions)"', { cwd: mindRoot, stdio: 'pipe' });
122
+ saveSyncState({
123
+ ...loadSyncState(),
124
+ lastPull: new Date().toISOString(),
125
+ conflicts: conflicts.map(f => ({ file: f, time: new Date().toISOString() })),
126
+ });
127
+ } catch (err) {
128
+ saveSyncState({ ...loadSyncState(), lastError: err.message, lastErrorTime: new Date().toISOString() });
129
+ }
130
+ }
131
+ }
132
+ }
133
+
134
+ // ── Exported API ────────────────────────────────────────────────────────────
135
+
136
+ let activeWatcher = null;
137
+ let activePullInterval = null;
138
+
139
+ /**
140
+ * Interactive sync init — configure remote git repo
141
+ */
142
+ export async function initSync(mindRoot) {
143
+ if (!mindRoot) { console.error(red('No mindRoot configured.')); process.exit(1); }
144
+
145
+ const readline = await import('node:readline');
146
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
147
+ const ask = (q) => new Promise(r => rl.question(q, r));
148
+
149
+ // 1. Ensure git repo
150
+ if (!isGitRepo(mindRoot)) {
151
+ console.log(dim('Initializing git repository...'));
152
+ execSync('git init', { cwd: mindRoot, stdio: 'inherit' });
153
+ execSync('git checkout -b main', { cwd: mindRoot, stdio: 'pipe' }).toString();
154
+ }
155
+
156
+ // 2. Remote URL
157
+ const currentRemote = getRemoteUrl(mindRoot);
158
+ const defaultUrl = currentRemote || '';
159
+ const urlPrompt = currentRemote
160
+ ? `${bold('Remote URL')} ${dim(`[${currentRemote}]`)}: `
161
+ : `${bold('Remote URL')} ${dim('(HTTPS or SSH)')}: `;
162
+ let remoteUrl = (await ask(urlPrompt)).trim() || defaultUrl;
163
+
164
+ if (!remoteUrl) {
165
+ console.error(red('Remote URL is required.'));
166
+ rl.close();
167
+ process.exit(1);
168
+ }
169
+
170
+ // 3. Token for HTTPS
171
+ let token = '';
172
+ if (remoteUrl.startsWith('https://')) {
173
+ token = (await ask(`${bold('Access Token')} ${dim('(GitHub PAT / GitLab PAT, leave empty if SSH)')}: `)).trim();
174
+ if (token) {
175
+ // Inject token into URL for credential storage
176
+ const urlObj = new URL(remoteUrl);
177
+ urlObj.username = 'oauth2';
178
+ urlObj.password = token;
179
+ const authUrl = urlObj.toString();
180
+ // Configure credential helper
181
+ try { execSync(`git config credential.helper store`, { cwd: mindRoot, stdio: 'pipe' }); } catch {}
182
+ // Store the credential
183
+ try {
184
+ const credInput = `protocol=${urlObj.protocol.replace(':', '')}\nhost=${urlObj.host}\nusername=oauth2\npassword=${token}\n\n`;
185
+ execSync('git credential approve', { cwd: mindRoot, input: credInput, stdio: 'pipe' });
186
+ } catch {}
187
+ }
188
+ }
189
+
190
+ // 4. Set remote
191
+ try {
192
+ execSync(`git remote add origin "${remoteUrl}"`, { cwd: mindRoot, stdio: 'pipe' });
193
+ } catch {
194
+ execSync(`git remote set-url origin "${remoteUrl}"`, { cwd: mindRoot, stdio: 'pipe' });
195
+ }
196
+
197
+ // 5. Test connection
198
+ console.log(dim('Testing connection...'));
199
+ try {
200
+ execSync('git ls-remote --exit-code origin', { cwd: mindRoot, stdio: 'pipe' });
201
+ console.log(green('✔ Connection successful'));
202
+ } catch {
203
+ console.error(red('✘ Could not connect to remote. Check your URL and credentials.'));
204
+ rl.close();
205
+ process.exit(1);
206
+ }
207
+
208
+ rl.close();
209
+
210
+ // 6. Save sync config
211
+ const syncConfig = {
212
+ enabled: true,
213
+ provider: 'git',
214
+ remote: 'origin',
215
+ branch: getBranch(mindRoot),
216
+ autoCommitInterval: 30,
217
+ autoPullInterval: 300,
218
+ };
219
+ saveSyncConfig(syncConfig);
220
+ console.log(green('✔ Sync configured'));
221
+
222
+ // 7. First sync: pull if remote has content, push otherwise
223
+ try {
224
+ const refs = gitExec('git ls-remote --heads origin', mindRoot);
225
+ if (refs) {
226
+ console.log(dim('Pulling from remote...'));
227
+ try {
228
+ execSync(`git pull origin ${syncConfig.branch} --allow-unrelated-histories`, { cwd: mindRoot, stdio: 'inherit' });
229
+ } catch {
230
+ // Might fail if empty or conflicts — that's fine for initial setup
231
+ console.log(yellow('Pull completed with warnings. Check for conflicts.'));
232
+ }
233
+ } else {
234
+ console.log(dim('Pushing to remote...'));
235
+ autoCommitAndPush(mindRoot);
236
+ }
237
+ } catch {
238
+ console.log(dim('Performing initial push...'));
239
+ autoCommitAndPush(mindRoot);
240
+ }
241
+ console.log(green('✔ Initial sync complete\n'));
242
+ }
243
+
244
+ /**
245
+ * Start file watcher + periodic pull
246
+ */
247
+ export async function startSyncDaemon(mindRoot) {
248
+ const config = loadSyncConfig();
249
+ if (!config.enabled) return null;
250
+ if (!mindRoot || !isGitRepo(mindRoot)) return null;
251
+
252
+ const chokidar = await import('chokidar');
253
+
254
+ // File watcher → debounced auto-commit + push
255
+ let commitTimer = null;
256
+ const watcher = chokidar.watch(mindRoot, {
257
+ ignored: [/(^|[/\\])\.git/, /node_modules/, /\.sync-conflict$/],
258
+ persistent: true,
259
+ ignoreInitial: true,
260
+ });
261
+ watcher.on('all', () => {
262
+ clearTimeout(commitTimer);
263
+ commitTimer = setTimeout(() => autoCommitAndPush(mindRoot), (config.autoCommitInterval || 30) * 1000);
264
+ });
265
+
266
+ // Periodic pull
267
+ const pullInterval = setInterval(() => autoPull(mindRoot), (config.autoPullInterval || 300) * 1000);
268
+
269
+ // Pull on startup
270
+ autoPull(mindRoot);
271
+
272
+ activeWatcher = watcher;
273
+ activePullInterval = pullInterval;
274
+
275
+ return { watcher, pullInterval };
276
+ }
277
+
278
+ /**
279
+ * Stop sync daemon
280
+ */
281
+ export function stopSyncDaemon() {
282
+ if (activeWatcher) {
283
+ activeWatcher.close();
284
+ activeWatcher = null;
285
+ }
286
+ if (activePullInterval) {
287
+ clearInterval(activePullInterval);
288
+ activePullInterval = null;
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Get current sync status
294
+ */
295
+ export function getSyncStatus(mindRoot) {
296
+ const config = loadSyncConfig();
297
+ const state = loadSyncState();
298
+
299
+ if (!config.enabled) {
300
+ return { enabled: false };
301
+ }
302
+
303
+ const remote = mindRoot ? getRemoteUrl(mindRoot) : null;
304
+ const branch = mindRoot ? getBranch(mindRoot) : null;
305
+ const unpushed = mindRoot ? getUnpushedCount(mindRoot) : '?';
306
+
307
+ return {
308
+ enabled: true,
309
+ provider: config.provider || 'git',
310
+ remote: remote || '(not configured)',
311
+ branch: branch || 'main',
312
+ lastSync: state.lastSync || null,
313
+ lastPull: state.lastPull || null,
314
+ unpushed,
315
+ conflicts: state.conflicts || [],
316
+ lastError: state.lastError || null,
317
+ autoCommitInterval: config.autoCommitInterval || 30,
318
+ autoPullInterval: config.autoPullInterval || 300,
319
+ };
320
+ }
321
+
322
+ /**
323
+ * Manual trigger of full sync cycle
324
+ */
325
+ export function manualSync(mindRoot) {
326
+ if (!mindRoot || !isGitRepo(mindRoot)) {
327
+ console.error(red('Not a git repository. Run `mindos sync init` first.'));
328
+ process.exit(1);
329
+ }
330
+ console.log(dim('Pulling...'));
331
+ autoPull(mindRoot);
332
+ console.log(dim('Committing & pushing...'));
333
+ autoCommitAndPush(mindRoot);
334
+ console.log(green('✔ Sync complete'));
335
+ }
336
+
337
+ /**
338
+ * List conflict files
339
+ */
340
+ export function listConflicts(mindRoot) {
341
+ const state = loadSyncState();
342
+ const conflicts = state.conflicts || [];
343
+ if (!conflicts.length) {
344
+ console.log(green('No conflicts'));
345
+ return [];
346
+ }
347
+ console.log(bold(`${conflicts.length} conflict(s):\n`));
348
+ for (const c of conflicts) {
349
+ console.log(` ${yellow('●')} ${c.file} ${dim(c.time)}`);
350
+ const conflictPath = resolve(mindRoot, c.file + '.sync-conflict');
351
+ if (existsSync(conflictPath)) {
352
+ console.log(dim(` Remote version saved: ${c.file}.sync-conflict`));
353
+ }
354
+ }
355
+ console.log();
356
+ return conflicts;
357
+ }
358
+
359
+ /**
360
+ * Enable/disable sync
361
+ */
362
+ export function setSyncEnabled(enabled) {
363
+ const config = loadSyncConfig();
364
+ config.enabled = enabled;
365
+ saveSyncConfig(config);
366
+ console.log(enabled ? green('✔ Auto-sync enabled') : yellow('Auto-sync disabled'));
367
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geminilight/mindos",
3
- "version": "0.1.9",
3
+ "version": "0.2.0",
4
4
  "description": "MindOS — Human-Agent Collaborative Mind System. Local-first knowledge base that syncs your mind to all AI Agents via MCP.",
5
5
  "keywords": [
6
6
  "mindos",
@@ -51,9 +51,13 @@
51
51
  "build": "mindos build",
52
52
  "start": "mindos start",
53
53
  "mcp": "mindos mcp",
54
- "test": "cd app && npx vitest run"
54
+ "test": "cd app && npx vitest run",
55
+ "release": "bash scripts/release.sh"
55
56
  },
56
57
  "engines": {
57
58
  "node": ">=18"
59
+ },
60
+ "dependencies": {
61
+ "chokidar": "^5.0.0"
58
62
  }
59
63
  }