@datafrog-io/n2n-nexus 0.1.8 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -16
- package/README_zh.md +53 -17
- package/build/index.js +38 -22
- package/build/resources/index.js +33 -12
- package/build/storage/index.js +8 -0
- package/build/storage/meetings.js +35 -2
- package/build/storage/sqlite-meeting.js +37 -5
- package/build/storage/sqlite.js +15 -3
- package/build/storage/store.js +11 -4
- package/build/storage/tasks.js +204 -0
- package/build/tools/handlers.js +336 -176
- package/build/tools/index.js +12 -1
- package/build/tools/schemas.js +275 -0
- package/build/utils/auth.js +11 -0
- package/build/utils/error.js +15 -0
- package/docs/ASSISTANT_GUIDE.md +52 -62
- package/docs/CHANGELOG.md +29 -0
- package/docs/CHANGELOG_zh.md +29 -0
- package/package.json +10 -9
- package/build/tools/definitions.js +0 -288
package/README.md
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# n2ns Nexus 🚀
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@datafrog-io/n2n-nexus)
|
|
4
|
-
[](https://www.npmjs.com/package/@datafrog-io/n2n-nexus)
|
|
5
|
+
[](https://modelcontextprotocol.io)
|
|
5
6
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
6
7
|
[](https://github.com/n2ns/n2n-nexus)
|
|
7
8
|
|
|
8
9
|
**n2ns Nexus** is a "Local Digital Asset Hub" designed for multi-AI assistant collaboration. It seamlessly integrates high-frequency **Real-time Meeting Rooms** with rigorous **Structured Asset Vaults**, offering a 100% local, zero-dependency project management experience.
|
|
9
10
|
|
|
10
|
-
> **Works with:** VS Code · Cursor · Windsurf · Zed · JetBrains · Theia · Google Antigravity
|
|
11
|
+
> **Works with:** Claude Code · Claude Desktop · VS Code · Cursor · Windsurf · Zed · JetBrains · Theia · Google Antigravity
|
|
11
12
|
|
|
12
13
|
## 🏛️ Architecture
|
|
13
14
|
|
|
@@ -78,32 +79,48 @@ To ensure clarity and prevent collisions in the flat local namespace, all Projec
|
|
|
78
79
|
- `mcp://nexus/session`: View current identity, role (Moderator/Regular), and active project.
|
|
79
80
|
|
|
80
81
|
### B. Project Asset Management
|
|
81
|
-
- `sync_project_assets`: **[Core]** Submit full Project Manifest and Internal Docs.
|
|
82
|
+
- `sync_project_assets`: **[Core/ASYNC]** Submit full Project Manifest and Internal Docs. Returns `taskId`.
|
|
82
83
|
- **Manifest**: Includes ID, Tech Stack, **Relations**, Repo URL, Local Path, API Spec, etc.
|
|
83
84
|
- **Schema v2.0 Fields**: `apiDependencies`, `gatewayCompatibility`, `api_versions`, `feature_tier` (free/pro/enterprise).
|
|
84
85
|
- `update_project`: Partially update Manifest fields (e.g., endpoints or description only).
|
|
85
|
-
- `rename_project`: Rename Project ID with automatic cascading updates to all dependency references.
|
|
86
|
+
- `rename_project`: **[ASYNC]** Rename Project ID with automatic cascading updates to all dependency references. Returns `taskId`.
|
|
86
87
|
- `upload_project_asset`: Upload binary/text files (Base64) to the project vault.
|
|
87
|
-
-
|
|
88
|
+
- **Read Operations**: Use Resources (e.g., `mcp://nexus/projects/{id}/manifest`) for all read-only access.
|
|
88
89
|
|
|
89
90
|
### C. Global Collaboration
|
|
90
|
-
- `
|
|
91
|
+
- `send_message`: Post a message to the team (Auto-routes to active meeting).
|
|
92
|
+
- `read_messages`: Retrieve latest logs from active meeting or global registry.
|
|
91
93
|
- `update_global_strategy`: Update the core strategic blueprint (`# Master Plan`).
|
|
92
94
|
- `get_global_topology`: Retrieve the network-wide project dependency graph.
|
|
93
|
-
- `sync_global_doc
|
|
94
|
-
|
|
95
|
-
### D.
|
|
95
|
+
- `sync_global_doc`: Create or update a shared cross-project document.
|
|
96
|
+
|
|
97
|
+
### D. Meeting Management
|
|
98
|
+
- `start_meeting`: Start a new tactical session for focused collaboration.
|
|
99
|
+
- `reopen_meeting`: Reactivate a `closed` or `archived` session to continue discussion.
|
|
100
|
+
- `end_meeting`: Conclude a meeting, lock history (**Moderator only**).
|
|
101
|
+
- `archive_meeting`: Move closed meetings to cold storage (**Moderator only**).
|
|
102
|
+
|
|
103
|
+
### E. Task Management (Phase 2 - ASYNC)
|
|
104
|
+
- `create_task`: Create a new background task. Link to meeting for traceability.
|
|
105
|
+
- `get_task`: Poll status, progress (0.0-1.0), and results of a task.
|
|
106
|
+
- `list_tasks`: Query all tasks with status filtering.
|
|
107
|
+
- `update_task`: Update progress or result (typically for workers).
|
|
108
|
+
- `cancel_task`: Cancel a pending or running task.
|
|
109
|
+
|
|
110
|
+
### F. Admin (Moderator Only)
|
|
96
111
|
- `moderator_maintenance`: Prune or clear system logs.
|
|
97
112
|
- `moderator_delete_project`: Completely remove a project and its assets.
|
|
98
113
|
|
|
99
114
|
## 📄 Resources (URI)
|
|
100
115
|
|
|
101
|
-
- `mcp://chat/global`: Real-time conversation history.
|
|
102
|
-
- `mcp://hub/registry`: Global project registry overview.
|
|
103
|
-
- `mcp://docs/global-strategy`: Strategic blueprint.
|
|
104
|
-
- `mcp://nexus/session`: Current session status.
|
|
105
|
-
- `mcp://
|
|
106
|
-
- `mcp://
|
|
116
|
+
- `mcp://nexus/chat/global`: Real-time conversation history.
|
|
117
|
+
- `mcp://nexus/hub/registry`: Global project registry overview.
|
|
118
|
+
- `mcp://nexus/docs/global-strategy`: Strategic blueprint.
|
|
119
|
+
- `mcp://nexus/session`: Current session status and identity.
|
|
120
|
+
- `mcp://nexus/status`: System operational status and storage mode.
|
|
121
|
+
- `mcp://nexus/active-meeting`: Real-time transcript of the current active meeting.
|
|
122
|
+
- `mcp://nexus/projects/{id}/manifest`: Full metadata for a specific project.
|
|
123
|
+
- `mcp://nexus/projects/{id}/internal-docs`: Internal technical docs for a specific project.
|
|
107
124
|
|
|
108
125
|
## 🚀 Quick Start
|
|
109
126
|
|
|
@@ -172,8 +189,8 @@ The following files demonstrate a real orchestration session where **4 AI agents
|
|
|
172
189
|
|
|
173
190
|
| File | Description |
|
|
174
191
|
|------|-------------|
|
|
175
|
-
| [📖 Discussion Log (Markdown)](docs/discussion_2025-12-29_en.md) | Human-readable meeting transcript with formatting |
|
|
176
192
|
| [📋 Meeting Minutes](docs/MEETING_MINUTES_2025-12-29.md) | Structured summary of decisions, action items, and test results |
|
|
193
|
+
| [📖 Discussion Log (Markdown)](docs/discussion_2025-12-29_en.md) | Human-readable meeting transcript with formatting |
|
|
177
194
|
| [📦 Discussion Log (JSON)](docs/discussion_2025-12-29_en.json) | Raw meeting room data for programmatic access |
|
|
178
195
|
|
|
179
196
|
**Highlights from this session**:
|
package/README_zh.md
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# n2ns Nexus 🚀
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@datafrog-io/n2n-nexus)
|
|
4
|
-
[](https://www.npmjs.com/package/@datafrog-io/n2n-nexus)
|
|
5
|
+
[](https://modelcontextprotocol.io)
|
|
5
6
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
6
7
|
[](https://github.com/n2ns/n2n-nexus)
|
|
7
8
|
|
|
8
9
|
**n2ns Nexus** 是一个专为多 AI 助手协同设计的“本地数字化资产中心”。它将高频的**实时会议室**与严谨的**结构化资产库**完美融合,提供 100% 本地化、零外部依赖的项目管理体验。
|
|
9
10
|
|
|
10
|
-
> **支持的 IDE:** VS Code · Cursor · Windsurf · Zed · JetBrains · Theia · Google Antigravity
|
|
11
|
+
> **支持的 IDE:** Claude Code · Claude Desktop · VS Code · Cursor · Windsurf · Zed · JetBrains · Theia · Google Antigravity
|
|
11
12
|
|
|
12
13
|
## 🏛️ 系统架构 (Architecture)
|
|
13
14
|
|
|
@@ -54,31 +55,47 @@ Nexus_Storage/
|
|
|
54
55
|
- `mcp://nexus/session`: 查看当前身份、角色(Moderator/Regular)及活动项目。
|
|
55
56
|
|
|
56
57
|
### B. 项目资产管理 (Project Assets)
|
|
57
|
-
- `sync_project_assets`: **[
|
|
58
|
+
- `sync_project_assets`: **[核心/异步]** 提交完整的项目 Manifest 和内部技术文档。返回 `taskId`。
|
|
58
59
|
- **Manifest**: 包含 ID、技术栈、**依赖关系 (Relations)**、仓库地址、本地路径、API Spec 等。
|
|
59
60
|
- `update_project`: 部分更新 Manifest 字段(如仅更新 endpoints 或 description)。
|
|
60
|
-
- `rename_project`: 重命名项目 ID
|
|
61
|
+
- `rename_project`: **[异步]** 重命名项目 ID,自动级联更新所有相关项目的依赖引用。返回 `taskId`。
|
|
61
62
|
- `upload_project_asset`: 上传二进制/文本文件(Base64)到项目库。
|
|
62
|
-
-
|
|
63
|
+
- **读取操作**: 全部转为资源访问模式 (例如:`mcp://nexus/projects/${id}/manifest`)。
|
|
63
64
|
|
|
64
65
|
### C. 全局协作 (Global Collaboration)
|
|
65
|
-
- `
|
|
66
|
-
- `
|
|
67
|
-
- `
|
|
68
|
-
- `
|
|
69
|
-
|
|
70
|
-
|
|
66
|
+
- `send_message`: 发送消息(如果有活跃会议,将自动路由至会议)。
|
|
67
|
+
- `read_messages`: 读取团队消息(自动选取活跃会议或全局日志)。
|
|
68
|
+
- `update_global_strategy`: 更新核心战略蓝图(`# Master Plan`)。
|
|
69
|
+
- `get_global_topology`: 获取全网项目依赖拓扑图。
|
|
70
|
+
- `sync_global_doc`: 创建或更新全局共享文档。
|
|
71
|
+
|
|
72
|
+
### D. 会议管理 (Tactical Meetings)
|
|
73
|
+
- `start_meeting`: 开启新的战术讨论会议。
|
|
74
|
+
- `reopen_meeting`: 重新开启已“关闭”或“归档”的会议。
|
|
75
|
+
- `end_meeting`: 结束会议,锁定历史记录 (**仅限管理员 Moderator**)。
|
|
76
|
+
- `archive_meeting`: 将已结束的会议移至存档 (**仅限管理员 Moderator**)。
|
|
77
|
+
|
|
78
|
+
### E. 任务管理 (Phase 2 - 异步)
|
|
79
|
+
- `create_task`: 创建新的后台任务。关联会议以实现溯源。
|
|
80
|
+
- `get_task`: 轮询任务状态、进度 (0.0-1.0) 和结果。
|
|
81
|
+
- `list_tasks`: 查询所有任务,支持状态过滤。
|
|
82
|
+
- `update_task`: 更新任务进度或结果(通常供 Worker 调用)。
|
|
83
|
+
- `cancel_task`: 取消待处理或运行中的任务。
|
|
84
|
+
|
|
85
|
+
### F. 管理员工具 (仅限 Moderator)
|
|
71
86
|
- `moderator_maintenance`: 清理或修剪系统日志。
|
|
72
87
|
- `moderator_delete_project`: 彻底删除项目及其所有资产。
|
|
73
88
|
|
|
74
89
|
## 📄 资源 URI (Resources)
|
|
75
90
|
|
|
76
|
-
- `mcp://chat/global`: 实时对话流历史。
|
|
77
|
-
- `mcp://hub/registry`: 全局项目注册表概览。
|
|
78
|
-
- `mcp://docs/global-strategy`: 战略总领文档。
|
|
79
|
-
- `mcp://nexus/session`:
|
|
80
|
-
- `mcp://
|
|
81
|
-
- `mcp://
|
|
91
|
+
- `mcp://nexus/chat/global`: 实时对话流历史。
|
|
92
|
+
- `mcp://nexus/hub/registry`: 全局项目注册表概览。
|
|
93
|
+
- `mcp://nexus/docs/global-strategy`: 战略总领文档。
|
|
94
|
+
- `mcp://nexus/session`: 当前会话状态标识。
|
|
95
|
+
- `mcp://nexus/status`: 系统运行状态与存储模式。
|
|
96
|
+
- `mcp://nexus/active-meeting`: 当前活跃会议实录。
|
|
97
|
+
- `mcp://nexus/projects/{id}/manifest`: 特定项目的完整元数据。
|
|
98
|
+
- `mcp://nexus/projects/{id}/internal-docs`: 特定项目的内部技术文档。
|
|
82
99
|
|
|
83
100
|
## 🚀 快速启动
|
|
84
101
|
|
|
@@ -139,5 +156,24 @@ npm run build
|
|
|
139
156
|
npm start -- --id Master-AI --root ./my-storage
|
|
140
157
|
```
|
|
141
158
|
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## 📋 实战案例:多 AI 协同
|
|
162
|
+
以下文件展示了一个真实的编排会话,**4 个 AI 助手** (Claude, ChatGPT, Gemini, Augment) 协同设计并实现了身份验证系统和 Edge-Sync 协议:
|
|
163
|
+
|
|
164
|
+
| 文件 | 说明 |
|
|
165
|
+
|------|-------------|
|
|
166
|
+
| [📋 会议纪要](docs/MEETING_MINUTES_2025-12-29.md) | 决策、行动项和测试结果的结构化摘要 |
|
|
167
|
+
| [📖 讨论日志 (Markdown)](docs/discussion_2025-12-29_en.md) | 包含格式化的可读会议记录 |
|
|
168
|
+
| [📦 讨论日志 (JSON)](docs/discussion_2025-12-29_en.json) | 用于程序化访问的原始会议室数据 |
|
|
169
|
+
|
|
170
|
+
**本次会话亮点**:
|
|
171
|
+
- 🔐 跨 4 个项目的 OAuth 验证链调试
|
|
172
|
+
- 📜 带有 RSA 签名和周期控制的 Edge-Sync 协议 v1.1.1 设计
|
|
173
|
+
- ✅ 所有集成测试通过(Gateway, Backbone, Hub, Nexus Core)
|
|
174
|
+
- 🏗️ 带有 `apiDependencies` 追踪的 Manifest Schema v2.0
|
|
175
|
+
|
|
176
|
+
> *这就是 AI 原生开发的协作方式。*
|
|
177
|
+
|
|
142
178
|
---
|
|
143
179
|
© 2025 datafrog.io. Built for Local-Only AI Workflows.
|
package/build/index.js
CHANGED
|
@@ -2,10 +2,17 @@
|
|
|
2
2
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ErrorCode, McpError, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { readFileSync } from "fs";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
5
8
|
import { CONFIG } from "./config.js";
|
|
6
9
|
import { StorageManager } from "./storage/index.js";
|
|
7
10
|
import { TOOL_DEFINITIONS, handleToolCall } from "./tools/index.js";
|
|
8
11
|
import { listResources, getResourceContent } from "./resources/index.js";
|
|
12
|
+
import { sanitizeErrorMessage } from "./utils/error.js";
|
|
13
|
+
import { checkModeratorPermission } from "./utils/auth.js";
|
|
14
|
+
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
15
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8"));
|
|
9
16
|
/**
|
|
10
17
|
* n2ns Nexus: Unified Project Asset & Collaboration Hub
|
|
11
18
|
*
|
|
@@ -15,26 +22,9 @@ class NexusServer {
|
|
|
15
22
|
server;
|
|
16
23
|
currentProject = null;
|
|
17
24
|
constructor() {
|
|
18
|
-
this.server = new Server({ name: "n2n-nexus", version:
|
|
25
|
+
this.server = new Server({ name: "n2n-nexus", version: pkg.version }, { capabilities: { resources: {}, tools: {}, prompts: {} } });
|
|
19
26
|
this.setupHandlers();
|
|
20
27
|
}
|
|
21
|
-
/**
|
|
22
|
-
* Validates moderator permissions for admin tools.
|
|
23
|
-
*/
|
|
24
|
-
checkModerator(toolName) {
|
|
25
|
-
if (!CONFIG.isModerator) {
|
|
26
|
-
throw new McpError(ErrorCode.InvalidRequest, `Forbidden: ${toolName} requires Moderator rights.`);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Strips internal file paths from error messages to prevent path exposure to AI.
|
|
31
|
-
*/
|
|
32
|
-
sanitizeErrorMessage(msg) {
|
|
33
|
-
let sanitized = msg.replace(/[A-Za-z]:\\[^\s:]+/g, "[internal-path]");
|
|
34
|
-
sanitized = sanitized.replace(/\/[^\s:]+\/[^\s:]*/g, "[internal-path]");
|
|
35
|
-
sanitized = sanitized.replace(/\.\.[\\/][^\s]*/g, "[internal-path]");
|
|
36
|
-
return sanitized;
|
|
37
|
-
}
|
|
38
28
|
setupHandlers() {
|
|
39
29
|
// --- Resource Listing ---
|
|
40
30
|
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
@@ -43,7 +33,7 @@ class NexusServer {
|
|
|
43
33
|
}
|
|
44
34
|
catch (error) {
|
|
45
35
|
const msg = error instanceof Error ? error.message : String(error);
|
|
46
|
-
throw new McpError(ErrorCode.InternalError, `Nexus Registry Error: ${
|
|
36
|
+
throw new McpError(ErrorCode.InternalError, `Nexus Registry Error: ${sanitizeErrorMessage(msg)}`);
|
|
47
37
|
}
|
|
48
38
|
});
|
|
49
39
|
// --- Resource Reading ---
|
|
@@ -61,7 +51,7 @@ class NexusServer {
|
|
|
61
51
|
if (error instanceof McpError)
|
|
62
52
|
throw error;
|
|
63
53
|
const msg = error instanceof Error ? error.message : String(error);
|
|
64
|
-
throw new McpError(ErrorCode.InternalError, `Nexus Resource Error: ${
|
|
54
|
+
throw new McpError(ErrorCode.InternalError, `Nexus Resource Error: ${sanitizeErrorMessage(msg)}`);
|
|
65
55
|
}
|
|
66
56
|
});
|
|
67
57
|
// --- Tool Listing ---
|
|
@@ -73,7 +63,7 @@ class NexusServer {
|
|
|
73
63
|
const { name, arguments: toolArgs } = request.params;
|
|
74
64
|
try {
|
|
75
65
|
if (name.startsWith("moderator_"))
|
|
76
|
-
|
|
66
|
+
checkModeratorPermission(name);
|
|
77
67
|
const result = await handleToolCall(name, toolArgs, {
|
|
78
68
|
currentProject: this.currentProject,
|
|
79
69
|
setCurrentProject: (id) => { this.currentProject = id; },
|
|
@@ -89,7 +79,7 @@ class NexusServer {
|
|
|
89
79
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
90
80
|
return {
|
|
91
81
|
isError: true,
|
|
92
|
-
content: [{ type: "text", text: `Nexus Error: ${
|
|
82
|
+
content: [{ type: "text", text: `Nexus Error: ${sanitizeErrorMessage(errorMessage)}` }]
|
|
93
83
|
};
|
|
94
84
|
}
|
|
95
85
|
});
|
|
@@ -137,8 +127,34 @@ class NexusServer {
|
|
|
137
127
|
});
|
|
138
128
|
}
|
|
139
129
|
async run() {
|
|
130
|
+
// Handle graceful shutdown
|
|
131
|
+
const shutdown = async (signal) => {
|
|
132
|
+
console.error(`\n[Nexus] Received ${signal}. Shutting down...`);
|
|
133
|
+
try {
|
|
134
|
+
// Post-departure log
|
|
135
|
+
const msg = `Nexus Session Terminated (IDE Closed).`;
|
|
136
|
+
await StorageManager.addGlobalLog(`SYSTEM:${CONFIG.instanceId}`, msg, "UPDATE");
|
|
137
|
+
console.error(`[Nexus:${CONFIG.instanceId}] Goodbye!`);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// Ignore if storage is already cleaned up
|
|
141
|
+
}
|
|
142
|
+
process.exit(0);
|
|
143
|
+
};
|
|
144
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
145
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
140
146
|
const transport = new StdioServerTransport();
|
|
141
147
|
await this.server.connect(transport);
|
|
148
|
+
// Announce presence
|
|
149
|
+
try {
|
|
150
|
+
await StorageManager.init();
|
|
151
|
+
const onlineMsg = `Nexus Session Active (IDE Opened). Role: ${CONFIG.isModerator ? "Moderator" : "Regular"}`;
|
|
152
|
+
await StorageManager.addGlobalLog(`SYSTEM:${CONFIG.instanceId}`, onlineMsg, "UPDATE");
|
|
153
|
+
console.error(`[Nexus:${CONFIG.instanceId}] ${onlineMsg}`);
|
|
154
|
+
}
|
|
155
|
+
catch (e) {
|
|
156
|
+
console.error("[Nexus] Failed to post online message:", e);
|
|
157
|
+
}
|
|
142
158
|
}
|
|
143
159
|
}
|
|
144
160
|
const server = new NexusServer();
|
package/build/resources/index.js
CHANGED
|
@@ -7,15 +7,15 @@ import { UnifiedMeetingStore } from "../storage/store.js";
|
|
|
7
7
|
*/
|
|
8
8
|
export async function getResourceContent(uri, currentProject) {
|
|
9
9
|
await StorageManager.init();
|
|
10
|
-
if (uri === "mcp://chat/global") {
|
|
10
|
+
if (uri === "mcp://nexus/chat/global") {
|
|
11
11
|
const text = await fs.readFile(StorageManager.globalDiscussion, "utf-8");
|
|
12
12
|
return { mimeType: "application/json", text };
|
|
13
13
|
}
|
|
14
|
-
if (uri === "mcp://hub/registry") {
|
|
14
|
+
if (uri === "mcp://nexus/hub/registry") {
|
|
15
15
|
const text = await fs.readFile(StorageManager.registryFile, "utf-8");
|
|
16
16
|
return { mimeType: "application/json", text };
|
|
17
17
|
}
|
|
18
|
-
if (uri === "mcp://docs/global-strategy") {
|
|
18
|
+
if (uri === "mcp://nexus/docs/global-strategy") {
|
|
19
19
|
const text = await fs.readFile(StorageManager.globalBlueprint, "utf-8");
|
|
20
20
|
return { mimeType: "text/markdown", text };
|
|
21
21
|
}
|
|
@@ -33,7 +33,7 @@ export async function getResourceContent(uri, currentProject) {
|
|
|
33
33
|
const storageInfo = await UnifiedMeetingStore.getStorageInfo();
|
|
34
34
|
const status = {
|
|
35
35
|
status: "online",
|
|
36
|
-
version: "0.
|
|
36
|
+
version: "0.2.0",
|
|
37
37
|
...storageInfo,
|
|
38
38
|
active_meetings_count: state.activeMeetings.length,
|
|
39
39
|
default_meeting: state.defaultMeetingId
|
|
@@ -46,22 +46,40 @@ export async function getResourceContent(uri, currentProject) {
|
|
|
46
46
|
return { mimeType: "application/json", text: JSON.stringify(active, null, 2) };
|
|
47
47
|
return { mimeType: "application/json", text: JSON.stringify({ message: "No active meeting" }, null, 2) };
|
|
48
48
|
}
|
|
49
|
+
if (uri === "mcp://nexus/meetings/list") {
|
|
50
|
+
const meetings = await UnifiedMeetingStore.listMeetings();
|
|
51
|
+
return { mimeType: "application/json", text: JSON.stringify(meetings, null, 2) };
|
|
52
|
+
}
|
|
53
|
+
if (uri === "mcp://nexus/docs/list") {
|
|
54
|
+
const docs = await StorageManager.listGlobalDocs();
|
|
55
|
+
return { mimeType: "application/json", text: JSON.stringify(docs, null, 2) };
|
|
56
|
+
}
|
|
49
57
|
if (uri.startsWith("mcp://nexus/meetings/")) {
|
|
50
58
|
const meetingId = uri.substring("mcp://nexus/meetings/".length);
|
|
51
59
|
const mtg = await UnifiedMeetingStore.getMeeting(meetingId);
|
|
52
60
|
if (mtg)
|
|
53
61
|
return { mimeType: "application/json", text: JSON.stringify(mtg, null, 2) };
|
|
54
62
|
}
|
|
63
|
+
if (uri.startsWith("mcp://nexus/docs/")) {
|
|
64
|
+
const docId = uri.substring("mcp://nexus/docs/".length);
|
|
65
|
+
if (docId === "global-strategy") {
|
|
66
|
+
const text = await fs.readFile(StorageManager.globalBlueprint, "utf-8");
|
|
67
|
+
return { mimeType: "text/markdown", text };
|
|
68
|
+
}
|
|
69
|
+
const text = await StorageManager.getGlobalDoc(docId);
|
|
70
|
+
if (text)
|
|
71
|
+
return { mimeType: "text/markdown", text };
|
|
72
|
+
}
|
|
55
73
|
// Dynamic Project Resources (Handles Namespaces)
|
|
56
|
-
if (uri.startsWith("mcp://
|
|
74
|
+
if (uri.startsWith("mcp://nexus/projects/")) {
|
|
57
75
|
if (uri.endsWith("/manifest")) {
|
|
58
|
-
const id = uri.substring("mcp://
|
|
76
|
+
const id = uri.substring("mcp://nexus/projects/".length, uri.lastIndexOf("/manifest"));
|
|
59
77
|
const manifest = await StorageManager.getProjectManifest(id);
|
|
60
78
|
if (manifest)
|
|
61
79
|
return { mimeType: "application/json", text: JSON.stringify(manifest, null, 2) };
|
|
62
80
|
}
|
|
63
81
|
if (uri.endsWith("/internal-docs")) {
|
|
64
|
-
const id = uri.substring("mcp://
|
|
82
|
+
const id = uri.substring("mcp://nexus/projects/".length, uri.lastIndexOf("/internal-docs"));
|
|
65
83
|
const text = await StorageManager.getProjectDocs(id);
|
|
66
84
|
if (text)
|
|
67
85
|
return { mimeType: "text/markdown", text };
|
|
@@ -77,9 +95,11 @@ export async function listResources() {
|
|
|
77
95
|
const projectIds = Object.keys(registry.projects);
|
|
78
96
|
return {
|
|
79
97
|
resources: [
|
|
80
|
-
{ uri: "mcp://chat/global", name: "Global Collaboration History", description: "Real-time discussion stream." },
|
|
81
|
-
{ uri: "mcp://hub/registry", name: "Global Project Registry", description: "Consolidated index of all local projects." },
|
|
82
|
-
{ uri: "mcp://docs/
|
|
98
|
+
{ uri: "mcp://nexus/chat/global", name: "Global Collaboration History", description: "Real-time discussion stream." },
|
|
99
|
+
{ uri: "mcp://nexus/hub/registry", name: "Global Project Registry", description: "Consolidated index of all local projects." },
|
|
100
|
+
{ uri: "mcp://nexus/docs/list", name: "Global Documentation Index", description: "List of all shared cross-project documents." },
|
|
101
|
+
{ uri: "mcp://nexus/docs/global-strategy", name: "Master Strategy Blueprint", description: "Top-level cross-project coordination document." },
|
|
102
|
+
{ uri: "mcp://nexus/meetings/list", name: "Meeting Registry", description: "Consolidated list of active and closed meetings." },
|
|
83
103
|
{ uri: "mcp://nexus/session", name: "Current Session Info", description: "Your identity and role in this Nexus instance." },
|
|
84
104
|
{ uri: "mcp://nexus/status", name: "System Status & Storage Mode", description: "Backend storage mode (sqlite/json) and active meeting counts." },
|
|
85
105
|
{ uri: "mcp://nexus/active-meeting", name: "Current Active Meeting", description: "Full transcript and participants of the current default meeting." },
|
|
@@ -92,14 +112,15 @@ export async function listResources() {
|
|
|
92
112
|
lib: "📦 Library", bot: "🤖 Bot", infra: "☁️ Infra", doc: "📄 Docs"
|
|
93
113
|
}[prefix] || "📁 Project";
|
|
94
114
|
return {
|
|
95
|
-
uri: `mcp://
|
|
115
|
+
uri: `mcp://nexus/projects/${id}/manifest`,
|
|
96
116
|
name: `${typeLabel}: ${id}`,
|
|
97
117
|
description: `Structured metadata (Tech stack, relations) for ${id}`
|
|
98
118
|
};
|
|
99
119
|
})
|
|
100
120
|
],
|
|
101
121
|
resourceTemplates: [
|
|
102
|
-
{ uriTemplate: "mcp://
|
|
122
|
+
{ uriTemplate: "mcp://nexus/projects/{projectId}/internal-docs", name: "Internal Project Docs", description: "Markdown-based detailed implementation plans." },
|
|
123
|
+
{ uriTemplate: "mcp://nexus/docs/{docId}", name: "Specific Global Doc", description: "Read a specific document from the global index." },
|
|
103
124
|
{ uriTemplate: "mcp://nexus/meetings/{meetingId}", name: "Meeting Insights", description: "Full transcript and decisions for a specific meeting." }
|
|
104
125
|
]
|
|
105
126
|
};
|
package/build/storage/index.js
CHANGED
|
@@ -24,6 +24,14 @@ export class StorageManager {
|
|
|
24
24
|
if (!await this.exists(this.globalBlueprint)) {
|
|
25
25
|
await fs.writeFile(this.globalBlueprint, "# Global Coordination Blueprint\n\nShared meeting space.");
|
|
26
26
|
}
|
|
27
|
+
// Initialize Phase 2 Tasks table (SQLite) - uses dynamic import to avoid circular dependency
|
|
28
|
+
try {
|
|
29
|
+
const { initTasksTable } = await import("./tasks.js");
|
|
30
|
+
initTasksTable();
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// SQLite may not be available or database not ready - will be initialized on first use
|
|
34
|
+
}
|
|
27
35
|
}
|
|
28
36
|
/**
|
|
29
37
|
* Proactively reads and validates JSON.
|
|
@@ -102,6 +102,7 @@ export class MeetingStore {
|
|
|
102
102
|
topic,
|
|
103
103
|
status: "active",
|
|
104
104
|
startTime: new Date().toISOString(),
|
|
105
|
+
initiator,
|
|
105
106
|
participants: [initiator],
|
|
106
107
|
messages: [],
|
|
107
108
|
decisions: []
|
|
@@ -152,13 +153,17 @@ export class MeetingStore {
|
|
|
152
153
|
/**
|
|
153
154
|
* End a meeting (close it)
|
|
154
155
|
*/
|
|
155
|
-
static async endMeeting(meetingId, summary) {
|
|
156
|
+
static async endMeeting(meetingId, summary, callerId) {
|
|
156
157
|
return this.stateLock.withLock(async () => {
|
|
157
158
|
const meeting = await this.getMeeting(meetingId);
|
|
158
159
|
if (!meeting)
|
|
159
160
|
throw new Error(`Meeting '${meetingId}' not found.`);
|
|
160
161
|
if (meeting.status !== "active")
|
|
161
162
|
throw new Error(`Meeting '${meetingId}' is already ${meeting.status}.`);
|
|
163
|
+
// Permission check: Only initiator can end
|
|
164
|
+
if (callerId && meeting.initiator && meeting.initiator !== callerId) {
|
|
165
|
+
throw new Error(`Permission denied: Only initiator (${meeting.initiator}) can end this meeting.`);
|
|
166
|
+
}
|
|
162
167
|
// Close the meeting
|
|
163
168
|
meeting.status = "closed";
|
|
164
169
|
meeting.endTime = new Date().toISOString();
|
|
@@ -182,15 +187,43 @@ export class MeetingStore {
|
|
|
182
187
|
/**
|
|
183
188
|
* Archive a closed meeting
|
|
184
189
|
*/
|
|
185
|
-
static async archiveMeeting(meetingId) {
|
|
190
|
+
static async archiveMeeting(meetingId, callerId) {
|
|
186
191
|
const meeting = await this.getMeeting(meetingId);
|
|
187
192
|
if (!meeting)
|
|
188
193
|
throw new Error(`Meeting '${meetingId}' not found.`);
|
|
189
194
|
if (meeting.status === "active")
|
|
190
195
|
throw new Error(`Meeting '${meetingId}' is still active. End it first.`);
|
|
196
|
+
// Permission check: Only initiator can archive
|
|
197
|
+
if (callerId && meeting.initiator && meeting.initiator !== callerId) {
|
|
198
|
+
throw new Error(`Permission denied: Only initiator (${meeting.initiator}) can archive this meeting.`);
|
|
199
|
+
}
|
|
191
200
|
meeting.status = "archived";
|
|
192
201
|
await fs.writeFile(this.getMeetingPath(meetingId), JSON.stringify(meeting, null, 2), "utf-8");
|
|
193
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* Reopen a closed or archived meeting
|
|
205
|
+
*/
|
|
206
|
+
static async reopenMeeting(meetingId, _callerId) {
|
|
207
|
+
return this.stateLock.withLock(async () => {
|
|
208
|
+
const meeting = await this.getMeeting(meetingId);
|
|
209
|
+
if (!meeting)
|
|
210
|
+
throw new Error(`Meeting '${meetingId}' not found.`);
|
|
211
|
+
if (meeting.status === "active")
|
|
212
|
+
throw new Error(`Meeting '${meetingId}' is already active.`);
|
|
213
|
+
// Update status to active
|
|
214
|
+
meeting.status = "active";
|
|
215
|
+
meeting.endTime = undefined;
|
|
216
|
+
await fs.writeFile(this.getMeetingPath(meetingId), JSON.stringify(meeting, null, 2), "utf-8");
|
|
217
|
+
// Update state
|
|
218
|
+
const state = await this.loadStateSafe();
|
|
219
|
+
if (!state.activeMeetings.includes(meetingId)) {
|
|
220
|
+
state.activeMeetings.push(meetingId);
|
|
221
|
+
}
|
|
222
|
+
state.defaultMeetingId = meetingId;
|
|
223
|
+
await this.saveState(state);
|
|
224
|
+
return meeting;
|
|
225
|
+
});
|
|
226
|
+
}
|
|
194
227
|
/**
|
|
195
228
|
* List all meetings with optional status filter
|
|
196
229
|
*/
|
|
@@ -39,10 +39,10 @@ export class SqliteMeetingStore {
|
|
|
39
39
|
const now = new Date().toISOString();
|
|
40
40
|
const participants = JSON.stringify([initiator]);
|
|
41
41
|
const stmt = db.prepare(`
|
|
42
|
-
INSERT INTO meetings (id, topic, status, participants, created_at)
|
|
43
|
-
VALUES (?, ?, 'active', ?, ?)
|
|
42
|
+
INSERT INTO meetings (id, topic, status, initiator, participants, created_at)
|
|
43
|
+
VALUES (?, ?, 'active', ?, ?, ?)
|
|
44
44
|
`);
|
|
45
|
-
stmt.run(id, topic, participants, now);
|
|
45
|
+
stmt.run(id, topic, initiator, participants, now);
|
|
46
46
|
// Update state
|
|
47
47
|
this.updateState("default_meeting", id);
|
|
48
48
|
const activeMeetings = this.getActiveMeetingIds();
|
|
@@ -53,6 +53,7 @@ export class SqliteMeetingStore {
|
|
|
53
53
|
topic,
|
|
54
54
|
status: "active",
|
|
55
55
|
startTime: now,
|
|
56
|
+
initiator,
|
|
56
57
|
participants: [initiator],
|
|
57
58
|
messages: [],
|
|
58
59
|
decisions: []
|
|
@@ -86,6 +87,7 @@ export class SqliteMeetingStore {
|
|
|
86
87
|
status: meeting.status,
|
|
87
88
|
startTime: meeting.created_at,
|
|
88
89
|
endTime: meeting.closed_at || undefined,
|
|
90
|
+
initiator: meeting.initiator || "Unknown",
|
|
89
91
|
participants: JSON.parse(meeting.participants),
|
|
90
92
|
messages,
|
|
91
93
|
decisions,
|
|
@@ -129,7 +131,7 @@ export class SqliteMeetingStore {
|
|
|
129
131
|
/**
|
|
130
132
|
* End a meeting
|
|
131
133
|
*/
|
|
132
|
-
static endMeeting(meetingId, summary) {
|
|
134
|
+
static endMeeting(meetingId, summary, callerId) {
|
|
133
135
|
const db = getDatabase();
|
|
134
136
|
const now = new Date().toISOString();
|
|
135
137
|
// Check meeting exists and is active
|
|
@@ -138,6 +140,10 @@ export class SqliteMeetingStore {
|
|
|
138
140
|
throw new Error(`Meeting '${meetingId}' not found.`);
|
|
139
141
|
if (meeting.status !== "active")
|
|
140
142
|
throw new Error(`Meeting '${meetingId}' is already ${meeting.status}.`);
|
|
143
|
+
// Permission check: Only initiator can end
|
|
144
|
+
if (callerId && meeting.initiator && meeting.initiator !== callerId) {
|
|
145
|
+
throw new Error(`Permission denied: Only initiator (${meeting.initiator}) can end this meeting.`);
|
|
146
|
+
}
|
|
141
147
|
// Update meeting
|
|
142
148
|
const stmt = db.prepare(`
|
|
143
149
|
UPDATE meetings SET status = 'closed', closed_at = ?, summary = ?
|
|
@@ -159,16 +165,42 @@ export class SqliteMeetingStore {
|
|
|
159
165
|
/**
|
|
160
166
|
* Archive a meeting
|
|
161
167
|
*/
|
|
162
|
-
static archiveMeeting(meetingId) {
|
|
168
|
+
static archiveMeeting(meetingId, callerId) {
|
|
163
169
|
const db = getDatabase();
|
|
164
170
|
const meeting = this.getMeeting(meetingId);
|
|
165
171
|
if (!meeting)
|
|
166
172
|
throw new Error(`Meeting '${meetingId}' not found.`);
|
|
167
173
|
if (meeting.status === "active")
|
|
168
174
|
throw new Error(`Meeting '${meetingId}' is still active. End it first.`);
|
|
175
|
+
// Permission check: Only initiator can archive
|
|
176
|
+
if (callerId && meeting.initiator && meeting.initiator !== callerId) {
|
|
177
|
+
throw new Error(`Permission denied: Only initiator (${meeting.initiator}) can archive this meeting.`);
|
|
178
|
+
}
|
|
169
179
|
const stmt = db.prepare("UPDATE meetings SET status = 'archived' WHERE id = ?");
|
|
170
180
|
stmt.run(meetingId);
|
|
171
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Reopen a closed or archived meeting
|
|
184
|
+
*/
|
|
185
|
+
static reopenMeeting(meetingId, _callerId) {
|
|
186
|
+
const db = getDatabase();
|
|
187
|
+
const meeting = this.getMeeting(meetingId);
|
|
188
|
+
if (!meeting)
|
|
189
|
+
throw new Error(`Meeting '${meetingId}' not found.`);
|
|
190
|
+
if (meeting.status === "active")
|
|
191
|
+
throw new Error(`Meeting '${meetingId}' is already active.`);
|
|
192
|
+
// Update status to active
|
|
193
|
+
const stmt = db.prepare("UPDATE meetings SET status = 'active', closed_at = NULL WHERE id = ?");
|
|
194
|
+
stmt.run(meetingId);
|
|
195
|
+
// Update state
|
|
196
|
+
const activeMeetings = this.getActiveMeetingIds();
|
|
197
|
+
if (!activeMeetings.includes(meetingId)) {
|
|
198
|
+
activeMeetings.push(meetingId);
|
|
199
|
+
this.updateState("active_meetings", JSON.stringify(activeMeetings));
|
|
200
|
+
}
|
|
201
|
+
this.updateState("default_meeting", meetingId);
|
|
202
|
+
return this.getMeeting(meetingId);
|
|
203
|
+
}
|
|
172
204
|
/**
|
|
173
205
|
* List meetings with optional status filter
|
|
174
206
|
*/
|
package/build/storage/sqlite.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Database from "better-sqlite3";
|
|
2
2
|
import path from "path";
|
|
3
|
+
import { createRequire } from "module";
|
|
3
4
|
import { CONFIG } from "../config.js";
|
|
4
5
|
let db = null;
|
|
5
6
|
/**
|
|
@@ -11,6 +12,7 @@ CREATE TABLE IF NOT EXISTS meetings (
|
|
|
11
12
|
id TEXT PRIMARY KEY,
|
|
12
13
|
topic TEXT NOT NULL,
|
|
13
14
|
status TEXT CHECK(status IN ('active', 'closed', 'archived')) DEFAULT 'active',
|
|
15
|
+
initiator TEXT,
|
|
14
16
|
participants TEXT DEFAULT '[]',
|
|
15
17
|
created_at TEXT NOT NULL,
|
|
16
18
|
closed_at TEXT,
|
|
@@ -67,6 +69,18 @@ export function initDatabase() {
|
|
|
67
69
|
db.pragma("journal_mode = WAL");
|
|
68
70
|
// Initialize schema
|
|
69
71
|
db.exec(SCHEMA);
|
|
72
|
+
// Migration: Add initiator column if it doesn't exist (Upgrade from v0.1.7)
|
|
73
|
+
try {
|
|
74
|
+
const columns = db.prepare("PRAGMA table_info(meetings)").all();
|
|
75
|
+
const hasInitiator = columns.some(c => c.name === "initiator");
|
|
76
|
+
if (!hasInitiator) {
|
|
77
|
+
console.error("[Nexus] Migrating database: Adding 'initiator' column to 'meetings' table.");
|
|
78
|
+
db.exec("ALTER TABLE meetings ADD COLUMN initiator TEXT");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
console.error("[Nexus] Migration check failed:", e);
|
|
83
|
+
}
|
|
70
84
|
// Initialize default state if not exists
|
|
71
85
|
const stmt = db.prepare("INSERT OR IGNORE INTO meeting_state (key, value) VALUES (?, ?)");
|
|
72
86
|
stmt.run("active_meetings", "[]");
|
|
@@ -93,10 +107,8 @@ export function closeDatabase() {
|
|
|
93
107
|
console.error("[Nexus] SQLite database closed");
|
|
94
108
|
}
|
|
95
109
|
}
|
|
96
|
-
/**
|
|
97
|
-
* Check if SQLite is available
|
|
98
|
-
*/
|
|
99
110
|
export function isSqliteAvailable() {
|
|
111
|
+
const require = createRequire(import.meta.url);
|
|
100
112
|
try {
|
|
101
113
|
// Try to load better-sqlite3
|
|
102
114
|
require("better-sqlite3");
|