@geminilight/mindos 0.1.8 → 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.
@@ -0,0 +1,27 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { green, yellow, dim } from './colors.js';
3
+ import { loadPids, clearPids } from './pid.js';
4
+
5
+ export function stopMindos() {
6
+ const pids = loadPids();
7
+ if (!pids.length) {
8
+ console.log(yellow('No PID file found, trying pattern-based stop...'));
9
+ try { execSync('pkill -f "next start|next dev" 2>/dev/null || true', { stdio: 'inherit' }); } catch {}
10
+ try { execSync('pkill -f "mcp/src/index" 2>/dev/null || true', { stdio: 'inherit' }); } catch {}
11
+ console.log(green('\u2714 Done'));
12
+ return;
13
+ }
14
+ let stopped = 0;
15
+ for (const pid of pids) {
16
+ try {
17
+ process.kill(pid, 'SIGTERM');
18
+ stopped++;
19
+ } catch {
20
+ // process already gone — ignore
21
+ }
22
+ }
23
+ clearPids();
24
+ console.log(stopped
25
+ ? green(`\u2714 Stopped ${stopped} process${stopped > 1 ? 'es' : ''}`)
26
+ : dim('No running processes found'));
27
+ }
@@ -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
+ }
@@ -0,0 +1,16 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { resolve } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { ROOT } from './constants.js';
5
+
6
+ export function run(command, cwd = ROOT) {
7
+ try {
8
+ execSync(command, { cwd, stdio: 'inherit', env: process.env });
9
+ } catch {
10
+ process.exit(1);
11
+ }
12
+ }
13
+
14
+ export function expandHome(p) {
15
+ return p.startsWith('~/') ? resolve(homedir(), p.slice(2)) : p;
16
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geminilight/mindos",
3
- "version": "0.1.8",
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
  }
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # ── Usage ────────────────────────────────────────────────────────────────
5
+ # npm run release [patch|minor|major] (default: patch)
6
+ # ─────────────────────────────────────────────────────────────────────────
7
+
8
+ BUMP="${1:-patch}"
9
+
10
+ # 1. Ensure clean working tree
11
+ if ! git diff --quiet || ! git diff --cached --quiet; then
12
+ echo "❌ Working tree is not clean. Commit or stash changes first."
13
+ exit 1
14
+ fi
15
+
16
+ # 2. Run tests
17
+ echo "🧪 Running tests..."
18
+ npm test
19
+ echo ""
20
+
21
+ # 3. Bump version (creates commit + tag automatically)
22
+ echo "📦 Bumping version ($BUMP)..."
23
+ npm version "$BUMP" -m "%s"
24
+ VERSION="v$(node -p "require('./package.json').version")"
25
+ echo " Version: $VERSION"
26
+ echo ""
27
+
28
+ # 4. Push commit + tag
29
+ echo "🚀 Pushing to origin..."
30
+ git push origin main
31
+ git push origin "$VERSION"
32
+ echo ""
33
+
34
+ # 5. Wait for CI (if gh is available)
35
+ if command -v gh &>/dev/null; then
36
+ echo "⏳ Waiting for CI publish workflow..."
37
+ TIMEOUT=120
38
+ ELAPSED=0
39
+ RUN_ID=""
40
+
41
+ # Wait for the workflow run to appear
42
+ while [ -z "$RUN_ID" ] && [ "$ELAPSED" -lt 30 ]; do
43
+ sleep 3
44
+ ELAPSED=$((ELAPSED + 3))
45
+ RUN_ID=$(gh run list --workflow=publish-npm.yml --limit=1 --json databaseId,headBranch --jq ".[0].databaseId" 2>/dev/null || true)
46
+ done
47
+
48
+ if [ -n "$RUN_ID" ]; then
49
+ gh run watch "$RUN_ID" --exit-status && echo "✅ Published $VERSION to npm" || echo "❌ CI failed — check: gh run view $RUN_ID --log"
50
+ else
51
+ echo "⚠️ Could not find CI run. Check manually: https://github.com/GeminiLight/mindos-dev/actions"
52
+ fi
53
+ else
54
+ echo "💡 Install 'gh' CLI to auto-watch CI status."
55
+ echo " Check publish status: https://github.com/GeminiLight/mindos-dev/actions"
56
+ fi
package/scripts/setup.js CHANGED
@@ -44,8 +44,8 @@ const T = {
44
44
  // step labels
45
45
  step: { en: (n, total) => `Step ${n}/${total}`, zh: (n, total) => `步骤 ${n}/${total}` },
46
46
  stepTitles: {
47
- en: ['Knowledge Base', 'Template', 'Ports', 'Auth Token', 'Web Password', 'AI Provider'],
48
- zh: ['知识库', '模板', '端口', 'Auth Token', 'Web 密码', 'AI 服务商'],
47
+ en: ['Knowledge Base', 'Template', 'Ports', 'Auth Token', 'Web Password', 'AI Provider', 'Start Mode'],
48
+ zh: ['知识库', '模板', '端口', 'Auth Token', 'Web 密码', 'AI 服务商', '启动方式'],
49
49
  },
50
50
 
51
51
  // path
@@ -97,6 +97,12 @@ const T = {
97
97
 
98
98
  // config
99
99
  cfgExists: { en: (p) => `${p} already exists. Overwrite?`, zh: (p) => `${p} 已存在,是否覆盖?` },
100
+
101
+ // start mode
102
+ startModePrompt: { en: 'Start Mode', zh: '启动方式' },
103
+ startModeOpts: { en: ['Background service (recommended, auto-start on boot)', 'Foreground (manual start each time)'], zh: ['后台服务(推荐,开机自启)', '前台运行(每次手动启动)'] },
104
+ startModeVals: ['daemon', 'start'],
105
+ startModeSkip: { en: ' → Daemon not supported on this platform, using foreground mode', zh: ' → 当前平台不支持后台服务,使用前台模式' },
100
106
  cfgKept: { en: '✔ Keeping existing config', zh: '✔ 保留现有配置' },
101
107
  cfgKeptNote: { en: ' Settings from this session were not saved', zh: ' 本次填写的设置未保存' },
102
108
  cfgSaved: { en: '✔ Config saved', zh: '✔ 配置已保存' },
@@ -153,7 +159,7 @@ const tf = (key, ...args) => {
153
159
 
154
160
  // ── Step header ───────────────────────────────────────────────────────────────
155
161
 
156
- const TOTAL_STEPS = 6;
162
+ const TOTAL_STEPS = 7;
157
163
  function stepHeader(n) {
158
164
  const title = T.stepTitles[uiLang][n - 1] ?? T.stepTitles.en[n - 1];
159
165
  const stepLabel = tf('step', n, TOTAL_STEPS);
@@ -639,13 +645,25 @@ async function main() {
639
645
  }
640
646
  }
641
647
 
648
+ // ── Step 7: Start Mode ──────────────────────────────────────────────────
649
+ write('\n');
650
+ stepHeader(7);
651
+
652
+ let startMode = 'start';
653
+ const daemonPlatform = process.platform === 'darwin' || process.platform === 'linux';
654
+ if (daemonPlatform) {
655
+ startMode = await select('startModePrompt', 'startModeOpts', 'startModeVals');
656
+ } else {
657
+ write(c.dim(t('startModeSkip') + '\n'));
658
+ }
659
+
642
660
  const config = {
643
661
  mindRoot: mindDir,
644
662
  port: webPort,
645
663
  mcpPort: mcpPort,
646
664
  authToken: authToken,
647
665
  webPassword: webPassword || '',
648
- startMode: 'start',
666
+ startMode: startMode,
649
667
  ai: {
650
668
  provider: isSkip ? existingAiProvider : (isAnthropic ? 'anthropic' : 'openai'),
651
669
  providers: existingProviders,
@@ -656,7 +674,7 @@ async function main() {
656
674
  writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');
657
675
  console.log(`\n${c.green(t('cfgSaved'))}: ${c.dim(CONFIG_PATH)}`);
658
676
 
659
- const installDaemon = process.argv.includes('--install-daemon');
677
+ const installDaemon = startMode === 'daemon' || process.argv.includes('--install-daemon');
660
678
  finish(mindDir, config.startMode, config.mcpPort, config.authToken, installDaemon);
661
679
  }
662
680