@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 CHANGED
@@ -1,13 +1,14 @@
1
1
  # n2ns Nexus 🚀
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@datafrog-io/n2n-nexus.svg)](https://www.npmjs.com/package/@datafrog-io/n2n-nexus)
4
- [![npm downloads](https://img.shields.io/npm/dm/@datafrog-io/n2n-nexus.svg)](https://www.npmjs.com/package/@datafrog-io/n2n-nexus)
4
+ [![npm downloads](https://img.shields.io/npm/dt/@datafrog-io/n2n-nexus.svg)](https://www.npmjs.com/package/@datafrog-io/n2n-nexus)
5
+ [![MCP](https://img.shields.io/badge/MCP-Compatible-purple)](https://modelcontextprotocol.io)
5
6
  [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
6
7
  [![GitHub](https://img.shields.io/github/stars/n2ns/n2n-nexus?style=social)](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
- - `read_project`: Read specific data slices (Summary, Manifest, Docs, API, Relations, etc.).
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
- - `post_global_discussion`: Broadcast cross-project messages.
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` / `list_global_docs` / `read_global_doc`: Manage global common documents.
94
-
95
- ### D. Admin (Moderator Only)
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://hub/projects/{id}/manifest`: Full metadata for a specific project.
106
- - `mcp://hub/projects/{id}/internal-docs`: Internal technical docs for a specific project.
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
  [![npm version](https://img.shields.io/npm/v/@datafrog-io/n2n-nexus.svg)](https://www.npmjs.com/package/@datafrog-io/n2n-nexus)
4
- [![npm downloads](https://img.shields.io/npm/dm/@datafrog-io/n2n-nexus.svg)](https://www.npmjs.com/package/@datafrog-io/n2n-nexus)
4
+ [![npm downloads](https://img.shields.io/npm/dt/@datafrog-io/n2n-nexus.svg)](https://www.npmjs.com/package/@datafrog-io/n2n-nexus)
5
+ [![MCP](https://img.shields.io/badge/MCP-Compatible-purple)](https://modelcontextprotocol.io)
5
6
  [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
6
7
  [![GitHub](https://img.shields.io/github/stars/n2ns/n2n-nexus?style=social)](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`: **[核心]** 提交完整的项目 Manifest 和内部技术文档。
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
- - `read_project`: 读取项目的特定数据切片(Summary, Manifest, Docs, API, Relations等)。
63
+ - **读取操作**: 全部转为资源访问模式 (例如:`mcp://nexus/projects/${id}/manifest`)。
63
64
 
64
65
  ### C. 全局协作 (Global Collaboration)
65
- - `post_global_discussion`: 发送跨项目广播消息。
66
- - `update_global_strategy`: 更新核心战略蓝图 (`# Master Plan`)。
67
- - `get_global_topology`:获取全网项目依赖关系图谱。
68
- - `sync_global_doc` / `list_global_docs` / `read_global_doc`: 管理全局通用文档。
69
-
70
- ### D. 管理员 (Moderator Only)
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://hub/projects/{id}/manifest`: 特定项目的完整元数据。
81
- - `mcp://hub/projects/{id}/internal-docs`: 特定项目的内部技术文档。
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: "0.1.8" }, { capabilities: { resources: {}, tools: {}, prompts: {} } });
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: ${this.sanitizeErrorMessage(msg)}`);
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: ${this.sanitizeErrorMessage(msg)}`);
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
- this.checkModerator(name);
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: ${this.sanitizeErrorMessage(errorMessage)}` }]
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();
@@ -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.1.8",
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://hub/projects/")) {
74
+ if (uri.startsWith("mcp://nexus/projects/")) {
57
75
  if (uri.endsWith("/manifest")) {
58
- const id = uri.substring("mcp://hub/projects/".length, uri.lastIndexOf("/manifest"));
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://hub/projects/".length, uri.lastIndexOf("/internal-docs"));
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/global-strategy", name: "Master Strategy Blueprint", description: "Top-level cross-project coordination." },
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://hub/projects/${id}/manifest`,
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://hub/projects/{projectId}/internal-docs", name: "Internal Project Docs", description: "Markdown-based detailed implementation plans." },
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
  };
@@ -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
  */
@@ -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");