ostacky 0.4.0 → 0.5.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/README.md +9 -9
- package/assets/agents/ostacky.md +78 -218
- package/assets/commands/install-stack.md +25 -0
- package/assets/commands/opsx-sync.md +2 -1
- package/assets/mcp/ask-user-server/index.js +271 -0
- package/assets/mcp/ask-user-server/package.json +6 -0
- package/assets/mcp/ostacky-controller/controller.test.mjs +447 -0
- package/assets/mcp/ostacky-controller/index.js +1017 -0
- package/assets/skills/execution-mode-evaluation/SKILL.md +45 -55
- package/assets/skills/subagent-driven-development/SKILL.md +9 -1
- package/assets/skills/writing-plans/SKILL.md +3 -19
- package/dist/cli.js +160 -34
- package/manifest.json +44 -28
- package/package.json +1 -1
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ask-user-server — MCP server that provides the ask_user tool.
|
|
5
|
+
*
|
|
6
|
+
* The tool blocks execution until the user responds via the terminal.
|
|
7
|
+
* Reads from /dev/tty (not process.stdin) to avoid conflicting with
|
|
8
|
+
* the MCP JSON-RPC protocol on stdin/stdout.
|
|
9
|
+
*
|
|
10
|
+
* Protocol: MCP (Model Context Protocol) via stdio transport.
|
|
11
|
+
* Messages are JSON-RPC 2.0, one JSON object per line, terminated by \n.
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* This server is configured as a local MCP server in opencode.jsonc:
|
|
15
|
+
* "ask-user": {
|
|
16
|
+
* "type": "local",
|
|
17
|
+
* "command": ["node", ".opencode/mcp/ask-user-server/index.js"],
|
|
18
|
+
* "enabled": true
|
|
19
|
+
* }
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { createInterface } from 'readline';
|
|
23
|
+
import { createReadStream, writeFileSync } from 'fs';
|
|
24
|
+
import { open } from 'fs/promises';
|
|
25
|
+
|
|
26
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
/** Writes a JSON-RPC message to stdout (MCP response). */
|
|
29
|
+
function send(id, result, isError = false) {
|
|
30
|
+
const msg = {
|
|
31
|
+
jsonrpc: '2.0',
|
|
32
|
+
...(id !== undefined ? { id } : {}),
|
|
33
|
+
};
|
|
34
|
+
if (isError) {
|
|
35
|
+
msg.error = result;
|
|
36
|
+
} else {
|
|
37
|
+
msg.result = result;
|
|
38
|
+
}
|
|
39
|
+
process.stdout.write(JSON.stringify(msg) + '\n');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Sends a JSON-RPC notification (no id). */
|
|
43
|
+
function notify(method, params) {
|
|
44
|
+
const msg = { jsonrpc: '2.0', method, params };
|
|
45
|
+
process.stdout.write(JSON.stringify(msg) + '\n');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Logs to stderr (visible to user, not part of MCP protocol). */
|
|
49
|
+
function log(msg) {
|
|
50
|
+
process.stderr.write(msg + '\n');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Reads a line of user input from /dev/tty (with fallback to process.stdin). */
|
|
54
|
+
async function readLineFromTTY(prompt) {
|
|
55
|
+
// Try /dev/tty first
|
|
56
|
+
if (process.platform !== 'win32') {
|
|
57
|
+
try {
|
|
58
|
+
const tty = createReadStream('/dev/tty');
|
|
59
|
+
return await new Promise((resolve, reject) => {
|
|
60
|
+
// If /dev/tty fails asynchronously, fall back immediately
|
|
61
|
+
tty.on('error', () => {
|
|
62
|
+
tty.destroy();
|
|
63
|
+
resolve(null); // signal fallback
|
|
64
|
+
});
|
|
65
|
+
tty.on('open', () => {
|
|
66
|
+
const rl = createInterface({ input: tty, output: process.stderr });
|
|
67
|
+
rl.question(prompt || '> ', (answer) => {
|
|
68
|
+
rl.close();
|
|
69
|
+
tty.destroy();
|
|
70
|
+
resolve(answer);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
} catch {
|
|
75
|
+
// Synchronous error (unlikely) → fallback
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Fallback: if /dev/tty failed or Windows
|
|
80
|
+
log(
|
|
81
|
+
'[ask-user] /dev/tty not available, reading from stdin. ' +
|
|
82
|
+
"If the prompt doesn't appear, check terminal settings."
|
|
83
|
+
);
|
|
84
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
85
|
+
return await new Promise((resolve) => {
|
|
86
|
+
rl.question(prompt || '> ', (answer) => {
|
|
87
|
+
rl.close();
|
|
88
|
+
resolve(answer);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ─── Tool handler ─────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
async function handleAskUser(args) {
|
|
96
|
+
const { question, options, context } = args || {};
|
|
97
|
+
|
|
98
|
+
if (!question) {
|
|
99
|
+
throw new Error("ask_user requires a 'question' field (string).");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Display to user via stderr
|
|
103
|
+
log('');
|
|
104
|
+
if (context) {
|
|
105
|
+
log(context);
|
|
106
|
+
}
|
|
107
|
+
log(`❓ ${question}`);
|
|
108
|
+
if (options && Array.isArray(options) && options.length > 0) {
|
|
109
|
+
log(`Opciones: ${options.join(', ')}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const answer = await readLineFromTTY('> ');
|
|
113
|
+
|
|
114
|
+
if (options && options.length > 0 && !options.includes(answer)) {
|
|
115
|
+
log(`⚠️ Respuesta no está entre las opciones: ${options.join(', ')}`);
|
|
116
|
+
log(`Se usará igual: "${answer}"`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { content: [{ type: 'text', text: JSON.stringify({ answer }) }] };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ─── Server capabilities ──────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
const CAPABILITIES = {
|
|
125
|
+
tools: {
|
|
126
|
+
ask_user: {
|
|
127
|
+
description:
|
|
128
|
+
'Ask the user a question and block execution until they respond. ' +
|
|
129
|
+
'Use this whenever you need a decision, confirmation, or input from the user.',
|
|
130
|
+
inputSchema: {
|
|
131
|
+
type: 'object',
|
|
132
|
+
properties: {
|
|
133
|
+
question: {
|
|
134
|
+
type: 'string',
|
|
135
|
+
description: 'The question to ask the user.',
|
|
136
|
+
},
|
|
137
|
+
options: {
|
|
138
|
+
type: 'array',
|
|
139
|
+
items: { type: 'string' },
|
|
140
|
+
description:
|
|
141
|
+
'Predefined answer options (optional). If provided, they are displayed to the user.',
|
|
142
|
+
},
|
|
143
|
+
context: {
|
|
144
|
+
type: 'string',
|
|
145
|
+
description: 'Additional context to display before the question (optional).',
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
required: ['question'],
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// ─── JSON-RPC dispatcher ──────────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
async function handleRequest(msg) {
|
|
157
|
+
const { id, method, params } = msg;
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
switch (method) {
|
|
161
|
+
// ── Lifecycle ───────────────────────────────────────────────
|
|
162
|
+
case 'initialize': {
|
|
163
|
+
send(id, {
|
|
164
|
+
protocolVersion: '2024-11-05',
|
|
165
|
+
capabilities: {
|
|
166
|
+
tools: {}, // we support tools
|
|
167
|
+
},
|
|
168
|
+
serverInfo: {
|
|
169
|
+
name: 'ask-user-server',
|
|
170
|
+
version: '0.5.0',
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
case 'notifications/initialized': {
|
|
177
|
+
// No response needed for notifications
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── Tools ───────────────────────────────────────────────────
|
|
182
|
+
case 'tools/list': {
|
|
183
|
+
send(id, {
|
|
184
|
+
tools: Object.entries(CAPABILITIES.tools).map(([name, def]) => ({
|
|
185
|
+
name,
|
|
186
|
+
description: def.description,
|
|
187
|
+
inputSchema: def.inputSchema,
|
|
188
|
+
})),
|
|
189
|
+
});
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
case 'tools/call': {
|
|
194
|
+
const { name, arguments: args } = params || {};
|
|
195
|
+
const toolDef = CAPABILITIES.tools[name];
|
|
196
|
+
|
|
197
|
+
if (!toolDef) {
|
|
198
|
+
send(
|
|
199
|
+
id,
|
|
200
|
+
{
|
|
201
|
+
code: -32601,
|
|
202
|
+
message: `Tool not found: ${name}`,
|
|
203
|
+
},
|
|
204
|
+
true
|
|
205
|
+
);
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const result = await handleAskUser(args);
|
|
210
|
+
send(id, result);
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ── Ping / unknown ──────────────────────────────────────────
|
|
215
|
+
case 'ping': {
|
|
216
|
+
send(id, {});
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
default: {
|
|
221
|
+
send(
|
|
222
|
+
id,
|
|
223
|
+
{
|
|
224
|
+
code: -32601,
|
|
225
|
+
message: `Method not found: ${method}`,
|
|
226
|
+
},
|
|
227
|
+
true
|
|
228
|
+
);
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
} catch (err) {
|
|
233
|
+
send(
|
|
234
|
+
id,
|
|
235
|
+
{
|
|
236
|
+
code: -32603,
|
|
237
|
+
message: err.message || 'Internal error',
|
|
238
|
+
},
|
|
239
|
+
true
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ─── Main loop ────────────────────────────────────────────────────────────────
|
|
245
|
+
|
|
246
|
+
let buffer = '';
|
|
247
|
+
|
|
248
|
+
process.stdin.setEncoding('utf-8');
|
|
249
|
+
process.stdin.on('data', (chunk) => {
|
|
250
|
+
buffer += chunk;
|
|
251
|
+
const lines = buffer.split('\n');
|
|
252
|
+
buffer = lines.pop() || ''; // keep incomplete line in buffer
|
|
253
|
+
|
|
254
|
+
for (const line of lines) {
|
|
255
|
+
const trimmed = line.trim();
|
|
256
|
+
if (!trimmed) continue;
|
|
257
|
+
try {
|
|
258
|
+
const msg = JSON.parse(trimmed);
|
|
259
|
+
handleRequest(msg);
|
|
260
|
+
} catch (err) {
|
|
261
|
+
log(`[ask-user] Invalid JSON-RPC: ${err.message}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
process.stdin.on('end', () => {
|
|
267
|
+
process.exit(0);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// Log startup
|
|
271
|
+
log('[ask-user-server] Started. Waiting for MCP messages...');
|