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.
- package/.claude/CLAUDE.md +43 -0
- package/.claude/agents/compiler.md +91 -0
- package/.claude/agents/direct-executor.md +98 -0
- package/.claude/agents/documenter.md +78 -0
- package/.claude/agents/finalizer.md +233 -0
- package/.claude/agents/implementer.md +112 -0
- package/.claude/agents/investigator.md +100 -0
- package/.claude/agents/release-manager.md +203 -0
- package/.claude/agents/reviewer.md +131 -0
- package/.claude/agents/technical-architect.md +403 -0
- package/.claude/agents/test-writer.md +159 -0
- package/.claude/agents/wiki-manager.md +147 -0
- package/.claude/instructions/agent-prompts.md +90 -0
- package/.claude/instructions/shared-rules.md +11 -0
- package/.claude/instructions/team-orchestrator-policy.md +165 -0
- package/.claude/memory.md +186 -0
- package/.claude/output-styles/engineer-manager.md +38 -0
- package/.claude/settings.json +40 -0
- package/.claude/settings.local.json +30 -0
- package/.claude/skills/load-policy/SKILL.md +13 -0
- package/.claude/skills/ref-idea/SKILL.md +44 -0
- package/.claude/skills/status-of/SKILL.md +38 -0
- package/.claude/skills/workspace-console/SKILL.md +49 -0
- package/.claude/statusline-command.sh +130 -0
- package/.claude/statusline.md +53 -0
- package/.claude/templates/approval-packet.template.html +171 -0
- package/.claude/templates/reference-workspaces.md +16 -0
- package/.claude/tools/cli-runner-server.cjs +220 -0
- package/.claude/tools/discord-send-server.cjs +188 -0
- package/.claude/tools/github-release-server.cjs +181 -0
- package/.claude/tools/glob-grep-server.cjs +267 -0
- package/.claude/tools/read-log-server.cjs +175 -0
- package/.github/codeql/codeql-config.yml +14 -0
- package/.github/workflows/build.yml +54 -0
- package/.github/workflows/codeql.yml +59 -0
- package/.github/workflows/coverage.yml +70 -0
- package/.github/workflows/publish.yml +103 -0
- package/.mcp.json +34 -0
- package/CHANGELOG.md +195 -0
- package/LICENSE +21 -0
- package/dist/cli/commands/devices.js +52 -0
- package/dist/cli/commands/energy.js +122 -0
- package/dist/cli/commands/login.js +54 -0
- package/dist/cli/help.js +50 -0
- package/dist/cli/main.js +56 -0
- package/dist/cli/session.js +13 -0
- package/dist/cli/types.js +1 -0
- package/dist/cli/utils.js +29 -0
- package/dist/cli.js +2 -0
- package/dist/constants/index.js +1 -0
- package/dist/core/domain/entities/ThinqDevice.js +43 -0
- package/dist/core/domain/value-objects/AirConditionerCapabilities.js +22 -0
- package/dist/core/domain/value-objects/ThinqFilterState.js +35 -0
- package/dist/core/domain/value-objects/ThinqSnapshot.js +125 -0
- package/dist/errors/AuthenticationError.js +26 -0
- package/dist/errors/BaseError.js +23 -0
- package/dist/errors/index.js +2 -0
- package/dist/model/LgThinkqPluginPlatformConfig.js +23 -0
- package/dist/model/LgThinqPluginPlatformConfig.js +23 -0
- package/dist/module.js +212 -0
- package/dist/platform/deviceRegistry.js +26 -0
- package/dist/platform/platformConfigManager.js +89 -0
- package/dist/platform/platformState.js +9 -0
- package/dist/platform/thinq/thinqAirConditionerCommandHandlers.js +190 -0
- package/dist/platform/thinq/thinqAirConditionerEndpointFactory.js +53 -0
- package/dist/platform/thinq/thinqAirConditionerFilterResetCommandHandler.js +54 -0
- package/dist/platform/thinq/thinqAirConditionerFilterStateSync.js +19 -0
- package/dist/platform/thinq/thinqAirConditionerStateSync.js +205 -0
- package/dist/platform/thinq/thinqDeviceConfigReconciler.js +26 -0
- package/dist/platform/thinq/thinqDeviceConfigurator.js +124 -0
- package/dist/platform/thinq/thinqDeviceDiscovery.js +33 -0
- package/dist/platform/thinq/thinqWasherCommandHandlers.js +54 -0
- package/dist/platform/thinq/thinqWasherEndpointFactory.js +6 -0
- package/dist/platform/thinq/thinqWasherStateSync.js +31 -0
- package/dist/platform/thinq/thinqWasherStopCommandResolver.js +38 -0
- package/dist/services/authentication/AccountAuthStrategy.js +135 -0
- package/dist/services/authentication/AuthContext.js +1 -0
- package/dist/services/authentication/AuthenticationCoordinator.js +29 -0
- package/dist/services/authentication/IAuthStrategy.js +1 -0
- package/dist/services/authentication/TokenAuthStrategy.js +27 -0
- package/dist/services/authentication/UserDataRepository.js +32 -0
- package/dist/services/thinq/gateway.js +61 -0
- package/dist/services/thinq/mqtt/mqttCertificate.js +63 -0
- package/dist/services/thinq/mqtt/mqttConnectionEvents.js +37 -0
- package/dist/services/thinq/mqtt/mqttKeyRepository.js +33 -0
- package/dist/services/thinq/mqtt/mqttRetry.js +25 -0
- package/dist/services/thinq/mqtt/thinqMqttListener.js +97 -0
- package/dist/services/thinq/serviceContainer.js +73 -0
- package/dist/services/thinq/session.js +48 -0
- package/dist/services/thinq/thinqApiClient.js +258 -0
- package/dist/services/thinq/thinqDeviceService.js +75 -0
- package/dist/services/thinq/thinqFilterMonitoringService.js +40 -0
- package/dist/settings.js +9 -0
- package/dist/tests/helpers/testUtils.js +87 -0
- package/dist/utils/mask.js +39 -0
- package/docs/README_CLI.md +166 -0
- package/eslint.config.js +194 -0
- package/matterbridge-lg-thinq-plugin.config.json +27 -0
- package/matterbridge-lg-thinq-plugin.schema.json +520 -0
- package/package.json +107 -0
- package/prettier.config.js +49 -0
- package/scripts/parse-junit-failures.mjs +45 -0
- package/scripts/parse-tsc-failures.mjs +48 -0
- package/scripts/run-build-local-summary.mjs +29 -0
- package/scripts/run-format-summary.mjs +36 -0
- package/scripts/run-lint-fix-summary.mjs +38 -0
- package/scripts/run-precommit-summary.mjs +94 -0
- package/src/cli/commands/devices.ts +67 -0
- package/src/cli/commands/energy.ts +160 -0
- package/src/cli/commands/login.ts +74 -0
- package/src/cli/help.ts +53 -0
- package/src/cli/main.ts +66 -0
- package/src/cli/session.ts +15 -0
- package/src/cli/types.ts +10 -0
- package/src/cli/utils.ts +30 -0
- package/src/cli.ts +3 -0
- package/src/constants/index.ts +8 -0
- package/src/core/domain/entities/ThinqDevice.test.ts +224 -0
- package/src/core/domain/entities/ThinqDevice.ts +74 -0
- package/src/core/domain/value-objects/AirConditionerCapabilities.ts +50 -0
- package/src/core/domain/value-objects/ThinqFilterState.ts +50 -0
- package/src/core/domain/value-objects/ThinqSnapshot.ts +162 -0
- package/src/errors/AuthenticationError.ts +37 -0
- package/src/errors/BaseError.ts +38 -0
- package/src/errors/index.ts +24 -0
- package/src/model/LgThinqPluginPlatformConfig.ts +94 -0
- package/src/module.ts +298 -0
- package/src/platform/deviceRegistry.ts +58 -0
- package/src/platform/platformConfigManager.ts +133 -0
- package/src/platform/platformState.ts +16 -0
- package/src/platform/thinq/thinqAirConditionerCommandHandlers.ts +291 -0
- package/src/platform/thinq/thinqAirConditionerEndpointFactory.ts +145 -0
- package/src/platform/thinq/thinqAirConditionerFilterResetCommandHandler.ts +96 -0
- package/src/platform/thinq/thinqAirConditionerFilterStateSync.ts +42 -0
- package/src/platform/thinq/thinqAirConditionerStateSync.ts +343 -0
- package/src/platform/thinq/thinqDeviceConfigReconciler.ts +42 -0
- package/src/platform/thinq/thinqDeviceConfigurator.test.ts +90 -0
- package/src/platform/thinq/thinqDeviceConfigurator.ts +191 -0
- package/src/platform/thinq/thinqDeviceDiscovery.test.ts +173 -0
- package/src/platform/thinq/thinqDeviceDiscovery.ts +51 -0
- package/src/platform/thinq/thinqWasherCommandHandlers.ts +120 -0
- package/src/platform/thinq/thinqWasherEndpointFactory.ts +37 -0
- package/src/platform/thinq/thinqWasherStateSync.ts +60 -0
- package/src/platform/thinq/thinqWasherStopCommandResolver.ts +62 -0
- package/src/services/authentication/AccountAuthStrategy.test.ts +358 -0
- package/src/services/authentication/AccountAuthStrategy.ts +209 -0
- package/src/services/authentication/AuthContext.ts +19 -0
- package/src/services/authentication/AuthenticationCoordinator.test.ts +217 -0
- package/src/services/authentication/AuthenticationCoordinator.ts +59 -0
- package/src/services/authentication/IAuthStrategy.ts +10 -0
- package/src/services/authentication/TokenAuthStrategy.test.ts +167 -0
- package/src/services/authentication/TokenAuthStrategy.ts +40 -0
- package/src/services/authentication/UserDataRepository.test.ts +149 -0
- package/src/services/authentication/UserDataRepository.ts +46 -0
- package/src/services/thinq/gateway.ts +86 -0
- package/src/services/thinq/mqtt/mqttCertificate.test.ts +344 -0
- package/src/services/thinq/mqtt/mqttCertificate.ts +102 -0
- package/src/services/thinq/mqtt/mqttConnectionEvents.test.ts +276 -0
- package/src/services/thinq/mqtt/mqttConnectionEvents.ts +76 -0
- package/src/services/thinq/mqtt/mqttKeyRepository.test.ts +164 -0
- package/src/services/thinq/mqtt/mqttKeyRepository.ts +43 -0
- package/src/services/thinq/mqtt/mqttRetry.test.ts +239 -0
- package/src/services/thinq/mqtt/mqttRetry.ts +43 -0
- package/src/services/thinq/mqtt/thinqMqttListener.test.ts +349 -0
- package/src/services/thinq/mqtt/thinqMqttListener.ts +120 -0
- package/src/services/thinq/serviceContainer.test.ts +203 -0
- package/src/services/thinq/serviceContainer.ts +105 -0
- package/src/services/thinq/session.test.ts +287 -0
- package/src/services/thinq/session.ts +69 -0
- package/src/services/thinq/thinqApiClient.test.ts +702 -0
- package/src/services/thinq/thinqApiClient.ts +418 -0
- package/src/services/thinq/thinqDeviceService.test.ts +415 -0
- package/src/services/thinq/thinqDeviceService.ts +101 -0
- package/src/services/thinq/thinqFilterMonitoringService.test.ts +264 -0
- package/src/services/thinq/thinqFilterMonitoringService.ts +53 -0
- package/src/settings.ts +18 -0
- package/src/tests/cli/commands/devices.test.ts +293 -0
- package/src/tests/cli/commands/login.test.ts +240 -0
- package/src/tests/cli/help.test.ts +46 -0
- package/src/tests/cli/main.test.ts +214 -0
- package/src/tests/cli/session.test.ts +198 -0
- package/src/tests/cli/utils.test.ts +194 -0
- package/src/tests/core/domain/value-objects/AirConditionerCapabilities.test.ts +685 -0
- package/src/tests/core/domain/value-objects/ThinqFilterState.test.ts +210 -0
- package/src/tests/core/domain/value-objects/ThinqSnapshot.test.ts +1035 -0
- package/src/tests/helpers/testUtils.ts +118 -0
- package/src/tests/model/LgThinqPluginPlatformConfig.test.ts +81 -0
- package/src/tests/platform/deviceRegistry.test.ts +177 -0
- package/src/tests/platform/platformConfigManager.test.ts +790 -0
- package/src/tests/platform/platformState.test.ts +41 -0
- package/src/tests/platform/thinq/thinqAirConditionerCommandHandlers.test.ts +879 -0
- package/src/tests/platform/thinq/thinqAirConditionerEndpointFactory.test.ts +993 -0
- package/src/tests/platform/thinq/thinqAirConditionerFilterResetCommandHandler.test.ts +508 -0
- package/src/tests/platform/thinq/thinqAirConditionerFilterStateSync.test.ts +212 -0
- package/src/tests/platform/thinq/thinqAirConditionerStateSync.test.ts +1586 -0
- package/src/tests/platform/thinq/thinqDeviceConfigReconciler.test.ts +269 -0
- package/src/tests/platform/thinq/thinqDeviceConfigurator.test.ts +492 -0
- package/src/tests/platform/thinq/thinqWasherCommandHandlers.test.ts +594 -0
- package/src/tests/platform/thinq/thinqWasherEndpointFactory.test.ts +84 -0
- package/src/tests/platform/thinq/thinqWasherStateSync.test.ts +373 -0
- package/src/tests/platform/thinq/thinqWasherStopCommandResolver.test.ts +392 -0
- package/src/tests/settings.test.ts +33 -0
- package/src/utils/mask.ts +50 -0
- package/tsconfig.base.json +25 -0
- package/tsconfig.json +9 -0
- package/tsconfig.production.json +13 -0
- package/tsconfig.vitest.json +5 -0
- package/vite.config.ts +43 -0
- package/wiki/memory-archive.md +12 -0
- 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
|
+
});
|