@geminilight/mindos 0.1.9 → 0.2.1

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,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/mcp/src/index.ts CHANGED
@@ -75,6 +75,23 @@ function truncate(text: string, limit = CHARACTER_LIMIT): string {
75
75
  return text.slice(0, limit) + `\n\n[... truncated at ${limit} characters. Use offset/limit params for paginated access.]`;
76
76
  }
77
77
 
78
+ // ─── Agent operation logging ────────────────────────────────────────────────
79
+
80
+ async function logOp(tool: string, params: Record<string, unknown>, result: 'ok' | 'error', message: string) {
81
+ try {
82
+ const entry = { ts: new Date().toISOString(), tool, params, result, message: message.slice(0, 200) };
83
+ const line = JSON.stringify(entry) + '\n';
84
+ // Append to .agent-log.json via the app API
85
+ await fetch(new URL("/api/file", BASE_URL).toString(), {
86
+ method: "POST",
87
+ headers: headers(),
88
+ body: JSON.stringify({ op: "append_to_file", path: ".agent-log.json", content: line }),
89
+ }).catch(() => {});
90
+ } catch {
91
+ // Logging should never break tool execution
92
+ }
93
+ }
94
+
78
95
  // ─── MCP Server ──────────────────────────────────────────────────────────────
79
96
 
80
97
  const server = new McpServer({ name: "mindos-mcp-server", version: "1.0.0" });
@@ -91,8 +108,10 @@ server.registerTool("mindos_list_files", {
91
108
  }, async ({ response_format }) => {
92
109
  try {
93
110
  const json = await get("/api/files", { format: response_format });
94
- return ok(typeof json.tree === "string" ? json.tree : JSON.stringify(json.tree ?? json, null, 2));
95
- } catch (e) { return error(String(e)); }
111
+ const result = typeof json.tree === "string" ? json.tree : JSON.stringify(json.tree ?? json, null, 2);
112
+ logOp("mindos_list_files", { response_format }, "ok", `${result.length} chars`);
113
+ return ok(result);
114
+ } catch (e) { logOp("mindos_list_files", { response_format }, "error", String(e)); return error(String(e)); }
96
115
  });
97
116
 
98
117
  // ── mindos_read_file ────────────────────────────────────────────────────────
@@ -115,8 +134,9 @@ server.registerTool("mindos_read_file", {
115
134
  const header = hasMore
116
135
  ? `[Showing characters ${offset}–${offset + slice.length} of ${content.length}. Use offset=${offset + limit} for next page.]\n\n`
117
136
  : offset > 0 ? `[Showing characters ${offset}–${offset + slice.length} of ${content.length}]\n\n` : "";
137
+ logOp("mindos_read_file", { path }, "ok", `${content.length} chars`);
118
138
  return ok(header + slice);
119
- } catch (e) { return error(String(e)); }
139
+ } catch (e) { logOp("mindos_read_file", { path }, "error", String(e)); return error(String(e)); }
120
140
  });
121
141
 
122
142
  // ── mindos_write_file ───────────────────────────────────────────────────────
@@ -131,8 +151,9 @@ server.registerTool("mindos_write_file", {
131
151
  }, async ({ path, content }) => {
132
152
  try {
133
153
  await post("/api/file", { op: "save_file", path, content });
154
+ logOp("mindos_write_file", { path }, "ok", `Wrote ${content.length} chars`);
134
155
  return ok(`Successfully wrote ${content.length} characters to "${path}"`);
135
- } catch (e) { return error(String(e)); }
156
+ } catch (e) { logOp("mindos_write_file", { path }, "error", String(e)); return error(String(e)); }
136
157
  });
137
158
 
138
159
  // ── mindos_create_file ──────────────────────────────────────────────────────
@@ -147,8 +168,9 @@ server.registerTool("mindos_create_file", {
147
168
  }, async ({ path, content }) => {
148
169
  try {
149
170
  await post("/api/file", { op: "create_file", path, content });
171
+ logOp("mindos_create_file", { path }, "ok", `Created ${content.length} chars`);
150
172
  return ok(`Created "${path}" (${content.length} characters)`);
151
- } catch (e) { return error(String(e)); }
173
+ } catch (e) { logOp("mindos_create_file", { path }, "error", String(e)); return error(String(e)); }
152
174
  });
153
175
 
154
176
  // ── mindos_delete_file ──────────────────────────────────────────────────────
@@ -163,8 +185,9 @@ server.registerTool("mindos_delete_file", {
163
185
  }, async ({ path }) => {
164
186
  try {
165
187
  await post("/api/file", { op: "delete_file", path });
188
+ logOp("mindos_delete_file", { path }, "ok", `Deleted "${path}"`);
166
189
  return ok(`Deleted "${path}"`);
167
- } catch (e) { return error(String(e)); }
190
+ } catch (e) { logOp("mindos_delete_file", { path }, "error", String(e)); return error(String(e)); }
168
191
  });
169
192
 
170
193
  // ── mindos_rename_file ──────────────────────────────────────────────────────
@@ -227,8 +250,9 @@ server.registerTool("mindos_search_notes", {
227
250
  if (modified_after) params.modified_after = modified_after;
228
251
  if (response_format) params.format = response_format;
229
252
  const json = await get("/api/search", params);
253
+ logOp("mindos_search_notes", { query, limit }, "ok", `Search completed`);
230
254
  return ok(truncate(JSON.stringify(json, null, 2)));
231
- } catch (e) { return error(String(e)); }
255
+ } catch (e) { logOp("mindos_search_notes", { query }, "error", String(e)); return error(String(e)); }
232
256
  });
233
257
 
234
258
  // ── mindos_get_recent ───────────────────────────────────────────────────────
@@ -313,8 +337,9 @@ server.registerTool("mindos_append_to_file", {
313
337
  }, async ({ path, content }) => {
314
338
  try {
315
339
  await post("/api/file", { op: "append_to_file", path, content });
340
+ logOp("mindos_append_to_file", { path }, "ok", `Appended ${content.length} chars`);
316
341
  return ok(`Appended ${content.length} character(s) to "${path}"`);
317
- } catch (e) { return error(String(e)); }
342
+ } catch (e) { logOp("mindos_append_to_file", { path }, "error", String(e)); return error(String(e)); }
318
343
  });
319
344
 
320
345
  // ── mindos_insert_after_heading ─────────────────────────────────────────────
@@ -330,8 +355,9 @@ server.registerTool("mindos_insert_after_heading", {
330
355
  }, async ({ path, heading, content }) => {
331
356
  try {
332
357
  await post("/api/file", { op: "insert_after_heading", path, heading, content });
358
+ logOp("mindos_insert_after_heading", { path, heading }, "ok", `Inserted after "${heading}"`);
333
359
  return ok(`Inserted content after heading "${heading}" in "${path}"`);
334
- } catch (e) { return error(String(e)); }
360
+ } catch (e) { logOp("mindos_insert_after_heading", { path, heading }, "error", String(e)); return error(String(e)); }
335
361
  });
336
362
 
337
363
  // ── mindos_update_section ───────────────────────────────────────────────────
@@ -347,8 +373,9 @@ server.registerTool("mindos_update_section", {
347
373
  }, async ({ path, heading, content }) => {
348
374
  try {
349
375
  await post("/api/file", { op: "update_section", path, heading, content });
376
+ logOp("mindos_update_section", { path, heading }, "ok", `Updated section "${heading}"`);
350
377
  return ok(`Updated section "${heading}" in "${path}"`);
351
- } catch (e) { return error(String(e)); }
378
+ } catch (e) { logOp("mindos_update_section", { path, heading }, "error", String(e)); return error(String(e)); }
352
379
  });
353
380
 
354
381
  // ── mindos_append_csv ───────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geminilight/mindos",
3
- "version": "0.1.9",
3
+ "version": "0.2.1",
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
@@ -6,7 +6,7 @@
6
6
  * Usage: npm run setup OR mindos onboard
7
7
  *
8
8
  * Steps:
9
- * 1. Choose knowledge base path → default ~/.mindos/my-mind
9
+ * 1. Choose knowledge base path → default ~/MindOS
10
10
  * 2. Choose template (en / zh / empty / custom) → copy to knowledge base path
11
11
  * 3. Choose ports (web + mcp) — checked for conflicts upfront
12
12
  * 4. Auth token (auto-generated or passphrase-seeded)
@@ -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,12 +97,20 @@ 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: '✔ 配置已保存' },
103
109
  yesNo: { en: '[y/N]', zh: '[y/N]' },
104
110
  yesNoDefault: { en: '[Y/n]', zh: '[Y/n]' },
105
111
  startNow: { en: 'Start MindOS now?', zh: '现在启动 MindOS?' },
112
+ syncSetup: { en: 'Set up cross-device sync via Git?', zh: '是否配置 Git 跨设备同步?' },
113
+ syncLater: { en: ' → Run `mindos sync init` anytime to set up sync later.', zh: ' → 随时运行 `mindos sync init` 配置同步。' },
106
114
 
107
115
  // next steps (onboard — keep it minimal, details shown on `mindos start`)
108
116
  nextSteps: {
@@ -153,7 +161,7 @@ const tf = (key, ...args) => {
153
161
 
154
162
  // ── Step header ───────────────────────────────────────────────────────────────
155
163
 
156
- const TOTAL_STEPS = 6;
164
+ const TOTAL_STEPS = 7;
157
165
  function stepHeader(n) {
158
166
  const title = T.stepTitles[uiLang][n - 1] ?? T.stepTitles.en[n - 1];
159
167
  const stepLabel = tf('step', n, TOTAL_STEPS);
@@ -639,13 +647,25 @@ async function main() {
639
647
  }
640
648
  }
641
649
 
650
+ // ── Step 7: Start Mode ──────────────────────────────────────────────────
651
+ write('\n');
652
+ stepHeader(7);
653
+
654
+ let startMode = 'start';
655
+ const daemonPlatform = process.platform === 'darwin' || process.platform === 'linux';
656
+ if (daemonPlatform) {
657
+ startMode = await select('startModePrompt', 'startModeOpts', 'startModeVals');
658
+ } else {
659
+ write(c.dim(t('startModeSkip') + '\n'));
660
+ }
661
+
642
662
  const config = {
643
663
  mindRoot: mindDir,
644
664
  port: webPort,
645
665
  mcpPort: mcpPort,
646
666
  authToken: authToken,
647
667
  webPassword: webPassword || '',
648
- startMode: 'start',
668
+ startMode: startMode,
649
669
  ai: {
650
670
  provider: isSkip ? existingAiProvider : (isAnthropic ? 'anthropic' : 'openai'),
651
671
  providers: existingProviders,
@@ -656,7 +676,16 @@ async function main() {
656
676
  writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');
657
677
  console.log(`\n${c.green(t('cfgSaved'))}: ${c.dim(CONFIG_PATH)}`);
658
678
 
659
- const installDaemon = process.argv.includes('--install-daemon');
679
+ // ── Sync setup (optional) ──────────────────────────────────────────────────
680
+ const wantSync = await askYesNo('syncSetup');
681
+ if (wantSync) {
682
+ const { initSync } = await import('../bin/lib/sync.js');
683
+ await initSync(mindDir);
684
+ } else {
685
+ console.log(c.dim(t('syncLater')));
686
+ }
687
+
688
+ const installDaemon = startMode === 'daemon' || process.argv.includes('--install-daemon');
660
689
  finish(mindDir, config.startMode, config.mcpPort, config.authToken, installDaemon);
661
690
  }
662
691
 
@@ -18,7 +18,7 @@ If you need to manually initialize:
18
18
  mindos onboard
19
19
 
20
20
  # 2) Or copy a preset manually to your knowledge base directory
21
- cp -r templates/en ~/.mindos/my-mind
21
+ cp -r templates/en ~/MindOS
22
22
  # then set mindRoot in ~/.mindos/config.json
23
23
 
24
24
  # 3) Start filling content from 👤 Profile (en) or 👤 画像 (zh)