-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathserver.js
More file actions
220 lines (197 loc) · 9.15 KB
/
server.js
File metadata and controls
220 lines (197 loc) · 9.15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
const express = require('express');
const { spawn } = require('child_process');
const http = require('http');
const app = express();
const port = 4000;
// Accept text bodies (treat request body as raw text/plain)
app.use(express.text({ type: '*/*' }));
/**
* POST /user-input
* Body expected: plain text (used as "reason" / context). If body is empty, behavior falls back to no automatic completion.
*
* Behavior:
* - Notify Termux (if available) with the reason/context.
* - Spawn a bash 'read -t 540' (9 minutes) to get terminal input.
* - If the read completes successfully, return the entered input.
* - If the read times out (or fails) AND the POST body contains any content, call the local OpenAI-compatible chat completions
* endpoint at http://localhost:4141/chat/completions with model "gpt-5-mini" asking it what input to return, and return that suggestion.
* - Otherwise respond with 'no response provided'.
*/
app.post('/user-input', async (req, res) => {
const bodyContent = typeof req.body === 'string' && req.body.trim().length ? req.body.trim() : '';
// Notify Termux (if available) that input is requested so the user sees a notification.
try {
const notifContent = bodyContent
? `Input requested: ${String(bodyContent).slice(0, 200)}`
: 'Input requested';
// Try to include a direct-reply input in the notification if termux-notification supports it.
// Many versions of termux-notification support '--input' / '--input-label' to enable inline reply.
// If that fails (non-zero exit) or spawn fails, fall back to a plain notification so behavior remains unchanged.
const replyArgs = ['--title', 'Input requested', '--content', notifContent, '--input', '--input-label', 'Reply'];
// Capture stderr so we can detect "unrecognized option" errors instead of inheriting them directly.
const notif = spawn('termux-notification', replyArgs, {
stdio: ['ignore', 'pipe', 'pipe']
});
let notifErr = '';
if (notif.stderr) {
notif.stderr.on('data', (d) => { notifErr += d.toString(); });
}
notif.on('close', (code) => {
if (code === 0) {
// Inline-reply notification succeeded.
return;
}
// Inline-reply failed (unsupported option or other error). Try plain notification.
const fallback = spawn('termux-notification', ['--title', 'Input requested', '--content', notifContent], {
stdio: ['ignore', 'pipe', 'pipe']
});
let fallbackErr = '';
if (fallback.stderr) {
fallback.stderr.on('data', (d) => { fallbackErr += d.toString(); });
}
fallback.on('close', (fcode) => {
if (fcode !== 0) {
// Both attempts failed; surface a concise warning including available stderr text.
const combined = (fallbackErr || notifErr || '').trim();
console.warn('termux-notification not available or failed:', combined || 'unknown error');
}
});
fallback.on('error', (err2) => {
console.warn('termux-notification not available or failed:', err2 && err2.message ? err2.message : err2);
});
});
notif.on('error', (err) => {
// Spawn-level error for inline-reply attempt; try fallback as well.
const fallback2 = spawn('termux-notification', ['--title', 'Input requested', '--content', notifContent], {
stdio: ['ignore', 'pipe', 'pipe']
});
let fallbackErr2 = '';
if (fallback2.stderr) {
fallback2.stderr.on('data', (d) => { fallbackErr2 += d.toString(); });
}
fallback2.on('close', (fcode) => {
if (fcode !== 0) {
const combined = (fallbackErr2 || (err && err.message) || '').trim();
console.warn('termux-notification not available or failed:', combined || 'unknown error');
}
});
fallback2.on('error', (err2) => {
console.warn('termux-notification not available or failed:', err2 && err2.message ? err2.message : err2);
});
});
} catch (e) {
console.warn('Failed to send Termux notification:', e && e.message ? e.message : e);
}
// This command will prompt for user input in the terminal where the server is running.
// It will wait up to 9 minutes (540 seconds).
const command = 'read -t 540 -p "Please enter your input and press Enter: " && echo "$REPLY"';
// Spawn a bash shell to execute the 'read' command.
const child = spawn('bash', ['-c', command], {
// inherit stdin so `read` can receive terminal input; capture stdout to get the response.
stdio: ['inherit', 'pipe', 'inherit']
});
let stdoutData = '';
child.stdout.on('data', (data) => {
stdoutData += data.toString();
});
child.on('close', async (code) => {
// If read succeeded (exit code 0), return what was entered.
if (code === 0) {
return res.send(stdoutData.trim());
}
// Non-zero exit code: likely timeout or other error.
// If request body has content, call local chat completions to ask what input to return automatically.
if (bodyContent) {
try {
const suggestion = await askLocalAssistantForInput(bodyContent);
if (suggestion) {
return res.send(suggestion.trim());
} else {
return res.send('no response provided');
}
} catch (err) {
console.error('Failed to get suggestion from local assistant:', err);
return res.send('no response provided');
}
}
// No body content or couldn't get suggestion.
return res.send('no response provided');
});
child.on('error', (err) => {
console.error('Failed to start subprocess.', err);
res.status(500).send('Internal Server Error');
});
});
/**
* askLocalAssistantForInput
* Calls the local OpenAI-compatible endpoint to get a suggested input based on the provided context.
* Returns a string suggestion or null.
*/
function askLocalAssistantForInput(context) {
return new Promise((resolve, reject) => {
const payload = {
model: 'gpt-5-mini',
messages: [
{
role: 'system',
content: 'You are an assistant that must suggest a single-line terminal input that best matches the user intent given the context. Respond only with the suggested input, no explanation.'
},
{
role: 'user',
content: `Context: ${context}\n\nPlease provide a single-line input string (as the user would type) that should be returned automatically because the user did not respond. Do not wrap the suggestion in quotes.`
}
],
max_tokens: 256
};
const data = JSON.stringify(payload);
const options = {
hostname: 'localhost',
port: 4141,
path: '/chat/completions',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(data)
},
timeout: 10000 // 10s timeout for the local request
};
const req = http.request(options, (res) => {
let respData = '';
res.setEncoding('utf8');
res.on('data', (chunk) => (respData += chunk));
res.on('end', () => {
try {
const parsed = JSON.parse(respData);
// Try to follow OpenAI Chat Completions response shape.
if (parsed && parsed.choices && parsed.choices.length > 0) {
const message = parsed.choices[0].message;
if (message && message.content) {
return resolve(message.content);
}
}
// Fallback: if server returned plain text, use that.
if (typeof parsed === 'string') {
return resolve(parsed);
}
return resolve(null);
} catch (e) {
// If response wasn't JSON, return raw text trimmed.
const raw = respData && respData.toString().trim();
return resolve(raw || null);
}
});
});
req.on('error', (e) => {
reject(e);
});
req.on('timeout', () => {
req.destroy(new Error('Local assistant request timed out'));
});
req.write(data);
req.end();
});
}
app.listen(port, () => {
console.log(`Server is running. Send a POST text/plain to:`);
console.log(`curl -X POST -H "Content-Type: text/plain" -d 'Confirm deployment target and version; awaiting user input' http://localhost:${port}/user-input`);
});