@rawwee/interactive-mcp 1.0.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/LICENSE +21 -0
- package/README.md +210 -0
- package/dist/commands/input/index.js +216 -0
- package/dist/commands/input/ui.js +231 -0
- package/dist/commands/intensive-chat/index.js +289 -0
- package/dist/commands/intensive-chat/ui.js +301 -0
- package/dist/components/InteractiveInput.js +420 -0
- package/dist/components/MarkdownText.js +285 -0
- package/dist/components/PromptStatus.js +46 -0
- package/dist/components/TextProgressBar.js +10 -0
- package/dist/components/interactive-input/autocomplete.js +127 -0
- package/dist/components/interactive-input/keyboard.js +51 -0
- package/dist/components/interactive-input/types.js +1 -0
- package/dist/constants.js +13 -0
- package/dist/index.js +318 -0
- package/dist/tool-definitions/intensive-chat.js +236 -0
- package/dist/tool-definitions/message-complete-notification.js +66 -0
- package/dist/tool-definitions/request-user-input.js +117 -0
- package/dist/tool-definitions/types.js +1 -0
- package/dist/utils/base-directory.js +44 -0
- package/dist/utils/clipboard.js +67 -0
- package/dist/utils/logger.js +65 -0
- package/dist/utils/search-root.js +85 -0
- package/dist/utils/spawn-detached-terminal.js +101 -0
- package/package.json +74 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import notifier from 'node-notifier';
|
|
5
|
+
import yargs from 'yargs';
|
|
6
|
+
import { hideBin } from 'yargs/helpers';
|
|
7
|
+
import { getCmdWindowInput } from './commands/input/index.js';
|
|
8
|
+
import { startIntensiveChatSession, askQuestionInSession, stopIntensiveChatSession, } from './commands/intensive-chat/index.js';
|
|
9
|
+
import { USER_INPUT_TIMEOUT_SECONDS, USER_INPUT_TIMEOUT_SENTINEL, } from './constants.js';
|
|
10
|
+
import logger from './utils/logger.js';
|
|
11
|
+
import { validateRepositoryBaseDirectory } from './utils/base-directory.js';
|
|
12
|
+
// Import tool definitions using the new structure
|
|
13
|
+
import { requestUserInputTool } from './tool-definitions/request-user-input.js';
|
|
14
|
+
import { messageCompleteNotificationTool } from './tool-definitions/message-complete-notification.js';
|
|
15
|
+
import { intensiveChatTools } from './tool-definitions/intensive-chat.js';
|
|
16
|
+
const allToolCapabilities = {
|
|
17
|
+
request_user_input: requestUserInputTool.capability,
|
|
18
|
+
message_complete_notification: messageCompleteNotificationTool.capability,
|
|
19
|
+
start_intensive_chat: intensiveChatTools.start.capability,
|
|
20
|
+
ask_intensive_chat: intensiveChatTools.ask.capability,
|
|
21
|
+
stop_intensive_chat: intensiveChatTools.stop.capability,
|
|
22
|
+
};
|
|
23
|
+
const argv = yargs(hideBin(process.argv))
|
|
24
|
+
.option('timeout', {
|
|
25
|
+
alias: 't',
|
|
26
|
+
type: 'number',
|
|
27
|
+
description: 'Default timeout for user input prompts in seconds',
|
|
28
|
+
default: USER_INPUT_TIMEOUT_SECONDS,
|
|
29
|
+
})
|
|
30
|
+
.option('disable-tools', {
|
|
31
|
+
alias: 'd',
|
|
32
|
+
type: 'string',
|
|
33
|
+
description: 'Comma-separated list of tool names to disable. Available options: request_user_input, message_complete_notification, intensive_chat (disables all intensive chat tools).',
|
|
34
|
+
default: '',
|
|
35
|
+
})
|
|
36
|
+
.help()
|
|
37
|
+
.alias('help', 'h')
|
|
38
|
+
.parseSync();
|
|
39
|
+
const globalTimeoutSeconds = argv.timeout;
|
|
40
|
+
const disabledTools = argv['disable-tools']
|
|
41
|
+
.split(',')
|
|
42
|
+
.map((tool) => tool.trim())
|
|
43
|
+
.filter(Boolean);
|
|
44
|
+
logger.info({
|
|
45
|
+
globalTimeoutSeconds,
|
|
46
|
+
disabledTools,
|
|
47
|
+
}, 'Interactive MCP server configuration loaded.');
|
|
48
|
+
// Store active intensive chat sessions
|
|
49
|
+
const activeChatSessions = new Map();
|
|
50
|
+
// --- Filter Capabilities Based on Args ---
|
|
51
|
+
// Helper function to check if a tool is effectively disabled (directly or via group)
|
|
52
|
+
const isToolDisabled = (toolName) => {
|
|
53
|
+
if (disabledTools.includes(toolName)) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
if ([
|
|
57
|
+
// Check if tool belongs to the intensive_chat group and the group is disabled
|
|
58
|
+
'start_intensive_chat',
|
|
59
|
+
'ask_intensive_chat',
|
|
60
|
+
'stop_intensive_chat',
|
|
61
|
+
].includes(toolName) &&
|
|
62
|
+
disabledTools.includes('intensive_chat')) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
};
|
|
67
|
+
// Create a new object with only the enabled tool capabilities
|
|
68
|
+
const enabledToolCapabilities = Object.fromEntries(Object.entries(allToolCapabilities).filter(([toolName]) => {
|
|
69
|
+
return !isToolDisabled(toolName);
|
|
70
|
+
})); // Assert type after filtering
|
|
71
|
+
// --- End Filter Capabilities Based on Args ---
|
|
72
|
+
// Helper function to check if a tool should be registered (used later)
|
|
73
|
+
const isToolEnabled = (toolName) => {
|
|
74
|
+
// A tool is enabled if it's present in the filtered capabilities
|
|
75
|
+
return toolName in enabledToolCapabilities;
|
|
76
|
+
};
|
|
77
|
+
// Initialize MCP server with FILTERED capabilities
|
|
78
|
+
const server = new McpServer({
|
|
79
|
+
name: 'Interactive MCP',
|
|
80
|
+
version: '1.0.0',
|
|
81
|
+
capabilities: {
|
|
82
|
+
tools: enabledToolCapabilities, // Use the filtered capabilities
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
// Conditionally register tools based on command-line arguments
|
|
86
|
+
if (isToolEnabled('request_user_input')) {
|
|
87
|
+
// Use properties from the imported tool object
|
|
88
|
+
server.tool('request_user_input',
|
|
89
|
+
// Need to handle description potentially being a function
|
|
90
|
+
typeof requestUserInputTool.description === 'function'
|
|
91
|
+
? requestUserInputTool.description(globalTimeoutSeconds)
|
|
92
|
+
: requestUserInputTool.description, requestUserInputTool.schema, // Use schema property
|
|
93
|
+
async (args) => {
|
|
94
|
+
// Use inferred args type
|
|
95
|
+
const { projectName, message, predefinedOptions, baseDirectory } = args;
|
|
96
|
+
try {
|
|
97
|
+
const validatedBaseDirectory = await validateRepositoryBaseDirectory(baseDirectory);
|
|
98
|
+
const promptMessage = `${projectName}: ${message}`;
|
|
99
|
+
const answer = await getCmdWindowInput(projectName, promptMessage, globalTimeoutSeconds, true, validatedBaseDirectory, predefinedOptions);
|
|
100
|
+
// Check for the specific timeout indicator
|
|
101
|
+
if (answer === USER_INPUT_TIMEOUT_SENTINEL) {
|
|
102
|
+
return {
|
|
103
|
+
content: [
|
|
104
|
+
{ type: 'text', text: 'User did not reply: Timeout occurred.' },
|
|
105
|
+
],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
// Empty string means user submitted empty input, non-empty is actual reply
|
|
109
|
+
else if (answer === '') {
|
|
110
|
+
return {
|
|
111
|
+
content: [{ type: 'text', text: 'User replied with empty input.' }],
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
const reply = `User replied: ${answer}`;
|
|
116
|
+
return { content: [{ type: 'text', text: reply }] };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
const errorMessage = error instanceof Error
|
|
121
|
+
? `Failed to request user input: ${error.message}`
|
|
122
|
+
: 'Failed to request user input: unknown error.';
|
|
123
|
+
return { content: [{ type: 'text', text: errorMessage }] };
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
if (isToolEnabled('message_complete_notification')) {
|
|
128
|
+
// Use properties from the imported tool object
|
|
129
|
+
server.tool('message_complete_notification',
|
|
130
|
+
// Description is a string here, but handle consistently
|
|
131
|
+
typeof messageCompleteNotificationTool.description === 'function'
|
|
132
|
+
? messageCompleteNotificationTool.description(globalTimeoutSeconds) // Should not happen based on definition, but safe
|
|
133
|
+
: messageCompleteNotificationTool.description, messageCompleteNotificationTool.schema, // Use schema property
|
|
134
|
+
(args) => {
|
|
135
|
+
// Use inferred args type
|
|
136
|
+
const { projectName, message } = args;
|
|
137
|
+
notifier.notify({ title: projectName, message });
|
|
138
|
+
return {
|
|
139
|
+
content: [
|
|
140
|
+
{
|
|
141
|
+
type: 'text',
|
|
142
|
+
text: 'Notification sent. You can now wait for user input.',
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
// --- Intensive Chat Tool Registrations ---
|
|
149
|
+
// Each tool must be checked individually based on filtered capabilities
|
|
150
|
+
if (isToolEnabled('start_intensive_chat')) {
|
|
151
|
+
// Use properties from the imported intensiveChatTools object
|
|
152
|
+
server.tool('start_intensive_chat',
|
|
153
|
+
// Description is a function here
|
|
154
|
+
typeof intensiveChatTools.start.description === 'function'
|
|
155
|
+
? intensiveChatTools.start.description(globalTimeoutSeconds)
|
|
156
|
+
: intensiveChatTools.start.description, intensiveChatTools.start.schema, // Use schema property
|
|
157
|
+
async (args) => {
|
|
158
|
+
// Use inferred args type
|
|
159
|
+
const { sessionTitle, baseDirectory } = args;
|
|
160
|
+
try {
|
|
161
|
+
const validatedBaseDirectory = await validateRepositoryBaseDirectory(baseDirectory);
|
|
162
|
+
// Start a new intensive chat session, passing global timeout
|
|
163
|
+
const sessionId = await startIntensiveChatSession(sessionTitle, validatedBaseDirectory, globalTimeoutSeconds);
|
|
164
|
+
// Track this session for the client
|
|
165
|
+
activeChatSessions.set(sessionId, sessionTitle);
|
|
166
|
+
return {
|
|
167
|
+
content: [
|
|
168
|
+
{
|
|
169
|
+
type: 'text',
|
|
170
|
+
text: `Intensive chat session started successfully. Session ID: ${sessionId}`,
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
let errorMessage = 'Failed to start intensive chat session.';
|
|
177
|
+
if (error instanceof Error) {
|
|
178
|
+
errorMessage = `Failed to start intensive chat session: ${error.message}`;
|
|
179
|
+
}
|
|
180
|
+
else if (typeof error === 'string') {
|
|
181
|
+
errorMessage = `Failed to start intensive chat session: ${error}`;
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
content: [
|
|
185
|
+
{
|
|
186
|
+
type: 'text',
|
|
187
|
+
text: errorMessage,
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
if (isToolEnabled('ask_intensive_chat')) {
|
|
195
|
+
// Use properties from the imported intensiveChatTools object
|
|
196
|
+
server.tool('ask_intensive_chat',
|
|
197
|
+
// Description is a string here
|
|
198
|
+
typeof intensiveChatTools.ask.description === 'function'
|
|
199
|
+
? intensiveChatTools.ask.description(globalTimeoutSeconds) // Should not happen, but safe
|
|
200
|
+
: intensiveChatTools.ask.description, intensiveChatTools.ask.schema, // Use schema property
|
|
201
|
+
async (args) => {
|
|
202
|
+
// Use inferred args type
|
|
203
|
+
const { sessionId, question, predefinedOptions, baseDirectory } = args;
|
|
204
|
+
// Check if session exists
|
|
205
|
+
if (!activeChatSessions.has(sessionId)) {
|
|
206
|
+
return {
|
|
207
|
+
content: [
|
|
208
|
+
{ type: 'text', text: 'Error: Invalid or expired session ID.' },
|
|
209
|
+
],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
const validatedBaseDirectory = await validateRepositoryBaseDirectory(baseDirectory);
|
|
214
|
+
// Ask the question in the session
|
|
215
|
+
const answer = await askQuestionInSession(sessionId, question, validatedBaseDirectory, predefinedOptions);
|
|
216
|
+
// Check for the specific timeout indicator
|
|
217
|
+
if (answer === USER_INPUT_TIMEOUT_SENTINEL) {
|
|
218
|
+
return {
|
|
219
|
+
content: [
|
|
220
|
+
{
|
|
221
|
+
type: 'text',
|
|
222
|
+
text: 'User did not reply to question in intensive chat: Timeout occurred.',
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
else if (answer === null) {
|
|
228
|
+
return {
|
|
229
|
+
content: [
|
|
230
|
+
{
|
|
231
|
+
type: 'text',
|
|
232
|
+
text: 'User closed intensive chat session before replying.',
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
// Empty string means user submitted empty input, non-empty is actual reply
|
|
238
|
+
else if (answer === '') {
|
|
239
|
+
return {
|
|
240
|
+
content: [
|
|
241
|
+
{
|
|
242
|
+
type: 'text',
|
|
243
|
+
text: 'User replied with empty input in intensive chat.',
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
return {
|
|
250
|
+
content: [{ type: 'text', text: `User replied: ${answer}` }],
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
let errorMessage = 'Failed to ask question in session.';
|
|
256
|
+
if (error instanceof Error) {
|
|
257
|
+
errorMessage = `Failed to ask question in session: ${error.message}`;
|
|
258
|
+
}
|
|
259
|
+
else if (typeof error === 'string') {
|
|
260
|
+
errorMessage = `Failed to ask question in session: ${error}`;
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
content: [
|
|
264
|
+
{
|
|
265
|
+
type: 'text',
|
|
266
|
+
text: errorMessage,
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
if (isToolEnabled('stop_intensive_chat')) {
|
|
274
|
+
// Use properties from the imported intensiveChatTools object
|
|
275
|
+
server.tool('stop_intensive_chat',
|
|
276
|
+
// Description is a string here
|
|
277
|
+
typeof intensiveChatTools.stop.description === 'function'
|
|
278
|
+
? intensiveChatTools.stop.description(globalTimeoutSeconds) // Should not happen, but safe
|
|
279
|
+
: intensiveChatTools.stop.description, intensiveChatTools.stop.schema, // Use schema property
|
|
280
|
+
async (args) => {
|
|
281
|
+
// Use inferred args type
|
|
282
|
+
const { sessionId } = args;
|
|
283
|
+
// Check if session exists
|
|
284
|
+
if (!activeChatSessions.has(sessionId)) {
|
|
285
|
+
return {
|
|
286
|
+
content: [
|
|
287
|
+
{ type: 'text', text: 'Error: Invalid or expired session ID.' },
|
|
288
|
+
],
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
try {
|
|
292
|
+
// Stop the session
|
|
293
|
+
const success = await stopIntensiveChatSession(sessionId);
|
|
294
|
+
// Remove session from map if successful
|
|
295
|
+
if (success) {
|
|
296
|
+
activeChatSessions.delete(sessionId);
|
|
297
|
+
}
|
|
298
|
+
const message = success
|
|
299
|
+
? 'Session stopped successfully.'
|
|
300
|
+
: 'Session not found or already stopped.';
|
|
301
|
+
return { content: [{ type: 'text', text: message }] };
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
let errorMessage = 'Failed to stop intensive chat session.';
|
|
305
|
+
if (error instanceof Error) {
|
|
306
|
+
errorMessage = `Failed to stop intensive chat session: ${error.message}`;
|
|
307
|
+
}
|
|
308
|
+
else if (typeof error === 'string') {
|
|
309
|
+
errorMessage = `Failed to stop intensive chat session: ${error}`;
|
|
310
|
+
}
|
|
311
|
+
return { content: [{ type: 'text', text: errorMessage }] };
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
// --- End Intensive Chat Tool Registrations ---
|
|
316
|
+
// Run the server over stdio
|
|
317
|
+
const transport = new StdioServerTransport();
|
|
318
|
+
await server.connect(transport);
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
// === Start Intensive Chat Definition ===
|
|
3
|
+
const startCapability = {
|
|
4
|
+
description: 'Start a persistent OpenTUI intensive chat session for gathering multiple answers quickly.',
|
|
5
|
+
parameters: {
|
|
6
|
+
type: 'object',
|
|
7
|
+
properties: {
|
|
8
|
+
sessionTitle: {
|
|
9
|
+
type: 'string',
|
|
10
|
+
description: 'Title for the intensive chat session',
|
|
11
|
+
},
|
|
12
|
+
baseDirectory: {
|
|
13
|
+
type: 'string',
|
|
14
|
+
description: 'Required absolute path to the current repository root (must be a git repo root; default autocomplete/search scope for this session)',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
required: ['sessionTitle', 'baseDirectory'],
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
const startDescription = (globalTimeoutSeconds) => `<description>
|
|
21
|
+
Start an intensive chat session (OpenTUI terminal UI) for gathering multiple answers quickly from the user.
|
|
22
|
+
**Highly recommended** for scenarios requiring a sequence of related inputs or confirmations.
|
|
23
|
+
Very useful for gathering multiple answers from the user in a short period of time.
|
|
24
|
+
Especially useful for brainstorming ideas or discussing complex topics with the user.
|
|
25
|
+
</description>
|
|
26
|
+
|
|
27
|
+
<importantNotes>
|
|
28
|
+
- (!important!) Opens a persistent console window that stays open for multiple questions.
|
|
29
|
+
- (!important!) Returns a session ID that **must** be used for subsequent questions via 'ask_intensive_chat'.
|
|
30
|
+
- (!important!) **Must** be closed with 'stop_intensive_chat' when finished gathering all inputs.
|
|
31
|
+
- (!important!) After starting a session, **immediately** continue asking all necessary questions using 'ask_intensive_chat' within the **same response message**. Do not end the response until the chat is closed with 'stop_intensive_chat'. This creates a seamless conversational flow for the user.
|
|
32
|
+
</importantNotes>
|
|
33
|
+
|
|
34
|
+
<whenToUseThisTool>
|
|
35
|
+
- When you need to collect a series of quick answers from the user (more than 2-3 questions)
|
|
36
|
+
- When setting up a project with multiple configuration options
|
|
37
|
+
- When guiding a user through a multi-step process requiring input at each stage
|
|
38
|
+
- When gathering sequential user preferences
|
|
39
|
+
- When you want to maintain context between multiple related questions efficiently
|
|
40
|
+
- When brainstorming ideas with the user interactively
|
|
41
|
+
</whenToUseThisTool>
|
|
42
|
+
|
|
43
|
+
<features>
|
|
44
|
+
- Opens a persistent OpenTUI window for continuous interaction
|
|
45
|
+
- Renders markdown prompts, including code/diff snippets, for richer question context
|
|
46
|
+
- Supports option mode + free-text mode while asking follow-up questions
|
|
47
|
+
- Configurable timeout for each question (set via -t/--timeout, defaults to ${globalTimeoutSeconds} seconds)
|
|
48
|
+
- Returns a session ID for subsequent interactions
|
|
49
|
+
- Keeps full chat history visible to the user
|
|
50
|
+
- Maintains state between questions
|
|
51
|
+
- Requires baseDirectory and pins autocomplete/search scope to the repository root
|
|
52
|
+
</features>
|
|
53
|
+
|
|
54
|
+
<bestPractices>
|
|
55
|
+
- Use a descriptive session title related to the task
|
|
56
|
+
- Start with a clear initial question when possible
|
|
57
|
+
- Do not ask the question if you have another tool that can answer the question
|
|
58
|
+
- e.g. when you searching file in the current repository, do not ask the question "Do you want to search for a file in the current repository?"
|
|
59
|
+
- e.g. prefer to use other tools to find the answer (Cursor tools or other MCP Server tools)
|
|
60
|
+
- Always store the returned session ID for later use
|
|
61
|
+
- Always close the session when you're done with stop_intensive_chat
|
|
62
|
+
</bestPractices>
|
|
63
|
+
|
|
64
|
+
<parameters>
|
|
65
|
+
- sessionTitle: Title for the intensive chat session (appears at the top of the console)
|
|
66
|
+
- baseDirectory: Required absolute path to the current repository root (must be a git repo root)
|
|
67
|
+
</parameters>
|
|
68
|
+
|
|
69
|
+
<examples>
|
|
70
|
+
- Start session for project setup: { "sessionTitle": "Project Configuration", "baseDirectory": "/workspace/project" }
|
|
71
|
+
- Start session with repository root scope: { "sessionTitle": "Project Configuration", "baseDirectory": "/workspace/project" }
|
|
72
|
+
</examples>`;
|
|
73
|
+
const startSchema = {
|
|
74
|
+
sessionTitle: z.string().describe('Title for the intensive chat session'),
|
|
75
|
+
baseDirectory: z
|
|
76
|
+
.string()
|
|
77
|
+
.describe('Required absolute path to the current repository root (must be a git repo root; default autocomplete/search scope for this session)'),
|
|
78
|
+
};
|
|
79
|
+
const startToolDefinition = {
|
|
80
|
+
capability: startCapability,
|
|
81
|
+
description: startDescription,
|
|
82
|
+
schema: startSchema,
|
|
83
|
+
};
|
|
84
|
+
// === Ask Intensive Chat Definition ===
|
|
85
|
+
const askCapability = {
|
|
86
|
+
description: 'Ask a question in an active OpenTUI intensive chat session.',
|
|
87
|
+
parameters: {
|
|
88
|
+
type: 'object',
|
|
89
|
+
properties: {
|
|
90
|
+
sessionId: {
|
|
91
|
+
type: 'string',
|
|
92
|
+
description: 'ID of the intensive chat session',
|
|
93
|
+
},
|
|
94
|
+
question: {
|
|
95
|
+
type: 'string',
|
|
96
|
+
description: 'Question to ask the user',
|
|
97
|
+
},
|
|
98
|
+
predefinedOptions: {
|
|
99
|
+
type: 'array',
|
|
100
|
+
items: { type: 'string' },
|
|
101
|
+
optional: true,
|
|
102
|
+
description: 'Predefined options for the user to choose from (optional)',
|
|
103
|
+
},
|
|
104
|
+
baseDirectory: {
|
|
105
|
+
type: 'string',
|
|
106
|
+
description: 'Required absolute path to the current repository root (must be a git repo root; autocomplete/search scope for this question)',
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
required: ['sessionId', 'question', 'baseDirectory'],
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
const askDescription = `<description>
|
|
113
|
+
Ask a new question in an active intensive chat session previously started with 'start_intensive_chat'.
|
|
114
|
+
</description>
|
|
115
|
+
|
|
116
|
+
<importantNotes>
|
|
117
|
+
- (!important!) Requires a valid session ID from 'start_intensive_chat'.
|
|
118
|
+
- (!important!) Supports predefined options for quick selection.
|
|
119
|
+
- (!important!) Returns the user's answer or indicates if they didn't respond.
|
|
120
|
+
- (!important!) **Use this repeatedly within the same response message** after 'start_intensive_chat' until all questions are asked.
|
|
121
|
+
</importantNotes>
|
|
122
|
+
|
|
123
|
+
<whenToUseThisTool>
|
|
124
|
+
- When continuing a series of questions in an intensive chat session.
|
|
125
|
+
- When you need the next piece of information in a multi-step process initiated via 'start_intensive_chat'.
|
|
126
|
+
- When offering multiple choice options to the user within the session.
|
|
127
|
+
- When gathering sequential information from the user within the session.
|
|
128
|
+
</whenToUseThisTool>
|
|
129
|
+
|
|
130
|
+
<features>
|
|
131
|
+
- Adds a new question to an existing chat session
|
|
132
|
+
- Supports predefined options for quick selection
|
|
133
|
+
- Returns the user's response
|
|
134
|
+
- Maintains the chat history in the console
|
|
135
|
+
- Requires baseDirectory for each question and scopes autocomplete/search to the repository root
|
|
136
|
+
</features>
|
|
137
|
+
|
|
138
|
+
<bestPractices>
|
|
139
|
+
- Ask one clear question at a time
|
|
140
|
+
- Provide predefined options when applicable
|
|
141
|
+
- Don't ask overly complex questions
|
|
142
|
+
- Keep questions focused on a single piece of information
|
|
143
|
+
</bestPractices>
|
|
144
|
+
|
|
145
|
+
<parameters>
|
|
146
|
+
- sessionId: ID of the intensive chat session (from start_intensive_chat)
|
|
147
|
+
- question: The question text to display to the user
|
|
148
|
+
- predefinedOptions: Array of predefined options for the user to choose from (optional)
|
|
149
|
+
- baseDirectory: Required absolute path to the current repository root (must be a git repo root)
|
|
150
|
+
</parameters>
|
|
151
|
+
|
|
152
|
+
<examples>
|
|
153
|
+
- Simple question: { "sessionId": "abcd1234", "question": "What is your project named?", "baseDirectory": "/workspace/project" }
|
|
154
|
+
- With predefined options: { "sessionId": "abcd1234", "question": "Would you like to use TypeScript?", "predefinedOptions": ["Yes", "No"], "baseDirectory": "/workspace/project" }
|
|
155
|
+
- Ask another repo-scoped question: { "sessionId": "abcd1234", "question": "Pick a file", "baseDirectory": "/workspace/project" }
|
|
156
|
+
</examples>`;
|
|
157
|
+
const askSchema = {
|
|
158
|
+
sessionId: z.string().describe('ID of the intensive chat session'),
|
|
159
|
+
question: z.string().describe('Question to ask the user'),
|
|
160
|
+
predefinedOptions: z
|
|
161
|
+
.array(z.string())
|
|
162
|
+
.optional()
|
|
163
|
+
.describe('Predefined options for the user to choose from (optional)'),
|
|
164
|
+
baseDirectory: z
|
|
165
|
+
.string()
|
|
166
|
+
.describe('Required absolute path to the current repository root (must be a git repo root; autocomplete/search scope for this question)'),
|
|
167
|
+
};
|
|
168
|
+
const askToolDefinition = {
|
|
169
|
+
capability: askCapability,
|
|
170
|
+
description: askDescription,
|
|
171
|
+
schema: askSchema,
|
|
172
|
+
};
|
|
173
|
+
// === Stop Intensive Chat Definition ===
|
|
174
|
+
const stopCapability = {
|
|
175
|
+
description: 'Stop and close an active intensive chat session.',
|
|
176
|
+
parameters: {
|
|
177
|
+
type: 'object',
|
|
178
|
+
properties: {
|
|
179
|
+
sessionId: {
|
|
180
|
+
type: 'string',
|
|
181
|
+
description: 'ID of the intensive chat session to stop',
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
required: ['sessionId'],
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
const stopDescription = `<description>
|
|
188
|
+
Stop and close an active intensive chat session. **Must be called** after all questions have been asked using 'ask_intensive_chat'.
|
|
189
|
+
</description>
|
|
190
|
+
|
|
191
|
+
<importantNotes>
|
|
192
|
+
- (!important!) Closes the console window for the intensive chat.
|
|
193
|
+
- (!important!) Frees up system resources.
|
|
194
|
+
- (!important!) **Should always be called** as the final step when finished with an intensive chat session, typically at the end of the response message where 'start_intensive_chat' was called.
|
|
195
|
+
</importantNotes>
|
|
196
|
+
|
|
197
|
+
<whenToUseThisTool>
|
|
198
|
+
- When you've completed gathering all needed information via 'ask_intensive_chat'.
|
|
199
|
+
- When the multi-step process requiring intensive chat is complete.
|
|
200
|
+
- When you're ready to move on to processing the collected information.
|
|
201
|
+
- When the user indicates they want to end the session (if applicable).
|
|
202
|
+
- As the final action related to the intensive chat flow within a single response message.
|
|
203
|
+
</whenToUseThisTool>
|
|
204
|
+
|
|
205
|
+
<features>
|
|
206
|
+
- Gracefully closes the console window
|
|
207
|
+
- Cleans up system resources
|
|
208
|
+
- Marks the session as complete
|
|
209
|
+
</features>
|
|
210
|
+
|
|
211
|
+
<bestPractices>
|
|
212
|
+
- Always stop sessions when you're done to free resources
|
|
213
|
+
- Provide a summary of the information collected before stopping
|
|
214
|
+
</bestPractices>
|
|
215
|
+
|
|
216
|
+
<parameters>
|
|
217
|
+
- sessionId: ID of the intensive chat session to stop
|
|
218
|
+
</parameters>
|
|
219
|
+
|
|
220
|
+
<examples>
|
|
221
|
+
- { "sessionId": "abcd1234" }
|
|
222
|
+
</examples>`;
|
|
223
|
+
const stopSchema = {
|
|
224
|
+
sessionId: z.string().describe('ID of the intensive chat session to stop'),
|
|
225
|
+
};
|
|
226
|
+
const stopToolDefinition = {
|
|
227
|
+
capability: stopCapability,
|
|
228
|
+
description: stopDescription,
|
|
229
|
+
schema: stopSchema,
|
|
230
|
+
};
|
|
231
|
+
// === Export Combined Intensive Chat Definitions ===
|
|
232
|
+
export const intensiveChatTools = {
|
|
233
|
+
start: startToolDefinition,
|
|
234
|
+
ask: askToolDefinition,
|
|
235
|
+
stop: stopToolDefinition,
|
|
236
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
// Define capability conforming to ToolCapabilityInfo
|
|
3
|
+
const capabilityInfo = {
|
|
4
|
+
description: 'Notify when a response has completed via OS notification.',
|
|
5
|
+
parameters: {
|
|
6
|
+
type: 'object',
|
|
7
|
+
properties: {
|
|
8
|
+
projectName: {
|
|
9
|
+
type: 'string',
|
|
10
|
+
description: 'Identifies the context/project making the notification (appears in notification title)',
|
|
11
|
+
},
|
|
12
|
+
message: {
|
|
13
|
+
type: 'string',
|
|
14
|
+
description: 'The specific notification text (appears in the body)',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
required: ['projectName', 'message'],
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
// Define description conforming to ToolRegistrationDescription
|
|
21
|
+
const registrationDescription = `<description>
|
|
22
|
+
Notify when a response has completed. Use this tool **once** at the end of **each and every** message to signal completion to the user.
|
|
23
|
+
</description>
|
|
24
|
+
|
|
25
|
+
<importantNotes>
|
|
26
|
+
- (!important!) **MANDATORY:** ONLY use this tool exactly once per message to signal completion. **Do not forget this step.**
|
|
27
|
+
</importantNotes>
|
|
28
|
+
|
|
29
|
+
<whenToUseThisTool>
|
|
30
|
+
- When you've completed answering a user's query
|
|
31
|
+
- When you've finished executing a task or a sequence of tool calls
|
|
32
|
+
- When a multi-step process is complete
|
|
33
|
+
- When you want to provide a summary of completed actions just before ending the response
|
|
34
|
+
</whenToUseThisTool>
|
|
35
|
+
|
|
36
|
+
<features>
|
|
37
|
+
- Cross-platform OS notifications (Windows, macOS, Linux)
|
|
38
|
+
- Reusable tool to signal end of message
|
|
39
|
+
- Should be called exactly once per LLM response
|
|
40
|
+
</features>
|
|
41
|
+
|
|
42
|
+
<bestPractices>
|
|
43
|
+
- Keep messages concise
|
|
44
|
+
- Use projectName consistently to group notifications by context
|
|
45
|
+
</bestPractices>
|
|
46
|
+
|
|
47
|
+
<parameters>
|
|
48
|
+
- projectName: Identifies the context/project making the notification (appears in notification title)
|
|
49
|
+
- message: The specific notification text (appears in the body)
|
|
50
|
+
</parameters>
|
|
51
|
+
|
|
52
|
+
<examples>
|
|
53
|
+
- { "projectName": "MyApp", "message": "Feature implementation complete. All tests passing." }
|
|
54
|
+
- { "projectName": "MyLib", "message": "Analysis complete: 3 issues found and fixed." }
|
|
55
|
+
</examples>`;
|
|
56
|
+
// Define the Zod schema (as a raw shape object)
|
|
57
|
+
const rawSchema = {
|
|
58
|
+
projectName: z.string().describe('Notification title'),
|
|
59
|
+
message: z.string().describe('Notification body'),
|
|
60
|
+
};
|
|
61
|
+
// Combine into a single ToolDefinition object
|
|
62
|
+
export const messageCompleteNotificationTool = {
|
|
63
|
+
capability: capabilityInfo,
|
|
64
|
+
description: registrationDescription,
|
|
65
|
+
schema: rawSchema, // Use the raw shape here
|
|
66
|
+
};
|