@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.
- package/README.md +42 -12
- package/README_zh.md +38 -5
- package/app/README.md +1 -1
- package/app/app/api/init/route.ts +56 -0
- package/app/app/api/sync/route.ts +124 -0
- package/app/app/layout.tsx +10 -1
- package/app/app/register-sw.tsx +15 -0
- package/app/components/HomeContent.tsx +8 -2
- package/app/components/OnboardingView.tsx +161 -0
- package/app/components/SettingsModal.tsx +10 -1
- package/app/components/Sidebar.tsx +28 -4
- package/app/components/SyncStatusBar.tsx +273 -0
- package/app/components/renderers/AgentInspectorRenderer.tsx +8 -5
- package/app/components/settings/SyncTab.tsx +311 -0
- package/app/components/settings/types.ts +1 -1
- package/app/lib/agent/log.ts +44 -0
- package/app/lib/agent/tools.ts +39 -18
- package/app/lib/i18n.ts +80 -2
- package/app/lib/renderers/index.ts +13 -0
- package/app/lib/settings.ts +2 -2
- package/app/public/icons/icon-192.png +0 -0
- package/app/public/icons/icon-512.png +0 -0
- package/app/public/manifest.json +26 -0
- package/app/public/sw.js +66 -0
- package/bin/cli.js +214 -10
- package/bin/lib/config.js +12 -1
- package/bin/lib/mcp-install.js +225 -70
- package/bin/lib/startup.js +24 -1
- package/bin/lib/sync.js +367 -0
- package/mcp/src/index.ts +37 -10
- package/package.json +6 -2
- package/scripts/release.sh +56 -0
- package/scripts/setup.js +35 -6
- package/templates/README.md +1 -1
package/bin/lib/mcp-install.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
// ──
|
|
211
|
+
// ── 2. shared transport (ask once, apply to all) ───────────────────────────
|
|
92
212
|
let transport = transportArg;
|
|
93
213
|
if (!transport) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
// ──
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
// ──
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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('
|
|
155
|
-
console.log(dim(` Config: ${absPath}\n`));
|
|
310
|
+
console.log(`\n${green('Done!')} ${agentKeys.length} agent(s) configured.\n`);
|
|
156
311
|
}
|
package/bin/lib/startup.js
CHANGED
|
@@ -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
|
}
|