group-chat-mcp 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/.github/workflows/publish.yml +32 -0
  2. package/CHANGELOG.md +42 -0
  3. package/LICENSE +21 -0
  4. package/README.md +30 -5
  5. package/dist/__tests__/check-inbox.test.d.ts +2 -0
  6. package/dist/__tests__/check-inbox.test.d.ts.map +1 -0
  7. package/dist/__tests__/check-inbox.test.js +85 -0
  8. package/dist/__tests__/check-inbox.test.js.map +1 -0
  9. package/dist/__tests__/cli-session-commands.test.d.ts +2 -0
  10. package/dist/__tests__/cli-session-commands.test.d.ts.map +1 -0
  11. package/dist/__tests__/cli-session-commands.test.js +145 -0
  12. package/dist/__tests__/cli-session-commands.test.js.map +1 -0
  13. package/dist/__tests__/cursor-hook.test.d.ts +2 -0
  14. package/dist/__tests__/cursor-hook.test.d.ts.map +1 -0
  15. package/dist/__tests__/cursor-hook.test.js +79 -0
  16. package/dist/__tests__/cursor-hook.test.js.map +1 -0
  17. package/dist/__tests__/installer-hooks.test.d.ts +2 -0
  18. package/dist/__tests__/installer-hooks.test.d.ts.map +1 -0
  19. package/dist/__tests__/installer-hooks.test.js +110 -0
  20. package/dist/__tests__/installer-hooks.test.js.map +1 -0
  21. package/dist/__tests__/session-state-service.test.d.ts +2 -0
  22. package/dist/__tests__/session-state-service.test.d.ts.map +1 -0
  23. package/dist/__tests__/session-state-service.test.js +65 -0
  24. package/dist/__tests__/session-state-service.test.js.map +1 -0
  25. package/dist/constants/env.d.ts +1 -0
  26. package/dist/constants/env.d.ts.map +1 -1
  27. package/dist/constants/env.js +1 -0
  28. package/dist/constants/env.js.map +1 -1
  29. package/dist/constants/settings-paths.d.ts +2 -0
  30. package/dist/constants/settings-paths.d.ts.map +1 -1
  31. package/dist/constants/settings-paths.js +4 -0
  32. package/dist/constants/settings-paths.js.map +1 -1
  33. package/dist/constants/storage.d.ts +1 -0
  34. package/dist/constants/storage.d.ts.map +1 -1
  35. package/dist/constants/storage.js +1 -0
  36. package/dist/constants/storage.js.map +1 -1
  37. package/dist/gchat.d.ts +13 -0
  38. package/dist/gchat.d.ts.map +1 -1
  39. package/dist/gchat.js +93 -6
  40. package/dist/gchat.js.map +1 -1
  41. package/dist/hooks/cursor-hook.d.ts +3 -0
  42. package/dist/hooks/cursor-hook.d.ts.map +1 -0
  43. package/dist/hooks/cursor-hook.js +106 -0
  44. package/dist/hooks/cursor-hook.js.map +1 -0
  45. package/dist/index.js +30 -9
  46. package/dist/index.js.map +1 -1
  47. package/dist/schemas/tool-schemas.d.ts +19 -0
  48. package/dist/schemas/tool-schemas.d.ts.map +1 -1
  49. package/dist/schemas/tool-schemas.js +9 -0
  50. package/dist/schemas/tool-schemas.js.map +1 -1
  51. package/dist/services/inbox-poller.d.ts.map +1 -1
  52. package/dist/services/inbox-poller.js +1 -15
  53. package/dist/services/inbox-poller.js.map +1 -1
  54. package/dist/services/installer-service.d.ts +2 -0
  55. package/dist/services/installer-service.d.ts.map +1 -1
  56. package/dist/services/installer-service.js +79 -1
  57. package/dist/services/installer-service.js.map +1 -1
  58. package/dist/services/session-state-service.d.ts +16 -0
  59. package/dist/services/session-state-service.d.ts.map +1 -0
  60. package/dist/services/session-state-service.js +96 -0
  61. package/dist/services/session-state-service.js.map +1 -0
  62. package/dist/services/state-service.d.ts +2 -0
  63. package/dist/services/state-service.d.ts.map +1 -1
  64. package/dist/services/state-service.js +12 -0
  65. package/dist/services/state-service.js.map +1 -1
  66. package/dist/services/tool-handlers.d.ts +0 -2
  67. package/dist/services/tool-handlers.d.ts.map +1 -1
  68. package/dist/services/tool-handlers.js +11 -27
  69. package/dist/services/tool-handlers.js.map +1 -1
  70. package/dist/types/hook-input.d.ts +7 -0
  71. package/dist/types/hook-input.d.ts.map +1 -0
  72. package/dist/types/hook-input.js +2 -0
  73. package/dist/types/hook-input.js.map +1 -0
  74. package/dist/types/hook-response.d.ts +5 -0
  75. package/dist/types/hook-response.d.ts.map +1 -0
  76. package/dist/types/hook-response.js +2 -0
  77. package/dist/types/hook-response.js.map +1 -0
  78. package/dist/types/index.d.ts +3 -0
  79. package/dist/types/index.d.ts.map +1 -1
  80. package/dist/types/parsed-command.d.ts +12 -3
  81. package/dist/types/parsed-command.d.ts.map +1 -1
  82. package/dist/types/parsed-error.d.ts +2 -1
  83. package/dist/types/parsed-error.d.ts.map +1 -1
  84. package/dist/types/session-state.d.ts +7 -0
  85. package/dist/types/session-state.d.ts.map +1 -0
  86. package/dist/types/session-state.js +2 -0
  87. package/dist/types/session-state.js.map +1 -0
  88. package/dist/{handlers/tool-handlers.d.ts → utils/notification-utils.d.ts} +3 -7
  89. package/dist/utils/notification-utils.d.ts.map +1 -0
  90. package/dist/utils/notification-utils.js +42 -0
  91. package/dist/utils/notification-utils.js.map +1 -0
  92. package/package.json +6 -1
  93. package/vitest.config.ts +7 -0
  94. package/dist/handlers/tool-handlers.d.ts.map +0 -1
  95. package/dist/handlers/tool-handlers.js +0 -198
  96. package/dist/handlers/tool-handlers.js.map +0 -1
@@ -0,0 +1,32 @@
1
+ name: Publish to npm
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ permissions:
8
+ contents: read
9
+ id-token: write
10
+
11
+ jobs:
12
+ publish:
13
+ runs-on: ubuntu-latest
14
+ environment: npm-publish
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+
18
+ - uses: actions/setup-node@v4
19
+ with:
20
+ node-version: 24
21
+ registry-url: https://registry.npmjs.org
22
+
23
+ - run: npm ci
24
+ - run: npm run build
25
+ - run: npm test
26
+
27
+ - name: Set version from tag
28
+ run: |
29
+ TAG="${GITHUB_REF_NAME#v}"
30
+ npm version "$TAG" --no-git-tag-version --allow-same-version
31
+
32
+ - run: npm publish --provenance --access public
package/CHANGELOG.md CHANGED
@@ -5,6 +5,48 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.1.3] - 2026-03-21
9
+
10
+ ### Added
11
+
12
+ - `read_notifications` MCP tool for pull-based notification retrieval (replaces push-based inbox poller for Cursor)
13
+ - Cursor session lifecycle hooks: `sessionStart`, `sessionEnd`, `beforeMCPExecution` via `hooks.json`
14
+ - `SessionStateService` for per-PID session state management with in-memory cache and stale session reaping
15
+ - `cursor-join` and `cursor-leave` CLI commands for hook-driven agent registration
16
+ - `GC_CLIENT_TYPE` environment variable to disable push-based inbox poller when set to `cursor`
17
+ - `writeNotificationToParticipants` and `formatNotificationContent` extracted to `src/utils/notification-utils.ts`
18
+ - `readAndClearInbox` method on `StateService` for atomic inbox read-and-clear
19
+ - Cursor installer writes `hooks.json` with idempotent merge logic preserving existing non-group-chat-mcp hooks
20
+ - Cursor `mcp.json` entry now includes `GC_CLIENT_TYPE` and `GC_POLL_INTERVAL_MS` in env block
21
+ - 28 new tests across 5 test files covering session state, CLI commands, cursor hook, installer hooks, and read_notifications
22
+ - Vitest configuration (`vitest.config.ts`)
23
+
24
+ ### Changed
25
+
26
+ - `ParsedCommand` changed from interface to discriminated union type with `cursor-join` and `cursor-leave` variants
27
+ - `ParsedError` extended with `missing-required-arg` variant and optional `message` field
28
+ - MCP server dynamically resolves agent ID from session state on each tool call (falls back to startup-registered ID)
29
+ - Inbox poller conditionally skipped when `GC_CLIENT_TYPE === 'cursor'`
30
+ - Vitest include updated to cover both `src/__tests__/` and `tests/` directories
31
+
32
+ ### Fixed
33
+
34
+ - `readStdin()` in cursor hook no longer leaks the timeout handle (added cleanup + `.unref()`)
35
+ - `--server-pid` validation rejects zero, negative, and non-integer values in both `cursor-join` and `cursor-leave`
36
+ - Existing installer tests updated to match new Cursor mcp.json format with `env` block
37
+
38
+ ## [0.1.2] - 2026-03-21
39
+
40
+ ### Added
41
+
42
+ - npm auto-publish workflow on GitHub release
43
+ - `repository` URL in `package.json` for npm provenance verification
44
+
45
+ ### Fixed
46
+
47
+ - CI pipeline: switched to Node 24 for npm OIDC Trusted Publishing support
48
+ - CI pipeline: resolved `NODE_AUTH_TOKEN` / OIDC auth conflicts for `npm publish`
49
+
8
50
  ## [0.1.1] - 2026-03-21
9
51
 
10
52
  ### Changed
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 appboypov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Group Chat MCP
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/group-chat-mcp)](https://www.npmjs.com/package/group-chat-mcp)
4
+ [![License](https://img.shields.io/github/license/appboypov/group-chat-mcp)](https://github.com/appboypov/group-chat-mcp/blob/main/LICENSE)
5
+ [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/)
6
+
3
7
  Multi-agent communication server using the Model Context Protocol. Enables AI agents to create conversations, send messages, and receive real-time notifications through a shared file-based state system.
4
8
 
5
9
  ## Features
@@ -7,6 +11,8 @@ Multi-agent communication server using the Model Context Protocol. Enables AI ag
7
11
  - Real-time multi-agent messaging via MCP channel notifications
8
12
  - Project-scoped and direct message conversations
9
13
  - Automatic agent registration and cleanup per session
14
+ - Per-session agent lifecycle for Cursor via hooks (`sessionStart`, `sessionEnd`, `beforeMCPExecution`)
15
+ - Pull-based inbox for clients without push notification support (`read_notifications`)
10
16
  - File-based shared state with atomic locking
11
17
  - Zero-config setup via `gchat install`
12
18
 
@@ -40,7 +46,7 @@ The installer prompts for:
40
46
  - **IDE**: Claude Code, Cursor, or Both
41
47
  - **Scope**: Global (all projects) or Local (current project only)
42
48
 
43
- For Claude Code, the installer registers the MCP server via `claude mcp add` (requires the Claude Code CLI on PATH). For Cursor, it writes the configuration to the appropriate settings file.
49
+ For Claude Code, the installer registers the MCP server via `claude mcp add` (requires the Claude Code CLI on PATH). For Cursor, it writes `mcp.json` with the server entry and `hooks.json` with session lifecycle hooks that register and unregister agents per chat session.
44
50
 
45
51
  To remove the configuration:
46
52
 
@@ -48,9 +54,9 @@ To remove the configuration:
48
54
  gchat uninstall
49
55
  ```
50
56
 
51
- ### Enable channel notifications
57
+ ### Claude Code: enable channel notifications
52
58
 
53
- Channel notifications allow agents to receive messages in real-time as they arrive. For Claude Code, start your session with:
59
+ Channel notifications allow agents to receive messages in real-time as they arrive. Start your session with:
54
60
 
55
61
  ```bash
56
62
  claude --dangerously-load-development-channels server:group-chat-mcp
@@ -58,6 +64,16 @@ claude --dangerously-load-development-channels server:group-chat-mcp
58
64
 
59
65
  Without this flag, agents can still read messages by calling `get_conversation`, but incoming messages will not be injected into the conversation automatically.
60
66
 
67
+ ### Cursor: session lifecycle
68
+
69
+ Cursor keeps MCP server processes alive across chat sessions. The installed hooks handle agent lifecycle automatically:
70
+
71
+ - **`sessionStart`** registers a new agent and joins the project conversation.
72
+ - **`sessionEnd`** leaves all conversations and unregisters the agent.
73
+ - **`beforeMCPExecution`** auto-approves group-chat-mcp tools and prompts for all other MCP servers.
74
+
75
+ Since Cursor does not support push notifications, agents use the `read_notifications` tool to poll for new messages.
76
+
61
77
  ## Usage
62
78
 
63
79
  After setup, the MCP server starts automatically when your IDE launches a session. Each session:
@@ -65,7 +81,7 @@ After setup, the MCP server starts automatically when your IDE launches a sessio
65
81
  1. Generates a unique agent ID
66
82
  2. Registers the agent in the shared state
67
83
  3. Joins the project conversation (one per project directory)
68
- 4. Polls for incoming notifications
84
+ 4. Polls for incoming notifications (Claude Code) or exposes `read_notifications` (Cursor)
69
85
  5. Cleans up on disconnect (leaves conversations, unregisters)
70
86
 
71
87
  Multiple agents in the same project directory share a single project conversation.
@@ -158,12 +174,21 @@ Leave a conversation.
158
174
 
159
175
  Returns confirmation. Notifies remaining participants.
160
176
 
177
+ ### read_notifications
178
+
179
+ Check for new messages and notifications from other agents. Returns all pending notifications and clears the inbox. Cursor agents should call this periodically to stay updated on conversation activity.
180
+
181
+ No input parameters.
182
+
183
+ Returns pending notifications or `"No new notifications."` if the inbox is empty.
184
+
161
185
  ## Configuration
162
186
 
163
187
  | Variable | Required | Default | Description |
164
188
  |----------|----------|---------|-------------|
165
189
  | GC_PROJECT_PATH | No | `process.cwd()` | Override the project directory path (must be an absolute path) |
166
- | GC_POLL_INTERVAL_MS | No | `2000` | Inbox polling interval in milliseconds |
190
+ | GC_POLL_INTERVAL_MS | No | `2000` (`5000` for Cursor) | Inbox polling interval in milliseconds. The Cursor installer sets this to `5000` via the mcp.json env block. |
191
+ | GC_CLIENT_TYPE | No | — | Set to `"cursor"` to disable the push-based inbox poller (set automatically by the Cursor installer) |
167
192
 
168
193
  ## License
169
194
 
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=check-inbox.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check-inbox.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/check-inbox.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,85 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import { StateService } from '../services/state-service.js';
6
+ import { handleToolCall } from '../services/tool-handlers.js';
7
+ import { NotificationType } from '../enums/notification-type.js';
8
+ import { writeJsonFile } from '../utils/file-utils.js';
9
+ import { formatNotificationContent } from '../utils/notification-utils.js';
10
+ let tmpDir;
11
+ let stateService;
12
+ beforeEach(async () => {
13
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gchat-inbox-test-'));
14
+ stateService = new StateService(tmpDir);
15
+ await stateService.init();
16
+ });
17
+ afterEach(async () => {
18
+ await fs.rm(tmpDir, { recursive: true, force: true });
19
+ });
20
+ describe('read_notifications', () => {
21
+ describe('Given 3 notifications in agent inbox', () => {
22
+ it('When read_notifications tool is called Then all 3 are returned', async () => {
23
+ const agent = await stateService.registerAgent('/test-project');
24
+ const inboxPath = path.join(tmpDir, 'inboxes', `${agent.id}.json`);
25
+ const notifications = [
26
+ { id: 'n1', type: NotificationType.Message, conversationId: 'conv-1', agentId: 'sender-1', content: 'Hello', timestamp: Date.now() },
27
+ { id: 'n2', type: NotificationType.Join, conversationId: 'conv-1', agentId: 'sender-2', content: 'sender-2 joined', timestamp: Date.now() },
28
+ { id: 'n3', type: NotificationType.Leave, conversationId: 'conv-1', agentId: 'sender-3', content: 'sender-3 left', timestamp: Date.now() },
29
+ ];
30
+ await writeJsonFile(inboxPath, notifications);
31
+ const result = await handleToolCall(stateService, 'read_notifications', agent.id, undefined);
32
+ const outputText = result.content[0].text;
33
+ const outputLines = outputText.split('\n');
34
+ const headerLine = outputLines[0];
35
+ const notificationLines = outputLines.slice(1).filter((line) => line.trim().length > 0);
36
+ expect(headerLine).toMatch(/^3 notification/);
37
+ expect(notificationLines.length).toBe(3);
38
+ });
39
+ it('When read_notifications tool is called Then inbox is empty after the call', async () => {
40
+ const agent = await stateService.registerAgent('/test-project');
41
+ const inboxPath = path.join(tmpDir, 'inboxes', `${agent.id}.json`);
42
+ const notifications = [
43
+ { id: 'n1', type: NotificationType.Message, conversationId: 'conv-1', agentId: 'sender-1', content: 'Hello', timestamp: Date.now() },
44
+ { id: 'n2', type: NotificationType.Join, conversationId: 'conv-1', agentId: 'sender-2', content: 'sender-2 joined', timestamp: Date.now() },
45
+ { id: 'n3', type: NotificationType.Leave, conversationId: 'conv-1', agentId: 'sender-3', content: 'sender-3 left', timestamp: Date.now() },
46
+ ];
47
+ await writeJsonFile(inboxPath, notifications);
48
+ await handleToolCall(stateService, 'read_notifications', agent.id, undefined);
49
+ const raw = await fs.readFile(inboxPath, 'utf-8');
50
+ const remaining = JSON.parse(raw);
51
+ expect(remaining).toEqual([]);
52
+ });
53
+ });
54
+ describe('Given empty inbox', () => {
55
+ it('When read_notifications tool is called Then result indicates no notifications and inbox remains empty', async () => {
56
+ const agent = await stateService.registerAgent('/test-project');
57
+ const result = await handleToolCall(stateService, 'read_notifications', agent.id, undefined);
58
+ expect(result.content).toBeDefined();
59
+ expect(result.content.length).toBeGreaterThan(0);
60
+ expect('isError' in result).toBe(false);
61
+ const inbox = await stateService.getInbox(agent.id);
62
+ expect(inbox).toEqual([]);
63
+ });
64
+ });
65
+ describe('Given agent has notifications', () => {
66
+ it('When read_notifications is called Then notifications are formatted using the same format as the inbox poller', async () => {
67
+ const agent = await stateService.registerAgent('/test-project');
68
+ const inboxPath = path.join(tmpDir, 'inboxes', `${agent.id}.json`);
69
+ const notifications = [
70
+ { id: 'n1', type: NotificationType.Message, conversationId: 'conv-1', agentId: 'sender-1', content: 'Hello', timestamp: Date.now() },
71
+ { id: 'n2', type: NotificationType.Join, conversationId: 'conv-1', agentId: 'sender-2', content: 'sender-2 joined', timestamp: Date.now() },
72
+ { id: 'n3', type: NotificationType.Leave, conversationId: 'conv-1', agentId: 'sender-3', content: 'sender-3 left', timestamp: Date.now() },
73
+ ];
74
+ await writeJsonFile(inboxPath, notifications);
75
+ const result = await handleToolCall(stateService, 'read_notifications', agent.id, undefined);
76
+ const outputText = result.content[0].text;
77
+ const outputLines = outputText.split('\n').slice(1);
78
+ for (let i = 0; i < notifications.length; i++) {
79
+ const expected = formatNotificationContent(notifications[i]);
80
+ expect(outputLines[i]).toBe(expected);
81
+ }
82
+ });
83
+ });
84
+ });
85
+ //# sourceMappingURL=check-inbox.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check-inbox.test.js","sourceRoot":"","sources":["../../src/__tests__/check-inbox.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=cli-session-commands.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-session-commands.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/cli-session-commands.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,145 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import { StateService } from '../services/state-service.js';
6
+ import { SessionStateService } from '../services/session-state-service.js';
7
+ import { handleCursorJoin, handleCursorLeave } from '../gchat.js';
8
+ import { ConversationType } from '../enums/conversation-type.js';
9
+ import { NotificationType } from '../enums/notification-type.js';
10
+ import { INBOXES_DIR } from '../constants/storage.js';
11
+ const UUID_V4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
12
+ let tmpDir;
13
+ let stateService;
14
+ let sessionStateService;
15
+ beforeEach(async () => {
16
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gchat-cli-test-'));
17
+ stateService = new StateService(tmpDir);
18
+ await stateService.init();
19
+ sessionStateService = new SessionStateService(tmpDir);
20
+ });
21
+ afterEach(async () => {
22
+ await fs.rm(tmpDir, { recursive: true, force: true });
23
+ });
24
+ describe('cursor-join', () => {
25
+ describe('Given project path and server PID', () => {
26
+ it('When cursor-join is called Then an agent UUID and conversation UUID are returned', async () => {
27
+ const projectPath = '/test-project';
28
+ const serverPid = 12345;
29
+ const result = await handleCursorJoin(projectPath, serverPid, { stateService, sessionStateService });
30
+ expect(result.agentId).toMatch(UUID_V4_REGEX);
31
+ expect(result.conversationId).toMatch(UUID_V4_REGEX);
32
+ });
33
+ it('When cursor-join is called Then the agent is registered and session state is written', async () => {
34
+ const projectPath = '/test-project';
35
+ const serverPid = 12345;
36
+ const result = await handleCursorJoin(projectPath, serverPid, { stateService, sessionStateService });
37
+ const registeredAgent = await stateService.getAgent(result.agentId);
38
+ expect(registeredAgent).not.toBeNull();
39
+ expect(registeredAgent.projectPath).toBe(projectPath);
40
+ const sessionResult = await sessionStateService.readSessionAgent(serverPid);
41
+ expect(sessionResult).toEqual({ agentId: result.agentId, projectPath });
42
+ });
43
+ it('When cursor-join is called Then the agent is a participant in the project conversation', async () => {
44
+ const projectPath = '/test-project';
45
+ const serverPid = 12345;
46
+ const result = await handleCursorJoin(projectPath, serverPid, { stateService, sessionStateService });
47
+ const conversation = await stateService.getConversation(result.conversationId);
48
+ expect(conversation).not.toBeNull();
49
+ expect(conversation.participants).toContain(result.agentId);
50
+ });
51
+ });
52
+ describe('Given project path with existing conversation and 1 participant', () => {
53
+ it('When cursor-join is called Then a join notification is written to the existing participant inbox', async () => {
54
+ const projectPath = '/test-project';
55
+ const agentA = await stateService.registerAgent(projectPath);
56
+ const conversation = await stateService.getOrCreateProjectConversation(projectPath);
57
+ await stateService.joinConversation(agentA.id, conversation.id);
58
+ const serverPidB = 54321;
59
+ const result = await handleCursorJoin(projectPath, serverPidB, { stateService, sessionStateService });
60
+ const inboxPath = path.join(tmpDir, INBOXES_DIR, `${agentA.id}.json`);
61
+ let notifications = [];
62
+ try {
63
+ const raw = await fs.readFile(inboxPath, 'utf-8');
64
+ notifications = JSON.parse(raw);
65
+ }
66
+ catch {
67
+ notifications = [];
68
+ }
69
+ const joinNotification = notifications.find((n) => n.type === NotificationType.Join && n.agentId === result.agentId);
70
+ expect(joinNotification).toBeDefined();
71
+ });
72
+ });
73
+ });
74
+ describe('cursor-leave', () => {
75
+ describe('Given agent X registered with PID 1234', () => {
76
+ it('When cursor-leave is called Then X is unregistered and session state is cleared', async () => {
77
+ const projectPath = '/test-project';
78
+ const serverPid = 1234;
79
+ const joinResult = await handleCursorJoin(projectPath, serverPid, { stateService, sessionStateService });
80
+ await handleCursorLeave(serverPid, { stateService, sessionStateService });
81
+ const registeredAgent = await stateService.getAgent(joinResult.agentId);
82
+ expect(registeredAgent).toBeNull();
83
+ const sessionResult = await sessionStateService.readSessionAgent(serverPid);
84
+ expect(sessionResult).toBeNull();
85
+ });
86
+ });
87
+ describe('Given no session state for PID 9999', () => {
88
+ it('When cursor-leave is called Then no error is thrown', async () => {
89
+ await expect(handleCursorLeave(9999, { stateService, sessionStateService })).resolves.toBeUndefined();
90
+ });
91
+ });
92
+ describe('Given agent X in conversation with agent Y', () => {
93
+ it('When cursor-leave is called for X Then Y has a leave notification in its inbox', async () => {
94
+ const projectPath = '/test-project';
95
+ const serverPidX = 1234;
96
+ const serverPidY = 5678;
97
+ const resultY = await handleCursorJoin(projectPath, serverPidY, { stateService, sessionStateService });
98
+ const resultX = await handleCursorJoin(projectPath, serverPidX, { stateService, sessionStateService });
99
+ await handleCursorLeave(serverPidX, { stateService, sessionStateService });
100
+ const inboxPath = path.join(tmpDir, INBOXES_DIR, `${resultY.agentId}.json`);
101
+ let notifications = [];
102
+ try {
103
+ const raw = await fs.readFile(inboxPath, 'utf-8');
104
+ notifications = JSON.parse(raw);
105
+ }
106
+ catch {
107
+ notifications = [];
108
+ }
109
+ const leaveNotification = notifications.find((n) => n.type === NotificationType.Leave && n.agentId === resultX.agentId);
110
+ expect(leaveNotification).toBeDefined();
111
+ });
112
+ });
113
+ describe('Given agent X in 2 conversations with server PID', () => {
114
+ it('When cursor-leave is called Then X is removed from both conversations and unregistered', async () => {
115
+ const projectPath = '/test-project';
116
+ const serverPid = 1234;
117
+ const joinResult = await handleCursorJoin(projectPath, serverPid, { stateService, sessionStateService });
118
+ const secondConversation = await stateService.createConversation({
119
+ type: ConversationType.Group,
120
+ name: 'Second conversation',
121
+ participants: [joinResult.agentId],
122
+ });
123
+ await handleCursorLeave(serverPid, { stateService, sessionStateService });
124
+ const agent = await stateService.getAgent(joinResult.agentId);
125
+ expect(agent).toBeNull();
126
+ const conv1 = await stateService.getConversation(joinResult.conversationId);
127
+ expect(conv1.participants).not.toContain(joinResult.agentId);
128
+ const conv2 = await stateService.getConversation(secondConversation.id);
129
+ expect(conv2.participants).not.toContain(joinResult.agentId);
130
+ });
131
+ });
132
+ describe('Given agent X as sole participant in conversation C', () => {
133
+ it('When cursor-leave is called Then conversation C is archived', async () => {
134
+ const projectPath = '/test-project';
135
+ const serverPid = 1234;
136
+ const joinResult = await handleCursorJoin(projectPath, serverPid, { stateService, sessionStateService });
137
+ await handleCursorLeave(serverPid, { stateService, sessionStateService });
138
+ const conversation = await stateService.getConversation(joinResult.conversationId);
139
+ expect(conversation).not.toBeNull();
140
+ expect(conversation.archivedAt).toBeDefined();
141
+ expect(typeof conversation.archivedAt).toBe('number');
142
+ });
143
+ });
144
+ });
145
+ //# sourceMappingURL=cli-session-commands.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-session-commands.test.js","sourceRoot":"","sources":["../../src/__tests__/cli-session-commands.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=cursor-hook.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cursor-hook.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/cursor-hook.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,79 @@
1
+ import { describe, it, expect, beforeAll } from 'vitest';
2
+ import { spawnSync, execFileSync } from 'node:child_process';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+ const HOOK_PATH = path.resolve(__dirname, '../../dist/hooks/cursor-hook.js');
7
+ const TEST_TIMEOUT = 15000;
8
+ beforeAll(() => {
9
+ execFileSync('npm', ['run', 'build'], {
10
+ cwd: path.resolve(__dirname, '../..'),
11
+ encoding: 'utf-8',
12
+ timeout: 30000,
13
+ });
14
+ });
15
+ function runHook(input) {
16
+ const result = spawnSync('node', [HOOK_PATH], {
17
+ input,
18
+ encoding: 'utf-8',
19
+ timeout: 10000,
20
+ });
21
+ if (result.error) {
22
+ throw result.error;
23
+ }
24
+ return JSON.parse(result.stdout.trim());
25
+ }
26
+ describe('cursor-hook', () => {
27
+ describe('Given stdin JSON with hook_event_name beforeMCPExecution and server group-chat-mcp', () => {
28
+ it('Then stdout contains permission allow', () => {
29
+ const response = runHook(JSON.stringify({
30
+ hook_event_name: 'beforeMCPExecution',
31
+ server: 'group-chat-mcp',
32
+ }));
33
+ expect(response.permission).toBe('allow');
34
+ }, TEST_TIMEOUT);
35
+ });
36
+ describe('Given stdin JSON with hook_event_name beforeMCPExecution and server some-other-mcp', () => {
37
+ it('Then stdout contains permission ask', () => {
38
+ const response = runHook(JSON.stringify({
39
+ hook_event_name: 'beforeMCPExecution',
40
+ server: 'some-other-mcp',
41
+ }));
42
+ expect(response.permission).toBe('ask');
43
+ }, TEST_TIMEOUT);
44
+ });
45
+ describe('Given invalid JSON on stdin', () => {
46
+ it('When the hook script runs Then it outputs permission allow', () => {
47
+ const response = runHook('not-valid-json{{{');
48
+ expect(response.permission).toBe('allow');
49
+ }, TEST_TIMEOUT);
50
+ });
51
+ describe('Given unknown hook_event_name', () => {
52
+ it('When the hook script runs Then it outputs permission allow', () => {
53
+ const response = runHook(JSON.stringify({
54
+ hook_event_name: 'unknownEvent',
55
+ }));
56
+ expect(response.permission).toBe('allow');
57
+ }, TEST_TIMEOUT);
58
+ });
59
+ describe('Given stdin JSON with hook_event_name sessionEnd', () => {
60
+ it('Then stdout contains permission allow', () => {
61
+ const response = runHook(JSON.stringify({
62
+ hook_event_name: 'sessionEnd',
63
+ }));
64
+ expect(response.permission).toBe('allow');
65
+ }, TEST_TIMEOUT);
66
+ });
67
+ describe('Given stdin JSON with hook_event_name sessionStart and workspace_roots', () => {
68
+ it('Then it registers an agent and stdout contains permission allow with agent_message', () => {
69
+ const response = runHook(JSON.stringify({
70
+ hook_event_name: 'sessionStart',
71
+ workspace_roots: ['/tmp/test-project-hook'],
72
+ }));
73
+ expect(response.permission).toBe('allow');
74
+ expect(response.agent_message).toBeDefined();
75
+ expect(response.agent_message.length).toBeGreaterThan(0);
76
+ }, TEST_TIMEOUT);
77
+ });
78
+ });
79
+ //# sourceMappingURL=cursor-hook.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cursor-hook.test.js","sourceRoot":"","sources":["../../src/__tests__/cursor-hook.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=installer-hooks.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"installer-hooks.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/installer-hooks.test.ts"],"names":[],"mappings":""}