@geminilight/mindos 0.2.0 → 0.3.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/README.md +3 -3
- package/README_zh.md +38 -5
- package/app/README.md +1 -1
- package/app/app/api/init/route.ts +22 -0
- package/app/app/api/settings/route.ts +3 -0
- package/app/app/api/setup/generate-token/route.ts +23 -0
- package/app/app/api/setup/route.ts +81 -0
- package/app/app/layout.tsx +10 -1
- package/app/app/page.tsx +5 -0
- package/app/app/register-sw.tsx +15 -0
- package/app/app/setup/page.tsx +9 -0
- package/app/components/HomeContent.tsx +8 -2
- package/app/components/OnboardingView.tsx +161 -0
- package/app/components/SettingsModal.tsx +7 -1
- package/app/components/SetupWizard.tsx +479 -0
- 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 +113 -21
- package/app/lib/agent/log.ts +44 -0
- package/app/lib/agent/tools.ts +39 -18
- package/app/lib/i18n.ts +170 -0
- package/app/lib/renderers/index.ts +13 -0
- package/app/lib/settings.ts +13 -2
- package/app/lib/template.ts +45 -0
- 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 +21 -0
- package/bin/lib/mcp-install.js +225 -70
- package/bin/lib/startup.js +24 -1
- package/mcp/src/index.ts +37 -10
- package/package.json +1 -1
- package/scripts/setup.js +105 -2
- package/templates/README.md +1 -1
package/README.md
CHANGED
|
@@ -130,7 +130,7 @@ mindos onboard
|
|
|
130
130
|
```
|
|
131
131
|
|
|
132
132
|
The setup wizard will guide you through:
|
|
133
|
-
1. Knowledge base path → default
|
|
133
|
+
1. Knowledge base path → default `~/MindOS`
|
|
134
134
|
2. Choose template (en / zh / empty / custom)
|
|
135
135
|
3. Ports (Web UI + MCP)
|
|
136
136
|
4. Auth token (auto-generated or passphrase-seeded)
|
|
@@ -153,7 +153,7 @@ Or skip the wizard and edit `~/.mindos/config.json` manually (see Config Referen
|
|
|
153
153
|
|
|
154
154
|
```json
|
|
155
155
|
{
|
|
156
|
-
"mindRoot": "
|
|
156
|
+
"mindRoot": "~/MindOS",
|
|
157
157
|
"port": 3000,
|
|
158
158
|
"mcpPort": 8787,
|
|
159
159
|
"authToken": "",
|
|
@@ -179,7 +179,7 @@ Or skip the wizard and edit `~/.mindos/config.json` manually (see Config Referen
|
|
|
179
179
|
|
|
180
180
|
| Field | Default | Description |
|
|
181
181
|
| :--- | :--- | :--- |
|
|
182
|
-
| `mindRoot` |
|
|
182
|
+
| `mindRoot` | `~/MindOS` | **Required**. Absolute path to the knowledge base root. |
|
|
183
183
|
| `port` | `3000` | Optional. Web app port. |
|
|
184
184
|
| `mcpPort` | `8787` | Optional. MCP server port. |
|
|
185
185
|
| `authToken` | — | Optional. Protects App `/api/*` and MCP `/mcp` with bearer token auth. For Agent / MCP clients. Recommended when exposed to a network. |
|
package/README_zh.md
CHANGED
|
@@ -82,6 +82,7 @@ MindOS 是一个**人机协同心智系统**——基于本地优先的协作知
|
|
|
82
82
|
- **引用同步**:通过引用与反向链接保持跨文件状态一致。
|
|
83
83
|
- **知识图谱**:可视化笔记间关系与依赖。
|
|
84
84
|
- **Git 时光机**:记录修改历史,支持审计与安全回滚。
|
|
85
|
+
- **跨设备同步**:通过 Git 自动 commit、push、pull —— 一台设备的编辑几分钟内同步到所有设备。
|
|
85
86
|
|
|
86
87
|
<details>
|
|
87
88
|
<summary><strong>即将到来</strong></summary>
|
|
@@ -131,12 +132,13 @@ mindos onboard --install-daemon
|
|
|
131
132
|
> `--install-daemon`:配置完成后,自动将 MindOS 安装为后台 OS 服务(关闭终端仍运行,崩溃自动重启)。
|
|
132
133
|
|
|
133
134
|
配置向导将引导你完成:
|
|
134
|
-
1. 知识库路径 → 默认
|
|
135
|
+
1. 知识库路径 → 默认 `~/MindOS`
|
|
135
136
|
2. 选择模板(en / zh / empty / custom)
|
|
136
137
|
3. 端口配置(Web UI + MCP)
|
|
137
138
|
4. Auth token(自动生成或口令派生)
|
|
138
139
|
5. Web UI 访问密码(可选)
|
|
139
140
|
6. 配置 AI Provider(Anthropic / OpenAI)+ API Key — 或选择 **skip**,稍后通过 `mindos config set` 补填
|
|
141
|
+
7. 启动模式 — **后台服务**(推荐,开机自启)或前台运行
|
|
140
142
|
|
|
141
143
|
配置自动保存到 `~/.mindos/config.json`。
|
|
142
144
|
|
|
@@ -153,34 +155,50 @@ mindos onboard --install-daemon
|
|
|
153
155
|
|
|
154
156
|
```json
|
|
155
157
|
{
|
|
156
|
-
"mindRoot": "
|
|
158
|
+
"mindRoot": "~/MindOS",
|
|
157
159
|
"port": 3000,
|
|
158
160
|
"mcpPort": 8787,
|
|
159
161
|
"authToken": "",
|
|
160
162
|
"webPassword": "",
|
|
163
|
+
"startMode": "daemon",
|
|
161
164
|
"ai": {
|
|
162
165
|
"provider": "anthropic",
|
|
163
166
|
"providers": {
|
|
164
167
|
"anthropic": { "apiKey": "sk-ant-...", "model": "claude-sonnet-4-6" },
|
|
165
168
|
"openai": { "apiKey": "sk-...", "model": "gpt-5.4", "baseUrl": "" }
|
|
166
169
|
}
|
|
170
|
+
},
|
|
171
|
+
"sync": {
|
|
172
|
+
"enabled": true,
|
|
173
|
+
"provider": "git",
|
|
174
|
+
"remote": "origin",
|
|
175
|
+
"branch": "main",
|
|
176
|
+
"autoCommitInterval": 30,
|
|
177
|
+
"autoPullInterval": 300
|
|
167
178
|
}
|
|
168
179
|
}
|
|
169
180
|
```
|
|
170
181
|
|
|
171
182
|
| 字段 | 默认值 | 说明 |
|
|
172
183
|
| :--- | :--- | :--- |
|
|
173
|
-
| `mindRoot` |
|
|
184
|
+
| `mindRoot` | `~/MindOS` | **必填**。知识库根目录的绝对路径 |
|
|
174
185
|
| `port` | `3000` | 可选。Web 服务端口 |
|
|
175
186
|
| `mcpPort` | `8787` | 可选。MCP 服务端口 |
|
|
176
187
|
| `authToken` | — | 可选。保护 App `/api/*` 和 MCP `/mcp` 的 Bearer Token 认证。供 Agent / MCP 客户端使用,暴露到网络时建议设置 |
|
|
177
188
|
| `webPassword` | — | 可选。为 Web UI 添加登录密码保护。供浏览器访问,与 `authToken` 相互独立 |
|
|
189
|
+
| `startMode` | `start` | 启动模式:`daemon`(后台服务,开机自启)、`start`(前台)或 `dev` |
|
|
178
190
|
| `ai.provider` | `anthropic` | 当前使用的 provider:`anthropic` 或 `openai` |
|
|
179
191
|
| `ai.providers.anthropic.apiKey` | — | Anthropic API Key |
|
|
180
192
|
| `ai.providers.anthropic.model` | `claude-sonnet-4-6` | Anthropic 模型 ID |
|
|
181
193
|
| `ai.providers.openai.apiKey` | — | OpenAI API Key |
|
|
182
194
|
| `ai.providers.openai.model` | `gpt-5.4` | OpenAI 模型 ID |
|
|
183
195
|
| `ai.providers.openai.baseUrl` | — | 可选。用于代理或 OpenAI 兼容 API 的自定义接口地址 |
|
|
196
|
+
| `sync.enabled` | `false` | 启用/禁用 Git 自动同步 |
|
|
197
|
+
| `sync.provider` | `git` | 同步方式(目前仅支持 `git`) |
|
|
198
|
+
| `sync.remote` | `origin` | Git 远程仓库名 |
|
|
199
|
+
| `sync.branch` | `main` | 同步分支 |
|
|
200
|
+
| `sync.autoCommitInterval` | `30` | 文件变更后自动 commit+push 的延迟秒数 |
|
|
201
|
+
| `sync.autoPullInterval` | `300` | 自动从远程 pull 的间隔秒数 |
|
|
184
202
|
|
|
185
203
|
多个 provider 可以同时配置,切换时只需修改 `ai.provider` 字段,无需重新填写 API Key。Shell 环境变量(`ANTHROPIC_API_KEY`、`OPENAI_API_KEY` 等)优先级高于配置文件。
|
|
186
204
|
|
|
@@ -192,6 +210,12 @@ mindos onboard --install-daemon
|
|
|
192
210
|
> [!TIP]
|
|
193
211
|
> 使用 `--install-daemon` 时,MindOS 会作为后台 OS 服务安装并自动启动,无需手动执行 `mindos start`。如果跳过了该参数,运行 `mindos start` 手动启动,或运行 `mindos update` 升级到最新版本。
|
|
194
212
|
|
|
213
|
+
在浏览器中打开 Web UI:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
mindos open
|
|
217
|
+
```
|
|
218
|
+
|
|
195
219
|
### 3. 通过 MindOS Agent 注入你的个人心智
|
|
196
220
|
|
|
197
221
|
1. 打开 MindOS GUI 中内置的 Agent 对话面板。
|
|
@@ -386,12 +410,13 @@ MindOS/
|
|
|
386
410
|
├── mcp/ # MCP Server — 将工具映射到 App API 的 HTTP 适配器
|
|
387
411
|
├── skills/ # MindOS Skills(`mindos`、`mindos-zh`)— Agent 工作流指南
|
|
388
412
|
├── templates/ # 预设模板(`en/`、`zh/`、`empty/`)— onboard 时复制到知识库目录
|
|
389
|
-
├── bin/ # CLI 入口(`mindos onboard`、`mindos start`、`mindos
|
|
413
|
+
├── bin/ # CLI 入口(`mindos onboard`、`mindos start`、`mindos open`、`mindos sync`、`mindos token`)
|
|
390
414
|
├── scripts/ # 配置向导与辅助脚本
|
|
391
415
|
└── README.md
|
|
392
416
|
|
|
393
417
|
~/.mindos/ # 用户数据目录(项目外,不会被提交)
|
|
394
|
-
├── config.json # 所有配置(AI 密钥、端口、Auth token
|
|
418
|
+
├── config.json # 所有配置(AI 密钥、端口、Auth token、同步设置)
|
|
419
|
+
├── sync-state.json # 同步状态(最后同步时间、冲突文件)
|
|
395
420
|
└── my-mind/ # 你的私有知识库(默认路径,onboard 时可自定义)
|
|
396
421
|
```
|
|
397
422
|
|
|
@@ -407,11 +432,19 @@ MindOS/
|
|
|
407
432
|
| `mindos start --daemon` | 安装并以后台 OS 服务方式启动(关闭终端仍运行,崩溃自动重启) |
|
|
408
433
|
| `mindos dev` | 启动 app + MCP 服务(开发模式,热更新) |
|
|
409
434
|
| `mindos dev --turbopack` | 开发模式 + Turbopack(更快的 HMR) |
|
|
435
|
+
| `mindos open` | 在默认浏览器中打开 Web UI |
|
|
410
436
|
| `mindos stop` | 停止正在运行的 MindOS 进程 |
|
|
411
437
|
| `mindos restart` | 停止后重新启动 |
|
|
412
438
|
| `mindos build` | 手动构建生产版本 |
|
|
413
439
|
| `mindos mcp` | 仅启动 MCP 服务 |
|
|
414
440
|
| `mindos token` | 查看当前 Auth token 及 MCP 配置片段 |
|
|
441
|
+
| `mindos sync` | 查看同步状态(`sync status` 的别名) |
|
|
442
|
+
| `mindos sync init` | 交互式配置 Git 远程同步 |
|
|
443
|
+
| `mindos sync status` | 查看同步状态:最后同步时间、未推送提交、冲突 |
|
|
444
|
+
| `mindos sync now` | 手动触发完整同步(commit + push + pull) |
|
|
445
|
+
| `mindos sync on` | 启用自动同步 |
|
|
446
|
+
| `mindos sync off` | 禁用自动同步 |
|
|
447
|
+
| `mindos sync conflicts` | 列出未解决的冲突文件 |
|
|
415
448
|
| `mindos gateway install` | 安装后台服务(Linux 用 systemd,macOS 用 LaunchAgent) |
|
|
416
449
|
| `mindos gateway uninstall` | 卸载后台服务 |
|
|
417
450
|
| `mindos gateway start` | 启动后台服务 |
|
package/app/README.md
CHANGED
|
@@ -16,7 +16,7 @@ mindos dev # or: mindos start
|
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
18
|
npm install
|
|
19
|
-
MIND_ROOT
|
|
19
|
+
MIND_ROOT=~/MindOS ANTHROPIC_API_KEY=sk-ant-... npm run dev
|
|
20
20
|
# Or copy .env.local.example to app/.env.local and fill in values
|
|
21
21
|
```
|
|
22
22
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { getMindRoot } from '@/lib/fs';
|
|
3
|
+
import { applyTemplate } from '@/lib/template';
|
|
4
|
+
|
|
5
|
+
export async function POST(req: NextRequest) {
|
|
6
|
+
try {
|
|
7
|
+
const body = await req.json();
|
|
8
|
+
const template = body.template as string;
|
|
9
|
+
|
|
10
|
+
const mindRoot = getMindRoot();
|
|
11
|
+
applyTemplate(template, mindRoot);
|
|
12
|
+
|
|
13
|
+
return NextResponse.json({ ok: true, template });
|
|
14
|
+
} catch (e) {
|
|
15
|
+
console.error('[/api/init] Error:', e);
|
|
16
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
17
|
+
const status = msg.startsWith('Invalid template') ? 400
|
|
18
|
+
: msg.includes('not found') ? 404
|
|
19
|
+
: 500;
|
|
20
|
+
return NextResponse.json({ error: msg }, { status });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -112,6 +112,9 @@ export async function POST(req: NextRequest) {
|
|
|
112
112
|
mindRoot: body.mindRoot ?? current.mindRoot,
|
|
113
113
|
webPassword: resolvedWebPassword,
|
|
114
114
|
authToken: resolvedAuthToken,
|
|
115
|
+
port: typeof body.port === 'number' ? body.port : current.port,
|
|
116
|
+
mcpPort: typeof body.mcpPort === 'number' ? body.mcpPort : current.mcpPort,
|
|
117
|
+
startMode: body.startMode ?? current.startMode,
|
|
115
118
|
};
|
|
116
119
|
|
|
117
120
|
writeSettings(next);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const dynamic = 'force-dynamic';
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
3
|
+
import { randomBytes, createHash } from 'crypto';
|
|
4
|
+
|
|
5
|
+
export async function POST(req: Request) {
|
|
6
|
+
try {
|
|
7
|
+
const { seed } = await req.json().catch(() => ({} as { seed?: string }));
|
|
8
|
+
let raw: string;
|
|
9
|
+
if (seed && typeof seed === 'string' && seed.trim()) {
|
|
10
|
+
raw = createHash('sha256').update(seed.trim()).digest('hex').slice(0, 24);
|
|
11
|
+
} else {
|
|
12
|
+
raw = randomBytes(12).toString('hex'); // 24 hex chars
|
|
13
|
+
}
|
|
14
|
+
// Format as xxxx-xxxx-xxxx-xxxx-xxxx-xxxx
|
|
15
|
+
const token = raw.match(/.{4}/g)!.join('-');
|
|
16
|
+
return NextResponse.json({ token });
|
|
17
|
+
} catch (e) {
|
|
18
|
+
return NextResponse.json(
|
|
19
|
+
{ error: e instanceof Error ? e.message : String(e) },
|
|
20
|
+
{ status: 500 },
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export const dynamic = 'force-dynamic';
|
|
2
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import { readSettings, writeSettings, ServerSettings } from '@/lib/settings';
|
|
6
|
+
import { applyTemplate } from '@/lib/template';
|
|
7
|
+
|
|
8
|
+
function expandHome(p: string): string {
|
|
9
|
+
if (p.startsWith('~/')) return p.replace('~', os.homedir());
|
|
10
|
+
if (p === '~') return os.homedir();
|
|
11
|
+
return p;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function POST(req: NextRequest) {
|
|
15
|
+
try {
|
|
16
|
+
const body = await req.json();
|
|
17
|
+
const { mindRoot, template, port, mcpPort, authToken, webPassword, ai } = body;
|
|
18
|
+
|
|
19
|
+
// Validate required fields
|
|
20
|
+
if (!mindRoot || typeof mindRoot !== 'string') {
|
|
21
|
+
return NextResponse.json({ error: 'mindRoot is required' }, { status: 400 });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const resolvedRoot = expandHome(mindRoot.trim());
|
|
25
|
+
|
|
26
|
+
// Validate ports
|
|
27
|
+
const webPort = typeof port === 'number' ? port : 3000;
|
|
28
|
+
const mcpPortNum = typeof mcpPort === 'number' ? mcpPort : 8787;
|
|
29
|
+
if (webPort < 1024 || webPort > 65535) {
|
|
30
|
+
return NextResponse.json({ error: `Invalid web port: ${webPort}` }, { status: 400 });
|
|
31
|
+
}
|
|
32
|
+
if (mcpPortNum < 1024 || mcpPortNum > 65535) {
|
|
33
|
+
return NextResponse.json({ error: `Invalid MCP port: ${mcpPortNum}` }, { status: 400 });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Apply template if mindRoot doesn't exist or is empty
|
|
37
|
+
const dirExists = fs.existsSync(resolvedRoot);
|
|
38
|
+
let dirEmpty = true;
|
|
39
|
+
if (dirExists) {
|
|
40
|
+
try {
|
|
41
|
+
const entries = fs.readdirSync(resolvedRoot).filter(e => !e.startsWith('.'));
|
|
42
|
+
dirEmpty = entries.length === 0;
|
|
43
|
+
} catch { /* treat as empty */ }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (template && (!dirExists || dirEmpty)) {
|
|
47
|
+
applyTemplate(template, resolvedRoot);
|
|
48
|
+
} else if (!dirExists) {
|
|
49
|
+
fs.mkdirSync(resolvedRoot, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Read current running port for portChanged detection
|
|
53
|
+
const current = readSettings();
|
|
54
|
+
const currentPort = current.port ?? 3000;
|
|
55
|
+
|
|
56
|
+
// Build config
|
|
57
|
+
const config: ServerSettings = {
|
|
58
|
+
ai: ai ?? current.ai,
|
|
59
|
+
mindRoot: resolvedRoot,
|
|
60
|
+
port: webPort,
|
|
61
|
+
mcpPort: mcpPortNum,
|
|
62
|
+
authToken: authToken ?? current.authToken,
|
|
63
|
+
webPassword: webPassword ?? '',
|
|
64
|
+
startMode: current.startMode,
|
|
65
|
+
setupPending: false, // clear the flag
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
writeSettings(config);
|
|
69
|
+
|
|
70
|
+
return NextResponse.json({
|
|
71
|
+
ok: true,
|
|
72
|
+
portChanged: webPort !== currentPort,
|
|
73
|
+
});
|
|
74
|
+
} catch (e) {
|
|
75
|
+
console.error('[/api/setup] Error:', e);
|
|
76
|
+
return NextResponse.json(
|
|
77
|
+
{ error: e instanceof Error ? e.message : String(e) },
|
|
78
|
+
{ status: 500 },
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
package/app/app/layout.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import ShellLayout from '@/components/ShellLayout';
|
|
|
6
6
|
import { TooltipProvider } from '@/components/ui/tooltip';
|
|
7
7
|
import { LocaleProvider } from '@/lib/LocaleContext';
|
|
8
8
|
import ErrorBoundary from '@/components/ErrorBoundary';
|
|
9
|
+
import RegisterSW from './register-sw';
|
|
9
10
|
|
|
10
11
|
const geistSans = Geist({
|
|
11
12
|
variable: '--font-geist-sans',
|
|
@@ -39,7 +40,13 @@ const lora = Lora({
|
|
|
39
40
|
export const metadata: Metadata = {
|
|
40
41
|
title: 'MindOS',
|
|
41
42
|
description: 'Personal knowledge base',
|
|
42
|
-
icons: { icon: '/logo-square.svg' },
|
|
43
|
+
icons: { icon: '/logo-square.svg', apple: '/icons/icon-192.png' },
|
|
44
|
+
manifest: '/manifest.json',
|
|
45
|
+
other: {
|
|
46
|
+
'mobile-web-app-capable': 'yes',
|
|
47
|
+
'apple-mobile-web-app-capable': 'yes',
|
|
48
|
+
'apple-mobile-web-app-status-bar-style': 'black-translucent',
|
|
49
|
+
},
|
|
43
50
|
};
|
|
44
51
|
|
|
45
52
|
export const viewport = {
|
|
@@ -63,6 +70,7 @@ export default function RootLayout({
|
|
|
63
70
|
return (
|
|
64
71
|
<html lang="en" suppressHydrationWarning>
|
|
65
72
|
<head>
|
|
73
|
+
<meta name="theme-color" content="#c8871e" />
|
|
66
74
|
{/* Patch Node.removeChild/insertBefore to swallow errors caused by browser
|
|
67
75
|
extensions (translators, Grammarly, etc.) that mutate the DOM between SSR
|
|
68
76
|
and hydration. See: https://github.com/facebook/react/issues/17256 */}
|
|
@@ -90,6 +98,7 @@ export default function RootLayout({
|
|
|
90
98
|
</ShellLayout>
|
|
91
99
|
</ErrorBoundary>
|
|
92
100
|
</TooltipProvider>
|
|
101
|
+
<RegisterSW />
|
|
93
102
|
</LocaleProvider>
|
|
94
103
|
</body>
|
|
95
104
|
</html>
|
package/app/app/page.tsx
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
import { redirect } from 'next/navigation';
|
|
2
|
+
import { readSettings } from '@/lib/settings';
|
|
1
3
|
import { getRecentlyModified } from '@/lib/fs';
|
|
2
4
|
import HomeContent from '@/components/HomeContent';
|
|
3
5
|
|
|
4
6
|
export default function HomePage() {
|
|
7
|
+
const settings = readSettings();
|
|
8
|
+
if (settings.setupPending) redirect('/setup');
|
|
9
|
+
|
|
5
10
|
let recent: { path: string; mtime: number }[] = [];
|
|
6
11
|
try {
|
|
7
12
|
recent = getRecentlyModified(15);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect } from 'react';
|
|
4
|
+
|
|
5
|
+
export default function RegisterSW() {
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
if ('serviceWorker' in navigator) {
|
|
8
|
+
navigator.serviceWorker.register('/sw.js').catch((err) => {
|
|
9
|
+
console.warn('[SW] Registration failed:', err);
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
}, []);
|
|
13
|
+
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { redirect } from 'next/navigation';
|
|
2
|
+
import { readSettings } from '@/lib/settings';
|
|
3
|
+
import SetupWizard from '@/components/SetupWizard';
|
|
4
|
+
|
|
5
|
+
export default function SetupPage() {
|
|
6
|
+
const settings = readSettings();
|
|
7
|
+
if (!settings.setupPending) redirect('/');
|
|
8
|
+
return <SetupWizard />;
|
|
9
|
+
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import Link from 'next/link';
|
|
4
|
-
import { FileText, Table, Clock, Sparkles, Puzzle, ArrowRight, FilePlus, Search, ChevronDown } from 'lucide-react';
|
|
4
|
+
import { FileText, Table, Clock, Sparkles, Puzzle, ArrowRight, FilePlus, Search, ChevronDown, Terminal } from 'lucide-react';
|
|
5
5
|
import { useState } from 'react';
|
|
6
6
|
import { useLocale } from '@/lib/LocaleContext';
|
|
7
7
|
import { encodePath, relativeTime } from '@/lib/utils';
|
|
8
8
|
import { getAllRenderers } from '@/lib/renderers/registry';
|
|
9
9
|
import '@/lib/renderers/index'; // registers all renderers
|
|
10
|
+
import OnboardingView from './OnboardingView';
|
|
10
11
|
|
|
11
12
|
interface RecentFile {
|
|
12
13
|
path: string;
|
|
@@ -21,7 +22,7 @@ const RENDERER_ENTRY: Record<string, string> = {
|
|
|
21
22
|
timeline: 'CHANGELOG.md',
|
|
22
23
|
backlinks: 'BACKLINKS.md',
|
|
23
24
|
summary: 'DAILY.md',
|
|
24
|
-
'agent-inspector': '
|
|
25
|
+
'agent-inspector': '.agent-log.json',
|
|
25
26
|
workflow: 'Workflow.md',
|
|
26
27
|
'diff-viewer': 'Agent-Diff.md',
|
|
27
28
|
'config-panel': 'CONFIG.json',
|
|
@@ -45,6 +46,11 @@ export default function HomeContent({ recent }: { recent: RecentFile[] }) {
|
|
|
45
46
|
const { t } = useLocale();
|
|
46
47
|
const [showAll, setShowAll] = useState(false);
|
|
47
48
|
|
|
49
|
+
// Empty knowledge base → show onboarding
|
|
50
|
+
if (recent.length === 0) {
|
|
51
|
+
return <OnboardingView />;
|
|
52
|
+
}
|
|
53
|
+
|
|
48
54
|
const formatTime = (mtime: number) => relativeTime(mtime, t.home.relativeTime);
|
|
49
55
|
|
|
50
56
|
const renderers = getAllRenderers();
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import { Sparkles, Globe, BookOpen, FileText, Loader2, GitBranch } from 'lucide-react';
|
|
6
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
7
|
+
|
|
8
|
+
type Template = 'en' | 'zh' | 'empty';
|
|
9
|
+
|
|
10
|
+
const TEMPLATES: Array<{
|
|
11
|
+
id: Template;
|
|
12
|
+
icon: React.ReactNode;
|
|
13
|
+
dirs: string[];
|
|
14
|
+
}> = [
|
|
15
|
+
{
|
|
16
|
+
id: 'en',
|
|
17
|
+
icon: <Globe size={20} />,
|
|
18
|
+
dirs: ['Profile/', 'Connections/', 'Notes/', 'Workflows/', 'Resources/', 'Projects/'],
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 'zh',
|
|
22
|
+
icon: <BookOpen size={20} />,
|
|
23
|
+
dirs: ['画像/', '关系/', '笔记/', '流程/', '资源/', '项目/'],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 'empty',
|
|
27
|
+
icon: <FileText size={20} />,
|
|
28
|
+
dirs: ['README.md', 'CONFIG.json', 'INSTRUCTION.md'],
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
export default function OnboardingView() {
|
|
33
|
+
const { t } = useLocale();
|
|
34
|
+
const router = useRouter();
|
|
35
|
+
const [loading, setLoading] = useState<Template | null>(null);
|
|
36
|
+
|
|
37
|
+
const ob = t.onboarding;
|
|
38
|
+
|
|
39
|
+
async function handleSelect(template: Template) {
|
|
40
|
+
setLoading(template);
|
|
41
|
+
try {
|
|
42
|
+
const res = await fetch('/api/init', {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers: { 'Content-Type': 'application/json' },
|
|
45
|
+
body: JSON.stringify({ template }),
|
|
46
|
+
});
|
|
47
|
+
if (!res.ok) {
|
|
48
|
+
const data = await res.json().catch(() => ({}));
|
|
49
|
+
throw new Error(data.error || `HTTP ${res.status}`);
|
|
50
|
+
}
|
|
51
|
+
router.refresh();
|
|
52
|
+
} catch (e) {
|
|
53
|
+
console.error('[Onboarding] init failed:', e);
|
|
54
|
+
setLoading(null);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className="content-width px-4 md:px-6 py-12 md:py-20">
|
|
60
|
+
{/* Header */}
|
|
61
|
+
<div className="text-center mb-10">
|
|
62
|
+
<div className="inline-flex items-center gap-2 mb-4">
|
|
63
|
+
<Sparkles size={18} style={{ color: 'var(--amber)' }} />
|
|
64
|
+
<h1
|
|
65
|
+
className="text-2xl font-semibold tracking-tight"
|
|
66
|
+
style={{ fontFamily: "'IBM Plex Mono', monospace", color: 'var(--foreground)' }}
|
|
67
|
+
>
|
|
68
|
+
MindOS
|
|
69
|
+
</h1>
|
|
70
|
+
</div>
|
|
71
|
+
<p
|
|
72
|
+
className="text-sm leading-relaxed max-w-md mx-auto"
|
|
73
|
+
style={{ color: 'var(--muted-foreground)' }}
|
|
74
|
+
>
|
|
75
|
+
{ob.subtitle}
|
|
76
|
+
</p>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
{/* Template cards */}
|
|
80
|
+
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 max-w-2xl mx-auto mb-10">
|
|
81
|
+
{TEMPLATES.map((tpl) => {
|
|
82
|
+
const isLoading = loading === tpl.id;
|
|
83
|
+
const isDisabled = loading !== null;
|
|
84
|
+
return (
|
|
85
|
+
<button
|
|
86
|
+
key={tpl.id}
|
|
87
|
+
disabled={isDisabled}
|
|
88
|
+
onClick={() => handleSelect(tpl.id)}
|
|
89
|
+
className="group relative flex flex-col items-start gap-3 p-5 rounded-xl border text-left transition-all duration-150 hover:border-amber-500/50 hover:bg-amber-500/5 disabled:opacity-60 disabled:cursor-not-allowed"
|
|
90
|
+
style={{ background: 'var(--card)', borderColor: 'var(--border)' }}
|
|
91
|
+
>
|
|
92
|
+
{/* Icon + title */}
|
|
93
|
+
<div className="flex items-center gap-2.5 w-full">
|
|
94
|
+
<span style={{ color: 'var(--amber)' }}>{tpl.icon}</span>
|
|
95
|
+
<span
|
|
96
|
+
className="text-sm font-semibold"
|
|
97
|
+
style={{ color: 'var(--foreground)', fontFamily: "'IBM Plex Sans', sans-serif" }}
|
|
98
|
+
>
|
|
99
|
+
{ob.templates[tpl.id].title}
|
|
100
|
+
</span>
|
|
101
|
+
{isLoading && (
|
|
102
|
+
<Loader2 size={14} className="animate-spin ml-auto" style={{ color: 'var(--amber)' }} />
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
{/* Description */}
|
|
107
|
+
<p className="text-xs leading-relaxed" style={{ color: 'var(--muted-foreground)' }}>
|
|
108
|
+
{ob.templates[tpl.id].desc}
|
|
109
|
+
</p>
|
|
110
|
+
|
|
111
|
+
{/* Directory preview */}
|
|
112
|
+
<div
|
|
113
|
+
className="w-full rounded-lg px-3 py-2 text-[11px] leading-relaxed"
|
|
114
|
+
style={{
|
|
115
|
+
background: 'var(--muted)',
|
|
116
|
+
fontFamily: "'IBM Plex Mono', monospace",
|
|
117
|
+
color: 'var(--muted-foreground)',
|
|
118
|
+
opacity: 0.8,
|
|
119
|
+
}}
|
|
120
|
+
>
|
|
121
|
+
{tpl.dirs.map((d) => (
|
|
122
|
+
<div key={d}>{d}</div>
|
|
123
|
+
))}
|
|
124
|
+
</div>
|
|
125
|
+
</button>
|
|
126
|
+
);
|
|
127
|
+
})}
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
{/* Import hint */}
|
|
131
|
+
<p
|
|
132
|
+
className="text-center text-xs leading-relaxed max-w-sm mx-auto"
|
|
133
|
+
style={{ color: 'var(--muted-foreground)', opacity: 0.6, fontFamily: "'IBM Plex Mono', monospace" }}
|
|
134
|
+
>
|
|
135
|
+
{ob.importHint}
|
|
136
|
+
</p>
|
|
137
|
+
|
|
138
|
+
{/* Sync hint card */}
|
|
139
|
+
<div
|
|
140
|
+
className="max-w-md mx-auto mt-6 flex items-center gap-3 px-4 py-3 rounded-lg border text-left"
|
|
141
|
+
style={{ borderColor: 'var(--border)', background: 'var(--card)' }}
|
|
142
|
+
>
|
|
143
|
+
<GitBranch size={16} style={{ color: 'var(--muted-foreground)', flexShrink: 0 }} />
|
|
144
|
+
<div className="min-w-0">
|
|
145
|
+
<p className="text-xs" style={{ color: 'var(--muted-foreground)' }}>
|
|
146
|
+
{ob.syncHint ?? 'Want cross-device sync? Run'}
|
|
147
|
+
{' '}
|
|
148
|
+
<code
|
|
149
|
+
className="font-mono px-1 py-0.5 rounded select-all"
|
|
150
|
+
style={{ background: 'var(--muted)', fontSize: '11px' }}
|
|
151
|
+
>
|
|
152
|
+
mindos sync init
|
|
153
|
+
</code>
|
|
154
|
+
{' '}
|
|
155
|
+
{ob.syncHintSuffix ?? 'in the terminal after setup.'}
|
|
156
|
+
</p>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
@@ -18,9 +18,10 @@ import { SyncTab } from './settings/SyncTab';
|
|
|
18
18
|
interface SettingsModalProps {
|
|
19
19
|
open: boolean;
|
|
20
20
|
onClose: () => void;
|
|
21
|
+
initialTab?: Tab;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
export default function SettingsModal({ open, onClose }: SettingsModalProps) {
|
|
24
|
+
export default function SettingsModal({ open, onClose, initialTab }: SettingsModalProps) {
|
|
24
25
|
const [tab, setTab] = useState<Tab>('ai');
|
|
25
26
|
const [data, setData] = useState<SettingsData | null>(null);
|
|
26
27
|
const [saving, setSaving] = useState(false);
|
|
@@ -48,6 +49,11 @@ export default function SettingsModal({ open, onClose }: SettingsModalProps) {
|
|
|
48
49
|
setStatus('idle');
|
|
49
50
|
}, [open]);
|
|
50
51
|
|
|
52
|
+
// Switch to requested tab when opening with initialTab
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (open && initialTab) setTab(initialTab);
|
|
55
|
+
}, [open, initialTab]);
|
|
56
|
+
|
|
51
57
|
// Apply font immediately
|
|
52
58
|
useEffect(() => {
|
|
53
59
|
const fontMap: Record<string, string> = {
|