@geminilight/mindos 0.5.70 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/app/api/ask/route.ts +124 -92
- package/app/app/api/mcp/agents/route.ts +53 -2
- package/app/app/api/mcp/status/route.ts +1 -1
- package/app/app/api/skills/route.ts +10 -114
- package/app/components/ActivityBar.tsx +3 -4
- package/app/components/CreateSpaceModal.tsx +31 -6
- package/app/components/FileTree.tsx +33 -2
- package/app/components/GuideCard.tsx +197 -131
- package/app/components/HomeContent.tsx +85 -18
- package/app/components/SidebarLayout.tsx +13 -0
- package/app/components/SpaceInitToast.tsx +173 -0
- package/app/components/agents/AgentDetailContent.tsx +32 -17
- package/app/components/agents/AgentsContentPage.tsx +2 -1
- package/app/components/agents/AgentsOverviewSection.tsx +1 -14
- package/app/components/agents/agents-content-model.ts +16 -8
- package/app/components/ask/AskContent.tsx +137 -50
- package/app/components/ask/MentionPopover.tsx +16 -8
- package/app/components/ask/SlashCommandPopover.tsx +62 -0
- package/app/components/settings/KnowledgeTab.tsx +61 -0
- package/app/components/walkthrough/steps.ts +11 -6
- package/app/hooks/useMention.ts +14 -6
- package/app/hooks/useSlashCommand.ts +114 -0
- package/app/lib/actions.ts +79 -2
- package/app/lib/agent/index.ts +1 -1
- package/app/lib/agent/prompt.ts +2 -0
- package/app/lib/agent/tools.ts +106 -0
- package/app/lib/core/create-space.ts +11 -4
- package/app/lib/core/index.ts +1 -1
- package/app/lib/i18n-en.ts +51 -46
- package/app/lib/i18n-zh.ts +50 -45
- package/app/lib/mcp-agents.ts +8 -0
- package/app/lib/pdf-extract.ts +33 -0
- package/app/lib/pi-integration/extensions.ts +68 -0
- package/app/lib/pi-integration/mcporter.ts +219 -0
- package/app/lib/pi-integration/session-store.ts +62 -0
- package/app/lib/pi-integration/skills.ts +116 -0
- package/app/lib/settings.ts +1 -1
- package/app/next-env.d.ts +1 -1
- package/app/next.config.ts +1 -1
- package/app/package.json +2 -0
- package/mcp/src/index.ts +29 -0
- package/package.json +1 -1
package/app/lib/i18n-zh.ts
CHANGED
|
@@ -53,6 +53,11 @@ export const zh = {
|
|
|
53
53
|
aiInit: 'AI 初始化内容',
|
|
54
54
|
aiInitHint: 'AI 将为此空间生成 README 和 INSTRUCTION',
|
|
55
55
|
aiInitNoKey: '在 设置 → AI 中配置 API 密钥以启用',
|
|
56
|
+
aiInitGenerating: (name: string) => `正在为「${name}」生成内容`,
|
|
57
|
+
aiInitReady: (name: string) => `「${name}」已就绪`,
|
|
58
|
+
aiInitReview: '查看',
|
|
59
|
+
aiInitDiscard: '撤销',
|
|
60
|
+
aiInitReverted: (name: string) => `「${name}」已恢复为模板`,
|
|
56
61
|
createSpace: '创建',
|
|
57
62
|
cancelCreate: '取消',
|
|
58
63
|
continueEditing: '继续编辑',
|
|
@@ -78,6 +83,9 @@ export const zh = {
|
|
|
78
83
|
hoursAgo: (n: number) => `${n} 小时前`,
|
|
79
84
|
daysAgo: (n: number) => `${n} 天前`,
|
|
80
85
|
},
|
|
86
|
+
cleanupExamples: (n: number) => `有 ${n} 个模板示例文件可以清理`,
|
|
87
|
+
cleanupExamplesButton: '一键清理',
|
|
88
|
+
cleanupExamplesDone: '示例文件已清理',
|
|
81
89
|
},
|
|
82
90
|
sidebar: {
|
|
83
91
|
files: '空间',
|
|
@@ -122,7 +130,7 @@ export const zh = {
|
|
|
122
130
|
},
|
|
123
131
|
ask: {
|
|
124
132
|
title: 'MindOS Agent',
|
|
125
|
-
placeholder: '
|
|
133
|
+
placeholder: '输入问题… @ 附加文件,/ 技能',
|
|
126
134
|
emptyPrompt: '可以问任何关于知识库的问题',
|
|
127
135
|
send: '发送',
|
|
128
136
|
newlineHint: '换行',
|
|
@@ -130,7 +138,9 @@ export const zh = {
|
|
|
130
138
|
panelComposerFooter: '拉高输入区',
|
|
131
139
|
panelComposerResetHint: '双击恢复默认高度',
|
|
132
140
|
panelComposerKeyboard: '方向键调节高度;Home/End 最小或最大',
|
|
133
|
-
attachFile: '
|
|
141
|
+
attachFile: '上下文',
|
|
142
|
+
uploadedFiles: '已上传',
|
|
143
|
+
skillsHint: '技能',
|
|
134
144
|
attachCurrent: '附加当前文件',
|
|
135
145
|
stopTitle: '停止',
|
|
136
146
|
connecting: '正在与你的心智一起思考...',
|
|
@@ -375,6 +385,9 @@ export const zh = {
|
|
|
375
385
|
enabledUnit: (n: number) => `${n} 已启用`,
|
|
376
386
|
agentCount: (n: number) => `${n} 个 Agent`,
|
|
377
387
|
runtimeActive: '活跃',
|
|
388
|
+
riskMcpStopped: 'MCP 服务未运行。',
|
|
389
|
+
riskDetected: (n: number) => `${n} 个已检测 Agent 待配置。`,
|
|
390
|
+
riskSkillsDisabled: '所有技能已禁用。',
|
|
378
391
|
},
|
|
379
392
|
mcp: {
|
|
380
393
|
title: 'MCP 连接',
|
|
@@ -408,6 +421,7 @@ export const zh = {
|
|
|
408
421
|
riskMcpStopped: 'MCP 服务未运行。',
|
|
409
422
|
riskDetected: (n: number) => `${n} 个已检测 Agent 待配置。`,
|
|
410
423
|
riskNotFound: (n: number) => `${n} 个 Agent 未在本机检测到。`,
|
|
424
|
+
riskSkillsDisabled: '所有技能已禁用。',
|
|
411
425
|
bulkReconnectFiltered: '全部重连',
|
|
412
426
|
bulkRunning: '正在批量重连...',
|
|
413
427
|
bulkSummary: (ok: number, failed: number) => `重连成功 ${ok} 个,失败 ${failed} 个。`,
|
|
@@ -816,6 +830,15 @@ export const zh = {
|
|
|
816
830
|
authTokenResetConfirm: '重新生成令牌?所有 MCP 客户端配置都需要更新。',
|
|
817
831
|
authTokenMcpPort: 'MCP 端口',
|
|
818
832
|
restartWalkthrough: '重新开始引导',
|
|
833
|
+
showHiddenFiles: '显示隐藏文件',
|
|
834
|
+
showHiddenFilesHint: '在文件树中显示以 . 开头的文件夹(.agents、.claude、.mindos 等)。',
|
|
835
|
+
cleanupExamples: '清理示例文件',
|
|
836
|
+
cleanupExamplesHint: '移除知识库中所有模板示例文件(🧪_example_*)。',
|
|
837
|
+
cleanupExamplesButton: '清理',
|
|
838
|
+
cleanupExamplesNone: '没有找到示例文件',
|
|
839
|
+
cleanupExamplesConfirm: (n: number) => `删除 ${n} 个示例文件?此操作不可撤销。`,
|
|
840
|
+
cleanupExamplesDone: (n: number) => `已移除 ${n} 个示例文件`,
|
|
841
|
+
cleanupExamplesScanning: '扫描中...',
|
|
819
842
|
},
|
|
820
843
|
sync: {
|
|
821
844
|
emptyTitle: '跨设备同步',
|
|
@@ -1167,50 +1190,36 @@ export const zh = {
|
|
|
1167
1190
|
welcomeLinkMCP: 'MCP 设置',
|
|
1168
1191
|
},
|
|
1169
1192
|
guide: {
|
|
1170
|
-
title: '
|
|
1193
|
+
title: '快速上手',
|
|
1171
1194
|
showGuide: '显示新手引导',
|
|
1172
1195
|
close: '关闭',
|
|
1173
1196
|
skip: '跳过',
|
|
1174
|
-
|
|
1175
|
-
title: '
|
|
1176
|
-
cta: '
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
profile: '你是谁、偏好、目标',
|
|
1180
|
-
notes: '日常捕捉:想法、会议、待办',
|
|
1181
|
-
connections: '你的人脉关系网',
|
|
1182
|
-
workflows: '可复用的工作流程 SOP',
|
|
1183
|
-
resources: '结构化数据:产品库、工具库',
|
|
1184
|
-
projects: '项目计划和进展',
|
|
1185
|
-
},
|
|
1186
|
-
instructionHint: '点击 INSTRUCTION.md 看看 AI 的行为规则。',
|
|
1187
|
-
emptyDesc: '你的知识库有 3 个核心文件:',
|
|
1188
|
-
emptyFiles: {
|
|
1189
|
-
instruction: 'INSTRUCTION.md — 所有 AI Agent 遵循的规则',
|
|
1190
|
-
readme: 'README.md — 目录索引和导航',
|
|
1191
|
-
config: 'CONFIG.json — 机器可读的配置偏好',
|
|
1192
|
-
},
|
|
1193
|
-
emptyHint: '你可以随时创建自己的目录结构。',
|
|
1194
|
-
progress: (n: number) => `已浏览 ${n}/1 个文件`,
|
|
1195
|
-
done: '完成',
|
|
1197
|
+
import: {
|
|
1198
|
+
title: '导入你的文件',
|
|
1199
|
+
cta: '导入',
|
|
1200
|
+
desc: '上传简历、项目文档、笔记——任何你想让 AI Agent 了解的内容。',
|
|
1201
|
+
button: '导入文件',
|
|
1196
1202
|
},
|
|
1197
1203
|
ai: {
|
|
1198
|
-
title: '
|
|
1199
|
-
cta: '
|
|
1200
|
-
|
|
1204
|
+
title: '感受 AI 读取内容',
|
|
1205
|
+
cta: '试试',
|
|
1206
|
+
desc: '你的文件已在知识库中。问问 MindOS Agent 它了解了什么:',
|
|
1207
|
+
prompt: '根据我的知识库介绍一下我——我是谁、在做什么?',
|
|
1201
1208
|
promptEmpty: '帮我设计一个适合我的知识库目录结构',
|
|
1202
1209
|
},
|
|
1203
|
-
|
|
1204
|
-
title: '
|
|
1205
|
-
|
|
1206
|
-
|
|
1210
|
+
agent: {
|
|
1211
|
+
title: '在其他 Agent 验证',
|
|
1212
|
+
cta: '复制提示词',
|
|
1213
|
+
desc: '打开 Cursor、Claude Code 或任意连接了 MCP 的 Agent,粘贴以下提示词:',
|
|
1214
|
+
copyPrompt: '读取我的 MindOS 知识库,概括我的背景,然后建议我接下来该做什么。',
|
|
1215
|
+
copy: '复制',
|
|
1216
|
+
copied: '已复制!',
|
|
1207
1217
|
},
|
|
1208
1218
|
done: {
|
|
1209
1219
|
title: '你已准备好使用 MindOS',
|
|
1210
1220
|
titleFinal: '你已掌握 MindOS 核心用法',
|
|
1211
1221
|
steps: [
|
|
1212
1222
|
{ hint: '下一步:试试把一篇文章存进来 →', prompt: '帮我把这篇文章的要点整理到 MindOS 里。' },
|
|
1213
|
-
{ hint: '下一步:试试在另一个 Agent 里调用知识库 →', prompt: '帮我按 MindOS 里的方案开始写代码。' },
|
|
1214
1223
|
{ hint: '下一步:试试把经验沉淀为 SOP →', prompt: '帮我把这次对话的经验沉淀到 MindOS,形成可复用的工作流。' },
|
|
1215
1224
|
],
|
|
1216
1225
|
},
|
|
@@ -1291,24 +1300,20 @@ prompt: '这是我的简历,读一下,把我的信息整理到 MindOS 里。
|
|
|
1291
1300
|
exploreCta: '探索更多用法 →',
|
|
1292
1301
|
steps: [
|
|
1293
1302
|
{
|
|
1294
|
-
title: '
|
|
1295
|
-
body: '
|
|
1296
|
-
},
|
|
1297
|
-
{
|
|
1298
|
-
title: '你的知识库',
|
|
1299
|
-
body: '在文件面板中浏览和管理你的笔记、画像和项目。',
|
|
1303
|
+
title: '你的项目记忆',
|
|
1304
|
+
body: '用 Space 管理项目、SOP 和偏好规则。数据存在本地,完全由你掌控。',
|
|
1300
1305
|
},
|
|
1301
1306
|
{
|
|
1302
|
-
title: 'AI
|
|
1303
|
-
body: '
|
|
1307
|
+
title: '不用重讲背景的 AI',
|
|
1308
|
+
body: 'MindOS Agent 自动读取整个知识库。问它项目相关的事,不需要重复交代背景。',
|
|
1304
1309
|
},
|
|
1305
1310
|
{
|
|
1306
|
-
title: '
|
|
1307
|
-
body: '
|
|
1311
|
+
title: '多 Agent 共享记忆',
|
|
1312
|
+
body: '通过 MCP 连接 Cursor、Claude Code、Windsurf,它们共享同一份项目记忆。',
|
|
1308
1313
|
},
|
|
1309
1314
|
{
|
|
1310
|
-
title: '
|
|
1311
|
-
body: '
|
|
1315
|
+
title: '回响 — 认知在积累',
|
|
1316
|
+
body: '关于你、每日回顾、成长轨迹——MindOS 帮你沉淀判断与偏好,让经验不断复利。',
|
|
1312
1317
|
},
|
|
1313
1318
|
],
|
|
1314
1319
|
},
|
package/app/lib/mcp-agents.ts
CHANGED
|
@@ -41,6 +41,14 @@ export interface SkillAgentRegistration {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
export const MCP_AGENTS: Record<string, AgentDef> = {
|
|
44
|
+
'mindos': {
|
|
45
|
+
name: 'MindOS',
|
|
46
|
+
project: null,
|
|
47
|
+
global: '~/.mindos/mcp.json',
|
|
48
|
+
key: 'mcpServers',
|
|
49
|
+
preferredTransport: 'stdio',
|
|
50
|
+
presenceDirs: ['~/.mindos/'],
|
|
51
|
+
},
|
|
44
52
|
'claude-code': {
|
|
45
53
|
name: 'Claude Code',
|
|
46
54
|
project: '.mcp.json',
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
function uint8ToBase64(bytes: Uint8Array): string {
|
|
2
|
+
let binary = '';
|
|
3
|
+
const chunkSize = 0x8000;
|
|
4
|
+
for (let i = 0; i < bytes.length; i += chunkSize) {
|
|
5
|
+
const chunk = bytes.subarray(i, i + chunkSize);
|
|
6
|
+
binary += String.fromCharCode(...chunk);
|
|
7
|
+
}
|
|
8
|
+
return btoa(binary);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function extractPdfText(file: File): Promise<string> {
|
|
12
|
+
const buffer = await file.arrayBuffer();
|
|
13
|
+
const dataBase64 = uint8ToBase64(new Uint8Array(buffer));
|
|
14
|
+
|
|
15
|
+
const res = await fetch('/api/extract-pdf', {
|
|
16
|
+
method: 'POST',
|
|
17
|
+
headers: { 'Content-Type': 'application/json' },
|
|
18
|
+
body: JSON.stringify({ name: file.name, dataBase64 }),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
let payload: { text?: string; extracted?: boolean; error?: string } = {};
|
|
22
|
+
try {
|
|
23
|
+
payload = await res.json();
|
|
24
|
+
} catch {
|
|
25
|
+
// ignore JSON parse error
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!res.ok) {
|
|
29
|
+
throw new Error(payload.error || `PDF extraction failed (${res.status})`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return payload.extracted ? (payload.text || '') : '';
|
|
33
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { DefaultResourceLoader, SettingsManager } from '@mariozechner/pi-coding-agent';
|
|
5
|
+
|
|
6
|
+
export function getMindosExtensionsDir(): string {
|
|
7
|
+
return path.join(os.homedir(), '.mindos', 'extensions');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Scan ~/.mindos/extensions/ for .ts files and index.ts in subdirs */
|
|
11
|
+
export function scanExtensionPaths(): string[] {
|
|
12
|
+
const dir = getMindosExtensionsDir();
|
|
13
|
+
if (!fs.existsSync(dir)) return [];
|
|
14
|
+
const paths: string[] = [];
|
|
15
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
16
|
+
if (entry.isFile() && entry.name.endsWith('.ts')) {
|
|
17
|
+
paths.push(path.join(dir, entry.name));
|
|
18
|
+
} else if (entry.isDirectory()) {
|
|
19
|
+
const indexPath = path.join(dir, entry.name, 'index.ts');
|
|
20
|
+
if (fs.existsSync(indexPath)) paths.push(indexPath);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return paths;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ExtensionSummary {
|
|
27
|
+
name: string;
|
|
28
|
+
path: string;
|
|
29
|
+
enabled: boolean;
|
|
30
|
+
tools: string[];
|
|
31
|
+
commands: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function getExtensionsList(
|
|
35
|
+
projectRoot: string,
|
|
36
|
+
_mindRoot: string,
|
|
37
|
+
disabledExtensions: string[] = [],
|
|
38
|
+
): Promise<ExtensionSummary[]> {
|
|
39
|
+
const settingsManager = SettingsManager.inMemory();
|
|
40
|
+
|
|
41
|
+
const loader = new DefaultResourceLoader({
|
|
42
|
+
cwd: projectRoot,
|
|
43
|
+
settingsManager,
|
|
44
|
+
systemPromptOverride: () => '',
|
|
45
|
+
appendSystemPromptOverride: () => [],
|
|
46
|
+
additionalSkillPaths: [],
|
|
47
|
+
additionalExtensionPaths: scanExtensionPaths(),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
await loader.reload();
|
|
52
|
+
const result = loader.getExtensions();
|
|
53
|
+
|
|
54
|
+
return result.extensions.map((ext) => {
|
|
55
|
+
const name = path.basename(ext.path, path.extname(ext.path));
|
|
56
|
+
return {
|
|
57
|
+
name,
|
|
58
|
+
path: ext.resolvedPath || ext.path,
|
|
59
|
+
enabled: !disabledExtensions.includes(name),
|
|
60
|
+
tools: [...ext.tools.keys()],
|
|
61
|
+
commands: [...ext.commands.keys()],
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.error('[getExtensionsList] Failed to load extensions:', error);
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { Type } from '@sinclair/typebox';
|
|
4
|
+
import type { AgentTool, AgentToolResult } from '@mariozechner/pi-agent-core';
|
|
5
|
+
import {
|
|
6
|
+
createRuntime,
|
|
7
|
+
createCallResult,
|
|
8
|
+
type Runtime,
|
|
9
|
+
type ServerToolInfo,
|
|
10
|
+
} from 'mcporter';
|
|
11
|
+
|
|
12
|
+
export interface McporterServerSummary {
|
|
13
|
+
name: string;
|
|
14
|
+
status: string;
|
|
15
|
+
durationMs?: number;
|
|
16
|
+
transport?: string;
|
|
17
|
+
error?: string;
|
|
18
|
+
issue?: { kind?: string; rawMessage?: string };
|
|
19
|
+
source?: { kind?: string; path?: string; importKind?: string };
|
|
20
|
+
tools?: McporterToolSummary[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface McporterToolSummary {
|
|
24
|
+
name: string;
|
|
25
|
+
description?: string;
|
|
26
|
+
inputSchema?: Record<string, unknown>;
|
|
27
|
+
options?: Array<Record<string, unknown>>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface McporterServerList {
|
|
31
|
+
mode?: string;
|
|
32
|
+
counts?: Record<string, number>;
|
|
33
|
+
servers: McporterServerSummary[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function textResult(text: string): AgentToolResult<Record<string, never>> {
|
|
37
|
+
return { content: [{ type: 'text', text }], details: {} };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizeNameSegment(value: string): string {
|
|
41
|
+
const normalized = value.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '');
|
|
42
|
+
return normalized || 'tool';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function toToolSchema(inputSchema?: Record<string, unknown>) {
|
|
46
|
+
if (!inputSchema || Object.keys(inputSchema).length === 0) {
|
|
47
|
+
return Type.Object({}, { additionalProperties: true });
|
|
48
|
+
}
|
|
49
|
+
return Type.Unsafe(inputSchema as any);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function extractJsonObject(text: string): string {
|
|
53
|
+
const first = text.indexOf('{');
|
|
54
|
+
const last = text.lastIndexOf('}');
|
|
55
|
+
if (first === -1 || last === -1 || last < first) {
|
|
56
|
+
throw new Error('Failed to parse mcporter output as JSON');
|
|
57
|
+
}
|
|
58
|
+
return text.slice(first, last + 1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ─── Singleton mcporter Runtime ──────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
const MCP_CONFIG_PATH = path.join(os.homedir(), '.mindos', 'mcp.json');
|
|
64
|
+
const TOOL_TIMEOUT_MS = 30_000;
|
|
65
|
+
|
|
66
|
+
let _runtime: Runtime | null = null;
|
|
67
|
+
let _runtimePromise: Promise<Runtime | null> | null = null;
|
|
68
|
+
|
|
69
|
+
async function getRuntime(): Promise<Runtime | null> {
|
|
70
|
+
if (_runtime) return _runtime;
|
|
71
|
+
if (_runtimePromise) return _runtimePromise;
|
|
72
|
+
|
|
73
|
+
_runtimePromise = (async () => {
|
|
74
|
+
try {
|
|
75
|
+
const rt = await createRuntime({
|
|
76
|
+
configPath: MCP_CONFIG_PATH,
|
|
77
|
+
clientInfo: { name: 'mindos', version: '1.0.0' },
|
|
78
|
+
});
|
|
79
|
+
_runtime = rt;
|
|
80
|
+
return rt;
|
|
81
|
+
} catch (err) {
|
|
82
|
+
console.warn('[mcporter] Failed to create runtime:', err instanceof Error ? err.message : err);
|
|
83
|
+
_runtimePromise = null;
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
})();
|
|
87
|
+
return _runtimePromise;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (typeof process !== 'undefined') {
|
|
91
|
+
const cleanup = () => {
|
|
92
|
+
if (_runtime) {
|
|
93
|
+
_runtime.close().catch(() => {});
|
|
94
|
+
_runtime = null;
|
|
95
|
+
_runtimePromise = null;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
process.on('exit', cleanup);
|
|
99
|
+
process.on('SIGINT', cleanup);
|
|
100
|
+
process.on('SIGTERM', cleanup);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function listMcporterServers(): Promise<McporterServerList> {
|
|
104
|
+
const rt = await getRuntime();
|
|
105
|
+
if (!rt) return { servers: [] };
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const names = rt.listServers();
|
|
109
|
+
if (names.length === 0) return { servers: [] };
|
|
110
|
+
|
|
111
|
+
const servers: McporterServerSummary[] = await Promise.all(
|
|
112
|
+
names.map(async (name) => {
|
|
113
|
+
try {
|
|
114
|
+
const def = rt.getDefinition(name);
|
|
115
|
+
const transport = def.command.kind;
|
|
116
|
+
const tools = await rt.listTools(name, { includeSchema: false });
|
|
117
|
+
return {
|
|
118
|
+
name,
|
|
119
|
+
status: 'ok',
|
|
120
|
+
transport,
|
|
121
|
+
tools: tools.map(toToolSummary),
|
|
122
|
+
};
|
|
123
|
+
} catch (err) {
|
|
124
|
+
return {
|
|
125
|
+
name,
|
|
126
|
+
status: 'error',
|
|
127
|
+
error: err instanceof Error ? err.message : String(err),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
return { servers };
|
|
134
|
+
} catch (err) {
|
|
135
|
+
console.warn('[mcporter] listServers failed:', err instanceof Error ? err.message : err);
|
|
136
|
+
return { servers: [] };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function toToolSummary(tool: ServerToolInfo): McporterToolSummary {
|
|
141
|
+
return {
|
|
142
|
+
name: tool.name,
|
|
143
|
+
description: tool.description,
|
|
144
|
+
inputSchema: tool.inputSchema as Record<string, unknown> | undefined,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function listMcporterTools(serverName: string): Promise<McporterServerSummary> {
|
|
149
|
+
const rt = await getRuntime();
|
|
150
|
+
if (!rt) return { name: serverName, status: 'not_configured', tools: [] };
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const tools = await rt.listTools(serverName, { includeSchema: true });
|
|
154
|
+
return {
|
|
155
|
+
name: serverName,
|
|
156
|
+
status: 'ok',
|
|
157
|
+
tools: tools.map(toToolSummary),
|
|
158
|
+
};
|
|
159
|
+
} catch (err) {
|
|
160
|
+
return {
|
|
161
|
+
name: serverName,
|
|
162
|
+
status: 'error',
|
|
163
|
+
error: err instanceof Error ? err.message : String(err),
|
|
164
|
+
tools: [],
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export async function callMcporterTool(
|
|
170
|
+
serverName: string,
|
|
171
|
+
toolName: string,
|
|
172
|
+
args: Record<string, unknown>,
|
|
173
|
+
): Promise<string> {
|
|
174
|
+
const rt = await getRuntime();
|
|
175
|
+
if (!rt) throw new Error(`MCP runtime not available. Ensure ~/.mindos/mcp.json is configured.`);
|
|
176
|
+
|
|
177
|
+
const raw = await rt.callTool(serverName, toolName, {
|
|
178
|
+
args,
|
|
179
|
+
timeoutMs: TOOL_TIMEOUT_MS,
|
|
180
|
+
});
|
|
181
|
+
const result = createCallResult(raw);
|
|
182
|
+
return result.text('\n') ?? JSON.stringify(raw);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function createMcporterAgentTools(servers: McporterServerSummary[]): AgentTool<any>[] {
|
|
186
|
+
const seenNames = new Set<string>();
|
|
187
|
+
const tools: AgentTool<any>[] = [];
|
|
188
|
+
|
|
189
|
+
for (const server of servers) {
|
|
190
|
+
if (server.status !== 'ok' || !server.tools?.length) continue;
|
|
191
|
+
|
|
192
|
+
for (const tool of server.tools) {
|
|
193
|
+
const baseName = `mcp__${normalizeNameSegment(server.name)}__${normalizeNameSegment(tool.name)}`;
|
|
194
|
+
let name = baseName;
|
|
195
|
+
let suffix = 2;
|
|
196
|
+
while (seenNames.has(name)) {
|
|
197
|
+
name = `${baseName}_${suffix++}`;
|
|
198
|
+
}
|
|
199
|
+
seenNames.add(name);
|
|
200
|
+
|
|
201
|
+
tools.push({
|
|
202
|
+
name,
|
|
203
|
+
label: `MCP ${server.name}: ${tool.name}`,
|
|
204
|
+
description: `MCP tool "${tool.name}" from server "${server.name}".${tool.description ? ` ${tool.description}` : ''}`,
|
|
205
|
+
parameters: toToolSchema(tool.inputSchema),
|
|
206
|
+
execute: async (_toolCallId, params) => {
|
|
207
|
+
try {
|
|
208
|
+
const output = await callMcporterTool(server.name, tool.name, (params ?? {}) as Record<string, unknown>);
|
|
209
|
+
return textResult(output || '(empty MCP response)');
|
|
210
|
+
} catch (error) {
|
|
211
|
+
return textResult(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return tools;
|
|
219
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { SessionManager } from '@mariozechner/pi-coding-agent';
|
|
5
|
+
|
|
6
|
+
function getSessionsRoot(): string {
|
|
7
|
+
return path.join(os.homedir(), '.mindos', 'sessions');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function getSessionDir(sessionId: string): string {
|
|
11
|
+
const safe = sessionId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
12
|
+
return path.join(getSessionsRoot(), safe);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function sessionDirExists(sessionId: string): boolean {
|
|
16
|
+
const sessionDir = getSessionDir(sessionId);
|
|
17
|
+
if (!fs.existsSync(sessionDir)) return false;
|
|
18
|
+
// Check if there's at least one .jsonl file
|
|
19
|
+
try {
|
|
20
|
+
return fs.readdirSync(sessionDir).some((f) => f.endsWith('.jsonl'));
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getOrCreateSessionManager(
|
|
27
|
+
sessionId: string | undefined,
|
|
28
|
+
cwd: string,
|
|
29
|
+
): SessionManager {
|
|
30
|
+
if (!sessionId) {
|
|
31
|
+
return SessionManager.inMemory(cwd);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const sessionDir = getSessionDir(sessionId);
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
fs.mkdirSync(sessionDir, { recursive: true });
|
|
38
|
+
|
|
39
|
+
if (sessionDirExists(sessionId)) {
|
|
40
|
+
// Reuse the most recent session in this directory
|
|
41
|
+
return SessionManager.continueRecent(cwd, sessionDir);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Brand new — create fresh session file
|
|
45
|
+
return SessionManager.create(cwd, sessionDir);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error(`[session-store] Failed to open/create session ${sessionId}, falling back to inMemory:`, error);
|
|
48
|
+
return SessionManager.inMemory(cwd);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function deleteSessionDir(sessionId: string): boolean {
|
|
53
|
+
const sessionDir = getSessionDir(sessionId);
|
|
54
|
+
if (!fs.existsSync(sessionDir)) return false;
|
|
55
|
+
try {
|
|
56
|
+
fs.rmSync(sessionDir, { recursive: true, force: true });
|
|
57
|
+
return true;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error(`[session-store] Failed to delete session dir ${sessionDir}:`, error);
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
export interface PiSkillInfo {
|
|
6
|
+
name: string;
|
|
7
|
+
description: string;
|
|
8
|
+
path: string;
|
|
9
|
+
source: 'builtin' | 'user';
|
|
10
|
+
enabled: boolean;
|
|
11
|
+
editable: boolean;
|
|
12
|
+
origin: 'app-builtin' | 'project-builtin' | 'mindos-user' | 'mindos-global';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ScanSkillOptions {
|
|
16
|
+
projectRoot: string;
|
|
17
|
+
mindRoot: string;
|
|
18
|
+
disabledSkills?: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function parseSkillMd(content: string): { name: string; description: string } {
|
|
22
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
23
|
+
if (!match) return { name: '', description: '' };
|
|
24
|
+
const yaml = match[1];
|
|
25
|
+
const nameMatch = yaml.match(/^name:\s*(.+)/m);
|
|
26
|
+
const descMatch = yaml.match(/^description:\s*>?\s*\n?([\s\S]*?)(?=\n\w|\n---)/m);
|
|
27
|
+
const name = nameMatch ? nameMatch[1].trim() : '';
|
|
28
|
+
let description = '';
|
|
29
|
+
if (descMatch) {
|
|
30
|
+
description = descMatch[1].trim().split('\n').map((l) => l.trim()).join(' ').slice(0, 200);
|
|
31
|
+
} else {
|
|
32
|
+
const simpleDesc = yaml.match(/^description:\s*(.+)/m);
|
|
33
|
+
if (simpleDesc) description = simpleDesc[1].trim().slice(0, 200);
|
|
34
|
+
}
|
|
35
|
+
return { name, description };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getPiSkillSearchDirs(projectRoot: string, mindRoot: string) {
|
|
39
|
+
return [
|
|
40
|
+
{
|
|
41
|
+
origin: 'app-builtin' as const,
|
|
42
|
+
dir: path.join(projectRoot, 'app', 'data', 'skills'),
|
|
43
|
+
pathLabel: 'app/data/skills',
|
|
44
|
+
source: 'builtin' as const,
|
|
45
|
+
editable: false,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
origin: 'project-builtin' as const,
|
|
49
|
+
dir: path.join(projectRoot, 'skills'),
|
|
50
|
+
pathLabel: 'skills',
|
|
51
|
+
source: 'builtin' as const,
|
|
52
|
+
editable: false,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
origin: 'mindos-user' as const,
|
|
56
|
+
dir: path.join(mindRoot, '.skills'),
|
|
57
|
+
pathLabel: '{mindRoot}/.skills',
|
|
58
|
+
source: 'user' as const,
|
|
59
|
+
editable: true,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
origin: 'mindos-global' as const,
|
|
63
|
+
dir: path.join(os.homedir(), '.mindos', 'skills'),
|
|
64
|
+
pathLabel: '~/.mindos/skills',
|
|
65
|
+
source: 'user' as const,
|
|
66
|
+
editable: true,
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function scanSkillDirs(options: ScanSkillOptions): PiSkillInfo[] {
|
|
72
|
+
const { projectRoot, mindRoot, disabledSkills = [] } = options;
|
|
73
|
+
const skills: PiSkillInfo[] = [];
|
|
74
|
+
const seen = new Set<string>();
|
|
75
|
+
|
|
76
|
+
for (const sourceDef of getPiSkillSearchDirs(projectRoot, mindRoot)) {
|
|
77
|
+
if (!fs.existsSync(sourceDef.dir)) continue;
|
|
78
|
+
|
|
79
|
+
for (const entry of fs.readdirSync(sourceDef.dir, { withFileTypes: true })) {
|
|
80
|
+
if (!entry.isDirectory()) continue;
|
|
81
|
+
const skillFile = path.join(sourceDef.dir, entry.name, 'SKILL.md');
|
|
82
|
+
if (!fs.existsSync(skillFile)) continue;
|
|
83
|
+
|
|
84
|
+
const content = fs.readFileSync(skillFile, 'utf-8');
|
|
85
|
+
const { name, description } = parseSkillMd(content);
|
|
86
|
+
const skillName = name || entry.name;
|
|
87
|
+
if (!skillName || seen.has(skillName)) continue;
|
|
88
|
+
|
|
89
|
+
seen.add(skillName);
|
|
90
|
+
skills.push({
|
|
91
|
+
name: skillName,
|
|
92
|
+
description,
|
|
93
|
+
path: `${sourceDef.pathLabel}/${entry.name}/SKILL.md`,
|
|
94
|
+
source: sourceDef.source,
|
|
95
|
+
enabled: !disabledSkills.includes(skillName),
|
|
96
|
+
editable: sourceDef.editable,
|
|
97
|
+
origin: sourceDef.origin,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return skills;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function readSkillContentByName(name: string, options: Omit<ScanSkillOptions, 'disabledSkills'>): string | null {
|
|
106
|
+
const { projectRoot, mindRoot } = options;
|
|
107
|
+
|
|
108
|
+
for (const sourceDef of getPiSkillSearchDirs(projectRoot, mindRoot)) {
|
|
109
|
+
const file = path.join(sourceDef.dir, name, 'SKILL.md');
|
|
110
|
+
if (fs.existsSync(file)) {
|
|
111
|
+
return fs.readFileSync(file, 'utf-8');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return null;
|
|
116
|
+
}
|