mcp-osp-prompt 1.0.2 → 1.0.4
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/dialog/native.js +51 -20
- package/package.json +1 -1
- package/prompt-manager.js +16 -0
- package/server.js +7 -4
- package/tools.js +131 -8
package/dialog/native.js
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
import os from 'os';
|
|
2
|
-
import { execSync } from 'child_process';
|
|
2
|
+
import { execSync, exec } from 'child_process';
|
|
3
|
+
import { promisify } from 'util';
|
|
3
4
|
import { escapeAppleScriptString } from './utils.js';
|
|
5
|
+
|
|
6
|
+
// 使用 promisify 将 exec 转为 Promise 版本,避免阻塞事件循环
|
|
7
|
+
const execAsync = promisify(exec);
|
|
8
|
+
|
|
9
|
+
// 🔧 JSON-RPC 心跳函数:通过 stdout 发送 notification,防止 MCP 客户端超时
|
|
10
|
+
let heartbeatCounter = 0;
|
|
11
|
+
function sendHeartbeat(context = 'dialog') {
|
|
12
|
+
heartbeatCounter++;
|
|
13
|
+
|
|
14
|
+
// 尝试使用 MCP 标准的 progress notification 格式
|
|
15
|
+
const notification = {
|
|
16
|
+
jsonrpc: '2.0',
|
|
17
|
+
method: 'notifications/progress',
|
|
18
|
+
params: {
|
|
19
|
+
progressToken: `heartbeat-${Date.now()}`,
|
|
20
|
+
progress: heartbeatCounter,
|
|
21
|
+
total: -1 // -1 表示未知总数
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// 写入 stdout,这是 MCP 客户端监听的通道
|
|
26
|
+
process.stdout.write(JSON.stringify(notification) + '\n');
|
|
27
|
+
// 同时写入 stderr 供调试(只保留一行日志)
|
|
28
|
+
console.error(`[Heartbeat #${heartbeatCounter}] progress notification sent`);
|
|
29
|
+
}
|
|
4
30
|
// 🟢 GREEN: Task 2.2 - 导入CFG配置用于native弹窗宽度设置
|
|
5
31
|
import { CFG } from '../config.js';
|
|
6
32
|
// ✅ UNIFIED: 导入统一的按钮管理常量
|
|
@@ -63,16 +89,21 @@ export async function showInputDialog({ title = '输入', message = '请输入
|
|
|
63
89
|
end if
|
|
64
90
|
`;
|
|
65
91
|
|
|
66
|
-
// 🆕
|
|
92
|
+
// 🆕 添加保活机制(5秒间隔,通过 stdout 发送 JSON-RPC notification)
|
|
67
93
|
const keepAliveInterval = setInterval(() => {
|
|
68
|
-
|
|
69
|
-
},
|
|
94
|
+
sendHeartbeat('input');
|
|
95
|
+
}, 5000);
|
|
70
96
|
|
|
71
97
|
try {
|
|
72
|
-
|
|
98
|
+
console.error('[Dialog] Calling osascript for input dialog with async exec (non-blocking)...');
|
|
99
|
+
|
|
100
|
+
// 🔧 关键修改:使用异步 exec 替代 execSync,避免阻塞事件循环
|
|
101
|
+
const { stdout } = await execAsync(`osascript -e '${applescript}'`, {
|
|
73
102
|
encoding: 'utf8',
|
|
74
|
-
timeout: 1800000 // 30 minutes timeout
|
|
75
|
-
})
|
|
103
|
+
timeout: 1800000 // 30 minutes timeout
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const result = stdout.trim();
|
|
76
107
|
|
|
77
108
|
clearInterval(keepAliveInterval); // 清理定时器
|
|
78
109
|
|
|
@@ -159,13 +190,13 @@ export async function showConfirmDialog({ title = '确认', message = '请选择
|
|
|
159
190
|
|
|
160
191
|
const platform = os.platform();
|
|
161
192
|
|
|
162
|
-
// 🆕
|
|
193
|
+
// 🆕 添加保活机制(5秒间隔,通过 stdout 发送 JSON-RPC notification)
|
|
163
194
|
const keepAliveInterval = setInterval(() => {
|
|
164
|
-
|
|
165
|
-
},
|
|
195
|
+
sendHeartbeat('button selection');
|
|
196
|
+
}, 5000);
|
|
166
197
|
|
|
167
198
|
try {
|
|
168
|
-
let
|
|
199
|
+
let result;
|
|
169
200
|
if (platform === 'darwin') {
|
|
170
201
|
// Use enhanced escaping for all text content to handle special characters safely
|
|
171
202
|
const safeTitle = escapeAppleScriptString(title).trim() || 'Confirmation';
|
|
@@ -183,31 +214,31 @@ export async function showConfirmDialog({ title = '确认', message = '请选择
|
|
|
183
214
|
const buttonsList = safeButtons.map(b => `"${b}"`).join(', ');
|
|
184
215
|
const script = `set selectedButton to button returned of (display dialog "${safeMessage}" with title "${safeTitle}" buttons {${buttonsList}} default button "${safeButtons[0]}")`;
|
|
185
216
|
|
|
186
|
-
|
|
217
|
+
console.error('[Dialog] Calling osascript with async exec (non-blocking)...');
|
|
187
218
|
|
|
188
|
-
|
|
219
|
+
// 🔧 关键修改:使用异步 exec 替代 execSync,避免阻塞事件循环
|
|
220
|
+
const { stdout } = await execAsync(`osascript -e '${script}'`, {
|
|
189
221
|
encoding: 'utf8',
|
|
190
|
-
timeout:
|
|
191
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
222
|
+
timeout: 3600000 // 60 minutes timeout
|
|
192
223
|
});
|
|
193
224
|
|
|
194
225
|
clearInterval(keepAliveInterval); // 清理定时器
|
|
195
226
|
|
|
196
|
-
const selectedButton =
|
|
227
|
+
const selectedButton = stdout.trim();
|
|
197
228
|
|
|
198
229
|
// Map safe button back to original button
|
|
199
230
|
const safeIndex = safeButtons.indexOf(selectedButton);
|
|
200
231
|
return safeIndex >= 0 ? buttons[safeIndex] : buttons[0];
|
|
201
232
|
|
|
202
233
|
} else {
|
|
203
|
-
// Linux zenity
|
|
234
|
+
// Linux zenity - 也改为异步
|
|
204
235
|
console.log('🐧 [ZENITY] Showing dialog...');
|
|
205
|
-
const
|
|
236
|
+
const { stdout } = await execAsync(`zenity --question --title="${title}" --text="${message}" --ok-label="${buttons[0]}" --cancel-label="${buttons[1]}"`, {
|
|
206
237
|
encoding: 'utf8',
|
|
207
|
-
timeout:
|
|
238
|
+
timeout: 3600000 // 60 minutes timeout
|
|
208
239
|
});
|
|
209
240
|
clearInterval(keepAliveInterval); // 清理定时器
|
|
210
|
-
return
|
|
241
|
+
return stdout.trim() || buttons[0];
|
|
211
242
|
}
|
|
212
243
|
} catch (error) {
|
|
213
244
|
clearInterval(keepAliveInterval); // 出错时也要清理
|
package/package.json
CHANGED
package/prompt-manager.js
CHANGED
|
@@ -308,6 +308,22 @@ class PromptManager {
|
|
|
308
308
|
},
|
|
309
309
|
required: ['random_string']
|
|
310
310
|
}
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
name: 'gitlab-mr-fetch',
|
|
314
|
+
type: 'handler',
|
|
315
|
+
handler: 'handleGitlabMrFetch',
|
|
316
|
+
description: 'Fetch GitLab Merge Request details and code diff',
|
|
317
|
+
schema: {
|
|
318
|
+
type: 'object',
|
|
319
|
+
properties: {
|
|
320
|
+
mr_url: {
|
|
321
|
+
type: 'string',
|
|
322
|
+
description: 'GitLab Merge Request URL, e.g., https://gitlab.com/group/project/-/merge_requests/123'
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
required: ['mr_url']
|
|
326
|
+
}
|
|
311
327
|
}
|
|
312
328
|
];
|
|
313
329
|
|
package/server.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import process from 'process';
|
|
3
3
|
import { initializePrompts, getToolsConfiguration } from './prompt-manager.js';
|
|
4
|
-
import { handleDevTool, handleDevManual, handleDevFeedback } from './tools.js';
|
|
4
|
+
import { handleDevTool, handleDevManual, handleDevFeedback, handleGitlabMrFetch } from './tools.js';
|
|
5
5
|
import { CFG } from './config.js';
|
|
6
6
|
import {
|
|
7
7
|
createSuccessResponse,
|
|
@@ -64,10 +64,10 @@ async function initializeServerSync() {
|
|
|
64
64
|
try {
|
|
65
65
|
console.error('[MCP-Server] Starting synchronous initialization...');
|
|
66
66
|
|
|
67
|
-
// 每
|
|
67
|
+
// 每5秒输出一次日志,保持连接活跃(防止客户端超时)
|
|
68
68
|
keepAliveInterval = setInterval(() => {
|
|
69
|
-
console.error('[MCP-KeepAlive]
|
|
70
|
-
},
|
|
69
|
+
console.error('[MCP-KeepAlive] I\'m alive, Heartbeat, Heartbeat...');
|
|
70
|
+
}, 5000);
|
|
71
71
|
|
|
72
72
|
// 同步初始化prompt管理器
|
|
73
73
|
globalToolsConfig = await initializePrompts();
|
|
@@ -233,6 +233,9 @@ async function handleToolCall(req) {
|
|
|
233
233
|
⚠️ **MANDATORY NEXT STEP REMINDER:** 在完成下一个实施步骤后,必须再次调用dev-feedback工具汇报进度并获取确认。每个步骤完成后都需要调用feedback工具 - 这是强制要求!`;
|
|
234
234
|
}
|
|
235
235
|
break;
|
|
236
|
+
case 'handleGitlabMrFetch':
|
|
237
|
+
resultText = await handleGitlabMrFetch(args);
|
|
238
|
+
break;
|
|
236
239
|
default:
|
|
237
240
|
throw new Error(`Unknown handler: ${tool.handler}`);
|
|
238
241
|
}
|
package/tools.js
CHANGED
|
@@ -3,6 +3,7 @@ import { getToolsConfiguration, getLocalPromptInfo } from './prompt-manager.js';
|
|
|
3
3
|
import { showInputDialog, showConfirmDialog, showSelectDialog, showPlanAdjustmentDialog } from './dialog/index.js';
|
|
4
4
|
// ✅ UNIFIED: 导入统一的按钮管理常量
|
|
5
5
|
import { BASE_DIALOG_BUTTONS, getSafeButtons, getStandardButtons } from './dialog/constants.js';
|
|
6
|
+
import { createAuthHeaders } from './platform-utils.js';
|
|
6
7
|
|
|
7
8
|
// 动态获取工具的source映射
|
|
8
9
|
function getToolSource(toolName) {
|
|
@@ -478,12 +479,17 @@ export async function handleDevManual() {
|
|
|
478
479
|
}
|
|
479
480
|
|
|
480
481
|
export async function handleDevFeedback(args) {
|
|
482
|
+
console.error('[DEBUG] handleDevFeedback called with args:', JSON.stringify(args, null, 2));
|
|
483
|
+
|
|
481
484
|
// Support both old simple format and new enhanced format
|
|
482
485
|
if (args.phase || args.context || args.allowPlanAdjustment) {
|
|
486
|
+
console.error('[DEBUG] Taking enhanced feedback branch');
|
|
483
487
|
// Use enhanced feedback for new format
|
|
484
488
|
return await enhancedDevFeedback(args);
|
|
485
489
|
}
|
|
486
490
|
|
|
491
|
+
console.error('[DEBUG] Taking legacy feedback branch');
|
|
492
|
+
|
|
487
493
|
// Legacy support for old format
|
|
488
494
|
const { title, message, options = [] } = args;
|
|
489
495
|
if (process.env.AUTOMATED_MODE === 'true') {
|
|
@@ -495,10 +501,7 @@ export async function handleDevFeedback(args) {
|
|
|
495
501
|
// 简化逻辑:直接传入options,函数内部自动判断场景
|
|
496
502
|
const finalOptions = getStandardButtons(options.length > 0 ? options : null);
|
|
497
503
|
|
|
498
|
-
|
|
499
|
-
const keepAliveInterval = setInterval(() => {
|
|
500
|
-
console.error('[MCP-KeepAlive] Waiting for user dialog interaction...');
|
|
501
|
-
}, 15000);
|
|
504
|
+
console.error('[DEBUG] Calling showConfirmDialog (heartbeat is handled in native.js)...');
|
|
502
505
|
|
|
503
506
|
try {
|
|
504
507
|
const result = await showConfirmDialog({
|
|
@@ -507,8 +510,6 @@ export async function handleDevFeedback(args) {
|
|
|
507
510
|
buttons: finalOptions
|
|
508
511
|
});
|
|
509
512
|
|
|
510
|
-
clearInterval(keepAliveInterval); // 用户选择后清理定时器
|
|
511
|
-
|
|
512
513
|
// If user selects a "modify" option, automatically show input dialog
|
|
513
514
|
if (result && (result.includes('修改') || result.includes('调整') || result.includes('建议'))) {
|
|
514
515
|
|
|
@@ -530,7 +531,6 @@ export async function handleDevFeedback(args) {
|
|
|
530
531
|
}
|
|
531
532
|
return result;
|
|
532
533
|
} catch (error) {
|
|
533
|
-
clearInterval(keepAliveInterval); // 出错时也要清理定时器
|
|
534
534
|
console.error('❌ [FEEDBACK ERROR]:', error.message);
|
|
535
535
|
|
|
536
536
|
// Check if this is a dialog system failure
|
|
@@ -565,4 +565,127 @@ export async function handleDevFeedback(args) {
|
|
|
565
565
|
}
|
|
566
566
|
|
|
567
567
|
// handleDevReload removed - configuration now loads automatically from environment and mcp.json
|
|
568
|
-
// No manual reload needed as per user requirements
|
|
568
|
+
// No manual reload needed as per user requirements
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Parse GitLab MR URL to extract project and MR IID
|
|
572
|
+
* @param {string} mrUrl - GitLab MR URL
|
|
573
|
+
* @returns {object} Parsed info { projectPath, mrIid, apiBase }
|
|
574
|
+
*/
|
|
575
|
+
function parseGitlabMrUrl(mrUrl) {
|
|
576
|
+
// Match: https://gitlab.com/group/project/-/merge_requests/123
|
|
577
|
+
// Or: https://gitlab.example.com/group/subgroup/project/-/merge_requests/123
|
|
578
|
+
const match = mrUrl.match(/^(https?:\/\/[^\/]+)\/(.+?)\/-\/merge_requests\/(\d+)/);
|
|
579
|
+
|
|
580
|
+
if (!match) {
|
|
581
|
+
throw new Error(`Invalid GitLab MR URL format: ${mrUrl}. Expected format: https://gitlab.com/group/project/-/merge_requests/123`);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return {
|
|
585
|
+
apiBase: match[1],
|
|
586
|
+
projectPath: match[2],
|
|
587
|
+
mrIid: match[3]
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Fetch GitLab Merge Request details and code diff
|
|
593
|
+
* @param {object} args - Arguments with mr_url
|
|
594
|
+
* @returns {string} Formatted MR information with diffs
|
|
595
|
+
*/
|
|
596
|
+
export async function handleGitlabMrFetch(args) {
|
|
597
|
+
const { mr_url } = args;
|
|
598
|
+
|
|
599
|
+
if (!mr_url) {
|
|
600
|
+
throw new Error('mr_url is required');
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const token = process.env.GIT_TOKEN;
|
|
604
|
+
if (!token) {
|
|
605
|
+
throw new Error('GIT_TOKEN environment variable is required for GitLab API access');
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const { apiBase, projectPath, mrIid } = parseGitlabMrUrl(mr_url);
|
|
609
|
+
const projectEnc = encodeURIComponent(projectPath);
|
|
610
|
+
const headers = createAuthHeaders('gitlab', token);
|
|
611
|
+
|
|
612
|
+
console.error(`[GitLab MR] Fetching MR ${mrIid} from ${projectPath}`);
|
|
613
|
+
|
|
614
|
+
try {
|
|
615
|
+
// Fetch MR details
|
|
616
|
+
const mrResponse = await fetch(
|
|
617
|
+
`${apiBase}/api/v4/projects/${projectEnc}/merge_requests/${mrIid}`,
|
|
618
|
+
{ headers }
|
|
619
|
+
);
|
|
620
|
+
|
|
621
|
+
if (!mrResponse.ok) {
|
|
622
|
+
const errorText = await mrResponse.text();
|
|
623
|
+
throw new Error(`GitLab API error (${mrResponse.status}): ${errorText}`);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const mrData = await mrResponse.json();
|
|
627
|
+
|
|
628
|
+
// Fetch MR changes (diffs)
|
|
629
|
+
const changesResponse = await fetch(
|
|
630
|
+
`${apiBase}/api/v4/projects/${projectEnc}/merge_requests/${mrIid}/changes`,
|
|
631
|
+
{ headers }
|
|
632
|
+
);
|
|
633
|
+
|
|
634
|
+
if (!changesResponse.ok) {
|
|
635
|
+
const errorText = await changesResponse.text();
|
|
636
|
+
throw new Error(`GitLab API error fetching changes (${changesResponse.status}): ${errorText}`);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const changesData = await changesResponse.json();
|
|
640
|
+
|
|
641
|
+
// Format the output
|
|
642
|
+
let output = `# GitLab Merge Request Details
|
|
643
|
+
|
|
644
|
+
## Basic Information
|
|
645
|
+
- **Title:** ${mrData.title}
|
|
646
|
+
- **MR IID:** !${mrData.iid}
|
|
647
|
+
- **Author:** ${mrData.author?.name || 'Unknown'} (@${mrData.author?.username || 'unknown'})
|
|
648
|
+
- **Source Branch:** ${mrData.source_branch}
|
|
649
|
+
- **Target Branch:** ${mrData.target_branch}
|
|
650
|
+
- **State:** ${mrData.state}
|
|
651
|
+
- **Created At:** ${mrData.created_at}
|
|
652
|
+
- **Updated At:** ${mrData.updated_at}
|
|
653
|
+
- **URL:** ${mr_url}
|
|
654
|
+
|
|
655
|
+
## Description
|
|
656
|
+
${mrData.description || '_No description provided_'}
|
|
657
|
+
|
|
658
|
+
## Changed Files (${changesData.changes?.length || 0} files)
|
|
659
|
+
|
|
660
|
+
`;
|
|
661
|
+
|
|
662
|
+
// Add file changes
|
|
663
|
+
if (changesData.changes && changesData.changes.length > 0) {
|
|
664
|
+
for (const change of changesData.changes) {
|
|
665
|
+
const status = change.new_file ? '🆕 New' :
|
|
666
|
+
change.deleted_file ? '🗑️ Deleted' :
|
|
667
|
+
change.renamed_file ? '📝 Renamed' : '✏️ Modified';
|
|
668
|
+
|
|
669
|
+
output += `### ${status}: \`${change.new_path}\`\n`;
|
|
670
|
+
|
|
671
|
+
if (change.renamed_file && change.old_path !== change.new_path) {
|
|
672
|
+
output += `_Renamed from: ${change.old_path}_\n`;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
output += '\n```diff\n';
|
|
676
|
+
output += change.diff || '_Binary file or no diff available_';
|
|
677
|
+
output += '\n```\n\n';
|
|
678
|
+
}
|
|
679
|
+
} else {
|
|
680
|
+
output += '_No changes found_\n';
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
console.error(`[GitLab MR] Successfully fetched MR with ${changesData.changes?.length || 0} changed files`);
|
|
684
|
+
|
|
685
|
+
return output;
|
|
686
|
+
|
|
687
|
+
} catch (error) {
|
|
688
|
+
console.error(`[GitLab MR] Error:`, error.message);
|
|
689
|
+
throw error;
|
|
690
|
+
}
|
|
691
|
+
}
|