@geminilight/mindos 0.2.0 → 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.
@@ -16,6 +16,141 @@ export const MCP_AGENTS = {
16
16
  'codebuddy': { name: 'CodeBuddy', project: null, global: '~/.claude-internal/.claude.json', key: 'mcpServers' },
17
17
  };
18
18
 
19
+ // ─── Interactive select (arrow keys) ──────────────────────────────────────────
20
+
21
+ /**
22
+ * Single select with arrow keys.
23
+ * ↑/↓ to move, Enter to confirm.
24
+ */
25
+ async function interactiveSelect(title, options) {
26
+ return new Promise((resolve) => {
27
+ let cursor = 0;
28
+ const { stdin, stdout } = process;
29
+
30
+ function render() {
31
+ // Move up to clear previous render (except first time)
32
+ stdout.write(`\x1b[${options.length + 1}A\x1b[J`);
33
+ draw();
34
+ }
35
+
36
+ function draw() {
37
+ stdout.write(`${bold(title)}\n`);
38
+ for (let i = 0; i < options.length; i++) {
39
+ const o = options[i];
40
+ const prefix = i === cursor ? cyan('❯') : ' ';
41
+ const label = i === cursor ? cyan(o.label) : o.label;
42
+ const hint = o.hint ? ` ${dim(`(${o.hint})`)}` : '';
43
+ stdout.write(` ${prefix} ${label}${hint}\n`);
44
+ }
45
+ }
46
+
47
+ // Initial draw
48
+ stdout.write('\n');
49
+ draw();
50
+
51
+ stdin.setRawMode(true);
52
+ stdin.resume();
53
+ stdin.setEncoding('utf-8');
54
+
55
+ function onKey(key) {
56
+ if (key === '\x1b[A') { // up
57
+ cursor = (cursor - 1 + options.length) % options.length;
58
+ render();
59
+ } else if (key === '\x1b[B') { // down
60
+ cursor = (cursor + 1) % options.length;
61
+ render();
62
+ } else if (key === '\r' || key === '\n') { // enter
63
+ cleanup();
64
+ resolve(options[cursor]);
65
+ } else if (key === '\x03') { // ctrl+c
66
+ cleanup();
67
+ process.exit(0);
68
+ }
69
+ }
70
+
71
+ function cleanup() {
72
+ stdin.removeListener('data', onKey);
73
+ stdin.setRawMode(false);
74
+ stdin.pause();
75
+ }
76
+
77
+ stdin.on('data', onKey);
78
+ });
79
+ }
80
+
81
+ /**
82
+ * Multi select with arrow keys.
83
+ * ↑/↓ to move, Space to toggle, A to toggle all, Enter to confirm.
84
+ */
85
+ async function interactiveMultiSelect(title, options) {
86
+ return new Promise((resolve) => {
87
+ let cursor = 0;
88
+ const selected = new Set();
89
+ const { stdin, stdout } = process;
90
+
91
+ function render() {
92
+ stdout.write(`\x1b[${options.length + 2}A\x1b[J`);
93
+ draw();
94
+ }
95
+
96
+ function draw() {
97
+ stdout.write(`${bold(title)} ${dim('(↑↓ move, Space select, A all, Enter confirm)')}\n`);
98
+ for (let i = 0; i < options.length; i++) {
99
+ const o = options[i];
100
+ const check = selected.has(i) ? green('✔') : dim('○');
101
+ const pointer = i === cursor ? cyan('❯') : ' ';
102
+ const label = i === cursor ? (selected.has(i) ? green(o.label) : cyan(o.label)) : (selected.has(i) ? green(o.label) : o.label);
103
+ const hint = o.hint ? ` ${dim(`(${o.hint})`)}` : '';
104
+ stdout.write(` ${pointer} ${check} ${label}${hint}\n`);
105
+ }
106
+ const count = selected.size;
107
+ stdout.write(dim(` ${count} selected\n`));
108
+ }
109
+
110
+ stdout.write('\n');
111
+ draw();
112
+
113
+ stdin.setRawMode(true);
114
+ stdin.resume();
115
+ stdin.setEncoding('utf-8');
116
+
117
+ function onKey(key) {
118
+ if (key === '\x1b[A') { // up
119
+ cursor = (cursor - 1 + options.length) % options.length;
120
+ render();
121
+ } else if (key === '\x1b[B') { // down
122
+ cursor = (cursor + 1) % options.length;
123
+ render();
124
+ } else if (key === ' ') { // space
125
+ if (selected.has(cursor)) selected.delete(cursor);
126
+ else selected.add(cursor);
127
+ render();
128
+ } else if (key === 'a' || key === 'A') { // toggle all
129
+ if (selected.size === options.length) selected.clear();
130
+ else options.forEach((_, i) => selected.add(i));
131
+ render();
132
+ } else if (key === '\r' || key === '\n') { // enter
133
+ cleanup();
134
+ const result = [...selected].sort().map(i => options[i]);
135
+ resolve(result);
136
+ } else if (key === '\x03') { // ctrl+c
137
+ cleanup();
138
+ process.exit(0);
139
+ }
140
+ }
141
+
142
+ function cleanup() {
143
+ stdin.removeListener('data', onKey);
144
+ stdin.setRawMode(false);
145
+ stdin.pause();
146
+ }
147
+
148
+ stdin.on('data', onKey);
149
+ });
150
+ }
151
+
152
+ // ─── Main install flow ────────────────────────────────────────────────────────
153
+
19
154
  export async function mcpInstall() {
20
155
  // Support both `mindos mcp install [agent] [flags]` and `mindos mcp [flags]`
21
156
  const sub = process.argv[3];
@@ -39,72 +174,65 @@ export async function mcpInstall() {
39
174
  const readline = await import('node:readline');
40
175
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
41
176
  const ask = (q) => new Promise(r => rl.question(q, r));
42
- const choose = async (prompt, options, { defaultIdx = 0, forcePrompt = false } = {}) => {
43
- if (hasYesFlag && !forcePrompt) return options[defaultIdx];
44
- console.log(`\n${bold(prompt)}\n`);
45
- options.forEach((o, i) => console.log(` ${dim(`${i + 1}.`)} ${o.label} ${o.hint ? dim(`(${o.hint})`) : ''}`));
46
- const ans = await ask(`\n${bold(`Enter number`)} ${dim(`[${defaultIdx + 1}]:`)} `);
47
- const idx = ans.trim() === '' ? defaultIdx : parseInt(ans.trim(), 10) - 1;
48
- return options[idx >= 0 && idx < options.length ? idx : defaultIdx];
49
- };
50
177
 
51
178
  console.log(`\n${bold('🔌 MindOS MCP Install')}\n`);
52
179
 
53
- // ── 1. agent ────────────────────────────────────────────────────────────────
54
- let agentKey = agentArg;
55
- if (!agentKey) {
56
- const keys = Object.keys(MCP_AGENTS);
57
- const picked = await choose('Which Agent would you like to configure?',
58
- keys.map(k => ({ label: MCP_AGENTS[k].name, hint: k, value: k })), { forcePrompt: true });
59
- agentKey = picked.value;
60
- }
61
-
62
- const agent = MCP_AGENTS[agentKey];
63
- if (!agent) {
64
- rl.close();
65
- console.error(red(`\nUnknown agent: ${agentKey}`));
66
- console.error(dim(`Supported: ${Object.keys(MCP_AGENTS).join(', ')}`));
67
- process.exit(1);
68
- }
180
+ // ── 1. agent(s) ──────────────────────────────────────────────────────────────
181
+ let agentKeys = agentArg ? [agentArg] : [];
69
182
 
70
- // ── 2. scope (only ask if agent supports both) ───────────────────────────────
71
- let isGlobal = hasGlobalFlag;
72
- if (!hasGlobalFlag) {
73
- if (agent.project && agent.global) {
74
- const picked = await choose('Install scope?', [
75
- { label: 'Project', hint: agent.project, value: 'project' },
76
- { label: 'Global', hint: agent.global, value: 'global' },
77
- ]);
78
- isGlobal = picked.value === 'global';
183
+ if (agentKeys.length === 0) {
184
+ const keys = Object.keys(MCP_AGENTS);
185
+ if (hasYesFlag) {
186
+ // -y mode: install all
187
+ agentKeys = keys;
79
188
  } else {
80
- isGlobal = !agent.project;
189
+ rl.close(); // close readline so raw mode works
190
+ const picked = await interactiveMultiSelect(
191
+ 'Which Agents to configure?',
192
+ keys.map(k => ({ label: MCP_AGENTS[k].name, hint: k, value: k })),
193
+ );
194
+ if (picked.length === 0) {
195
+ console.log(dim('\nNo agents selected. Exiting.\n'));
196
+ process.exit(0);
197
+ }
198
+ agentKeys = picked.map(p => p.value);
81
199
  }
82
200
  }
83
201
 
84
- const configPath = isGlobal ? agent.global : agent.project;
85
- if (!configPath) {
86
- rl.close();
87
- console.error(red(`${agent.name} does not support ${isGlobal ? 'global' : 'project'} scope.`));
88
- process.exit(1);
202
+ // Validate all keys first
203
+ for (const key of agentKeys) {
204
+ if (!MCP_AGENTS[key]) {
205
+ console.error(red(`\nUnknown agent: ${key}`));
206
+ console.error(dim(`Supported: ${Object.keys(MCP_AGENTS).join(', ')}`));
207
+ process.exit(1);
208
+ }
89
209
  }
90
210
 
91
- // ── 3. transport ─────────────────────────────────────────────────────────────
211
+ // ── 2. shared transport (ask once, apply to all) ───────────────────────────
92
212
  let transport = transportArg;
93
213
  if (!transport) {
94
- const picked = await choose('Transport type?', [
95
- { label: 'stdio', hint: 'local, no server process needed (recommended)' },
96
- { label: 'http', hint: 'URL-based, use when server is running separately or remotely' },
97
- ]);
98
- transport = picked.label;
214
+ if (hasYesFlag) {
215
+ transport = 'stdio';
216
+ } else {
217
+ const picked = await interactiveSelect('Transport type?', [
218
+ { label: 'stdio', hint: 'local, no server process needed (recommended)' },
219
+ { label: 'http', hint: 'URL-based, use when server is running separately or remotely' },
220
+ ]);
221
+ transport = picked.label;
222
+ }
99
223
  }
100
224
 
101
- // ── 4. url + token (only for http) ───────────────────────────────────────────
225
+ // ── 3. url + token (only for http) ─────────────────────────────────────────
102
226
  let url = urlArg;
103
227
  let token = tokenArg;
104
228
 
105
229
  if (transport === 'http') {
230
+ // Re-open readline for text input
231
+ const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
232
+ const ask2 = (q) => new Promise(r => rl2.question(q, r));
233
+
106
234
  if (!url) {
107
- url = hasYesFlag ? 'http://localhost:8787/mcp' : (await ask(`${bold('MCP URL')} ${dim('[http://localhost:8787/mcp]:')} `)).trim() || 'http://localhost:8787/mcp';
235
+ url = hasYesFlag ? 'http://localhost:8787/mcp' : (await ask2(`${bold('MCP URL')} ${dim('[http://localhost:8787/mcp]:')} `)).trim() || 'http://localhost:8787/mcp';
108
236
  }
109
237
 
110
238
  if (!token) {
@@ -112,45 +240,72 @@ export async function mcpInstall() {
112
240
  if (token) {
113
241
  console.log(dim(` Using auth token from ~/.mindos/config.json`));
114
242
  } else if (!hasYesFlag) {
115
- token = (await ask(`${bold('Auth token')} ${dim('(leave blank to skip):')} `)).trim();
243
+ token = (await ask2(`${bold('Auth token')} ${dim('(leave blank to skip):')} `)).trim();
116
244
  } else {
117
245
  console.log(yellow(` Warning: no auth token found in ~/.mindos/config.json — config will have no auth.`));
118
246
  console.log(dim(` Run \`mindos onboard\` to set one, or pass --token <token>.`));
119
247
  }
120
248
  }
121
- }
122
249
 
123
- rl.close();
250
+ rl2.close();
251
+ }
124
252
 
125
- // ── build entry ──────────────────────────────────────────────────────────────
253
+ // ── 4. build entry ─────────────────────────────────────────────────────────
126
254
  const entry = transport === 'stdio'
127
255
  ? { type: 'stdio', command: 'mindos', args: ['mcp'], env: { MCP_TRANSPORT: 'stdio' } }
128
256
  : token
129
257
  ? { url, headers: { Authorization: `Bearer ${token}` } }
130
258
  : { url };
131
259
 
132
- // ── read + merge existing config ─────────────────────────────────────────────
133
- const absPath = expandHome(configPath);
134
- let config = {};
135
- if (existsSync(absPath)) {
136
- try { config = JSON.parse(readFileSync(absPath, 'utf-8')); } catch {
137
- console.error(red(`\nFailed to parse existing config: ${absPath}`));
138
- process.exit(1);
260
+ // ── 5. install for each selected agent ─────────────────────────────────────
261
+ for (const agentKey of agentKeys) {
262
+ const agent = MCP_AGENTS[agentKey];
263
+
264
+ // scope
265
+ let isGlobal = hasGlobalFlag;
266
+ if (!hasGlobalFlag) {
267
+ if (agent.project && agent.global) {
268
+ if (hasYesFlag) {
269
+ isGlobal = false; // default to project
270
+ } else {
271
+ const picked = await interactiveSelect(`[${agent.name}] Install scope?`, [
272
+ { label: 'Project', hint: agent.project, value: 'project' },
273
+ { label: 'Global', hint: agent.global, value: 'global' },
274
+ ]);
275
+ isGlobal = picked.value === 'global';
276
+ }
277
+ } else {
278
+ isGlobal = !agent.project;
279
+ }
139
280
  }
140
- }
141
281
 
142
- if (!config[agent.key]) config[agent.key] = {};
143
- const existed = !!config[agent.key].mindos;
144
- config[agent.key].mindos = entry;
282
+ const configPath = isGlobal ? agent.global : agent.project;
283
+ if (!configPath) {
284
+ console.error(red(` ${agent.name} does not support ${isGlobal ? 'global' : 'project'} scope — skipping.`));
285
+ continue;
286
+ }
145
287
 
146
- // ── preview + write ──────────────────────────────────────────────────────────
147
- console.log(`\n${bold('Preview:')} ${dim(absPath)}\n`);
148
- console.log(dim(JSON.stringify({ [agent.key]: { mindos: entry } }, null, 2)));
288
+ // read + merge
289
+ const absPath = expandHome(configPath);
290
+ let config = {};
291
+ if (existsSync(absPath)) {
292
+ try { config = JSON.parse(readFileSync(absPath, 'utf-8')); } catch {
293
+ console.error(red(` Failed to parse existing config: ${absPath} — skipping.`));
294
+ continue;
295
+ }
296
+ }
149
297
 
150
- const dir = resolve(absPath, '..');
151
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
152
- writeFileSync(absPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
298
+ if (!config[agent.key]) config[agent.key] = {};
299
+ const existed = !!config[agent.key].mindos;
300
+ config[agent.key].mindos = entry;
301
+
302
+ // write
303
+ const dir = resolve(absPath, '..');
304
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
305
+ writeFileSync(absPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
306
+
307
+ console.log(`${green('✔')} ${existed ? 'Updated' : 'Installed'} MindOS MCP for ${bold(agent.name)} ${dim(`→ ${absPath}`)}`);
308
+ }
153
309
 
154
- console.log(`\n${green('\u2714')} ${existed ? 'Updated' : 'Installed'} MindOS MCP for ${bold(agent.name)}`);
155
- console.log(dim(` Config: ${absPath}\n`));
310
+ console.log(`\n${green('Done!')} ${agentKeys.length} agent(s) configured.\n`);
156
311
  }
@@ -1,7 +1,8 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { networkInterfaces } from 'node:os';
3
3
  import { CONFIG_PATH } from './constants.js';
4
- import { bold, dim, cyan, green } from './colors.js';
4
+ import { bold, dim, cyan, green, yellow } from './colors.js';
5
+ import { getSyncStatus } from './sync.js';
5
6
 
6
7
  export function getLocalIP() {
7
8
  try {
@@ -47,5 +48,27 @@ export function printStartupInfo(webPort, mcpPort) {
47
48
  }
48
49
  console.log(dim('\n Install Skills (optional):'));
49
50
  console.log(dim(' npx skills add https://github.com/GeminiLight/MindOS --skill mindos -g -y'));
51
+
52
+ // Sync status
53
+ const mindRoot = config.mindRoot;
54
+ if (mindRoot) {
55
+ try {
56
+ const syncStatus = getSyncStatus(mindRoot);
57
+ if (syncStatus.enabled) {
58
+ if (syncStatus.lastError) {
59
+ console.log(`\n ${yellow('!')} Sync ${yellow('error')}: ${syncStatus.lastError}`);
60
+ } else if (syncStatus.conflicts && syncStatus.conflicts.length > 0) {
61
+ console.log(`\n ${yellow('!')} Sync ${yellow(`${syncStatus.conflicts.length} conflict(s)`)} ${dim('run `mindos sync conflicts` to view')}`);
62
+ } else {
63
+ const unpushed = parseInt(syncStatus.unpushed || '0', 10);
64
+ const extra = unpushed > 0 ? ` ${dim(`(${unpushed} unpushed)`)}` : '';
65
+ console.log(`\n ${green('●')} Sync ${green('enabled')} ${dim(syncStatus.remote || 'origin')}${extra}`);
66
+ }
67
+ } else {
68
+ console.log(`\n ${dim('○')} Sync ${dim('not configured')} ${dim('run `mindos sync init` to set up')}`);
69
+ }
70
+ } catch { /* sync check is best-effort */ }
71
+ }
72
+
50
73
  console.log(`${'─'.repeat(53)}\n`);
51
74
  }
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.2.0",
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",
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)
@@ -109,6 +109,8 @@ const T = {
109
109
  yesNo: { en: '[y/N]', zh: '[y/N]' },
110
110
  yesNoDefault: { en: '[Y/n]', zh: '[Y/n]' },
111
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` 配置同步。' },
112
114
 
113
115
  // next steps (onboard — keep it minimal, details shown on `mindos start`)
114
116
  nextSteps: {
@@ -674,6 +676,15 @@ async function main() {
674
676
  writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');
675
677
  console.log(`\n${c.green(t('cfgSaved'))}: ${c.dim(CONFIG_PATH)}`);
676
678
 
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
+
677
688
  const installDaemon = startMode === 'daemon' || process.argv.includes('--install-daemon');
678
689
  finish(mindDir, config.startMode, config.mcpPort, config.authToken, installDaemon);
679
690
  }
@@ -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)