@datafrog-io/n2n-nexus 0.1.8 β†’ 0.2.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/README.md CHANGED
@@ -1,13 +1,16 @@
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
12
+
13
+ πŸ“– **Documentation:** [CHANGELOG](CHANGELOG.md) | [TODO](TODO.md) | [δΈ­ζ–‡ζ–‡ζ‘£](docs/README_zh.md) | [AI Assistant Guide](docs/ASSISTANT_GUIDE.md)
11
14
 
12
15
  ## πŸ›οΈ Architecture
13
16
 
@@ -30,19 +33,21 @@ Nexus stores all data in the local file system (customizable path), ensuring com
30
33
  Nexus_Storage/
31
34
  β”œβ”€β”€ global/
32
35
  β”‚ β”œβ”€β”€ blueprint.md # Master Strategy
33
- β”‚ β”œβ”€β”€ discussion.json # Chat History
34
- β”‚ β”œβ”€β”€ docs_index.json # Global Docs Metadata
36
+ β”‚ β”œβ”€β”€ discussion.json # Global Chat History (fallback)
37
+ β”‚ β”œβ”€β”€ docs_index.json # Global Docs Index
35
38
  β”‚ └── docs/ # Global Markdown Docs
36
39
  β”‚ β”œβ”€β”€ coding-standards.md
37
40
  β”‚ └── deployment-flow.md
38
41
  β”œβ”€β”€ projects/
39
- β”‚ β”œβ”€β”€ my-app/
40
- β”‚ β”‚ β”œβ”€β”€ manifest.json # Project Metadata
41
- β”‚ β”‚ β”œβ”€β”€ internal_blueprint.md
42
- β”‚ β”‚ └── assets/ # Binary Assets
43
- β”‚ └── ...
42
+ β”‚ └── {project-id}/
43
+ β”‚ β”œβ”€β”€ manifest.json # Project Metadata
44
+ β”‚ β”œβ”€β”€ internal_blueprint.md # Technical Implementation Docs
45
+ β”‚ └── assets/ # Binary Assets (images, PDFs)
46
+ β”œβ”€β”€ meetings/ # Meeting files (JSON fallback mode)
47
+ β”‚ └── {meeting-id}.json
44
48
  β”œβ”€β”€ registry.json # Global Project Index
45
- └── archives/ # (Reserved for backups)
49
+ β”œβ”€β”€ archives/ # Reserved for backups
50
+ └── nexus.db # SQLite Database (meetings, tasks, state)
46
51
  ```
47
52
 
48
53
  **Self-healing**: Core data files (e.g., `registry.json`, `discussion.json`) include automatic detection and repair mechanisms. If files are corrupted or missing, the system automatically rebuilds the initial state to ensure uninterrupted service.
@@ -78,32 +83,55 @@ To ensure clarity and prevent collisions in the flat local namespace, all Projec
78
83
  - `mcp://nexus/session`: View current identity, role (Moderator/Regular), and active project.
79
84
 
80
85
  ### B. Project Asset Management
81
- - `sync_project_assets`: **[Core]** Submit full Project Manifest and Internal Docs.
86
+ - `sync_project_assets`: **[Core/ASYNC]** Submit full Project Manifest and Internal Docs. Returns `taskId`.
82
87
  - **Manifest**: Includes ID, Tech Stack, **Relations**, Repo URL, Local Path, API Spec, etc.
83
88
  - **Schema v2.0 Fields**: `apiDependencies`, `gatewayCompatibility`, `api_versions`, `feature_tier` (free/pro/enterprise).
84
89
  - `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.
90
+ - `rename_project`: **[ASYNC]** Rename Project ID with automatic cascading updates to all dependency references. Returns `taskId`.
86
91
  - `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.).
92
+ - **Read Operations**: Use Resources (e.g., `mcp://nexus/projects/{id}/manifest`) for all read-only access.
88
93
 
89
94
  ### C. Global Collaboration
90
- - `post_global_discussion`: Broadcast cross-project messages.
95
+ - `send_message`: Post a message to the team (Auto-routes to active meeting).
96
+ - `read_messages`: **[Incremental]** Returns only unread messages per IDE instance. Server tracks read cursor automatically.
91
97
  - `update_global_strategy`: Update the core strategic blueprint (`# Master Plan`).
92
- - `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)
98
+ - `get_global_topology`: **[Progressive]** Default: summary list. With `projectId`: detailed subgraph.
99
+ - `sync_global_doc`: Create or update a shared cross-project document.
100
+
101
+ ### D. Meeting Management
102
+ - `start_meeting`: Start a new tactical session for focused collaboration.
103
+ - `reopen_meeting`: Reactivate a `closed` or `archived` session to continue discussion.
104
+ - `end_meeting`: Conclude a meeting, lock history (**Moderator only**).
105
+ - `archive_meeting`: Move closed meetings to cold storage (**Moderator only**).
106
+
107
+ ### E. Task Management (Phase 2 - ASYNC)
108
+ - `create_task`: Create a new background task. Link to meeting for traceability.
109
+ - `get_task`: Poll status, progress (0.0-1.0), and results of a task.
110
+ - `list_tasks`: Query all tasks with status filtering.
111
+ - `update_task`: Update progress or result (typically for workers).
112
+ - `cancel_task`: Cancel a pending or running task.
113
+
114
+ ### F. Admin (Moderator Only)
96
115
  - `moderator_maintenance`: Prune or clear system logs.
97
116
  - `moderator_delete_project`: Completely remove a project and its assets.
98
117
 
99
118
  ## πŸ“„ Resources (URI)
100
119
 
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.
120
+ **Core Resources (Static):**
121
+ - `mcp://nexus/chat/global`: Real-time conversation history.
122
+ - `mcp://nexus/hub/registry`: Global project registry - **read this first to discover project IDs**.
123
+ - `mcp://nexus/docs/global-strategy`: Strategic blueprint.
124
+ - `mcp://nexus/docs/list`: Index of shared documents.
125
+ - `mcp://nexus/meetings/list`: List of active and closed meetings.
126
+ - `mcp://nexus/session`: Current session status and identity.
127
+ - `mcp://nexus/status`: System operational status and storage mode.
128
+ - `mcp://nexus/active-meeting`: Real-time transcript of the current active meeting.
129
+
130
+ **Resource Templates (Use registry to discover IDs):**
131
+ - `mcp://nexus/projects/{projectId}/manifest`: Full metadata for a specific project.
132
+ - `mcp://nexus/projects/{projectId}/internal-docs`: Internal technical docs for a project.
133
+ - `mcp://nexus/docs/{docId}`: Read a specific shared document.
134
+ - `mcp://nexus/meetings/{meetingId}`: Full transcript for a specific meeting.
107
135
 
108
136
  ## πŸš€ Quick Start
109
137
 
@@ -172,8 +200,8 @@ The following files demonstrate a real orchestration session where **4 AI agents
172
200
 
173
201
  | File | Description |
174
202
  |------|-------------|
175
- | [πŸ“– Discussion Log (Markdown)](docs/discussion_2025-12-29_en.md) | Human-readable meeting transcript with formatting |
176
203
  | [πŸ“‹ Meeting Minutes](docs/MEETING_MINUTES_2025-12-29.md) | Structured summary of decisions, action items, and test results |
204
+ | [πŸ“– Discussion Log (Markdown)](docs/discussion_2025-12-29_en.md) | Human-readable meeting transcript with formatting |
177
205
  | [πŸ“¦ Discussion Log (JSON)](docs/discussion_2025-12-29_en.json) | Raw meeting room data for programmatic access |
178
206
 
179
207
  **Highlights from this session**:
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.1",
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 };
@@ -71,36 +89,31 @@ export async function getResourceContent(uri, currentProject) {
71
89
  }
72
90
  /**
73
91
  * Returns the list of available resources for MCP ListResourcesRequestSchema
92
+ *
93
+ * OPTIMIZATION: No longer lists individual projects dynamically.
94
+ * Uses resourceTemplates instead - AI should query registry first.
74
95
  */
75
96
  export async function listResources() {
76
97
  const registry = await StorageManager.listRegistry();
77
- const projectIds = Object.keys(registry.projects);
98
+ const projectCount = Object.keys(registry.projects).length;
78
99
  return {
79
100
  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." },
83
- { uri: "mcp://nexus/session", name: "Current Session Info", description: "Your identity and role in this Nexus instance." },
84
- { uri: "mcp://nexus/status", name: "System Status & Storage Mode", description: "Backend storage mode (sqlite/json) and active meeting counts." },
85
- { uri: "mcp://nexus/active-meeting", name: "Current Active Meeting", description: "Full transcript and participants of the current default meeting." },
86
- ...projectIds.map(id => {
87
- const prefix = id.split("_")[0];
88
- const typeLabel = {
89
- web: "🌐 Website", api: "βš™οΈ API", chrome: "🧩 Chrome Ext",
90
- vscode: "πŸ’» VSCode Ext", mcp: "πŸ”Œ MCP Server", android: "πŸ“± Android",
91
- ios: "🍎 iOS", flutter: "πŸ“² Flutter", desktop: "πŸ–₯️ Desktop",
92
- lib: "πŸ“¦ Library", bot: "πŸ€– Bot", infra: "☁️ Infra", doc: "πŸ“„ Docs"
93
- }[prefix] || "πŸ“ Project";
94
- return {
95
- uri: `mcp://hub/projects/${id}/manifest`,
96
- name: `${typeLabel}: ${id}`,
97
- description: `Structured metadata (Tech stack, relations) for ${id}`
98
- };
99
- })
101
+ // Core resources (static, always available)
102
+ { uri: "mcp://nexus/chat/global", name: "Global Chat", description: "Real-time discussion stream." },
103
+ { uri: "mcp://nexus/hub/registry", name: "Project Registry", description: `Index of ${projectCount} registered projects. Read this first to discover project IDs.` },
104
+ { uri: "mcp://nexus/docs/list", name: "Docs Index", description: "List of shared cross-project documents." },
105
+ { uri: "mcp://nexus/docs/global-strategy", name: "Strategy Blueprint", description: "Top-level coordination document." },
106
+ { uri: "mcp://nexus/meetings/list", name: "Meetings List", description: "Active and closed meetings." },
107
+ { uri: "mcp://nexus/session", name: "Session Info", description: "Your identity and role." },
108
+ { uri: "mcp://nexus/status", name: "System Status", description: "Storage mode and active meeting count." },
109
+ { uri: "mcp://nexus/active-meeting", name: "Active Meeting", description: "Current default meeting transcript." }
100
110
  ],
101
111
  resourceTemplates: [
102
- { uriTemplate: "mcp://hub/projects/{projectId}/internal-docs", name: "Internal Project Docs", description: "Markdown-based detailed implementation plans." },
103
- { uriTemplate: "mcp://nexus/meetings/{meetingId}", name: "Meeting Insights", description: "Full transcript and decisions for a specific meeting." }
112
+ // Project resources - use registry to discover IDs first
113
+ { uriTemplate: "mcp://nexus/projects/{projectId}/manifest", name: "Project Manifest", description: "Metadata for a specific project. Get projectId from registry." },
114
+ { uriTemplate: "mcp://nexus/projects/{projectId}/internal-docs", name: "Project Docs", description: "Internal implementation guide for a project." },
115
+ { uriTemplate: "mcp://nexus/docs/{docId}", name: "Global Doc", description: "Read a specific shared document." },
116
+ { uriTemplate: "mcp://nexus/meetings/{meetingId}", name: "Meeting Details", description: "Full transcript for a specific meeting." }
104
117
  ]
105
118
  };
106
119
  }
@@ -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.
@@ -96,21 +104,76 @@ export class StorageManager {
96
104
  await fs.writeFile(path.join(assetDir, fileName), content);
97
105
  return path.join("projects", id, "assets", fileName);
98
106
  }
99
- static async calculateTopology() {
107
+ /**
108
+ * Calculate project dependency topology.
109
+ *
110
+ * Context7-style progressive loading:
111
+ * - Default: Returns summary (project list + stats) - lightweight
112
+ * - With projectId: Returns detailed subgraph for that project
113
+ */
114
+ static async calculateTopology(projectId) {
100
115
  const registry = await this.listRegistry();
101
116
  const projectIds = Object.keys(registry.projects);
102
- const nodes = [];
103
- const edges = [];
117
+ // Collect manifests
118
+ const manifests = new Map();
119
+ let totalEdges = 0;
104
120
  for (const id of projectIds) {
105
121
  const manifest = await this.getProjectManifest(id);
106
- if (!manifest)
107
- continue;
108
- nodes.push({ id, name: manifest.name, type: "project" });
109
- (manifest.relations || []).forEach(rel => {
110
- edges.push({ from: id, to: rel.targetId, type: rel.type });
122
+ if (manifest) {
123
+ manifests.set(id, manifest);
124
+ totalEdges += (manifest.relations || []).length;
125
+ }
126
+ }
127
+ // === FOCUSED MODE: Return detailed subgraph ===
128
+ if (projectId) {
129
+ const targetManifest = manifests.get(projectId);
130
+ if (!targetManifest) {
131
+ return { mode: "focused", projectId, error: "Project not found", nodes: [], edges: [] };
132
+ }
133
+ const nodes = [];
134
+ const edges = [];
135
+ // Add target node
136
+ nodes.push({ id: projectId, name: targetManifest.name });
137
+ // Outgoing relations
138
+ const connectedIds = new Set();
139
+ (targetManifest.relations || []).forEach(rel => {
140
+ edges.push({ from: projectId, to: rel.targetId, type: rel.type });
141
+ connectedIds.add(rel.targetId);
111
142
  });
143
+ // Incoming relations
144
+ for (const [id, manifest] of manifests) {
145
+ if (id === projectId)
146
+ continue;
147
+ (manifest.relations || []).forEach(rel => {
148
+ if (rel.targetId === projectId) {
149
+ edges.push({ from: id, to: projectId, type: rel.type });
150
+ connectedIds.add(id);
151
+ }
152
+ });
153
+ }
154
+ // Add connected nodes
155
+ for (const id of connectedIds) {
156
+ const m = manifests.get(id);
157
+ if (m)
158
+ nodes.push({ id, name: m.name });
159
+ }
160
+ return { mode: "focused", projectId, nodes, edges };
112
161
  }
113
- return { nodes, edges };
162
+ // === LIST MODE: Return lightweight summary ===
163
+ const projects = Array.from(manifests.entries()).map(([id, m]) => ({
164
+ id,
165
+ name: m.name,
166
+ relationsCount: (m.relations || []).length
167
+ }));
168
+ return {
169
+ mode: "list",
170
+ summary: {
171
+ totalProjects: projects.length,
172
+ totalEdges,
173
+ hint: "Call with projectId to get detailed subgraph"
174
+ },
175
+ projects
176
+ };
114
177
  }
115
178
  // --- Discussion & Log Management ---
116
179
  /**
@@ -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
  */