@geminilight/mindos 0.7.2 → 0.7.4
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/_standalone/.mindos-build-version +1 -1
- package/_standalone/.next/BUILD_ID +1 -1
- package/_standalone/.next/app-path-routes-manifest.json +19 -19
- package/_standalone/.next/build-manifest.json +3 -3
- package/_standalone/.next/cache/.previewinfo +1 -1
- package/_standalone/.next/cache/.rscinfo +1 -1
- package/_standalone/.next/cache/config.json +3 -3
- package/_standalone/.next/prerender-manifest.json +3 -3
- package/_standalone/.next/react-loadable-manifest.json +4 -4
- package/_standalone/.next/server/app/.well-known/agent-card.json/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/_global-error.html +2 -2
- package/_standalone/.next/server/app/_global-error.rsc +1 -1
- package/_standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/_standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/_standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/_standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/_standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/_standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/_standalone/.next/server/app/_not-found/page.js +1 -1
- package/_standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/agents/[agentKey]/page.js +1 -1
- package/_standalone/.next/server/app/agents/[agentKey]/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/agents/[agentKey]/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/agents/page.js +1 -1
- package/_standalone/.next/server/app/agents/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/agents/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/a2a/agents/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/a2a/delegations/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/a2a/discover/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/a2a/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/acp/config/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/acp/detect/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/acp/install/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/acp/registry/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/acp/session/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/agent-activity/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/agents/copy-skill/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/agents/custom/detect/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/agents/custom/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/ask/route.js +4 -4
- package/_standalone/.next/server/app/api/ask/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/ask-sessions/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/auth/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/backlinks/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/bootstrap/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/changes/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/channels/verify/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/connect/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/embedding/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/export/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/extract-docx/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/extract-pdf/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/file/import/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/file/raw/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/file/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/graph/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/health/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/im/activity/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/im/config/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/im/feishu/long-connection/event/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/im/feishu/long-connection/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/im/status/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/im/test/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/im/webhook/feishu/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/im/webhook-status/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/inbox/clip/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/inbox/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/init/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/lint/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/mcp/agents/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/mcp/direct-tools/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/mcp/install/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/mcp/install-skill/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/mcp/restart/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/mcp/status/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/mcp/tools/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/mcp/uninstall/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/monitoring/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/obsidian/compat-report/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/obsidian/import/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/recent-files/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/restart/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/search/prewarm/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/search/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/settings/list-models/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/settings/reset-token/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/settings/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/settings/test-key/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/setup/check-path/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/setup/check-port/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/setup/generate-token/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/setup/ls/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/setup/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/skills/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/space-overview/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/sync/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/tree-version/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/uninstall/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/update-check/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/update-status/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/workflows/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/capture/history/page.js +1 -1
- package/_standalone/.next/server/app/capture/history/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/capture/history/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/capture/page.js +1 -1
- package/_standalone/.next/server/app/capture/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/capture/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/changelog/page.js +1 -1
- package/_standalone/.next/server/app/changelog/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/changelog/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/changes/page.js +1 -1
- package/_standalone/.next/server/app/changes/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/changes/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/echo/[segment]/page.js +1 -1
- package/_standalone/.next/server/app/echo/[segment]/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/echo/[segment]/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/echo/page.js +1 -1
- package/_standalone/.next/server/app/echo/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/echo/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/explore/page.js +1 -1
- package/_standalone/.next/server/app/explore/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/explore/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/help/page.js +1 -1
- package/_standalone/.next/server/app/help/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/help/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/inbox/history/page.js +1 -1
- package/_standalone/.next/server/app/inbox/history/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/inbox/history/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/login/page.js +1 -1
- package/_standalone/.next/server/app/login/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/page.js +1 -1
- package/_standalone/.next/server/app/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/setup/page.js +1 -1
- package/_standalone/.next/server/app/setup/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/todo/page.js +1 -1
- package/_standalone/.next/server/app/todo/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/todo/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/trash/page.js +2 -2
- package/_standalone/.next/server/app/trash/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/view/[...path]/page.js +2 -2
- package/_standalone/.next/server/app/view/[...path]/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/view/[...path]/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/wiki/page.js +2 -0
- package/_standalone/.next/server/app/wiki/page.js.nft.json +1 -0
- package/_standalone/.next/server/app/wiki/page_client-reference-manifest.js +1 -0
- package/_standalone/.next/server/app-paths-manifest.json +19 -19
- package/_standalone/.next/server/chunks/2250.js +1 -1
- package/_standalone/.next/server/chunks/{1057.js → 3861.js} +2 -2
- package/_standalone/.next/server/chunks/4802.js +30 -30
- package/_standalone/.next/server/chunks/8388.js +1 -1
- package/_standalone/.next/server/middleware-build-manifest.js +1 -1
- package/_standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/_standalone/.next/server/pages/500.html +2 -2
- package/_standalone/.next/server/server-reference-manifest.js +1 -1
- package/_standalone/.next/server/server-reference-manifest.json +1 -1
- package/_standalone/.next/static/chunks/{5581-bae715e40d227b5f.js → 5581-beecbc4ca5625aa9.js} +2 -2
- package/_standalone/.next/static/chunks/6762.c871d0bf3f45ba87.js +1 -0
- package/_standalone/.next/static/chunks/{8064-e65acd2762132099.js → 8064-acac37daf946082b.js} +1 -1
- package/_standalone/.next/static/chunks/{3985.695651f6b5cd768c.js → 8984.91fb8cde1983c564.js} +2 -2
- package/_standalone/.next/static/chunks/app/{layout-0cf6f2a65f605a0d.js → layout-20faba3bc0af7cd2.js} +19 -19
- package/_standalone/.next/static/chunks/app/trash/page-0c2c67929b71ef71.js +1 -0
- package/_standalone/.next/static/chunks/app/view/[...path]/not-found-fd06cc989103ebe7.js +1 -0
- package/_standalone/.next/static/chunks/app/view/[...path]/page-dba70888697ba910.js +12 -0
- package/_standalone/.next/static/chunks/app/wiki/page-9bc1fec84d343290.js +14 -0
- package/_standalone/.next/static/chunks/webpack-3c1d0331f1da64b8.js +1 -0
- package/_standalone/.next/trace +97 -97
- package/_standalone/MINDOS_ARCHITECTURE_DIAGRAM.md +488 -0
- package/_standalone/MINDOS_EXPLORATION_SUMMARY.md +229 -0
- package/_standalone/MINDOS_INFRASTRUCTURE_ANALYSIS.md +732 -0
- package/_standalone/__tests__/api/mcp-install.test.ts +8 -2
- package/_standalone/__tests__/core/embedding-provider.test.ts +78 -0
- package/_standalone/__tests__/skills/mindos-skill-copy-alignment.test.ts +10 -4
- package/_standalone/components/ask/AskHeader.tsx +25 -18
- package/_standalone/components/ask/SessionHistoryPanel.tsx +21 -12
- package/_standalone/components/settings/AiTab.tsx +3 -0
- package/_standalone/data/skills/mindos/SKILL.md +269 -0
- package/_standalone/data/skills/mindos/references/write-supplement.md +119 -0
- package/_standalone/data/skills/mindos-zh/SKILL.md +227 -0
- package/_standalone/data/skills/mindos-zh/references/write-supplement.md +119 -0
- package/app/__tests__/api/mcp-install.test.ts +8 -2
- package/app/__tests__/core/embedding-provider.test.ts +78 -0
- package/app/__tests__/skills/mindos-skill-copy-alignment.test.ts +10 -4
- package/app/app/api/ask/route.ts +14 -9
- package/app/app/view/[...path]/ViewPageClient.tsx +15 -12
- package/app/app/view/[...path]/not-found.tsx +9 -5
- package/app/components/ask/AskHeader.tsx +25 -18
- package/app/components/ask/SessionHistoryPanel.tsx +21 -12
- package/app/components/settings/AiTab.tsx +3 -0
- package/app/eslint.config.mjs +18 -0
- package/app/lib/core/embedding-provider.ts +12 -4
- package/app/lib/i18n/modules/settings.ts +2 -0
- package/app/package-lock.json +20214 -0
- package/app/tsconfig.tsbuildinfo +1 -0
- package/app/vitest.config.ts +14 -0
- package/assets/demo-flow-zh.html +622 -0
- package/assets/images/demo-flow-dark.png +0 -0
- package/assets/images/demo-flow-dark.webp +0 -0
- package/assets/images/demo-flow-light.png +0 -0
- package/assets/images/demo-flow-light.webp +0 -0
- package/assets/images/demo-flow-zh-dark.png +0 -0
- package/assets/images/demo-flow-zh-dark.webp +0 -0
- package/assets/images/demo-flow-zh-light.png +0 -0
- package/assets/images/demo-flow-zh-light.webp +0 -0
- package/assets/images/gui-sync-cv.png +0 -0
- package/assets/images/gui-sync-cv.webp +0 -0
- package/assets/images/mindos-chat.png +0 -0
- package/assets/images/mindos-chat.webp +0 -0
- package/assets/images/mindos-dashboard.png +0 -0
- package/assets/images/mindos-dashboard.webp +0 -0
- package/assets/images/mindos-echo.png +0 -0
- package/assets/images/mindos-echo.webp +0 -0
- package/assets/images/mindos-home.png +0 -0
- package/assets/images/mindos-home.webp +0 -0
- package/assets/images/wechat-qr.png +0 -0
- package/bin/lib/mcp-build.js +8 -0
- package/mcp/package-lock.json +2202 -0
- package/mcp/src/index.ts +783 -0
- package/package.json +13 -1
- package/.env.local.example +0 -38
- package/.playwright-cli/page-2026-04-12T12-26-53-393Z.yml +0 -6
- package/.playwright-cli/page-2026-04-12T12-27-20-256Z.yml +0 -120
- package/.syncinclude +0 -105
- package/MINDOS_SEARCH_DIAGRAM.txt +0 -243
- package/_standalone/.next/static/chunks/576.3cae31209383ddbd.js +0 -1
- package/_standalone/.next/static/chunks/app/trash/page-085f121c0815d542.js +0 -1
- package/_standalone/.next/static/chunks/app/view/[...path]/not-found-2a6eec67e91eaaf9.js +0 -1
- package/_standalone/.next/static/chunks/app/view/[...path]/page-faeaf8c09c1c6d7c.js +0 -12
- package/_standalone/.next/static/chunks/webpack-a1bb35f2d540e463.js +0 -1
- package/scripts/build-runtime-archive.sh +0 -151
- package/scripts/fix-postcss-deps.cjs +0 -75
- package/scripts/gen-renderer-index.js +0 -64
- package/scripts/hooks/block-public-merge.sh +0 -42
- package/scripts/hooks/pre-merge-commit +0 -4
- package/scripts/hooks/pre-push +0 -37
- package/scripts/hooks/prepare-commit-msg +0 -12
- package/scripts/migrate-agent-audit-log.js +0 -170
- package/scripts/migrate-agent-diff.js +0 -146
- package/scripts/parse-syncinclude.sh +0 -92
- package/scripts/prepare-standalone.mjs +0 -83
- package/scripts/release.sh +0 -145
- package/scripts/setup.js +0 -1427
- package/scripts/test-oss-upload.sh +0 -100
- package/scripts/verify-standalone.mjs +0 -129
- package/scripts/write-build-stamp.js +0 -40
- /package/_standalone/.next/static/{q5RP_Mx8BrCfvVDnLpRRc → 0UlbV2rA2i4B-8YYg41wQ}/_buildManifest.js +0 -0
- /package/_standalone/.next/static/{q5RP_Mx8BrCfvVDnLpRRc → 0UlbV2rA2i4B-8YYg41wQ}/_ssgManifest.js +0 -0
package/scripts/setup.js
DELETED
|
@@ -1,1427 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* MindOS interactive setup script
|
|
5
|
-
*
|
|
6
|
-
* Usage: npm run setup OR mindos onboard
|
|
7
|
-
*
|
|
8
|
-
* Steps:
|
|
9
|
-
* 1. Choose knowledge base path → default ~/MindOS/mind (same as GUI)
|
|
10
|
-
* 2. Choose template (en / zh / empty / custom) → copy to knowledge base path
|
|
11
|
-
* 3. Choose ports (web + mcp) — checked for conflicts upfront
|
|
12
|
-
* 4. Auth token (auto-generated or passphrase-seeded)
|
|
13
|
-
* 5. Web UI password (optional)
|
|
14
|
-
* 6. Choose AI provider + API Key → write ~/.mindos/config.json
|
|
15
|
-
* 7. Print next steps
|
|
16
|
-
*
|
|
17
|
-
* Language switching:
|
|
18
|
-
* ← → keys switch UI language (en/zh) at any prompt
|
|
19
|
-
* ↑ ↓ keys navigate select options
|
|
20
|
-
* Enter confirms
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import { existsSync, cpSync, writeFileSync, readFileSync, mkdirSync, createWriteStream, rmSync, openSync } from 'node:fs';
|
|
24
|
-
import { resolve, dirname, join } from 'node:path';
|
|
25
|
-
import { homedir, tmpdir, networkInterfaces } from 'node:os';
|
|
26
|
-
import { fileURLToPath } from 'node:url';
|
|
27
|
-
import { createInterface } from 'node:readline';
|
|
28
|
-
import { pipeline } from 'node:stream/promises';
|
|
29
|
-
import { execSync, spawn } from 'node:child_process';
|
|
30
|
-
import { randomBytes, createHash } from 'node:crypto';
|
|
31
|
-
import { createConnection } from 'node:net';
|
|
32
|
-
import http from 'node:http';
|
|
33
|
-
import { MCP_AGENTS, SKILL_AGENT_REGISTRY, detectAgentPresence } from '../bin/lib/mcp-agents.js';
|
|
34
|
-
|
|
35
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
36
|
-
const ROOT = resolve(__dirname, '..');
|
|
37
|
-
const MINDOS_DIR = resolve(homedir(), '.mindos');
|
|
38
|
-
const CONFIG_PATH = resolve(MINDOS_DIR, 'config.json');
|
|
39
|
-
const LOG_PATH = resolve(MINDOS_DIR, 'mindos.log');
|
|
40
|
-
|
|
41
|
-
// ── i18n ─────────────────────────────────────────────────────────────────────
|
|
42
|
-
|
|
43
|
-
const T = {
|
|
44
|
-
title: { en: '🧠 MindOS Setup', zh: '🧠 MindOS 初始化' },
|
|
45
|
-
langHint: { en: ' ← → 切换中文 ↑ ↓ navigate Enter confirm', zh: ' ← → switch to English ↑ ↓ 上下切换 Enter 确认' },
|
|
46
|
-
|
|
47
|
-
// mode selection
|
|
48
|
-
modePrompt: { en: 'How would you like to set up?', zh: '选择配置方式' },
|
|
49
|
-
modeOpts: { en: ['Continue here in terminal (CLI)', 'Open browser to set up (recommended)'], zh: ['在终端继续配置(CLI)', '打开浏览器配置(推荐)'] },
|
|
50
|
-
modeVals: ['cli', 'gui'],
|
|
51
|
-
guiStarting: { en: '⏳ Starting MindOS, please wait...', zh: '⏳ 正在启动 MindOS,请稍候...' },
|
|
52
|
-
guiReady: { en: (url) => `🌐 Opening setup in browser: ${url}`, zh: (url) => `🌐 在浏览器中打开配置页面: ${url}` },
|
|
53
|
-
guiOpenFailed: { en: (url) => ` Could not open browser automatically. Open this URL manually:\n ${url}`, zh: (url) => ` 无法自动打开浏览器,请手动访问:\n ${url}` },
|
|
54
|
-
|
|
55
|
-
// step labels
|
|
56
|
-
step: { en: (n, total) => `Step ${n}/${total}`, zh: (n, total) => `步骤 ${n}/${total}` },
|
|
57
|
-
stepTitles: {
|
|
58
|
-
en: ['Knowledge Base', 'AI Provider', 'Ports', 'Auth Token', 'Web Password', 'Start Mode', 'Connection Mode', 'Agent Connection'],
|
|
59
|
-
zh: ['知识库', 'AI 服务商', '端口', 'Auth Token', 'Web 密码', '启动方式', '连接模式', 'Agent 连接'],
|
|
60
|
-
},
|
|
61
|
-
|
|
62
|
-
// path
|
|
63
|
-
pathPrompt: { en: 'Folder name or absolute path', zh: '文件夹名或绝对路径' },
|
|
64
|
-
pathHintInline: { en: (base) => `stored under ${base + '/'}`, zh: (base) => `存储在 ${base + '/'}` },
|
|
65
|
-
pathResolved: { en: (p) => ` → ${c.dim(p)}`, zh: (p) => ` → ${c.dim(p)}` },
|
|
66
|
-
|
|
67
|
-
// existing kb
|
|
68
|
-
kbExists: { en: (p) => ` ${c.yellow('⚠')} Directory already exists: ${c.dim(p)}`, zh: (p) => ` ${c.yellow('⚠')} 目录已存在:${c.dim(p)}` },
|
|
69
|
-
kbExistsFiles: { en: 'Contents', zh: '目录内容' },
|
|
70
|
-
kbExistsOpts: { en: ['Use this directory', 'Choose a different path'], zh: ['使用此目录', '重新选择路径'] },
|
|
71
|
-
kbExistsVals: ['use', 'reselect'],
|
|
72
|
-
kbCreated: { en: '✔ Knowledge base initialized', zh: '✔ 知识库已初始化' },
|
|
73
|
-
|
|
74
|
-
// template
|
|
75
|
-
tplPrompt: { en: 'Template', zh: '模板' },
|
|
76
|
-
tplOptions: { en: ['en — English template', 'zh — Chinese template', 'empty — blank files only', 'custom — local path or URL'], zh: ['en — 英文模板', 'zh — 中文模板', 'empty — 仅基础文件', 'custom — 本地路径或 URL'] },
|
|
77
|
-
tplValues: ['en', 'zh', 'empty', 'custom'],
|
|
78
|
-
tplNotFound: { en: '✘ Template not found', zh: '✘ 模板目录不存在' },
|
|
79
|
-
customPrompt: { en: 'Path or URL', zh: '路径或 URL' },
|
|
80
|
-
customEmpty: { en: ' Path cannot be empty, please try again', zh: ' 路径不能为空,请重新输入' },
|
|
81
|
-
downloading: { en: '⏳ Downloading template...', zh: '⏳ 正在下载模板...' },
|
|
82
|
-
dlDone: { en: '✔ Template downloaded', zh: '✔ 模板下载完成' },
|
|
83
|
-
|
|
84
|
-
// ports
|
|
85
|
-
webPortPrompt: { en: 'Web UI port', zh: 'Web UI 端口' },
|
|
86
|
-
mcpPortPrompt: { en: 'MCP server port', zh: 'MCP 服务端口' },
|
|
87
|
-
portInUse: { en: (p) => ` ⚠ Port ${p} is already in use, choose another`, zh: (p) => ` ⚠ 端口 ${p} 已被占用,请换一个` },
|
|
88
|
-
portInvalid: { en: (p) => ` ⚠ Invalid port "${p}", must be 1024–65535`, zh: (p) => ` ⚠ 端口 "${p}" 无效,需在 1024–65535 之间` },
|
|
89
|
-
|
|
90
|
-
// auth
|
|
91
|
-
authPrompt: { en: 'Auth token seed (Enter to auto-generate)', zh: 'Auth token 种子(回车自动生成)' },
|
|
92
|
-
tokenGenerated: { en: '✔ Auth token', zh: '✔ Auth token' },
|
|
93
|
-
|
|
94
|
-
// web password
|
|
95
|
-
webPassPrompt: { en: 'Web UI password (leave empty = no password protection)', zh: 'Web UI 访问密码(留空 = 不设密码)' },
|
|
96
|
-
webPassWarn: { en: ' ⚠ No Web UI password — anyone on the network can access the UI', zh: ' ⚠ 未设置 Web UI 密码,局域网内任何人均可访问' },
|
|
97
|
-
webPassSkip: { en: 'Skip password protection anyway?', zh: '确认不设密码继续?' },
|
|
98
|
-
|
|
99
|
-
// provider
|
|
100
|
-
providerPrompt: { en: 'AI Provider', zh: 'AI 服务商' },
|
|
101
|
-
providerOpts: { en: ['anthropic', 'openai', 'skip — configure later via `mindos config set`'], zh: ['anthropic', 'openai', 'skip — 稍后通过 `mindos config set` 配置'] },
|
|
102
|
-
providerVals: ['anthropic', 'openai', 'skip'],
|
|
103
|
-
providerSkip: { en: ' → AI provider skipped. Run `mindos config set ai.provider <anthropic|openai>` later.', zh: ' → 已跳过 AI 配置,稍后运行 `mindos config set ai.provider <anthropic|openai>` 补填。' },
|
|
104
|
-
anthropicKey: { en: 'Anthropic API Key (sk-ant-...)', zh: 'Anthropic API Key (sk-ant-...)' },
|
|
105
|
-
openaiKey: { en: 'OpenAI API Key (sk-...)', zh: 'OpenAI API Key (sk-...)' },
|
|
106
|
-
openaiBase: { en: 'OpenAI Base URL (leave empty for default)', zh: 'OpenAI Base URL(留空使用默认)' },
|
|
107
|
-
apiKeyWarn: { en: ' ⚠ No API key entered — AI features will not work until you add one to ~/.mindos/config.json', zh: ' ⚠ 未填写 API Key,AI 功能将无法使用,可后续在 ~/.mindos/config.json 中补填' },
|
|
108
|
-
|
|
109
|
-
// config
|
|
110
|
-
cfgExists: { en: (p) => `${p} already exists. Overwrite?`, zh: (p) => `${p} 已存在,是否覆盖?` },
|
|
111
|
-
|
|
112
|
-
// start mode
|
|
113
|
-
startModePrompt: { en: 'Start Mode', zh: '启动方式' },
|
|
114
|
-
startModeOpts: { en: ['Background service (recommended, auto-start on boot)', 'Foreground (manual start each time)'], zh: ['后台服务(推荐,开机自启)', '前台运行(每次手动启动)'] },
|
|
115
|
-
startModeVals: ['daemon', 'start'],
|
|
116
|
-
startModeSkip: { en: ' → Daemon not supported on this platform, using foreground mode', zh: ' → 当前平台不支持后台服务,使用前台模式' },
|
|
117
|
-
cfgKept: { en: '✔ Keeping existing config', zh: '✔ 保留现有配置' },
|
|
118
|
-
cfgKeptNote: { en: ' Settings from this session were not saved', zh: ' 本次填写的设置未保存' },
|
|
119
|
-
cfgSaved: { en: '✔ Config saved', zh: '✔ 配置已保存' },
|
|
120
|
-
cfgConfirm: { en: 'Save this configuration?', zh: '保存此配置?' },
|
|
121
|
-
cfgAborted: { en: '✘ Setup cancelled. Run `mindos onboard` to try again.', zh: '✘ 设置已取消。运行 `mindos onboard` 重新开始。' },
|
|
122
|
-
yesNo: { en: '[y/N]', zh: '[y/N]' },
|
|
123
|
-
yesNoDefault: { en: '[Y/n]', zh: '[Y/n]' },
|
|
124
|
-
startNow: { en: 'Start MindOS now?', zh: '现在启动 MindOS?' },
|
|
125
|
-
syncSetup: { en: 'Set up cross-device sync via Git?', zh: '是否配置 Git 跨设备同步?' },
|
|
126
|
-
syncLater: { en: ' → Run `mindos sync init` anytime to set up sync later.', zh: ' → 随时运行 `mindos sync init` 配置同步。' },
|
|
127
|
-
|
|
128
|
-
// mode selection step (Step 7)
|
|
129
|
-
modeSelectHint: { en: 'Connection mode (Space to toggle, Enter to confirm):', zh: '连接模式(空格切换,Enter 确认):' },
|
|
130
|
-
modeCli: { en: 'CLI Operate KB via command line', zh: 'CLI 通过命令行操作知识库' },
|
|
131
|
-
modeCliHint: { en: 'recommended, more token-efficient', zh: '推荐,更省 token' },
|
|
132
|
-
modeMcp: { en: 'MCP Connect via MCP protocol', zh: 'MCP 通过 MCP 协议连接' },
|
|
133
|
-
modeMcpHint: { en: 'optional, may consume more tokens', zh: '可选,可能消耗更多 token' },
|
|
134
|
-
modeNoneError: { en: 'Please select at least one mode.', zh: '请至少选择一种模式。' },
|
|
135
|
-
|
|
136
|
-
// agent config install (internal, Skill concept hidden from user)
|
|
137
|
-
skillInstallFail: { en: (name, msg) => ` ${c.red('✘')} Configuration failed: ${msg}`, zh: (name, msg) => ` ${c.red('✘')} 配置失败:${msg}` },
|
|
138
|
-
|
|
139
|
-
// restart prompts (re-onboard with config changes)
|
|
140
|
-
restartRequired: { en: 'Config changed. Service restart required.', zh: '配置已变更,需要重启服务。' },
|
|
141
|
-
restartNow: { en: 'Restart now?', zh: '立即重启?' },
|
|
142
|
-
changesOnNextStart: { en: 'Changes will take effect on next start.', zh: '变更将在下次启动时生效。' },
|
|
143
|
-
|
|
144
|
-
// next steps (onboard — keep it minimal, details shown on `mindos start`)
|
|
145
|
-
nextSteps: {
|
|
146
|
-
en: (cmd) => [
|
|
147
|
-
'─────────────────────────────────────────────',
|
|
148
|
-
'🚀 Setup complete! Start MindOS:\n',
|
|
149
|
-
` ${c.cyan(cmd)}`,
|
|
150
|
-
` ${c.dim('Connection details and auth token will be shown on startup.')}\n`,
|
|
151
|
-
'─────────────────────────────────────────────',
|
|
152
|
-
],
|
|
153
|
-
zh: (cmd) => [
|
|
154
|
-
'─────────────────────────────────────────────',
|
|
155
|
-
'🚀 初始化完成!启动 MindOS:\n',
|
|
156
|
-
` ${c.cyan(cmd)}`,
|
|
157
|
-
` ${c.dim('连接信息和 Auth token 将在启动后显示。')}\n`,
|
|
158
|
-
'─────────────────────────────────────────────',
|
|
159
|
-
],
|
|
160
|
-
},
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
// ── Terminal helpers ──────────────────────────────────────────────────────────
|
|
164
|
-
|
|
165
|
-
const ESC = '\x1b';
|
|
166
|
-
const CLEAR_LINE = '\r\x1b[K';
|
|
167
|
-
const CURSOR_UP = (n) => n > 0 ? `\x1b[${n}A` : '';
|
|
168
|
-
const HIDE_CURSOR = '\x1b[?25l';
|
|
169
|
-
const SHOW_CURSOR = '\x1b[?25h';
|
|
170
|
-
|
|
171
|
-
const c = process.stdout.isTTY ? {
|
|
172
|
-
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
173
|
-
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
174
|
-
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
175
|
-
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
176
|
-
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
177
|
-
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
178
|
-
} : { bold: s=>s, dim: s=>s, cyan: s=>s, green: s=>s, red: s=>s, yellow: s=>s };
|
|
179
|
-
|
|
180
|
-
function write(s) { process.stdout.write(s); }
|
|
181
|
-
|
|
182
|
-
// ── State ─────────────────────────────────────────────────────────────────────
|
|
183
|
-
|
|
184
|
-
function detectSystemLang() {
|
|
185
|
-
// Check env vars by precedence (LC_ALL > LC_MESSAGES > LANG > LANGUAGE)
|
|
186
|
-
// For LANGUAGE, only use the first entry before ':' (it's a priority list)
|
|
187
|
-
const candidates = [
|
|
188
|
-
process.env.LC_ALL,
|
|
189
|
-
process.env.LC_MESSAGES,
|
|
190
|
-
process.env.LANG,
|
|
191
|
-
...(process.env.LANGUAGE ? [process.env.LANGUAGE.split(':')[0]] : []),
|
|
192
|
-
];
|
|
193
|
-
const first = candidates.find(Boolean);
|
|
194
|
-
if (first) {
|
|
195
|
-
return first.toLowerCase().startsWith('zh') ? 'zh' : 'en';
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Fallback: Intl API (works on Windows where LANG is often unset)
|
|
199
|
-
try {
|
|
200
|
-
const locale = Intl.DateTimeFormat().resolvedOptions().locale;
|
|
201
|
-
if (locale.toLowerCase().startsWith('zh')) return 'zh';
|
|
202
|
-
} catch {}
|
|
203
|
-
|
|
204
|
-
return 'en';
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
let uiLang = detectSystemLang();
|
|
208
|
-
const t = (key) => T[key]?.[uiLang] ?? T[key]?.en ?? '';
|
|
209
|
-
const tf = (key, ...args) => {
|
|
210
|
-
const v = T[key]?.[uiLang] ?? T[key]?.en;
|
|
211
|
-
return typeof v === 'function' ? v(...args) : v ?? '';
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
// ── Step header ───────────────────────────────────────────────────────────────
|
|
215
|
-
|
|
216
|
-
const TOTAL_STEPS = 8;
|
|
217
|
-
function stepHeader(n) {
|
|
218
|
-
const title = T.stepTitles[uiLang][n - 1] ?? T.stepTitles.en[n - 1];
|
|
219
|
-
const stepLabel = tf('step', n, TOTAL_STEPS);
|
|
220
|
-
console.log(`\n${c.bold(title)} ${c.dim(stepLabel)}`);
|
|
221
|
-
console.log(c.dim('─'.repeat(44)));
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// ── Raw-mode key reader ───────────────────────────────────────────────────────
|
|
225
|
-
|
|
226
|
-
function readKey() {
|
|
227
|
-
return new Promise((resolve) => {
|
|
228
|
-
const { stdin } = process;
|
|
229
|
-
if (!stdin.isTTY) return resolve(null);
|
|
230
|
-
stdin.setRawMode(true);
|
|
231
|
-
stdin.resume();
|
|
232
|
-
stdin.setEncoding('utf8');
|
|
233
|
-
const onData = (chunk) => {
|
|
234
|
-
stdin.setRawMode(false);
|
|
235
|
-
stdin.pause();
|
|
236
|
-
stdin.removeListener('data', onData);
|
|
237
|
-
resolve(chunk);
|
|
238
|
-
};
|
|
239
|
-
stdin.on('data', onData);
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// ── Select prompt ─────────────────────────────────────────────────────────────
|
|
244
|
-
|
|
245
|
-
async function select(labelKey, optionsKey, valuesKey = null) {
|
|
246
|
-
let idx = 0;
|
|
247
|
-
let lastLineCount = 0;
|
|
248
|
-
|
|
249
|
-
const render = (first = false) => {
|
|
250
|
-
const opts = T[optionsKey][uiLang];
|
|
251
|
-
const lines = [
|
|
252
|
-
`${c.bold(t(labelKey) + ':')}`,
|
|
253
|
-
...opts.map((o, i) => {
|
|
254
|
-
const [val, ...rest] = o.split(' — ');
|
|
255
|
-
const desc = rest.join(' — ');
|
|
256
|
-
const item = desc ? `${c.cyan(val)} ${c.dim('—')} ${c.dim(desc)}` : c.cyan(val);
|
|
257
|
-
return i === idx
|
|
258
|
-
? ` ${c.cyan('❯')} ${item}`
|
|
259
|
-
: ` ${c.dim(val)}${desc ? c.dim(' — ' + desc) : ''}`;
|
|
260
|
-
}),
|
|
261
|
-
];
|
|
262
|
-
if (!first && lastLineCount > 0) {
|
|
263
|
-
write(`${CURSOR_UP(lastLineCount)}\r\x1b[J`);
|
|
264
|
-
}
|
|
265
|
-
lastLineCount = lines.length;
|
|
266
|
-
write(lines.join('\n') + '\n');
|
|
267
|
-
};
|
|
268
|
-
|
|
269
|
-
write(HIDE_CURSOR);
|
|
270
|
-
render(true);
|
|
271
|
-
|
|
272
|
-
while (true) {
|
|
273
|
-
const key = await readKey();
|
|
274
|
-
if (key === null) {
|
|
275
|
-
write(SHOW_CURSOR);
|
|
276
|
-
return (valuesKey ? T[valuesKey] : T[optionsKey][uiLang])[0];
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
const opts = T[optionsKey][uiLang];
|
|
280
|
-
|
|
281
|
-
if (key === '\r' || key === '\n') {
|
|
282
|
-
write(SHOW_CURSOR);
|
|
283
|
-
write(`${CURSOR_UP(lastLineCount)}\r\x1b[J`);
|
|
284
|
-
const displayLabel = opts[idx].split(' — ')[0];
|
|
285
|
-
write(`${c.bold(t(labelKey) + ':')} ${c.cyan(displayLabel)}\n`);
|
|
286
|
-
return valuesKey ? T[valuesKey][idx] : opts[idx];
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
if (key === `${ESC}[A`) { idx = (idx - 1 + opts.length) % opts.length; render(); }
|
|
290
|
-
else if (key === `${ESC}[B`) { idx = (idx + 1) % opts.length; render(); }
|
|
291
|
-
else if (key === `${ESC}[C` || key === `${ESC}[D`) {
|
|
292
|
-
uiLang = uiLang === 'en' ? 'zh' : 'en';
|
|
293
|
-
idx = Math.min(idx, T[optionsKey][uiLang].length - 1);
|
|
294
|
-
render();
|
|
295
|
-
}
|
|
296
|
-
else if (key === '\x03') { write(SHOW_CURSOR); process.exit(1); }
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// ── Text input prompt ─────────────────────────────────────────────────────────
|
|
301
|
-
|
|
302
|
-
async function askText(labelKey, defaultValue = '', hintKey = '', ...hintArgs) {
|
|
303
|
-
const buildPrompt = () => {
|
|
304
|
-
const label = c.bold(t(labelKey));
|
|
305
|
-
const def = defaultValue ? ` ${c.dim('[' + defaultValue + ']')}` : '';
|
|
306
|
-
const hintStr = hintKey ? tf(hintKey, ...hintArgs) : '';
|
|
307
|
-
const h = hintStr ? ` ${c.dim('← ' + hintStr)}` : '';
|
|
308
|
-
return `${label}${def}${h}: `;
|
|
309
|
-
};
|
|
310
|
-
|
|
311
|
-
if (!process.stdin.isTTY) {
|
|
312
|
-
return new Promise((resolve) => {
|
|
313
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
314
|
-
rl.question(buildPrompt(), (ans) => { rl.close(); resolve(ans.trim() || defaultValue); });
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
const render = (buf, first = false) => {
|
|
319
|
-
if (!first) write(`\r\x1b[K`);
|
|
320
|
-
write(`${buildPrompt()}${c.cyan(buf)}`);
|
|
321
|
-
};
|
|
322
|
-
|
|
323
|
-
write(HIDE_CURSOR);
|
|
324
|
-
let buf = '';
|
|
325
|
-
render(buf, true);
|
|
326
|
-
write(SHOW_CURSOR);
|
|
327
|
-
|
|
328
|
-
process.stdin.setRawMode(true);
|
|
329
|
-
process.stdin.resume();
|
|
330
|
-
process.stdin.setEncoding('utf8');
|
|
331
|
-
|
|
332
|
-
return new Promise((resolve) => {
|
|
333
|
-
const onData = (chunk) => {
|
|
334
|
-
if (chunk === '\x03') { write('\n'); process.exit(1); }
|
|
335
|
-
|
|
336
|
-
if (chunk === '\r' || chunk === '\n') {
|
|
337
|
-
process.stdin.setRawMode(false);
|
|
338
|
-
process.stdin.pause();
|
|
339
|
-
process.stdin.removeListener('data', onData);
|
|
340
|
-
write('\n');
|
|
341
|
-
resolve(buf || defaultValue);
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
if (chunk === `${ESC}[C` || chunk === `${ESC}[D`) {
|
|
346
|
-
uiLang = uiLang === 'en' ? 'zh' : 'en';
|
|
347
|
-
render(buf);
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
if (chunk === '\x7f' || chunk === '\b') {
|
|
352
|
-
if (buf.length > 0) { buf = buf.slice(0, -1); render(buf); }
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
if (chunk.startsWith(ESC)) return;
|
|
357
|
-
buf += chunk;
|
|
358
|
-
render(buf);
|
|
359
|
-
};
|
|
360
|
-
process.stdin.on('data', onData);
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// ── Yes/No prompt ─────────────────────────────────────────────────────────────
|
|
365
|
-
|
|
366
|
-
async function askYesNo(labelKey, arg = '', defaultYes = false) {
|
|
367
|
-
const label = typeof T[labelKey][uiLang] === 'function' ? T[labelKey][uiLang](arg) : t(labelKey);
|
|
368
|
-
const hintKey = defaultYes ? 'yesNoDefault' : 'yesNo';
|
|
369
|
-
if (!process.stdin.isTTY) {
|
|
370
|
-
return new Promise((resolve) => {
|
|
371
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
372
|
-
rl.question(`${c.bold(label)} ${c.dim(t(hintKey))}: `, (ans) => {
|
|
373
|
-
const v = ans.trim().toLowerCase();
|
|
374
|
-
rl.close(); resolve(defaultYes ? v !== 'n' : v === 'y');
|
|
375
|
-
});
|
|
376
|
-
});
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
write(HIDE_CURSOR);
|
|
380
|
-
const render = (buf = '', first = false) => {
|
|
381
|
-
if (!first) write(`\r\x1b[K`);
|
|
382
|
-
write(`${c.bold(label)} ${c.dim(t(hintKey))}: ${c.cyan(buf)}`);
|
|
383
|
-
};
|
|
384
|
-
render('', true);
|
|
385
|
-
write(SHOW_CURSOR);
|
|
386
|
-
|
|
387
|
-
process.stdin.setRawMode(true);
|
|
388
|
-
process.stdin.resume();
|
|
389
|
-
process.stdin.setEncoding('utf8');
|
|
390
|
-
|
|
391
|
-
return new Promise((resolve) => {
|
|
392
|
-
let buf = '';
|
|
393
|
-
const onData = (chunk) => {
|
|
394
|
-
if (chunk === '\x03') { write('\n'); process.exit(1); }
|
|
395
|
-
|
|
396
|
-
if (chunk === '\r' || chunk === '\n') {
|
|
397
|
-
process.stdin.setRawMode(false);
|
|
398
|
-
process.stdin.pause();
|
|
399
|
-
process.stdin.removeListener('data', onData);
|
|
400
|
-
write('\n');
|
|
401
|
-
const v = buf.toLowerCase();
|
|
402
|
-
resolve(defaultYes ? v !== 'n' : v === 'y');
|
|
403
|
-
return;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
if (chunk === `${ESC}[C` || chunk === `${ESC}[D`) {
|
|
407
|
-
uiLang = uiLang === 'en' ? 'zh' : 'en';
|
|
408
|
-
render(buf);
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
if (chunk === '\x7f' || chunk === '\b') {
|
|
413
|
-
if (buf.length > 0) { buf = buf.slice(0, -1); render(buf); }
|
|
414
|
-
return;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
if (chunk.startsWith(ESC)) return;
|
|
418
|
-
buf += chunk;
|
|
419
|
-
render(buf);
|
|
420
|
-
};
|
|
421
|
-
process.stdin.on('data', onData);
|
|
422
|
-
});
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
const askYesNoDefault = (labelKey, arg = '') => askYesNo(labelKey, arg, true);
|
|
426
|
-
|
|
427
|
-
// ── Port helpers ──────────────────────────────────────────────────────────────
|
|
428
|
-
|
|
429
|
-
function isPortInUse(port) {
|
|
430
|
-
return new Promise((resolve) => {
|
|
431
|
-
const sock = createConnection({ port, host: '127.0.0.1' });
|
|
432
|
-
const cleanup = (result) => { sock.destroy(); resolve(result); };
|
|
433
|
-
// On localhost, timeout means no response — treat as free (same as bin/lib/port.js)
|
|
434
|
-
sock.setTimeout(500, () => cleanup(false));
|
|
435
|
-
sock.once('connect', () => cleanup(true));
|
|
436
|
-
sock.once('error', (err) => {
|
|
437
|
-
// ECONNREFUSED = nothing listening → free; other errors = treat as in-use
|
|
438
|
-
cleanup(err.code !== 'ECONNREFUSED');
|
|
439
|
-
});
|
|
440
|
-
});
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
async function isSelfPort(port) {
|
|
444
|
-
try {
|
|
445
|
-
return await new Promise((resolve) => {
|
|
446
|
-
const req = http.get(`http://127.0.0.1:${port}/api/health`, { timeout: 800 }, (res) => {
|
|
447
|
-
let body = '';
|
|
448
|
-
res.on('data', chunk => { body += chunk; });
|
|
449
|
-
res.on('end', () => {
|
|
450
|
-
try {
|
|
451
|
-
const data = JSON.parse(body);
|
|
452
|
-
// 200 with service=mindos → definitely us.
|
|
453
|
-
// 401 Unauthorized → also us (webPassword is set).
|
|
454
|
-
resolve(data.service === 'mindos' || res.statusCode === 401);
|
|
455
|
-
} catch {
|
|
456
|
-
// Non-JSON but got a response on /api/health → likely us
|
|
457
|
-
resolve(res.statusCode === 401 || res.statusCode === 200);
|
|
458
|
-
}
|
|
459
|
-
});
|
|
460
|
-
});
|
|
461
|
-
req.on('error', () => resolve(false));
|
|
462
|
-
req.on('timeout', () => { req.destroy(); resolve(false); });
|
|
463
|
-
});
|
|
464
|
-
} catch { return false; }
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
async function findFreePort(from) {
|
|
468
|
-
for (let p = from; p <= 65535; p++) {
|
|
469
|
-
if (!await isPortInUse(p)) return p;
|
|
470
|
-
}
|
|
471
|
-
return from; // fallback (extremely unlikely)
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
async function askPort(labelKey, defaultPort) {
|
|
475
|
-
let port = defaultPort;
|
|
476
|
-
// If the default port is in use, check if it's our own service (self = ok to keep)
|
|
477
|
-
if (await isPortInUse(port)) {
|
|
478
|
-
if (await isSelfPort(port)) {
|
|
479
|
-
// Already running on this port — keep it as default
|
|
480
|
-
} else {
|
|
481
|
-
port = await findFreePort(port + 1);
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
while (true) {
|
|
485
|
-
const input = (await askText(labelKey, String(port))).trim();
|
|
486
|
-
const parsed = parseInt(input, 10);
|
|
487
|
-
if (!parsed || parsed < 1024 || parsed > 65535) {
|
|
488
|
-
write(c.yellow(tf('portInvalid', input) + '\n'));
|
|
489
|
-
continue;
|
|
490
|
-
}
|
|
491
|
-
if (await isPortInUse(parsed)) {
|
|
492
|
-
// Check if it's our own service — acceptable to keep
|
|
493
|
-
if (await isSelfPort(parsed)) {
|
|
494
|
-
return parsed;
|
|
495
|
-
}
|
|
496
|
-
const next = await findFreePort(parsed + 1);
|
|
497
|
-
write(c.yellow(tf('portInUse', parsed) + '\n'));
|
|
498
|
-
port = next;
|
|
499
|
-
continue;
|
|
500
|
-
}
|
|
501
|
-
return parsed;
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
// ── Token generation ──────────────────────────────────────────────────────────
|
|
506
|
-
|
|
507
|
-
function generateToken(passphrase = '') {
|
|
508
|
-
let bytes;
|
|
509
|
-
if (passphrase) {
|
|
510
|
-
const salt = randomBytes(16).toString('hex');
|
|
511
|
-
bytes = createHash('sha256').update(passphrase + salt).digest();
|
|
512
|
-
} else {
|
|
513
|
-
bytes = randomBytes(24);
|
|
514
|
-
}
|
|
515
|
-
const hex = bytes.toString('hex').slice(0, 24);
|
|
516
|
-
return hex.match(/.{4}/g).join('-');
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
// ── Template handler ──────────────────────────────────────────────────────────
|
|
520
|
-
|
|
521
|
-
function parseGithubDir(url) {
|
|
522
|
-
const m = url.match(/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)\/?(.*)$/);
|
|
523
|
-
if (!m) return null;
|
|
524
|
-
const [, owner, repo, ref, subdir] = m;
|
|
525
|
-
return { tarball: `https://api.github.com/repos/${owner}/${repo}/tarball/${ref}`, subdir: subdir || '' };
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
async function downloadAndExtract(url, destDir) {
|
|
529
|
-
const tmp = join(tmpdir(), `mindos-tpl-${Date.now()}`);
|
|
530
|
-
mkdirSync(tmp, { recursive: true });
|
|
531
|
-
const tarPath = join(tmp, 'tpl.tar.gz');
|
|
532
|
-
|
|
533
|
-
let fetchUrl = url;
|
|
534
|
-
let subdir = '';
|
|
535
|
-
const gh = parseGithubDir(url);
|
|
536
|
-
if (gh) { fetchUrl = gh.tarball; subdir = gh.subdir; }
|
|
537
|
-
|
|
538
|
-
const res = await fetch(fetchUrl, { headers: { 'User-Agent': 'mindos-init' }, redirect: 'follow' });
|
|
539
|
-
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
|
|
540
|
-
await pipeline(res.body, createWriteStream(tarPath));
|
|
541
|
-
|
|
542
|
-
const extractDir = join(tmp, 'extracted');
|
|
543
|
-
mkdirSync(extractDir, { recursive: true });
|
|
544
|
-
execSync(`tar -xzf "${tarPath}" -C "${extractDir}"`);
|
|
545
|
-
|
|
546
|
-
const { readdirSync, statSync } = await import('node:fs');
|
|
547
|
-
let contentRoot = extractDir;
|
|
548
|
-
const entries = readdirSync(extractDir);
|
|
549
|
-
if (entries.length === 1 && statSync(join(extractDir, entries[0])).isDirectory()) {
|
|
550
|
-
contentRoot = join(extractDir, entries[0]);
|
|
551
|
-
}
|
|
552
|
-
if (subdir) contentRoot = join(contentRoot, subdir);
|
|
553
|
-
|
|
554
|
-
cpSync(contentRoot, destDir, { recursive: true, filter: (src) => !src.endsWith('.gitkeep') });
|
|
555
|
-
rmSync(tmp, { recursive: true, force: true });
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
async function applyTemplate(tpl, mindDir) {
|
|
559
|
-
if (tpl === 'custom') {
|
|
560
|
-
let source = '';
|
|
561
|
-
while (!source) {
|
|
562
|
-
source = (await askText('customPrompt')).trim();
|
|
563
|
-
if (!source) write(c.yellow(t('customEmpty') + '\n'));
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
const isUrl = source.startsWith('http://') || source.startsWith('https://');
|
|
567
|
-
if (isUrl) {
|
|
568
|
-
write(c.yellow(t('downloading') + '\n'));
|
|
569
|
-
await downloadAndExtract(source, mindDir);
|
|
570
|
-
console.log(`${c.green(t('dlDone'))}: ${c.dim(mindDir)}`);
|
|
571
|
-
} else {
|
|
572
|
-
const localPath = resolve(source);
|
|
573
|
-
if (!existsSync(localPath)) {
|
|
574
|
-
console.error(c.red(`${t('tplNotFound')}: ${localPath}`));
|
|
575
|
-
process.exit(1);
|
|
576
|
-
}
|
|
577
|
-
cpSync(localPath, mindDir, { recursive: true, filter: (src) => !src.endsWith('.gitkeep') });
|
|
578
|
-
console.log(`${c.green(t('kbCreated'))}: ${c.dim(mindDir)}`);
|
|
579
|
-
}
|
|
580
|
-
} else {
|
|
581
|
-
const tplDir = resolve(ROOT, 'templates', tpl);
|
|
582
|
-
if (!existsSync(tplDir)) {
|
|
583
|
-
console.error(c.red(`${t('tplNotFound')}: ${tplDir}`));
|
|
584
|
-
process.exit(1);
|
|
585
|
-
}
|
|
586
|
-
cpSync(tplDir, mindDir, { recursive: true, filter: (src) => !src.endsWith('.gitkeep') });
|
|
587
|
-
console.log(`${c.green(t('kbCreated'))}: ${c.dim(mindDir)}`);
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
// MCP_AGENTS imported from bin/lib/mcp-agents.js
|
|
592
|
-
|
|
593
|
-
function expandHomePath(p) {
|
|
594
|
-
return p.startsWith('~/') ? resolve(homedir(), p.slice(2)) : p;
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
/** Parse JSONC: JSON plus comments (e.g. VS Code configs). Returns {} for empty input. */
|
|
598
|
-
function parseJsonc(text) {
|
|
599
|
-
let stripped = text.replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*$)/gm, (m, g) => g ? '' : m);
|
|
600
|
-
stripped = stripped.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
601
|
-
if (!stripped.trim()) return {};
|
|
602
|
-
return JSON.parse(stripped);
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
/** Detect if an agent already has mindos configured (for pre-selection). */
|
|
606
|
-
function isAgentInstalled(agentKey) {
|
|
607
|
-
const agent = MCP_AGENTS[agentKey];
|
|
608
|
-
if (!agent) return false;
|
|
609
|
-
for (const cfgPath of [agent.global, agent.project]) {
|
|
610
|
-
if (!cfgPath) continue;
|
|
611
|
-
const abs = expandHomePath(cfgPath);
|
|
612
|
-
if (!existsSync(abs)) continue;
|
|
613
|
-
try {
|
|
614
|
-
const config = parseJsonc(readFileSync(abs, 'utf-8'));
|
|
615
|
-
if (config[agent.key]?.mindos) return true;
|
|
616
|
-
} catch { /* ignore */ }
|
|
617
|
-
}
|
|
618
|
-
return false;
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
/**
|
|
622
|
-
* Step 7: mode selection — CLI (default-on) vs MCP (default-off).
|
|
623
|
-
* Returns { cli: boolean, mcp: boolean }.
|
|
624
|
-
*/
|
|
625
|
-
async function runModeSelect() {
|
|
626
|
-
const modes = [
|
|
627
|
-
{ key: 'cli', label: t('modeCli'), hint: t('modeCliHint'), preselect: true },
|
|
628
|
-
{ key: 'mcp', label: t('modeMcp'), hint: t('modeMcpHint'), preselect: false },
|
|
629
|
-
];
|
|
630
|
-
|
|
631
|
-
return new Promise((resolve) => {
|
|
632
|
-
let cursor = 0;
|
|
633
|
-
const chosen = new Set(modes.map((m, i) => m.preselect ? i : -1).filter(i => i >= 0));
|
|
634
|
-
|
|
635
|
-
const render = (first = false, errMsg = '') => {
|
|
636
|
-
if (!first) write(`\x1b[${modes.length + (errMsg ? 3 : 2)}A\x1b[J`);
|
|
637
|
-
write(`${c.bold(t('modeSelectHint'))}\n`);
|
|
638
|
-
for (let i = 0; i < modes.length; i++) {
|
|
639
|
-
const m = modes[i];
|
|
640
|
-
const check = chosen.has(i) ? c.green('✔') : c.dim('○');
|
|
641
|
-
const pointer = i === cursor ? c.cyan('❯') : ' ';
|
|
642
|
-
const label = i === cursor ? (chosen.has(i) ? c.green(m.label) : c.cyan(m.label)) : (chosen.has(i) ? c.green(m.label) : m.label);
|
|
643
|
-
write(` ${pointer} ${check} ${label} ${c.dim('(' + m.hint + ')')}\n`);
|
|
644
|
-
}
|
|
645
|
-
if (errMsg) {
|
|
646
|
-
write(` ${c.yellow('⚠ ' + errMsg)}\n`);
|
|
647
|
-
}
|
|
648
|
-
write(c.dim(` ${chosen.size} ${uiLang === 'zh' ? '已选' : 'selected'}\n`));
|
|
649
|
-
};
|
|
650
|
-
|
|
651
|
-
write('\n');
|
|
652
|
-
render(true);
|
|
653
|
-
|
|
654
|
-
process.stdin.setRawMode(true);
|
|
655
|
-
process.stdin.resume();
|
|
656
|
-
process.stdin.setEncoding('utf8');
|
|
657
|
-
|
|
658
|
-
const onKey = (key) => {
|
|
659
|
-
if (key === '\x03') { cleanup(); process.exit(1); }
|
|
660
|
-
if (key === `${ESC}[A`) { cursor = (cursor - 1 + modes.length) % modes.length; render(); }
|
|
661
|
-
else if (key === `${ESC}[B`) { cursor = (cursor + 1) % modes.length; render(); }
|
|
662
|
-
else if (key === ' ') {
|
|
663
|
-
if (chosen.has(cursor)) chosen.delete(cursor); else chosen.add(cursor);
|
|
664
|
-
render();
|
|
665
|
-
} else if (key === '\r' || key === '\n') {
|
|
666
|
-
if (chosen.size === 0) {
|
|
667
|
-
render(false, t('modeNoneError'));
|
|
668
|
-
return;
|
|
669
|
-
}
|
|
670
|
-
cleanup();
|
|
671
|
-
resolve({
|
|
672
|
-
cli: chosen.has(0),
|
|
673
|
-
mcp: chosen.has(1),
|
|
674
|
-
});
|
|
675
|
-
}
|
|
676
|
-
};
|
|
677
|
-
|
|
678
|
-
const cleanup = () => {
|
|
679
|
-
process.stdin.removeListener('data', onKey);
|
|
680
|
-
process.stdin.setRawMode(false);
|
|
681
|
-
process.stdin.pause();
|
|
682
|
-
};
|
|
683
|
-
|
|
684
|
-
process.stdin.on('data', onKey);
|
|
685
|
-
});
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
/**
|
|
689
|
-
* Agent multi-select UI — shared by both CLI-only and MCP modes.
|
|
690
|
-
* Returns array of selected agent keys.
|
|
691
|
-
*/
|
|
692
|
-
async function runAgentSelect() {
|
|
693
|
-
const keys = Object.keys(MCP_AGENTS);
|
|
694
|
-
|
|
695
|
-
const options = keys.map(k => {
|
|
696
|
-
const installed = isAgentInstalled(k);
|
|
697
|
-
const present = detectAgentPresence(k);
|
|
698
|
-
return {
|
|
699
|
-
label: MCP_AGENTS[k].name,
|
|
700
|
-
hint: installed ? (uiLang === 'zh' ? '已配置' : 'configured') : present ? (uiLang === 'zh' ? '已检测' : 'detected') : (uiLang === 'zh' ? '未找到' : 'not found'),
|
|
701
|
-
value: k,
|
|
702
|
-
preselect: installed || present,
|
|
703
|
-
};
|
|
704
|
-
});
|
|
705
|
-
|
|
706
|
-
options.sort((a, b) => {
|
|
707
|
-
const rank = (o) => o.hint.includes('configured') || o.hint.includes('已配置') ? 0 : o.preselect ? 1 : 2;
|
|
708
|
-
return rank(a) - rank(b);
|
|
709
|
-
});
|
|
710
|
-
|
|
711
|
-
const selected = await new Promise((resolveSelected) => {
|
|
712
|
-
let cursor = 0;
|
|
713
|
-
const chosen = new Set(options.map((o, i) => o.preselect ? i : -1).filter(i => i >= 0));
|
|
714
|
-
|
|
715
|
-
const render = (first = false) => {
|
|
716
|
-
if (!first) write(`\x1b[${options.length + 2}A\x1b[J`);
|
|
717
|
-
write(`${c.bold(uiLang === 'zh' ? '选择 Agent:' : 'Select agents:')} ${c.dim(uiLang === 'zh' ? '(↑↓ 移动 空格 切换 D 已检测 A 全选 Enter 确认)' : '(↑↓ move Space toggle D detected A all Enter confirm)')}\n`);
|
|
718
|
-
for (let i = 0; i < options.length; i++) {
|
|
719
|
-
const o = options[i];
|
|
720
|
-
const check = chosen.has(i) ? c.green('✔') : c.dim('○');
|
|
721
|
-
const pointer = i === cursor ? c.cyan('❯') : ' ';
|
|
722
|
-
const label = i === cursor ? (chosen.has(i) ? c.green(o.label) : c.cyan(o.label)) : (chosen.has(i) ? c.green(o.label) : o.label);
|
|
723
|
-
write(` ${pointer} ${check} ${label} ${c.dim('(' + o.hint + ')')}\n`);
|
|
724
|
-
}
|
|
725
|
-
write(c.dim(` ${chosen.size} ${uiLang === 'zh' ? '已选' : 'selected'}\n`));
|
|
726
|
-
};
|
|
727
|
-
|
|
728
|
-
write('\n');
|
|
729
|
-
render(true);
|
|
730
|
-
|
|
731
|
-
process.stdin.setRawMode(true);
|
|
732
|
-
process.stdin.resume();
|
|
733
|
-
process.stdin.setEncoding('utf8');
|
|
734
|
-
|
|
735
|
-
const onKey = (key) => {
|
|
736
|
-
if (key === '\x03') { cleanup(); process.exit(1); }
|
|
737
|
-
if (key === `${ESC}[A`) { cursor = (cursor - 1 + options.length) % options.length; render(); }
|
|
738
|
-
else if (key === `${ESC}[B`) { cursor = (cursor + 1) % options.length; render(); }
|
|
739
|
-
else if (key === ' ') {
|
|
740
|
-
if (chosen.has(cursor)) chosen.delete(cursor); else chosen.add(cursor);
|
|
741
|
-
render();
|
|
742
|
-
} else if (key === 'a' || key === 'A') {
|
|
743
|
-
if (chosen.size === options.length) chosen.clear();
|
|
744
|
-
else options.forEach((_, i) => chosen.add(i));
|
|
745
|
-
render();
|
|
746
|
-
} else if (key === 'd' || key === 'D') {
|
|
747
|
-
chosen.clear();
|
|
748
|
-
options.forEach((o, i) => { if (o.preselect) chosen.add(i); });
|
|
749
|
-
render();
|
|
750
|
-
} else if (key === '\r' || key === '\n') {
|
|
751
|
-
cleanup();
|
|
752
|
-
resolveSelected([...chosen].sort().map(i => options[i].value));
|
|
753
|
-
}
|
|
754
|
-
};
|
|
755
|
-
|
|
756
|
-
const cleanup = () => {
|
|
757
|
-
process.stdin.removeListener('data', onKey);
|
|
758
|
-
process.stdin.setRawMode(false);
|
|
759
|
-
process.stdin.pause();
|
|
760
|
-
};
|
|
761
|
-
|
|
762
|
-
process.stdin.on('data', onKey);
|
|
763
|
-
});
|
|
764
|
-
|
|
765
|
-
if (selected.length === 0) {
|
|
766
|
-
write(c.dim(uiLang === 'zh' ? ' → 未选择 Agent。\n' : ' → No agents selected.\n'));
|
|
767
|
-
}
|
|
768
|
-
return selected;
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
/**
|
|
772
|
-
* Write MCP config into selected agents' config files.
|
|
773
|
-
* Agent selection is handled separately by runAgentSelect().
|
|
774
|
-
*/
|
|
775
|
-
function installMcpConfig(selected, mcpPort, authToken) {
|
|
776
|
-
const entry = { type: 'stdio', command: 'mindos', args: ['mcp'], env: { MCP_TRANSPORT: 'stdio' } };
|
|
777
|
-
|
|
778
|
-
for (const agentKey of selected) {
|
|
779
|
-
const agent = MCP_AGENTS[agentKey];
|
|
780
|
-
const cfgPath = agent.global || agent.project;
|
|
781
|
-
if (!cfgPath) continue;
|
|
782
|
-
const abs = expandHomePath(cfgPath);
|
|
783
|
-
try {
|
|
784
|
-
let config = {};
|
|
785
|
-
if (existsSync(abs)) config = parseJsonc(readFileSync(abs, 'utf-8'));
|
|
786
|
-
if (!config[agent.key]) config[agent.key] = {};
|
|
787
|
-
config[agent.key].mindos = entry;
|
|
788
|
-
const dir = resolve(abs, '..');
|
|
789
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
790
|
-
writeFileSync(abs, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
791
|
-
} catch { /* logged by caller via agent count */ }
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
/* ── Skill auto-install ────────────────────────────────────────────────────── */
|
|
796
|
-
|
|
797
|
-
const UNIVERSAL_AGENTS = new Set([
|
|
798
|
-
'cline', 'codex', 'cursor', 'gemini-cli',
|
|
799
|
-
'github-copilot', 'kimi-cli', 'opencode', 'warp',
|
|
800
|
-
]);
|
|
801
|
-
const SKILL_UNSUPPORTED = new Set([]);
|
|
802
|
-
|
|
803
|
-
/**
|
|
804
|
-
* Install the appropriate MindOS Skill to selected agents via `npx skills add`.
|
|
805
|
-
* @param {string} template - 'en' | 'zh' | 'empty' | 'custom'
|
|
806
|
-
* @param {string[]} selectedAgents - MCP agent keys from the multi-select step
|
|
807
|
-
*/
|
|
808
|
-
function runSkillInstallStep(template, selectedAgents) {
|
|
809
|
-
if (!selectedAgents || selectedAgents.length === 0) return true;
|
|
810
|
-
|
|
811
|
-
const skillName = template === 'zh' ? 'mindos-zh' : 'mindos';
|
|
812
|
-
const localSource = resolve(ROOT, 'skills');
|
|
813
|
-
const githubSource = 'GeminiLight/MindOS';
|
|
814
|
-
|
|
815
|
-
const additionalAgents = selectedAgents
|
|
816
|
-
.flatMap((key) => {
|
|
817
|
-
if (SKILL_UNSUPPORTED.has(key)) return [];
|
|
818
|
-
if (UNIVERSAL_AGENTS.has(key)) return [];
|
|
819
|
-
const reg = SKILL_AGENT_REGISTRY[key];
|
|
820
|
-
if (!reg) return [key];
|
|
821
|
-
if (reg.mode === 'unsupported') return [];
|
|
822
|
-
if (reg.mode === 'universal') return [];
|
|
823
|
-
return [reg.skillAgentName || key];
|
|
824
|
-
});
|
|
825
|
-
|
|
826
|
-
// Direct-copy skill for 'unsupported' agents (e.g., QClaw, WorkBuddy, Lingma)
|
|
827
|
-
const unsupportedAgents = selectedAgents.filter((key) => {
|
|
828
|
-
const reg = SKILL_AGENT_REGISTRY[key];
|
|
829
|
-
return reg?.mode === 'unsupported';
|
|
830
|
-
});
|
|
831
|
-
for (const key of unsupportedAgents) {
|
|
832
|
-
const agent = MCP_AGENTS[key];
|
|
833
|
-
if (!agent) continue;
|
|
834
|
-
const skillSourceDir = resolve(ROOT, 'skills', skillName);
|
|
835
|
-
if (!existsSync(skillSourceDir)) continue;
|
|
836
|
-
const targetDir = expandHomePath(agent.presenceDirs?.[0] ?? agent.global.replace(/\/[^/]+$/, '/'));
|
|
837
|
-
const targetSkillDir = resolve(targetDir, 'skills', skillName);
|
|
838
|
-
try {
|
|
839
|
-
if (!existsSync(targetSkillDir)) {
|
|
840
|
-
cpSync(skillSourceDir, targetSkillDir, { recursive: true });
|
|
841
|
-
}
|
|
842
|
-
} catch { /* best-effort copy for unsupported agents */ }
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
const agentFlags = additionalAgents.length > 0
|
|
846
|
-
? additionalAgents.map(a => `-a ${a}`).join(' ')
|
|
847
|
-
: '-a universal';
|
|
848
|
-
|
|
849
|
-
const sources = [githubSource, localSource];
|
|
850
|
-
|
|
851
|
-
for (const source of sources) {
|
|
852
|
-
const quotedSource = /[/\\]/.test(source) ? `"${source}"` : source;
|
|
853
|
-
const cmd = `npx skills add ${quotedSource} --skill ${skillName} ${agentFlags} -g -y`;
|
|
854
|
-
try {
|
|
855
|
-
execSync(cmd, {
|
|
856
|
-
encoding: 'utf-8',
|
|
857
|
-
timeout: 30_000,
|
|
858
|
-
env: { ...process.env, NODE_ENV: 'production' },
|
|
859
|
-
stdio: 'pipe',
|
|
860
|
-
});
|
|
861
|
-
return true;
|
|
862
|
-
} catch { /* try next source */ }
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
return false;
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
// ── GUI Setup ─────────────────────────────────────────────────────────────────
|
|
869
|
-
|
|
870
|
-
function openBrowser(url) {
|
|
871
|
-
try {
|
|
872
|
-
const platform = process.platform;
|
|
873
|
-
if (platform === 'darwin') {
|
|
874
|
-
execSync(`open "${url}"`, { stdio: 'ignore' });
|
|
875
|
-
} else if (platform === 'linux') {
|
|
876
|
-
// Check for WSL
|
|
877
|
-
const isWSL = existsSync('/proc/version') &&
|
|
878
|
-
readFileSync('/proc/version', 'utf-8').toLowerCase().includes('microsoft');
|
|
879
|
-
if (isWSL) {
|
|
880
|
-
execSync(`cmd.exe /c start "${url}"`, { stdio: 'ignore' });
|
|
881
|
-
} else {
|
|
882
|
-
execSync(`xdg-open "${url}"`, { stdio: 'ignore' });
|
|
883
|
-
}
|
|
884
|
-
} else {
|
|
885
|
-
execSync(`cmd.exe /c start "${url}"`, { stdio: 'ignore' });
|
|
886
|
-
}
|
|
887
|
-
return true;
|
|
888
|
-
} catch {
|
|
889
|
-
return false;
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
async function startGuiSetup() {
|
|
894
|
-
// Ensure ~/.mindos directory exists
|
|
895
|
-
mkdirSync(MINDOS_DIR, { recursive: true });
|
|
896
|
-
|
|
897
|
-
// Read or create config, set setupPending
|
|
898
|
-
let config = {};
|
|
899
|
-
try { config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')); } catch { /* ignore */ }
|
|
900
|
-
|
|
901
|
-
const isFirstTime = !config.mindRoot;
|
|
902
|
-
|
|
903
|
-
// Clean up zombie process from a previous abandoned setup session.
|
|
904
|
-
if (config.setupPort) {
|
|
905
|
-
try {
|
|
906
|
-
const { killByPort } = await import('../bin/lib/stop.js');
|
|
907
|
-
killByPort(Number(config.setupPort));
|
|
908
|
-
// Brief wait for port to free
|
|
909
|
-
await new Promise(r => setTimeout(r, 500));
|
|
910
|
-
} catch { /* best effort */ }
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
config.setupPending = true;
|
|
914
|
-
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');
|
|
915
|
-
|
|
916
|
-
// Determine which port to use for the setup wizard
|
|
917
|
-
let usePort;
|
|
918
|
-
if (isFirstTime) {
|
|
919
|
-
// First-time onboard: use a temporary port (scan from 9100) so the user's
|
|
920
|
-
// chosen port in Step 3 can differ without a mid-setup restart.
|
|
921
|
-
// 9100 is chosen to avoid conflicts with common services (5000=AirPlay, 3456/8080=dev).
|
|
922
|
-
usePort = await findFreePort(9100);
|
|
923
|
-
} else {
|
|
924
|
-
// Re-onboard: service is already running on config.port — reuse it.
|
|
925
|
-
const existingPort = config.port || 3456;
|
|
926
|
-
if (await isSelfPort(existingPort)) {
|
|
927
|
-
// Service already running — just open the setup page, no need to spawn.
|
|
928
|
-
const url = `http://localhost:${existingPort}/setup`;
|
|
929
|
-
console.log(`\n${c.green(tf('guiReady', url))}\n`);
|
|
930
|
-
const opened = openBrowser(url);
|
|
931
|
-
if (!opened) console.log(c.dim(tf('guiOpenFailed', url)));
|
|
932
|
-
process.exit(0);
|
|
933
|
-
}
|
|
934
|
-
// Service not running — start on existing port
|
|
935
|
-
if (await isPortInUse(existingPort)) {
|
|
936
|
-
// Port occupied — try stopping leftover MindOS processes first
|
|
937
|
-
try {
|
|
938
|
-
const { stopMindos } = await import('../bin/lib/stop.js');
|
|
939
|
-
stopMindos();
|
|
940
|
-
// stopMindos() sends SIGTERM synchronously — wait for both web and mcp
|
|
941
|
-
// ports to free, since `start` will assertPortFree on both.
|
|
942
|
-
const { waitForPortFree } = await import('../bin/lib/gateway.js');
|
|
943
|
-
const mcpPort = config.mcpPort || 8781;
|
|
944
|
-
const [webFreed, mcpFreed] = await Promise.all([
|
|
945
|
-
waitForPortFree(existingPort),
|
|
946
|
-
waitForPortFree(mcpPort),
|
|
947
|
-
]);
|
|
948
|
-
usePort = webFreed ? existingPort : await findFreePort(9100);
|
|
949
|
-
} catch {
|
|
950
|
-
usePort = await findFreePort(9100);
|
|
951
|
-
}
|
|
952
|
-
} else {
|
|
953
|
-
usePort = existingPort;
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
write(c.yellow(t('guiStarting') + '\n'));
|
|
958
|
-
|
|
959
|
-
// Start the server in the background
|
|
960
|
-
// Record the temporary setup port in config so stopMindos() can clean it up
|
|
961
|
-
// if the user abandons setup mid-way or closes the browser without completing.
|
|
962
|
-
config.setupPort = usePort;
|
|
963
|
-
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');
|
|
964
|
-
|
|
965
|
-
// Pass MINDOS_WEB_PORT (not PORT) so loadConfig() won't override with the
|
|
966
|
-
// config file port — this is critical when we need a temporary port.
|
|
967
|
-
const cliPath = resolve(__dirname, '../bin/cli.js');
|
|
968
|
-
const logFd = openSync(LOG_PATH, 'a');
|
|
969
|
-
const child = spawn(process.execPath, [cliPath, 'start'], {
|
|
970
|
-
detached: true,
|
|
971
|
-
stdio: ['ignore', logFd, logFd],
|
|
972
|
-
env: { ...process.env, MINDOS_WEB_PORT: String(usePort) },
|
|
973
|
-
});
|
|
974
|
-
child.unref();
|
|
975
|
-
|
|
976
|
-
// First-time install hint
|
|
977
|
-
if (isFirstTime) {
|
|
978
|
-
write(c.dim(' First run: installing dependencies and building app (may take a few minutes)...\n'));
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
// Wait for the server to be ready (10min timeout — first run involves npm install + build)
|
|
982
|
-
const { waitForHttp } = await import('../bin/lib/gateway.js');
|
|
983
|
-
const ready = await waitForHttp(usePort, { retries: 600, intervalMs: 1000, label: 'MindOS', logFile: LOG_PATH });
|
|
984
|
-
|
|
985
|
-
if (!ready) {
|
|
986
|
-
write(c.red('\n✘ Server failed to start.\n'));
|
|
987
|
-
if (existsSync(LOG_PATH)) {
|
|
988
|
-
write(c.dim(`\n Last log output (${LOG_PATH}):\n`));
|
|
989
|
-
try {
|
|
990
|
-
const lines = readFileSync(LOG_PATH, 'utf-8').trim().split('\n').slice(-15);
|
|
991
|
-
for (const line of lines) write(c.dim(` ${line}\n`));
|
|
992
|
-
} catch {}
|
|
993
|
-
}
|
|
994
|
-
write(c.dim(`\n Full logs: mindos logs\n`));
|
|
995
|
-
write(c.dim(` Manual start: mindos start\n\n`));
|
|
996
|
-
process.exit(1);
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
const url = `http://localhost:${usePort}/setup`;
|
|
1000
|
-
console.log(`\n${c.green(tf('guiReady', url))}\n`);
|
|
1001
|
-
|
|
1002
|
-
const opened = openBrowser(url);
|
|
1003
|
-
if (!opened) {
|
|
1004
|
-
console.log(c.dim(tf('guiOpenFailed', url)));
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
process.exit(0);
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
1011
|
-
|
|
1012
|
-
async function main() {
|
|
1013
|
-
console.log(`\n${c.bold(t('title'))}\n\n${c.dim(t('langHint'))}\n`);
|
|
1014
|
-
|
|
1015
|
-
// ── Mode selection: CLI or GUI ───────────────────────────────────────────
|
|
1016
|
-
const mode = await select('modePrompt', 'modeOpts', 'modeVals');
|
|
1017
|
-
|
|
1018
|
-
if (mode === 'gui') {
|
|
1019
|
-
await startGuiSetup();
|
|
1020
|
-
return;
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
// ── CLI mode continues below ─────────────────────────────────────────────
|
|
1024
|
-
|
|
1025
|
-
// ── Early overwrite check ─────────────────────────────────────────────────
|
|
1026
|
-
if (existsSync(CONFIG_PATH)) {
|
|
1027
|
-
let existing = {};
|
|
1028
|
-
try { existing = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')); } catch {}
|
|
1029
|
-
|
|
1030
|
-
const mask = (s) => s ? s.slice(0, 4) + '••••••••' + s.slice(-2) : c.dim('(not set)');
|
|
1031
|
-
const row = (label, val) => ` ${c.dim(label.padEnd(18))} ${val}`;
|
|
1032
|
-
const providers = existing.ai?.providers;
|
|
1033
|
-
const anthropicKey = providers?.anthropic?.apiKey || existing.ai?.anthropicApiKey || '';
|
|
1034
|
-
const openaiKey = providers?.openai?.apiKey || existing.ai?.openaiApiKey || '';
|
|
1035
|
-
|
|
1036
|
-
console.log(c.bold('\nExisting config:'));
|
|
1037
|
-
console.log(row('Knowledge base:', c.cyan(existing.mindRoot || '(not set)')));
|
|
1038
|
-
console.log(row('Web port:', c.cyan(String(existing.port || '3456'))));
|
|
1039
|
-
console.log(row('MCP port:', c.cyan(String(existing.mcpPort || '8781'))));
|
|
1040
|
-
console.log(row('Auth token:', existing.authToken ? mask(existing.authToken) : c.dim('(not set)')));
|
|
1041
|
-
console.log(row('Web password:', existing.webPassword ? '••••••••' : c.dim('(none)')));
|
|
1042
|
-
console.log(row('AI provider:', c.cyan(existing.ai?.provider || '(not set)')));
|
|
1043
|
-
if (anthropicKey) console.log(row('Anthropic key:', mask(anthropicKey)));
|
|
1044
|
-
if (openaiKey) console.log(row('OpenAI key:', mask(openaiKey)));
|
|
1045
|
-
write('\n');
|
|
1046
|
-
|
|
1047
|
-
const overwrite = await askYesNo('cfgExists', CONFIG_PATH);
|
|
1048
|
-
if (!overwrite) {
|
|
1049
|
-
const existingMode = existing.startMode || 'start';
|
|
1050
|
-
const existingMcpPort = existing.mcpPort || 8781;
|
|
1051
|
-
const existingAuth = existing.authToken || '';
|
|
1052
|
-
const existingMindRoot = existing.mindRoot || resolve(homedir(), 'MindOS', 'mind');
|
|
1053
|
-
console.log(`\n${c.green(t('cfgKept'))} ${c.dim(CONFIG_PATH)}`);
|
|
1054
|
-
write(c.dim(t('cfgKeptNote') + '\n'));
|
|
1055
|
-
const installDaemon = process.argv.includes('--install-daemon');
|
|
1056
|
-
finish(existingMindRoot, existingMode, existingMcpPort, existingAuth, installDaemon);
|
|
1057
|
-
return;
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
// ── Step 1: Knowledge base path ───────────────────────────────────────────
|
|
1062
|
-
stepHeader(1);
|
|
1063
|
-
|
|
1064
|
-
// Resume: read existing config to offer current values as defaults
|
|
1065
|
-
let resumeCfg = {};
|
|
1066
|
-
try { resumeCfg = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')); } catch { /* first run */ }
|
|
1067
|
-
|
|
1068
|
-
const { readdirSync } = await import('node:fs');
|
|
1069
|
-
let mindDir;
|
|
1070
|
-
let selectedTemplate = 'en'; // hoisted — set by template selection or inferred from existing config
|
|
1071
|
-
// Infer template from existing config's disabledSkills or UI language
|
|
1072
|
-
if (resumeCfg.disabledSkills?.includes('mindos')) {
|
|
1073
|
-
selectedTemplate = 'zh';
|
|
1074
|
-
} else if (resumeCfg.disabledSkills?.includes('mindos-zh')) {
|
|
1075
|
-
selectedTemplate = 'en';
|
|
1076
|
-
} else {
|
|
1077
|
-
selectedTemplate = uiLang; // fallback to UI language for first-time existing KB
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
// Default KB path: existing mindRoot if set, otherwise ~/MindOS (same as GUI default)
|
|
1081
|
-
const HOME = homedir();
|
|
1082
|
-
const kbDefault = resumeCfg.mindRoot || resolve(HOME, 'MindOS', 'mind');
|
|
1083
|
-
|
|
1084
|
-
while (true) {
|
|
1085
|
-
const input = (await askText('pathPrompt', kbDefault)).trim();
|
|
1086
|
-
// Handle absolute paths for all platforms (Unix: /path, Windows: C:\path or C:/path)
|
|
1087
|
-
const isAbsolute = input.startsWith('/') || /^[A-Za-z]:[/\\]/.test(input);
|
|
1088
|
-
const resolved = input.startsWith('~/')
|
|
1089
|
-
? resolve(HOME, input.slice(2))
|
|
1090
|
-
: isAbsolute ? resolve(input) : resolve(HOME, input);
|
|
1091
|
-
write(tf('pathResolved', resolved) + '\n');
|
|
1092
|
-
mindDir = resolved;
|
|
1093
|
-
|
|
1094
|
-
if (existsSync(mindDir)) {
|
|
1095
|
-
// show contents
|
|
1096
|
-
let entries = [];
|
|
1097
|
-
try { entries = readdirSync(mindDir).filter(e => !e.startsWith('.')); } catch {}
|
|
1098
|
-
write('\n');
|
|
1099
|
-
write(tf('kbExists', mindDir) + '\n');
|
|
1100
|
-
if (entries.length) {
|
|
1101
|
-
const label = T.kbExistsFiles[uiLang] ?? T.kbExistsFiles.en;
|
|
1102
|
-
write(` ${c.dim(label + ':')} ${entries.slice(0, 8).map(e => c.dim(e)).join(' ')}${entries.length > 8 ? c.dim(' …') : ''}\n`);
|
|
1103
|
-
} else {
|
|
1104
|
-
write(` ${c.dim('(empty)')}\n`);
|
|
1105
|
-
}
|
|
1106
|
-
write('\n');
|
|
1107
|
-
|
|
1108
|
-
const choice = await select('kbExistsFiles', 'kbExistsOpts', 'kbExistsVals');
|
|
1109
|
-
if (choice === 'reselect') { write('\n'); continue; }
|
|
1110
|
-
break;
|
|
1111
|
-
} else {
|
|
1112
|
-
// ── Template selection (part of Step 1) ─────────────────────────────
|
|
1113
|
-
write('\n');
|
|
1114
|
-
selectedTemplate = await select('tplPrompt', 'tplOptions', 'tplValues');
|
|
1115
|
-
mkdirSync(mindDir, { recursive: true });
|
|
1116
|
-
await applyTemplate(selectedTemplate, mindDir);
|
|
1117
|
-
break;
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
// ── Step 2: AI Provider + API Key ─────────────────────────────────────────
|
|
1122
|
-
write('\n');
|
|
1123
|
-
stepHeader(2);
|
|
1124
|
-
|
|
1125
|
-
const provider = await select('providerPrompt', 'providerOpts', 'providerVals');
|
|
1126
|
-
const isSkip = provider === 'skip';
|
|
1127
|
-
const isAnthropic = provider === 'anthropic';
|
|
1128
|
-
|
|
1129
|
-
// preserve existing provider configs (use resumeCfg already read at top of main)
|
|
1130
|
-
let existingProviders = {
|
|
1131
|
-
anthropic: { apiKey: '', model: 'claude-sonnet-4-6' },
|
|
1132
|
-
openai: { apiKey: '', model: 'gpt-5.4', baseUrl: '' },
|
|
1133
|
-
};
|
|
1134
|
-
let existingAiProvider = 'anthropic';
|
|
1135
|
-
if (resumeCfg.ai?.providers) {
|
|
1136
|
-
existingProviders = { ...existingProviders, ...resumeCfg.ai.providers };
|
|
1137
|
-
} else if (resumeCfg.ai?.anthropicApiKey) {
|
|
1138
|
-
existingProviders.anthropic = { apiKey: resumeCfg.ai.anthropicApiKey || '', model: resumeCfg.ai.anthropicModel || 'claude-sonnet-4-6' };
|
|
1139
|
-
existingProviders.openai = { apiKey: resumeCfg.ai.openaiApiKey || '', model: resumeCfg.ai.openaiModel || 'gpt-5.4', baseUrl: resumeCfg.ai.openaiBaseUrl || '' };
|
|
1140
|
-
}
|
|
1141
|
-
if (resumeCfg.ai?.provider) existingAiProvider = resumeCfg.ai.provider;
|
|
1142
|
-
|
|
1143
|
-
if (isSkip) {
|
|
1144
|
-
write(c.dim(t('providerSkip') + '\n'));
|
|
1145
|
-
} else {
|
|
1146
|
-
let apiKey = '';
|
|
1147
|
-
let baseUrl = '';
|
|
1148
|
-
if (isAnthropic) {
|
|
1149
|
-
apiKey = await askText('anthropicKey');
|
|
1150
|
-
} else {
|
|
1151
|
-
apiKey = await askText('openaiKey');
|
|
1152
|
-
baseUrl = await askText('openaiBase');
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
if (!apiKey) {
|
|
1156
|
-
write(c.yellow(t('apiKeyWarn') + '\n'));
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
if (isAnthropic) {
|
|
1160
|
-
existingProviders.anthropic = { apiKey, model: existingProviders.anthropic?.model || 'claude-sonnet-4-6' };
|
|
1161
|
-
} else {
|
|
1162
|
-
existingProviders.openai = { apiKey, model: existingProviders.openai?.model || 'gpt-5.4', baseUrl: baseUrl || '' };
|
|
1163
|
-
}
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
// ── Step 3: Ports ─────────────────────────────────────────────────────────
|
|
1167
|
-
write('\n');
|
|
1168
|
-
stepHeader(3);
|
|
1169
|
-
const existingCfg = resumeCfg;
|
|
1170
|
-
const defaultWebPort = typeof existingCfg.port === 'number' ? existingCfg.port : 3456;
|
|
1171
|
-
const defaultMcpPort = typeof existingCfg.mcpPort === 'number' ? existingCfg.mcpPort : (defaultWebPort === 8781 ? 8782 : 8781);
|
|
1172
|
-
let webPort, mcpPort;
|
|
1173
|
-
while (true) {
|
|
1174
|
-
webPort = await askPort('webPortPrompt', defaultWebPort);
|
|
1175
|
-
mcpPort = await askPort('mcpPortPrompt', defaultMcpPort);
|
|
1176
|
-
if (webPort !== mcpPort) break;
|
|
1177
|
-
write(c.yellow(` ⚠ ${uiLang === 'zh' ? 'Web 端口和 MCP 端口不能相同,请重新选择' : 'Web port and MCP port must be different — please choose again'}\n`));
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
// ── Step 4: Auth token ────────────────────────────────────────────────────
|
|
1181
|
-
write('\n');
|
|
1182
|
-
stepHeader(4);
|
|
1183
|
-
// Resume: if config already has a token, offer it as the default (Enter = keep)
|
|
1184
|
-
const existingToken = existingCfg.authToken || '';
|
|
1185
|
-
let authToken;
|
|
1186
|
-
if (existingToken) {
|
|
1187
|
-
const masked = existingToken.length > 8 ? existingToken.slice(0, 8) + '····' : existingToken;
|
|
1188
|
-
write(c.dim(` ${uiLang === 'zh' ? '现有 token:' : 'Current token:'} ${c.cyan(masked)}\n`));
|
|
1189
|
-
const keepToken = await askYesNoDefault('cfgConfirm');
|
|
1190
|
-
if (keepToken) {
|
|
1191
|
-
authToken = existingToken;
|
|
1192
|
-
console.log(`${c.green(t('tokenGenerated'))}: ${c.cyan(existingToken.slice(0, 8) + '····')}`);
|
|
1193
|
-
} else {
|
|
1194
|
-
const authSeed = await askText('authPrompt');
|
|
1195
|
-
authToken = generateToken(authSeed);
|
|
1196
|
-
console.log(`${c.green(t('tokenGenerated'))}: ${c.cyan(authToken)}`);
|
|
1197
|
-
}
|
|
1198
|
-
} else {
|
|
1199
|
-
const authSeed = await askText('authPrompt');
|
|
1200
|
-
authToken = generateToken(authSeed);
|
|
1201
|
-
console.log(`${c.green(t('tokenGenerated'))}: ${c.cyan(authToken)}`);
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
// ── Step 5: Web UI password ───────────────────────────────────────────────
|
|
1205
|
-
write('\n');
|
|
1206
|
-
stepHeader(5);
|
|
1207
|
-
let webPassword = '';
|
|
1208
|
-
const existingPassword = existingCfg.webPassword || '';
|
|
1209
|
-
if (existingPassword) {
|
|
1210
|
-
write(c.dim(` ${uiLang === 'zh' ? '已设置密码(Enter 保留)' : 'Password is set (Enter to keep)'}\n`));
|
|
1211
|
-
const keepPass = await askYesNoDefault('cfgConfirm');
|
|
1212
|
-
if (keepPass) {
|
|
1213
|
-
webPassword = existingPassword;
|
|
1214
|
-
}
|
|
1215
|
-
}
|
|
1216
|
-
if (!webPassword) {
|
|
1217
|
-
while (true) {
|
|
1218
|
-
webPassword = await askText('webPassPrompt');
|
|
1219
|
-
if (webPassword) break;
|
|
1220
|
-
write(c.yellow(t('webPassWarn') + '\n'));
|
|
1221
|
-
const confirmed = await askYesNo('webPassSkip');
|
|
1222
|
-
if (confirmed) break;
|
|
1223
|
-
}
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
// ── Step 6: Start Mode ──────────────────────────────────────────────────
|
|
1227
|
-
write('\n');
|
|
1228
|
-
stepHeader(6);
|
|
1229
|
-
|
|
1230
|
-
let startMode = 'start';
|
|
1231
|
-
const daemonPlatform = process.platform === 'darwin' || process.platform === 'linux';
|
|
1232
|
-
if (daemonPlatform) {
|
|
1233
|
-
startMode = await select('startModePrompt', 'startModeOpts', 'startModeVals');
|
|
1234
|
-
} else {
|
|
1235
|
-
write(c.dim(t('startModeSkip') + '\n'));
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
const config = {
|
|
1239
|
-
mindRoot: mindDir,
|
|
1240
|
-
port: webPort,
|
|
1241
|
-
mcpPort: mcpPort,
|
|
1242
|
-
authToken: authToken,
|
|
1243
|
-
webPassword: webPassword || '',
|
|
1244
|
-
startMode: startMode,
|
|
1245
|
-
disabledSkills: selectedTemplate === 'zh' ? ['mindos'] : ['mindos-zh'],
|
|
1246
|
-
ai: {
|
|
1247
|
-
provider: isSkip ? existingAiProvider : (isAnthropic ? 'anthropic' : 'openai'),
|
|
1248
|
-
providers: existingProviders,
|
|
1249
|
-
},
|
|
1250
|
-
};
|
|
1251
|
-
|
|
1252
|
-
// ── Configuration Summary & Confirmation ──────────────────────────────────
|
|
1253
|
-
const maskPw = (s) => s ? '•'.repeat(Math.min(s.length, 8)) : '';
|
|
1254
|
-
const maskTk = (s) => s && s.length > 8 ? s.slice(0, 8) + '····' : (s ? s.slice(0, 4) + '····' : '');
|
|
1255
|
-
const sep = '━'.repeat(40);
|
|
1256
|
-
write(`\n${sep}\n`);
|
|
1257
|
-
write(`${c.bold(uiLang === 'zh' ? '配置摘要' : 'Configuration Summary')}\n`);
|
|
1258
|
-
write(`${sep}\n`);
|
|
1259
|
-
write(` ${c.dim('Knowledge base:')} ${mindDir}\n`);
|
|
1260
|
-
write(` ${c.dim('Web port:')} ${webPort}\n`);
|
|
1261
|
-
write(` ${c.dim('MCP port:')} ${mcpPort}\n`);
|
|
1262
|
-
write(` ${c.dim('Auth token:')} ${maskTk(authToken)}\n`);
|
|
1263
|
-
if (webPassword) write(` ${c.dim('Web password:')} ${maskPw(webPassword)}\n`);
|
|
1264
|
-
write(` ${c.dim('AI provider:')} ${config.ai.provider}\n`);
|
|
1265
|
-
write(` ${c.dim('Start mode:')} ${startMode}\n`);
|
|
1266
|
-
write(`${sep}\n`);
|
|
1267
|
-
|
|
1268
|
-
const confirmSave = await askYesNoDefault('cfgConfirm');
|
|
1269
|
-
if (!confirmSave) {
|
|
1270
|
-
console.log(c.red(t('cfgAborted')));
|
|
1271
|
-
process.exit(0);
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
const isResuming = Object.keys(resumeCfg).length > 0;
|
|
1275
|
-
const needsRestart = isResuming && (
|
|
1276
|
-
config.port !== (resumeCfg.port ?? 3456) ||
|
|
1277
|
-
config.mcpPort !== (resumeCfg.mcpPort ?? 8781) ||
|
|
1278
|
-
config.mindRoot !== (resumeCfg.mindRoot ?? '') ||
|
|
1279
|
-
config.authToken !== (resumeCfg.authToken ?? '') ||
|
|
1280
|
-
config.webPassword !== (resumeCfg.webPassword ?? '')
|
|
1281
|
-
);
|
|
1282
|
-
|
|
1283
|
-
mkdirSync(MINDOS_DIR, { recursive: true });
|
|
1284
|
-
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');
|
|
1285
|
-
console.log(`\n${c.green(t('cfgSaved'))}: ${c.dim(CONFIG_PATH)}`);
|
|
1286
|
-
|
|
1287
|
-
// ── Skill rules are now built into SKILL.md — no install needed ──────────
|
|
1288
|
-
// .mindos/user-preferences.md will be created on first preference capture or via `mindos init-skills`.
|
|
1289
|
-
|
|
1290
|
-
// ── Step 7: Connection Mode ─────────────────────────────────────────────────
|
|
1291
|
-
write('\n');
|
|
1292
|
-
stepHeader(7);
|
|
1293
|
-
const modes = await runModeSelect();
|
|
1294
|
-
|
|
1295
|
-
// ── Step 8: Agent Connection ────────────────────────────────────────────────
|
|
1296
|
-
write('\n');
|
|
1297
|
-
stepHeader(8);
|
|
1298
|
-
const selectedAgents = await runAgentSelect();
|
|
1299
|
-
|
|
1300
|
-
if (selectedAgents.length > 0) {
|
|
1301
|
-
const agentCount = selectedAgents.length;
|
|
1302
|
-
write('\n' + c.dim(uiLang === 'zh'
|
|
1303
|
-
? ` 正在为 ${agentCount} 个 Agent 配置连接…`
|
|
1304
|
-
: ` Configuring connection for ${agentCount} agent${agentCount > 1 ? 's' : ''}…`) + '\n');
|
|
1305
|
-
|
|
1306
|
-
if (modes.mcp) {
|
|
1307
|
-
installMcpConfig(selectedAgents, mcpPort, authToken);
|
|
1308
|
-
}
|
|
1309
|
-
const skillOk = runSkillInstallStep(selectedTemplate, selectedAgents);
|
|
1310
|
-
|
|
1311
|
-
if (skillOk) {
|
|
1312
|
-
write(c.green(uiLang === 'zh'
|
|
1313
|
-
? ` ✔ ${agentCount} 个 Agent 已配置完成\n`
|
|
1314
|
-
: ` ✔ ${agentCount} agent${agentCount > 1 ? 's' : ''} configured\n`));
|
|
1315
|
-
} else {
|
|
1316
|
-
write(c.yellow(uiLang === 'zh'
|
|
1317
|
-
? ` ⚠ ${agentCount} 个 Agent 配置部分完成,CLI 指令文档安装失败\n`
|
|
1318
|
-
: ` ⚠ ${agentCount} agent${agentCount > 1 ? 's' : ''} partially configured — CLI guide install failed\n`));
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
if (!modes.mcp) {
|
|
1322
|
-
write(c.dim(uiLang === 'zh'
|
|
1323
|
-
? ' → 如需启用 MCP 模式,随时运行 `mindos mcp install`。\n'
|
|
1324
|
-
: ' → To enable MCP mode later, run `mindos mcp install`.\n'));
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
// ── Sync setup (optional) ──────────────────────────────────────────────────
|
|
1329
|
-
const wantSync = await askYesNo('syncSetup');
|
|
1330
|
-
if (wantSync) {
|
|
1331
|
-
const { initSync } = await import('../bin/lib/sync.js');
|
|
1332
|
-
await initSync(mindDir);
|
|
1333
|
-
} else {
|
|
1334
|
-
console.log(c.dim(t('syncLater')));
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
// ── Register CLI globally if not already in PATH ────────────────────────────
|
|
1338
|
-
ensureCliInPath();
|
|
1339
|
-
|
|
1340
|
-
const installDaemon = startMode === 'daemon' || process.argv.includes('--install-daemon');
|
|
1341
|
-
finish(mindDir, config.startMode, config.mcpPort, config.authToken, installDaemon, needsRestart, resumeCfg.port ?? 3456);
|
|
1342
|
-
}
|
|
1343
|
-
|
|
1344
|
-
function ensureCliInPath() {
|
|
1345
|
-
try {
|
|
1346
|
-
const checkCmd = process.platform === 'win32' ? 'where mindos' : 'command -v mindos';
|
|
1347
|
-
execSync(checkCmd, { stdio: 'ignore' });
|
|
1348
|
-
return; // already in PATH (npm -g install or previous link)
|
|
1349
|
-
} catch { /* not found */ }
|
|
1350
|
-
|
|
1351
|
-
// Only auto-register for source/dev installations.
|
|
1352
|
-
// Desktop (Electron) manages its own process; npx is ephemeral.
|
|
1353
|
-
const isDesktop = !!(process.env.ELECTRON_RUN_AS_NODE || process.env.MINDOS_DESKTOP);
|
|
1354
|
-
const isDevInstall = existsSync(resolve(ROOT, '.git'));
|
|
1355
|
-
if (isDesktop || !isDevInstall) return;
|
|
1356
|
-
|
|
1357
|
-
write('\n');
|
|
1358
|
-
try {
|
|
1359
|
-
execSync('npm link', { cwd: ROOT, stdio: 'ignore' });
|
|
1360
|
-
write(c.green(uiLang === 'zh'
|
|
1361
|
-
? ' ✔ mindos CLI 已注册到全局路径\n'
|
|
1362
|
-
: ' ✔ mindos CLI registered globally\n'));
|
|
1363
|
-
} catch {
|
|
1364
|
-
write(c.yellow(uiLang === 'zh'
|
|
1365
|
-
? ' ⚠ 无法自动注册 CLI,请手动运行:npm link\n'
|
|
1366
|
-
: ' ⚠ Could not register CLI automatically. Run manually: npm link\n'));
|
|
1367
|
-
}
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
function getLocalIP() {
|
|
1371
|
-
for (const ifaces of Object.values(networkInterfaces())) {
|
|
1372
|
-
for (const iface of ifaces) {
|
|
1373
|
-
if (iface.family === 'IPv4' && !iface.internal) return iface.address;
|
|
1374
|
-
}
|
|
1375
|
-
}
|
|
1376
|
-
return null;
|
|
1377
|
-
}
|
|
1378
|
-
|
|
1379
|
-
async function finish(mindDir, startMode = 'start', mcpPort = 8781, authToken = '', installDaemon = false, needsRestart = false, oldPort = 3456) {
|
|
1380
|
-
// startMode 'daemon' stored in config is equivalent to installDaemon flag
|
|
1381
|
-
if (startMode === 'daemon') {
|
|
1382
|
-
installDaemon = true;
|
|
1383
|
-
startMode = 'start';
|
|
1384
|
-
}
|
|
1385
|
-
if (needsRestart) {
|
|
1386
|
-
const isRunning = await isSelfPort(oldPort);
|
|
1387
|
-
if (isRunning) {
|
|
1388
|
-
write(c.yellow(t('restartRequired') + '\n'));
|
|
1389
|
-
const doRestart = await askYesNoDefault('restartNow');
|
|
1390
|
-
if (doRestart) {
|
|
1391
|
-
const cliPath = resolve(__dirname, '../bin/cli.js');
|
|
1392
|
-
// Use 'restart' (stop → start) instead of bare 'start' which would
|
|
1393
|
-
// fail assertPortFree because the old process is still running.
|
|
1394
|
-
execSync(`node "${cliPath}" restart`, { stdio: 'inherit' });
|
|
1395
|
-
} else {
|
|
1396
|
-
write(c.dim(t('restartManual') + '\n'));
|
|
1397
|
-
}
|
|
1398
|
-
return;
|
|
1399
|
-
} else {
|
|
1400
|
-
write(c.dim(t('changesOnNextStart') + '\n'));
|
|
1401
|
-
// fall through to normal Start now? prompt
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
1404
|
-
|
|
1405
|
-
const startCmd = installDaemon ? 'mindos start --daemon' : (startMode === 'dev' ? 'mindos dev' : 'mindos start');
|
|
1406
|
-
const lines = T.nextSteps[uiLang](startCmd);
|
|
1407
|
-
console.log('');
|
|
1408
|
-
lines.forEach((l) => console.log(l));
|
|
1409
|
-
|
|
1410
|
-
const doStart = await askYesNoDefault('startNow');
|
|
1411
|
-
if (doStart) {
|
|
1412
|
-
const { execSync: exec } = await import('node:child_process');
|
|
1413
|
-
const cliPath = resolve(__dirname, '../bin/cli.js');
|
|
1414
|
-
if (installDaemon) {
|
|
1415
|
-
// Install and start as background service — returns immediately
|
|
1416
|
-
exec(`node "${cliPath}" start --daemon`, { stdio: 'inherit' });
|
|
1417
|
-
} else {
|
|
1418
|
-
exec(`node "${cliPath}" ${startMode}`, { stdio: 'inherit' });
|
|
1419
|
-
}
|
|
1420
|
-
}
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
main().catch((err) => {
|
|
1424
|
-
write(SHOW_CURSOR);
|
|
1425
|
-
console.error(err);
|
|
1426
|
-
process.exit(1);
|
|
1427
|
-
});
|