@geminilight/mindos 0.5.70 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/app/app/api/ask/route.ts +122 -92
  2. package/app/app/api/mcp/agents/route.ts +53 -2
  3. package/app/app/api/mcp/status/route.ts +1 -1
  4. package/app/app/api/skills/route.ts +10 -114
  5. package/app/components/ActivityBar.tsx +3 -4
  6. package/app/components/CreateSpaceModal.tsx +31 -6
  7. package/app/components/FileTree.tsx +33 -2
  8. package/app/components/GuideCard.tsx +197 -131
  9. package/app/components/HomeContent.tsx +85 -18
  10. package/app/components/SidebarLayout.tsx +13 -0
  11. package/app/components/SpaceInitToast.tsx +173 -0
  12. package/app/components/agents/AgentDetailContent.tsx +32 -17
  13. package/app/components/agents/AgentsContentPage.tsx +2 -1
  14. package/app/components/agents/AgentsOverviewSection.tsx +1 -14
  15. package/app/components/agents/agents-content-model.ts +16 -8
  16. package/app/components/ask/AskContent.tsx +137 -50
  17. package/app/components/ask/MentionPopover.tsx +16 -8
  18. package/app/components/ask/SlashCommandPopover.tsx +62 -0
  19. package/app/components/settings/KnowledgeTab.tsx +61 -0
  20. package/app/components/walkthrough/steps.ts +11 -6
  21. package/app/hooks/useMention.ts +14 -6
  22. package/app/hooks/useSlashCommand.ts +114 -0
  23. package/app/lib/actions.ts +79 -2
  24. package/app/lib/agent/index.ts +1 -1
  25. package/app/lib/agent/prompt.ts +2 -0
  26. package/app/lib/agent/tools.ts +106 -0
  27. package/app/lib/core/create-space.ts +11 -4
  28. package/app/lib/core/index.ts +1 -1
  29. package/app/lib/i18n-en.ts +51 -46
  30. package/app/lib/i18n-zh.ts +50 -45
  31. package/app/lib/mcp-agents.ts +8 -0
  32. package/app/lib/pdf-extract.ts +33 -0
  33. package/app/lib/pi-integration/extensions.ts +45 -0
  34. package/app/lib/pi-integration/mcporter.ts +219 -0
  35. package/app/lib/pi-integration/session-store.ts +62 -0
  36. package/app/lib/pi-integration/skills.ts +116 -0
  37. package/app/lib/settings.ts +1 -1
  38. package/app/next-env.d.ts +1 -1
  39. package/app/next.config.ts +1 -1
  40. package/app/package.json +2 -0
  41. package/mcp/src/index.ts +29 -0
  42. package/package.json +1 -1
@@ -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: '开始使用 MindOS',
1193
+ title: '快速上手',
1171
1194
  showGuide: '显示新手引导',
1172
1195
  close: '关闭',
1173
1196
  skip: '跳过',
1174
- kb: {
1175
- title: '探索你的知识库',
1176
- cta: '开始',
1177
- fullDesc: '你的知识库有 6 个区域,试试点开看看:',
1178
- dirs: {
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: ' AI 对话',
1199
- cta: '开始',
1200
- prompt: '读一下我的知识库,帮我把自我介绍写进 Profile。',
1204
+ title: '感受 AI 读取内容',
1205
+ cta: '试试',
1206
+ desc: '你的文件已在知识库中。问问 MindOS Agent 它了解了什么:',
1207
+ prompt: '根据我的知识库介绍一下我——我是谁、在做什么?',
1201
1208
  promptEmpty: '帮我设计一个适合我的知识库目录结构',
1202
1209
  },
1203
- sync: {
1204
- title: '同步笔记',
1205
- optional: '可选',
1206
- cta: '设置',
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: '这是你的 Activity Bar — 在这里切换文件、搜索、插件和 Agent。',
1296
- },
1297
- {
1298
- title: '你的知识库',
1299
- body: '在文件面板中浏览和管理你的笔记、画像和项目。',
1303
+ title: '你的项目记忆',
1304
+ body: ' Space 管理项目、SOP 和偏好规则。数据存在本地,完全由你掌控。',
1300
1305
  },
1301
1306
  {
1302
- title: 'AI 对话',
1303
- body: '和了解你整个知识库的 AI 对话。随时按 ⌘/ 启动。',
1307
+ title: '不用重讲背景的 AI',
1308
+ body: 'MindOS Agent 自动读取整个知识库。问它项目相关的事,不需要重复交代背景。',
1304
1309
  },
1305
1310
  {
1306
- title: '快速搜索',
1307
- body: ' ⌘K 即可搜索所有笔记,瞬间找到任何文件。',
1311
+ title: '多 Agent 共享记忆',
1312
+ body: '通过 MCP 连接 Cursor、Claude Code、Windsurf,它们共享同一份项目记忆。',
1308
1313
  },
1309
1314
  {
1310
- title: '设置',
1311
- body: '在这里配置 AI 服务商、MCP 连接、同步和外观。',
1315
+ title: '回响 — 认知在积累',
1316
+ body: '关于你、每日回顾、成长轨迹——MindOS 帮你沉淀判断与偏好,让经验不断复利。',
1312
1317
  },
1313
1318
  ],
1314
1319
  },
@@ -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,45 @@
1
+ import path from 'path';
2
+ import { DefaultResourceLoader, SettingsManager } from '@mariozechner/pi-coding-agent';
3
+
4
+ export interface ExtensionSummary {
5
+ name: string;
6
+ path: string;
7
+ enabled: boolean;
8
+ tools: string[];
9
+ commands: string[];
10
+ }
11
+
12
+ export async function getExtensionsList(
13
+ projectRoot: string,
14
+ _mindRoot: string,
15
+ disabledExtensions: string[] = [],
16
+ ): Promise<ExtensionSummary[]> {
17
+ const settingsManager = SettingsManager.inMemory();
18
+
19
+ const loader = new DefaultResourceLoader({
20
+ cwd: projectRoot,
21
+ settingsManager,
22
+ systemPromptOverride: () => '',
23
+ appendSystemPromptOverride: () => [],
24
+ additionalSkillPaths: [],
25
+ });
26
+
27
+ try {
28
+ await loader.reload();
29
+ const result = loader.getExtensions();
30
+
31
+ return result.extensions.map((ext) => {
32
+ const name = path.basename(ext.path, path.extname(ext.path));
33
+ return {
34
+ name,
35
+ path: ext.resolvedPath || ext.path,
36
+ enabled: !disabledExtensions.includes(name),
37
+ tools: [...ext.tools.keys()],
38
+ commands: [...ext.commands.keys()],
39
+ };
40
+ });
41
+ } catch (error) {
42
+ console.error('[getExtensionsList] Failed to load extensions:', error);
43
+ return [];
44
+ }
45
+ }
@@ -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
+ }
@@ -32,7 +32,7 @@ export interface GuideState {
32
32
  step1Done: boolean; // 至少浏览过 1 个文件
33
33
  askedAI: boolean; // 至少发过 1 条 AI 消息
34
34
  nextStepIndex: number; // 0=C2, 1=C3, 2=C4, 3=全部完成
35
- walkthroughStep?: number; // -1=not started, 0-4=current step, 5=completed
35
+ walkthroughStep?: number; // undefined=not started, 0-3=current step, 4=completed
36
36
  walkthroughDismissed?: boolean; // user skipped walkthrough
37
37
  }
38
38
 
package/app/next-env.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /// <reference types="next" />
2
2
  /// <reference types="next/image-types/global" />
3
- import "./.next/types/routes.d.ts";
3
+ import "./.next/dev/types/routes.d.ts";
4
4
 
5
5
  // NOTE: This file should not be edited
6
6
  // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.