@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 +53 -25
- package/build/index.js +38 -22
- package/build/resources/index.js +43 -30
- package/build/storage/index.js +72 -9
- package/build/storage/meetings.js +35 -2
- package/build/storage/sqlite-meeting.js +75 -13
- package/build/storage/sqlite.js +25 -3
- package/build/storage/store.js +22 -8
- package/build/storage/tasks.js +204 -0
- package/build/tools/definitions.js +152 -145
- package/build/tools/handlers.js +355 -184
- package/build/tools/index.js +9 -1
- package/build/tools/schemas.js +278 -0
- package/build/utils/auth.js +11 -0
- package/build/utils/error.js +15 -0
- package/docs/ASSISTANT_GUIDE.md +58 -62
- package/docs/CHANGELOG_zh.md +54 -141
- package/docs/README_zh.md +190 -0
- package/docs/TODO_zh.md +71 -0
- package/package.json +9 -8
- package/README_zh.md +0 -143
- package/docs/CHANGELOG.md +0 -141
package/README.md
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
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
|
|
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
|
|
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
|
-
β
|
|
40
|
-
β
|
|
41
|
-
β
|
|
42
|
-
β
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
-
- `
|
|
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`:
|
|
93
|
-
- `sync_global_doc
|
|
94
|
-
|
|
95
|
-
### D.
|
|
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
|
-
|
|
102
|
-
- `mcp://
|
|
103
|
-
- `mcp://
|
|
104
|
-
- `mcp://nexus/
|
|
105
|
-
- `mcp://
|
|
106
|
-
- `mcp://
|
|
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:
|
|
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.1
|
|
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://
|
|
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 };
|
|
@@ -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
|
|
98
|
+
const projectCount = Object.keys(registry.projects).length;
|
|
78
99
|
return {
|
|
79
100
|
resources: [
|
|
80
|
-
|
|
81
|
-
{ uri: "mcp://
|
|
82
|
-
{ uri: "mcp://
|
|
83
|
-
{ uri: "mcp://nexus/
|
|
84
|
-
{ uri: "mcp://nexus/
|
|
85
|
-
{ uri: "mcp://nexus/
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
103
|
-
{ uriTemplate: "mcp://nexus/
|
|
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
|
}
|
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.
|
|
@@ -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
|
-
|
|
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
|
-
|
|
103
|
-
const
|
|
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 (
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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
|
*/
|