matterbridge-lg-thinq-plugin 0.1.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.
Files changed (210) hide show
  1. package/.claude/CLAUDE.md +43 -0
  2. package/.claude/agents/compiler.md +91 -0
  3. package/.claude/agents/direct-executor.md +98 -0
  4. package/.claude/agents/documenter.md +78 -0
  5. package/.claude/agents/finalizer.md +233 -0
  6. package/.claude/agents/implementer.md +112 -0
  7. package/.claude/agents/investigator.md +100 -0
  8. package/.claude/agents/release-manager.md +203 -0
  9. package/.claude/agents/reviewer.md +131 -0
  10. package/.claude/agents/technical-architect.md +403 -0
  11. package/.claude/agents/test-writer.md +159 -0
  12. package/.claude/agents/wiki-manager.md +147 -0
  13. package/.claude/instructions/agent-prompts.md +90 -0
  14. package/.claude/instructions/shared-rules.md +11 -0
  15. package/.claude/instructions/team-orchestrator-policy.md +165 -0
  16. package/.claude/memory.md +186 -0
  17. package/.claude/output-styles/engineer-manager.md +38 -0
  18. package/.claude/settings.json +40 -0
  19. package/.claude/settings.local.json +30 -0
  20. package/.claude/skills/load-policy/SKILL.md +13 -0
  21. package/.claude/skills/ref-idea/SKILL.md +44 -0
  22. package/.claude/skills/status-of/SKILL.md +38 -0
  23. package/.claude/skills/workspace-console/SKILL.md +49 -0
  24. package/.claude/statusline-command.sh +130 -0
  25. package/.claude/statusline.md +53 -0
  26. package/.claude/templates/approval-packet.template.html +171 -0
  27. package/.claude/templates/reference-workspaces.md +16 -0
  28. package/.claude/tools/cli-runner-server.cjs +220 -0
  29. package/.claude/tools/discord-send-server.cjs +188 -0
  30. package/.claude/tools/github-release-server.cjs +181 -0
  31. package/.claude/tools/glob-grep-server.cjs +267 -0
  32. package/.claude/tools/read-log-server.cjs +175 -0
  33. package/.github/codeql/codeql-config.yml +14 -0
  34. package/.github/workflows/build.yml +54 -0
  35. package/.github/workflows/codeql.yml +59 -0
  36. package/.github/workflows/coverage.yml +70 -0
  37. package/.github/workflows/publish.yml +103 -0
  38. package/.mcp.json +34 -0
  39. package/CHANGELOG.md +195 -0
  40. package/LICENSE +21 -0
  41. package/dist/cli/commands/devices.js +52 -0
  42. package/dist/cli/commands/energy.js +122 -0
  43. package/dist/cli/commands/login.js +54 -0
  44. package/dist/cli/help.js +50 -0
  45. package/dist/cli/main.js +56 -0
  46. package/dist/cli/session.js +13 -0
  47. package/dist/cli/types.js +1 -0
  48. package/dist/cli/utils.js +29 -0
  49. package/dist/cli.js +2 -0
  50. package/dist/constants/index.js +1 -0
  51. package/dist/core/domain/entities/ThinqDevice.js +43 -0
  52. package/dist/core/domain/value-objects/AirConditionerCapabilities.js +22 -0
  53. package/dist/core/domain/value-objects/ThinqFilterState.js +35 -0
  54. package/dist/core/domain/value-objects/ThinqSnapshot.js +125 -0
  55. package/dist/errors/AuthenticationError.js +26 -0
  56. package/dist/errors/BaseError.js +23 -0
  57. package/dist/errors/index.js +2 -0
  58. package/dist/model/LgThinkqPluginPlatformConfig.js +23 -0
  59. package/dist/model/LgThinqPluginPlatformConfig.js +23 -0
  60. package/dist/module.js +212 -0
  61. package/dist/platform/deviceRegistry.js +26 -0
  62. package/dist/platform/platformConfigManager.js +89 -0
  63. package/dist/platform/platformState.js +9 -0
  64. package/dist/platform/thinq/thinqAirConditionerCommandHandlers.js +190 -0
  65. package/dist/platform/thinq/thinqAirConditionerEndpointFactory.js +53 -0
  66. package/dist/platform/thinq/thinqAirConditionerFilterResetCommandHandler.js +54 -0
  67. package/dist/platform/thinq/thinqAirConditionerFilterStateSync.js +19 -0
  68. package/dist/platform/thinq/thinqAirConditionerStateSync.js +205 -0
  69. package/dist/platform/thinq/thinqDeviceConfigReconciler.js +26 -0
  70. package/dist/platform/thinq/thinqDeviceConfigurator.js +124 -0
  71. package/dist/platform/thinq/thinqDeviceDiscovery.js +33 -0
  72. package/dist/platform/thinq/thinqWasherCommandHandlers.js +54 -0
  73. package/dist/platform/thinq/thinqWasherEndpointFactory.js +6 -0
  74. package/dist/platform/thinq/thinqWasherStateSync.js +31 -0
  75. package/dist/platform/thinq/thinqWasherStopCommandResolver.js +38 -0
  76. package/dist/services/authentication/AccountAuthStrategy.js +135 -0
  77. package/dist/services/authentication/AuthContext.js +1 -0
  78. package/dist/services/authentication/AuthenticationCoordinator.js +29 -0
  79. package/dist/services/authentication/IAuthStrategy.js +1 -0
  80. package/dist/services/authentication/TokenAuthStrategy.js +27 -0
  81. package/dist/services/authentication/UserDataRepository.js +32 -0
  82. package/dist/services/thinq/gateway.js +61 -0
  83. package/dist/services/thinq/mqtt/mqttCertificate.js +63 -0
  84. package/dist/services/thinq/mqtt/mqttConnectionEvents.js +37 -0
  85. package/dist/services/thinq/mqtt/mqttKeyRepository.js +33 -0
  86. package/dist/services/thinq/mqtt/mqttRetry.js +25 -0
  87. package/dist/services/thinq/mqtt/thinqMqttListener.js +97 -0
  88. package/dist/services/thinq/serviceContainer.js +73 -0
  89. package/dist/services/thinq/session.js +48 -0
  90. package/dist/services/thinq/thinqApiClient.js +258 -0
  91. package/dist/services/thinq/thinqDeviceService.js +75 -0
  92. package/dist/services/thinq/thinqFilterMonitoringService.js +40 -0
  93. package/dist/settings.js +9 -0
  94. package/dist/tests/helpers/testUtils.js +87 -0
  95. package/dist/utils/mask.js +39 -0
  96. package/docs/README_CLI.md +166 -0
  97. package/eslint.config.js +194 -0
  98. package/matterbridge-lg-thinq-plugin.config.json +27 -0
  99. package/matterbridge-lg-thinq-plugin.schema.json +520 -0
  100. package/package.json +107 -0
  101. package/prettier.config.js +49 -0
  102. package/scripts/parse-junit-failures.mjs +45 -0
  103. package/scripts/parse-tsc-failures.mjs +48 -0
  104. package/scripts/run-build-local-summary.mjs +29 -0
  105. package/scripts/run-format-summary.mjs +36 -0
  106. package/scripts/run-lint-fix-summary.mjs +38 -0
  107. package/scripts/run-precommit-summary.mjs +94 -0
  108. package/src/cli/commands/devices.ts +67 -0
  109. package/src/cli/commands/energy.ts +160 -0
  110. package/src/cli/commands/login.ts +74 -0
  111. package/src/cli/help.ts +53 -0
  112. package/src/cli/main.ts +66 -0
  113. package/src/cli/session.ts +15 -0
  114. package/src/cli/types.ts +10 -0
  115. package/src/cli/utils.ts +30 -0
  116. package/src/cli.ts +3 -0
  117. package/src/constants/index.ts +8 -0
  118. package/src/core/domain/entities/ThinqDevice.test.ts +224 -0
  119. package/src/core/domain/entities/ThinqDevice.ts +74 -0
  120. package/src/core/domain/value-objects/AirConditionerCapabilities.ts +50 -0
  121. package/src/core/domain/value-objects/ThinqFilterState.ts +50 -0
  122. package/src/core/domain/value-objects/ThinqSnapshot.ts +162 -0
  123. package/src/errors/AuthenticationError.ts +37 -0
  124. package/src/errors/BaseError.ts +38 -0
  125. package/src/errors/index.ts +24 -0
  126. package/src/model/LgThinqPluginPlatformConfig.ts +94 -0
  127. package/src/module.ts +298 -0
  128. package/src/platform/deviceRegistry.ts +58 -0
  129. package/src/platform/platformConfigManager.ts +133 -0
  130. package/src/platform/platformState.ts +16 -0
  131. package/src/platform/thinq/thinqAirConditionerCommandHandlers.ts +291 -0
  132. package/src/platform/thinq/thinqAirConditionerEndpointFactory.ts +145 -0
  133. package/src/platform/thinq/thinqAirConditionerFilterResetCommandHandler.ts +96 -0
  134. package/src/platform/thinq/thinqAirConditionerFilterStateSync.ts +42 -0
  135. package/src/platform/thinq/thinqAirConditionerStateSync.ts +343 -0
  136. package/src/platform/thinq/thinqDeviceConfigReconciler.ts +42 -0
  137. package/src/platform/thinq/thinqDeviceConfigurator.test.ts +90 -0
  138. package/src/platform/thinq/thinqDeviceConfigurator.ts +191 -0
  139. package/src/platform/thinq/thinqDeviceDiscovery.test.ts +173 -0
  140. package/src/platform/thinq/thinqDeviceDiscovery.ts +51 -0
  141. package/src/platform/thinq/thinqWasherCommandHandlers.ts +120 -0
  142. package/src/platform/thinq/thinqWasherEndpointFactory.ts +37 -0
  143. package/src/platform/thinq/thinqWasherStateSync.ts +60 -0
  144. package/src/platform/thinq/thinqWasherStopCommandResolver.ts +62 -0
  145. package/src/services/authentication/AccountAuthStrategy.test.ts +358 -0
  146. package/src/services/authentication/AccountAuthStrategy.ts +209 -0
  147. package/src/services/authentication/AuthContext.ts +19 -0
  148. package/src/services/authentication/AuthenticationCoordinator.test.ts +217 -0
  149. package/src/services/authentication/AuthenticationCoordinator.ts +59 -0
  150. package/src/services/authentication/IAuthStrategy.ts +10 -0
  151. package/src/services/authentication/TokenAuthStrategy.test.ts +167 -0
  152. package/src/services/authentication/TokenAuthStrategy.ts +40 -0
  153. package/src/services/authentication/UserDataRepository.test.ts +149 -0
  154. package/src/services/authentication/UserDataRepository.ts +46 -0
  155. package/src/services/thinq/gateway.ts +86 -0
  156. package/src/services/thinq/mqtt/mqttCertificate.test.ts +344 -0
  157. package/src/services/thinq/mqtt/mqttCertificate.ts +102 -0
  158. package/src/services/thinq/mqtt/mqttConnectionEvents.test.ts +276 -0
  159. package/src/services/thinq/mqtt/mqttConnectionEvents.ts +76 -0
  160. package/src/services/thinq/mqtt/mqttKeyRepository.test.ts +164 -0
  161. package/src/services/thinq/mqtt/mqttKeyRepository.ts +43 -0
  162. package/src/services/thinq/mqtt/mqttRetry.test.ts +239 -0
  163. package/src/services/thinq/mqtt/mqttRetry.ts +43 -0
  164. package/src/services/thinq/mqtt/thinqMqttListener.test.ts +349 -0
  165. package/src/services/thinq/mqtt/thinqMqttListener.ts +120 -0
  166. package/src/services/thinq/serviceContainer.test.ts +203 -0
  167. package/src/services/thinq/serviceContainer.ts +105 -0
  168. package/src/services/thinq/session.test.ts +287 -0
  169. package/src/services/thinq/session.ts +69 -0
  170. package/src/services/thinq/thinqApiClient.test.ts +702 -0
  171. package/src/services/thinq/thinqApiClient.ts +418 -0
  172. package/src/services/thinq/thinqDeviceService.test.ts +415 -0
  173. package/src/services/thinq/thinqDeviceService.ts +101 -0
  174. package/src/services/thinq/thinqFilterMonitoringService.test.ts +264 -0
  175. package/src/services/thinq/thinqFilterMonitoringService.ts +53 -0
  176. package/src/settings.ts +18 -0
  177. package/src/tests/cli/commands/devices.test.ts +293 -0
  178. package/src/tests/cli/commands/login.test.ts +240 -0
  179. package/src/tests/cli/help.test.ts +46 -0
  180. package/src/tests/cli/main.test.ts +214 -0
  181. package/src/tests/cli/session.test.ts +198 -0
  182. package/src/tests/cli/utils.test.ts +194 -0
  183. package/src/tests/core/domain/value-objects/AirConditionerCapabilities.test.ts +685 -0
  184. package/src/tests/core/domain/value-objects/ThinqFilterState.test.ts +210 -0
  185. package/src/tests/core/domain/value-objects/ThinqSnapshot.test.ts +1035 -0
  186. package/src/tests/helpers/testUtils.ts +118 -0
  187. package/src/tests/model/LgThinqPluginPlatformConfig.test.ts +81 -0
  188. package/src/tests/platform/deviceRegistry.test.ts +177 -0
  189. package/src/tests/platform/platformConfigManager.test.ts +790 -0
  190. package/src/tests/platform/platformState.test.ts +41 -0
  191. package/src/tests/platform/thinq/thinqAirConditionerCommandHandlers.test.ts +879 -0
  192. package/src/tests/platform/thinq/thinqAirConditionerEndpointFactory.test.ts +993 -0
  193. package/src/tests/platform/thinq/thinqAirConditionerFilterResetCommandHandler.test.ts +508 -0
  194. package/src/tests/platform/thinq/thinqAirConditionerFilterStateSync.test.ts +212 -0
  195. package/src/tests/platform/thinq/thinqAirConditionerStateSync.test.ts +1586 -0
  196. package/src/tests/platform/thinq/thinqDeviceConfigReconciler.test.ts +269 -0
  197. package/src/tests/platform/thinq/thinqDeviceConfigurator.test.ts +492 -0
  198. package/src/tests/platform/thinq/thinqWasherCommandHandlers.test.ts +594 -0
  199. package/src/tests/platform/thinq/thinqWasherEndpointFactory.test.ts +84 -0
  200. package/src/tests/platform/thinq/thinqWasherStateSync.test.ts +373 -0
  201. package/src/tests/platform/thinq/thinqWasherStopCommandResolver.test.ts +392 -0
  202. package/src/tests/settings.test.ts +33 -0
  203. package/src/utils/mask.ts +50 -0
  204. package/tsconfig.base.json +25 -0
  205. package/tsconfig.json +9 -0
  206. package/tsconfig.production.json +13 -0
  207. package/tsconfig.vitest.json +5 -0
  208. package/vite.config.ts +43 -0
  209. package/wiki/memory-archive.md +12 -0
  210. package/wiki/reference-workspaces.md +23 -0
@@ -0,0 +1,188 @@
1
+ 'use strict';
2
+
3
+ // MCP stdio server — sends a message to a Discord channel as the configured
4
+ // bot. Reads DISCORD_BOT_TOKEN from ~/.claude/channels/discord/.env.
5
+ // Node built-ins only; no npm deps.
6
+
7
+ const fs = require('fs');
8
+ const os = require('os');
9
+ const path = require('path');
10
+ const https = require('https');
11
+ const readline = require('readline');
12
+
13
+ const ENV_FILE = path.join(os.homedir(), '.claude', 'channels', 'discord', '.env');
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Transport helpers
17
+ // ---------------------------------------------------------------------------
18
+
19
+ function send(obj) {
20
+ process.stdout.write(JSON.stringify(obj) + '\n');
21
+ }
22
+
23
+ function sendResult(id, content) {
24
+ send({
25
+ jsonrpc: '2.0',
26
+ id,
27
+ result: {
28
+ content: [{ type: 'text', text: content }],
29
+ },
30
+ });
31
+ }
32
+
33
+ function sendError(id, code, message) {
34
+ send({
35
+ jsonrpc: '2.0',
36
+ id,
37
+ error: { code, message },
38
+ });
39
+ }
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Token loading
43
+ // ---------------------------------------------------------------------------
44
+
45
+ function readBotToken() {
46
+ if (!fs.existsSync(ENV_FILE)) {
47
+ return null;
48
+ }
49
+ const content = fs.readFileSync(ENV_FILE, 'utf8');
50
+ const match = content.match(/^DISCORD_BOT_TOKEN=(.+)$/m);
51
+ return match ? match[1].trim() : null;
52
+ }
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // DiscordSend tool implementation
56
+ // ---------------------------------------------------------------------------
57
+
58
+ function postMessage(channelId, content, token) {
59
+ return new Promise((resolve) => {
60
+ const payload = JSON.stringify({ content });
61
+ const req = https.request(
62
+ {
63
+ hostname: 'discord.com',
64
+ path: `/api/v10/channels/${encodeURIComponent(channelId)}/messages`,
65
+ method: 'POST',
66
+ headers: {
67
+ Authorization: `Bot ${token}`,
68
+ 'Content-Type': 'application/json',
69
+ 'Content-Length': Buffer.byteLength(payload),
70
+ },
71
+ },
72
+ (res) => {
73
+ let body = '';
74
+ res.on('data', (chunk) => (body += chunk));
75
+ res.on('end', () => resolve({ status: res.statusCode, body }));
76
+ },
77
+ );
78
+ req.on('error', (err) => resolve({ status: 0, body: err.message }));
79
+ req.write(payload);
80
+ req.end();
81
+ });
82
+ }
83
+
84
+ async function discordSend({ channelId, message }) {
85
+ if (!channelId) {
86
+ return 'Error: "channelId" is required.';
87
+ }
88
+ if (!message) {
89
+ return 'Error: "message" is required.';
90
+ }
91
+
92
+ const token = readBotToken();
93
+ if (!token) {
94
+ return `Error: DISCORD_BOT_TOKEN not found in ${ENV_FILE}. Run /discord:configure <token> first.`;
95
+ }
96
+
97
+ const { status, body } = await postMessage(channelId, message, token);
98
+
99
+ if (status >= 200 && status < 300) {
100
+ return `Message sent to channel ${channelId} (HTTP ${status}).`;
101
+ }
102
+ return `Error: failed to send message (HTTP ${status}): ${body}`;
103
+ }
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // Tool definitions
107
+ // ---------------------------------------------------------------------------
108
+
109
+ const TOOL_DEFINITIONS = [
110
+ {
111
+ name: 'DiscordSend',
112
+ description:
113
+ 'Send a message to a Discord channel as the configured bot, using a saved bot token (~/.claude/channels/discord/.env). The bot must already be a member of the server owning the channel, with Send Messages permission. Use for proactively pushing a message to a known channel ID — not for replying to inbound DMs (that flow uses the discord plugin\'s own reply tool).',
114
+ inputSchema: {
115
+ type: 'object',
116
+ properties: {
117
+ channelId: { type: 'string', description: 'Discord channel snowflake ID to send the message to' },
118
+ message: { type: 'string', description: 'Message text to send' },
119
+ },
120
+ required: ['channelId', 'message'],
121
+ },
122
+ },
123
+ ];
124
+
125
+ // ---------------------------------------------------------------------------
126
+ // Request dispatcher
127
+ // ---------------------------------------------------------------------------
128
+
129
+ function handleRequest(req) {
130
+ const { id, method, params } = req;
131
+
132
+ if (id === undefined || id === null) {
133
+ return;
134
+ }
135
+
136
+ if (method === 'initialize') {
137
+ send({
138
+ jsonrpc: '2.0',
139
+ id,
140
+ result: {
141
+ protocolVersion: '2024-11-05',
142
+ serverInfo: { name: 'discord-send-server', version: '1.0.0' },
143
+ capabilities: { tools: {} },
144
+ },
145
+ });
146
+ return;
147
+ }
148
+
149
+ if (method === 'tools/list') {
150
+ send({
151
+ jsonrpc: '2.0',
152
+ id,
153
+ result: { tools: TOOL_DEFINITIONS },
154
+ });
155
+ return;
156
+ }
157
+
158
+ if (method === 'tools/call') {
159
+ const { name, arguments: args } = params || {};
160
+ if (name !== 'DiscordSend') {
161
+ sendError(id, -32601, `Unknown tool: ${name}`);
162
+ return;
163
+ }
164
+ discordSend(args || {})
165
+ .then((text) => sendResult(id, text))
166
+ .catch((err) => sendResult(id, `Error: ${err.message}`));
167
+ return;
168
+ }
169
+
170
+ sendError(id, -32601, `Method not found: ${method}`);
171
+ }
172
+
173
+ // ---------------------------------------------------------------------------
174
+ // Main — read newline-delimited JSON from stdin
175
+ // ---------------------------------------------------------------------------
176
+
177
+ const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
178
+
179
+ rl.on('line', (line) => {
180
+ const trimmed = line.trim();
181
+ if (!trimmed) return;
182
+ try {
183
+ const req = JSON.parse(trimmed);
184
+ handleRequest(req);
185
+ } catch (_) {
186
+ // Malformed JSON — ignore
187
+ }
188
+ });
@@ -0,0 +1,181 @@
1
+ 'use strict';
2
+
3
+ // MCP stdio server — creates a GitHub release via the `gh` CLI. GitHub
4
+ // creates the git tag automatically (pointing at the target branch's HEAD)
5
+ // if it doesn't already exist. Node built-ins only; no npm deps.
6
+
7
+ const { execFileSync } = require('child_process');
8
+ const readline = require('readline');
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Transport helpers
12
+ // ---------------------------------------------------------------------------
13
+
14
+ function send(obj) {
15
+ process.stdout.write(JSON.stringify(obj) + '\n');
16
+ }
17
+
18
+ function sendResult(id, content) {
19
+ send({
20
+ jsonrpc: '2.0',
21
+ id,
22
+ result: {
23
+ content: [{ type: 'text', text: content }],
24
+ },
25
+ });
26
+ }
27
+
28
+ function sendError(id, code, message) {
29
+ send({
30
+ jsonrpc: '2.0',
31
+ id,
32
+ error: { code, message },
33
+ });
34
+ }
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // GitHubRelease tool implementation
38
+ // ---------------------------------------------------------------------------
39
+
40
+ function createRelease({ tag, targetBranch, notes, title, prerelease, repoPath }) {
41
+ if (!tag) {
42
+ return 'Error: "tag" is required.';
43
+ }
44
+ if (!targetBranch) {
45
+ return 'Error: "targetBranch" is required.';
46
+ }
47
+ if (!notes) {
48
+ return 'Error: "notes" is required.';
49
+ }
50
+
51
+ const isPrerelease = typeof prerelease === 'boolean' ? prerelease : /-rc\d*$/i.test(tag);
52
+ const releaseTitle = title || tag;
53
+
54
+ const args = [
55
+ 'release',
56
+ 'create',
57
+ tag,
58
+ '--target',
59
+ targetBranch,
60
+ '--title',
61
+ releaseTitle,
62
+ '--notes',
63
+ notes,
64
+ ];
65
+ if (isPrerelease) {
66
+ args.push('--prerelease');
67
+ }
68
+
69
+ try {
70
+ const stdout = execFileSync('gh', args, {
71
+ encoding: 'utf8',
72
+ cwd: repoPath || process.cwd(),
73
+ stdio: ['ignore', 'pipe', 'pipe'],
74
+ });
75
+ return `Release created (${isPrerelease ? 'prerelease' : 'release'}): ${stdout.trim()}`;
76
+ } catch (err) {
77
+ const stderr = err.stderr ? err.stderr.toString().trim() : '';
78
+ const stdout = err.stdout ? err.stdout.toString().trim() : '';
79
+ return `Error: gh release create failed.\n${stderr || stdout || err.message}`;
80
+ }
81
+ }
82
+
83
+ // ---------------------------------------------------------------------------
84
+ // Tool definitions
85
+ // ---------------------------------------------------------------------------
86
+
87
+ const TOOL_DEFINITIONS = [
88
+ {
89
+ name: 'GitHubRelease',
90
+ description:
91
+ 'Create a GitHub release using the gh CLI (must already be authenticated via `gh auth login`). GitHub creates the git tag automatically, pointing at the target branch\'s current HEAD, if the tag does not already exist. Prerelease is auto-detected from the tag (matches -rcN suffix) unless explicitly overridden.',
92
+ inputSchema: {
93
+ type: 'object',
94
+ properties: {
95
+ tag: { type: 'string', description: 'Release/tag name, e.g. "1.1.7" or "1.1.7-rc07"' },
96
+ targetBranch: { type: 'string', description: 'Branch the tag should point at, e.g. "master" or "dev"' },
97
+ notes: { type: 'string', description: 'Release notes body (e.g. the changelog entry text)' },
98
+ title: { type: 'string', description: 'Release title (defaults to the tag if omitted)' },
99
+ prerelease: {
100
+ type: 'boolean',
101
+ description: 'Force prerelease flag on/off. If omitted, auto-detected from tag (-rcN suffix -> true).',
102
+ },
103
+ repoPath: {
104
+ type: 'string',
105
+ description: 'Path to the local git repository (defaults to current working directory)',
106
+ },
107
+ },
108
+ required: ['tag', 'targetBranch', 'notes'],
109
+ },
110
+ },
111
+ ];
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // Request dispatcher
115
+ // ---------------------------------------------------------------------------
116
+
117
+ function handleRequest(req) {
118
+ const { id, method, params } = req;
119
+
120
+ if (id === undefined || id === null) {
121
+ return;
122
+ }
123
+
124
+ if (method === 'initialize') {
125
+ send({
126
+ jsonrpc: '2.0',
127
+ id,
128
+ result: {
129
+ protocolVersion: '2024-11-05',
130
+ serverInfo: { name: 'github-release-server', version: '1.0.0' },
131
+ capabilities: { tools: {} },
132
+ },
133
+ });
134
+ return;
135
+ }
136
+
137
+ if (method === 'tools/list') {
138
+ send({
139
+ jsonrpc: '2.0',
140
+ id,
141
+ result: { tools: TOOL_DEFINITIONS },
142
+ });
143
+ return;
144
+ }
145
+
146
+ if (method === 'tools/call') {
147
+ const { name, arguments: args } = params || {};
148
+ try {
149
+ let text;
150
+ if (name === 'GitHubRelease') {
151
+ text = createRelease(args || {});
152
+ } else {
153
+ sendError(id, -32601, `Unknown tool: ${name}`);
154
+ return;
155
+ }
156
+ sendResult(id, text);
157
+ } catch (err) {
158
+ sendResult(id, `Error: ${err.message}`);
159
+ }
160
+ return;
161
+ }
162
+
163
+ sendError(id, -32601, `Method not found: ${method}`);
164
+ }
165
+
166
+ // ---------------------------------------------------------------------------
167
+ // Main — read newline-delimited JSON from stdin
168
+ // ---------------------------------------------------------------------------
169
+
170
+ const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
171
+
172
+ rl.on('line', (line) => {
173
+ const trimmed = line.trim();
174
+ if (!trimmed) return;
175
+ try {
176
+ const req = JSON.parse(trimmed);
177
+ handleRequest(req);
178
+ } catch (_) {
179
+ // Malformed JSON — ignore
180
+ }
181
+ });
@@ -0,0 +1,267 @@
1
+ 'use strict';
2
+
3
+ // MCP stdio server — Glob + Grep tools
4
+ // Node built-ins only; no npm dependencies.
5
+
6
+ const { execSync } = require('child_process');
7
+ const readline = require('readline');
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Transport helpers
11
+ // ---------------------------------------------------------------------------
12
+
13
+ function send(obj) {
14
+ process.stdout.write(JSON.stringify(obj) + '\n');
15
+ }
16
+
17
+ function sendResult(id, content) {
18
+ send({
19
+ jsonrpc: '2.0',
20
+ id,
21
+ result: {
22
+ content: [{ type: 'text', text: content }],
23
+ },
24
+ });
25
+ }
26
+
27
+ function sendError(id, code, message) {
28
+ send({
29
+ jsonrpc: '2.0',
30
+ id,
31
+ error: { code, message },
32
+ });
33
+ }
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Glob tool implementation
37
+ // ---------------------------------------------------------------------------
38
+
39
+ /**
40
+ * Convert a glob pattern (with optional base path override) to a `find` command
41
+ * and execute it. Returns newline-joined paths or '(no results)'.
42
+ */
43
+ function detectRg() {
44
+ try {
45
+ execSync('which rg', { stdio: 'ignore' });
46
+ return true;
47
+ } catch (_) {
48
+ return false;
49
+ }
50
+ }
51
+
52
+ function runGlob({ pattern, path: basePath }) {
53
+ const searchPath = basePath || '.';
54
+
55
+ const segments = pattern.split('/');
56
+
57
+ let staticSegments = [];
58
+ let wildcardStart = -1;
59
+ for (let i = 0; i < segments.length; i++) {
60
+ if (segments[i].includes('*') || segments[i].includes('?')) {
61
+ wildcardStart = i;
62
+ break;
63
+ }
64
+ staticSegments.push(segments[i]);
65
+ }
66
+
67
+ const dynamicSegments = wildcardStart >= 0 ? segments.slice(wildcardStart) : segments;
68
+ const filePattern = dynamicSegments[dynamicSegments.length - 1];
69
+ const dirSegments = dynamicSegments.slice(0, dynamicSegments.length - 1);
70
+
71
+ let findBase = searchPath;
72
+ if (staticSegments.length > 0) {
73
+ findBase = searchPath.replace(/\/+$/, '') + '/' + staticSegments.join('/');
74
+ }
75
+
76
+ const hasRecursiveGlob = dirSegments.some((s) => s === '**');
77
+ const maxDepth = hasRecursiveGlob ? null : dirSegments.length + 1;
78
+
79
+ const safeFilePattern = filePattern.replace(/'/g, "'\\''");
80
+ const safeFindBase = findBase.replace(/'/g, "'\\''");
81
+
82
+ let cmd;
83
+ if (detectRg()) {
84
+ // rg --files respects .gitignore by default
85
+ const depthFlag = maxDepth != null ? `--max-depth ${maxDepth}` : '';
86
+ cmd = `rg --files ${depthFlag} --glob '${safeFilePattern}' -- '${safeFindBase}' 2>/dev/null | sort`;
87
+ } else {
88
+ // find fallback: excludes .git/node_modules but does not parse .gitignore
89
+ const depthFlag = maxDepth != null ? `-maxdepth ${maxDepth}` : '';
90
+ cmd = `find '${safeFindBase}' ${depthFlag} -name '${safeFilePattern}' -not -path '*/.git/*' -not -path '*/node_modules/*' 2>/dev/null | sort`;
91
+ }
92
+
93
+ try {
94
+ const output = execSync(cmd, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
95
+ const trimmed = output.trim();
96
+ return trimmed.length > 0 ? trimmed : '(no results)';
97
+ } catch (err) {
98
+ const output = (err.stdout || '').trim();
99
+ return output.length > 0 ? output : '(no results)';
100
+ }
101
+ }
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // Grep tool implementation
105
+ // ---------------------------------------------------------------------------
106
+
107
+ function runGrep({ pattern, path: searchPath, glob, type, '-i': caseInsensitive, output_mode, context, head_limit }) {
108
+ const base = searchPath || '.';
109
+ const mode = output_mode || 'files_with_matches';
110
+
111
+ let cmd;
112
+
113
+ if (detectRg()) {
114
+ const flags = ['--no-heading'];
115
+ if (caseInsensitive) flags.push('-i');
116
+ if (glob) flags.push(`--glob '${glob.replace(/'/g, "'\\''")}'`);
117
+ if (type) flags.push(`--type '${type.replace(/'/g, "'\\''")}'`);
118
+ if (mode === 'files_with_matches') flags.push('-l');
119
+ else if (mode === 'count') flags.push('-c');
120
+ if (context != null && mode === 'content') flags.push(`-C ${Number(context)}`);
121
+ flags.push(`-- '${pattern.replace(/'/g, "'\\''")}'`);
122
+ flags.push(`'${base.replace(/'/g, "'\\''")}'`);
123
+ cmd = `rg ${flags.join(' ')} 2>/dev/null`;
124
+ } else {
125
+ // grep fallback: excludes .git/node_modules but does not parse .gitignore
126
+ const flags = ['-r', '-E', '--exclude-dir=.git', '--exclude-dir=node_modules'];
127
+ if (caseInsensitive) flags.push('-i');
128
+ if (glob) flags.push(`--include='${glob.replace(/'/g, "'\\''")}'`);
129
+ if (mode === 'files_with_matches') flags.push('-l');
130
+ else if (mode === 'count') flags.push('-c');
131
+ if (context != null && mode === 'content') flags.push(`-C ${Number(context)}`);
132
+ flags.push(`-- '${pattern.replace(/'/g, "'\\''")}'`);
133
+ flags.push(`'${base.replace(/'/g, "'\\''")}'`);
134
+ cmd = `grep ${flags.join(' ')} 2>/dev/null`;
135
+ }
136
+
137
+ let output = '';
138
+ try {
139
+ output = execSync(cmd, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
140
+ } catch (err) {
141
+ // grep/rg exit 1 means no matches (not an error per se)
142
+ output = err.stdout || '';
143
+ }
144
+
145
+ output = output.trim();
146
+
147
+ if (head_limit != null && output.length > 0) {
148
+ const lines = output.split('\n');
149
+ output = lines.slice(0, Number(head_limit)).join('\n');
150
+ }
151
+
152
+ return output.length > 0 ? output : '(no results)';
153
+ }
154
+
155
+ // ---------------------------------------------------------------------------
156
+ // Tool definitions
157
+ // ---------------------------------------------------------------------------
158
+
159
+ const TOOL_DEFINITIONS = [
160
+ {
161
+ name: 'Glob',
162
+ description: 'Fast file pattern matching tool. Returns matching file paths sorted by modification time.',
163
+ inputSchema: {
164
+ type: 'object',
165
+ properties: {
166
+ pattern: { type: 'string', description: 'Glob pattern to match files against' },
167
+ path: { type: 'string', description: 'Directory to search in (optional)' },
168
+ },
169
+ required: ['pattern'],
170
+ },
171
+ },
172
+ {
173
+ name: 'Grep',
174
+ description: 'Search file contents with regex. Supports glob filter, type filter, context lines, and output modes.',
175
+ inputSchema: {
176
+ type: 'object',
177
+ properties: {
178
+ pattern: { type: 'string', description: 'Regex pattern to search for' },
179
+ path: { type: 'string', description: 'File or directory to search in (optional)' },
180
+ glob: { type: 'string', description: 'Glob pattern to filter files (e.g. "*.ts")' },
181
+ type: { type: 'string', description: 'File type to search (e.g. "js", "py")' },
182
+ '-i': { type: 'boolean', description: 'Case insensitive search' },
183
+ output_mode: {
184
+ type: 'string',
185
+ enum: ['content', 'files_with_matches', 'count'],
186
+ description: 'Output mode (default: files_with_matches)',
187
+ },
188
+ context: { type: 'number', description: 'Lines of context around matches (content mode only)' },
189
+ head_limit: { type: 'number', description: 'Limit output to first N lines' },
190
+ },
191
+ required: ['pattern'],
192
+ },
193
+ },
194
+ ];
195
+
196
+ // ---------------------------------------------------------------------------
197
+ // Request dispatcher
198
+ // ---------------------------------------------------------------------------
199
+
200
+ function handleRequest(req) {
201
+ const { jsonrpc, id, method, params } = req;
202
+
203
+ // Notifications (no id) — no response
204
+ if (id === undefined || id === null) {
205
+ return;
206
+ }
207
+
208
+ if (method === 'initialize') {
209
+ send({
210
+ jsonrpc: '2.0',
211
+ id,
212
+ result: {
213
+ protocolVersion: '2024-11-05',
214
+ serverInfo: { name: 'glob-grep-server', version: '1.0.0' },
215
+ capabilities: { tools: {} },
216
+ },
217
+ });
218
+ return;
219
+ }
220
+
221
+ if (method === 'tools/list') {
222
+ send({
223
+ jsonrpc: '2.0',
224
+ id,
225
+ result: { tools: TOOL_DEFINITIONS },
226
+ });
227
+ return;
228
+ }
229
+
230
+ if (method === 'tools/call') {
231
+ const { name, arguments: args } = params || {};
232
+ try {
233
+ let text;
234
+ if (name === 'Glob') {
235
+ text = runGlob(args || {});
236
+ } else if (name === 'Grep') {
237
+ text = runGrep(args || {});
238
+ } else {
239
+ sendError(id, -32601, `Unknown tool: ${name}`);
240
+ return;
241
+ }
242
+ sendResult(id, text);
243
+ } catch (err) {
244
+ sendResult(id, `Error: ${err.message}`);
245
+ }
246
+ return;
247
+ }
248
+
249
+ sendError(id, -32601, `Method not found: ${method}`);
250
+ }
251
+
252
+ // ---------------------------------------------------------------------------
253
+ // Main — read newline-delimited JSON from stdin
254
+ // ---------------------------------------------------------------------------
255
+
256
+ const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
257
+
258
+ rl.on('line', (line) => {
259
+ const trimmed = line.trim();
260
+ if (!trimmed) return;
261
+ try {
262
+ const req = JSON.parse(trimmed);
263
+ handleRequest(req);
264
+ } catch (_) {
265
+ // Malformed JSON — ignore
266
+ }
267
+ });