@terminai/a2a-server 0.21.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 +5 -0
- package/dist/.last_build +0 -0
- package/dist/a2a-server.mjs +415698 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/src/agent/executor.d.ts +41 -0
- package/dist/src/agent/executor.js +408 -0
- package/dist/src/agent/executor.js.map +1 -0
- package/dist/src/agent/task.d.ts +67 -0
- package/dist/src/agent/task.js +799 -0
- package/dist/src/agent/task.js.map +1 -0
- package/dist/src/agent/task.test.d.ts +7 -0
- package/dist/src/agent/task.test.js +435 -0
- package/dist/src/agent/task.test.js.map +1 -0
- package/dist/src/agent/task.token.test.d.ts +7 -0
- package/dist/src/agent/task.token.test.js +53 -0
- package/dist/src/agent/task.token.test.js.map +1 -0
- package/dist/src/auth/llmAuthManager.d.ts +39 -0
- package/dist/src/auth/llmAuthManager.js +209 -0
- package/dist/src/auth/llmAuthManager.js.map +1 -0
- package/dist/src/auth/llmAuthManager.test.d.ts +7 -0
- package/dist/src/auth/llmAuthManager.test.js +92 -0
- package/dist/src/auth/llmAuthManager.test.js.map +1 -0
- package/dist/src/commands/command-registry.d.ts +16 -0
- package/dist/src/commands/command-registry.js +35 -0
- package/dist/src/commands/command-registry.js.map +1 -0
- package/dist/src/commands/command-registry.test.d.ts +7 -0
- package/dist/src/commands/command-registry.test.js +100 -0
- package/dist/src/commands/command-registry.test.js.map +1 -0
- package/dist/src/commands/extensions.d.ts +19 -0
- package/dist/src/commands/extensions.js +26 -0
- package/dist/src/commands/extensions.js.map +1 -0
- package/dist/src/commands/extensions.test.d.ts +7 -0
- package/dist/src/commands/extensions.test.js +70 -0
- package/dist/src/commands/extensions.test.js.map +1 -0
- package/dist/src/commands/init.d.ts +16 -0
- package/dist/src/commands/init.js +111 -0
- package/dist/src/commands/init.js.map +1 -0
- package/dist/src/commands/init.test.d.ts +7 -0
- package/dist/src/commands/init.test.js +146 -0
- package/dist/src/commands/init.test.js.map +1 -0
- package/dist/src/commands/restore.d.ts +21 -0
- package/dist/src/commands/restore.js +126 -0
- package/dist/src/commands/restore.js.map +1 -0
- package/dist/src/commands/restore.test.d.ts +7 -0
- package/dist/src/commands/restore.test.js +111 -0
- package/dist/src/commands/restore.test.js.map +1 -0
- package/dist/src/commands/types.d.ts +33 -0
- package/dist/src/commands/types.js +8 -0
- package/dist/src/commands/types.js.map +1 -0
- package/dist/src/config/config.d.ts +24 -0
- package/dist/src/config/config.js +140 -0
- package/dist/src/config/config.js.map +1 -0
- package/dist/src/config/extension.d.ts +12 -0
- package/dist/src/config/extension.js +105 -0
- package/dist/src/config/extension.js.map +1 -0
- package/dist/src/config/settings.d.ts +15 -0
- package/dist/src/config/settings.js +20 -0
- package/dist/src/config/settings.js.map +1 -0
- package/dist/src/config/settings.test.d.ts +7 -0
- package/dist/src/config/settings.test.js +170 -0
- package/dist/src/config/settings.test.js.map +1 -0
- package/dist/src/http/app.d.ts +17 -0
- package/dist/src/http/app.js +399 -0
- package/dist/src/http/app.js.map +1 -0
- package/dist/src/http/app.test.d.ts +7 -0
- package/dist/src/http/app.test.js +1048 -0
- package/dist/src/http/app.test.js.map +1 -0
- package/dist/src/http/auth.d.ts +21 -0
- package/dist/src/http/auth.js +55 -0
- package/dist/src/http/auth.js.map +1 -0
- package/dist/src/http/auth.test.d.ts +7 -0
- package/dist/src/http/auth.test.js +53 -0
- package/dist/src/http/auth.test.js.map +1 -0
- package/dist/src/http/authRoutes.test.d.ts +7 -0
- package/dist/src/http/authRoutes.test.js +169 -0
- package/dist/src/http/authRoutes.test.js.map +1 -0
- package/dist/src/http/cors.d.ts +8 -0
- package/dist/src/http/cors.js +96 -0
- package/dist/src/http/cors.js.map +1 -0
- package/dist/src/http/cors.test.d.ts +7 -0
- package/dist/src/http/cors.test.js +62 -0
- package/dist/src/http/cors.test.js.map +1 -0
- package/dist/src/http/deferredAuth.test.d.ts +7 -0
- package/dist/src/http/deferredAuth.test.js +45 -0
- package/dist/src/http/deferredAuth.test.js.map +1 -0
- package/dist/src/http/endpoints.test.d.ts +7 -0
- package/dist/src/http/endpoints.test.js +149 -0
- package/dist/src/http/endpoints.test.js.map +1 -0
- package/dist/src/http/llmAuthMiddleware.d.ts +9 -0
- package/dist/src/http/llmAuthMiddleware.js +37 -0
- package/dist/src/http/llmAuthMiddleware.js.map +1 -0
- package/dist/src/http/relay.d.ts +28 -0
- package/dist/src/http/relay.js +342 -0
- package/dist/src/http/relay.js.map +1 -0
- package/dist/src/http/relay.test.d.ts +7 -0
- package/dist/src/http/relay.test.js +149 -0
- package/dist/src/http/relay.test.js.map +1 -0
- package/dist/src/http/replay.d.ts +19 -0
- package/dist/src/http/replay.js +90 -0
- package/dist/src/http/replay.js.map +1 -0
- package/dist/src/http/replay.test.d.ts +7 -0
- package/dist/src/http/replay.test.js +78 -0
- package/dist/src/http/replay.test.js.map +1 -0
- package/dist/src/http/requestStorage.d.ts +11 -0
- package/dist/src/http/requestStorage.js +9 -0
- package/dist/src/http/requestStorage.js.map +1 -0
- package/dist/src/http/routes/auth.d.ts +9 -0
- package/dist/src/http/routes/auth.js +125 -0
- package/dist/src/http/routes/auth.js.map +1 -0
- package/dist/src/http/server.d.ts +8 -0
- package/dist/src/http/server.js +28 -0
- package/dist/src/http/server.js.map +1 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.js +11 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/persistence/gcs.d.ts +25 -0
- package/dist/src/persistence/gcs.js +248 -0
- package/dist/src/persistence/gcs.js.map +1 -0
- package/dist/src/persistence/gcs.test.d.ts +7 -0
- package/dist/src/persistence/gcs.test.js +335 -0
- package/dist/src/persistence/gcs.test.js.map +1 -0
- package/dist/src/persistence/remoteAuthStore.d.ts +21 -0
- package/dist/src/persistence/remoteAuthStore.js +74 -0
- package/dist/src/persistence/remoteAuthStore.js.map +1 -0
- package/dist/src/types.d.ts +100 -0
- package/dist/src/types.js +49 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/utils/envAliases.d.ts +7 -0
- package/dist/src/utils/envAliases.js +9 -0
- package/dist/src/utils/envAliases.js.map +1 -0
- package/dist/src/utils/executor_utils.d.ts +8 -0
- package/dist/src/utils/executor_utils.js +42 -0
- package/dist/src/utils/executor_utils.js.map +1 -0
- package/dist/src/utils/logger.d.ts +9 -0
- package/dist/src/utils/logger.js +26 -0
- package/dist/src/utils/logger.js.map +1 -0
- package/dist/src/utils/redactSecrets.d.ts +16 -0
- package/dist/src/utils/redactSecrets.js +72 -0
- package/dist/src/utils/redactSecrets.js.map +1 -0
- package/dist/src/utils/redactSecrets.test.d.ts +7 -0
- package/dist/src/utils/redactSecrets.test.js +62 -0
- package/dist/src/utils/redactSecrets.test.js.map +1 -0
- package/dist/src/utils/testing_utils.d.ts +48 -0
- package/dist/src/utils/testing_utils.js +173 -0
- package/dist/src/utils/testing_utils.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/web-client/app.js +526 -0
- package/dist/web-client/index.html +43 -0
- package/dist/web-client/package.json +10 -0
- package/dist/web-client/relay-client.js +330 -0
- package/dist/web-client/style.css +189 -0
- package/package.json +53 -0
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
import { RelayClient } from './relay-client.js';
|
|
2
|
+
|
|
3
|
+
const chat = document.getElementById('chat');
|
|
4
|
+
const input = document.getElementById('input');
|
|
5
|
+
const sendButton = document.getElementById('send');
|
|
6
|
+
const statusDot = document.getElementById('status-dot');
|
|
7
|
+
const statusText = document.getElementById('status-text');
|
|
8
|
+
|
|
9
|
+
let activeTaskId = null;
|
|
10
|
+
let currentAssistantEl = null;
|
|
11
|
+
|
|
12
|
+
// Relay State
|
|
13
|
+
let relayClient = null;
|
|
14
|
+
|
|
15
|
+
// Check for Relay Params
|
|
16
|
+
const fragment = new URLSearchParams(window.location.hash.substring(1));
|
|
17
|
+
const relayUrl = fragment.get('relay');
|
|
18
|
+
const sessionId = fragment.get('session');
|
|
19
|
+
const keyBase64 = fragment.get('key');
|
|
20
|
+
|
|
21
|
+
// Strip fragment to prevent key leakage
|
|
22
|
+
if (relayUrl && sessionId && keyBase64) {
|
|
23
|
+
history.replaceState(
|
|
24
|
+
null,
|
|
25
|
+
'',
|
|
26
|
+
window.location.pathname + window.location.search,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function setStatus(text, color = 'var(--muted)') {
|
|
31
|
+
statusDot.style.background = color;
|
|
32
|
+
statusText.textContent = text;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (relayUrl && sessionId && keyBase64) {
|
|
36
|
+
console.log('Using Cloud Relay Mode');
|
|
37
|
+
relayClient = new RelayClient(
|
|
38
|
+
relayUrl,
|
|
39
|
+
sessionId,
|
|
40
|
+
keyBase64,
|
|
41
|
+
(msg) => handleA2aEvent(msg), // onMessage
|
|
42
|
+
(text, color) => setStatus(text, color), // onStatus
|
|
43
|
+
() => renderPairingPrompt(), // onPairingRequired
|
|
44
|
+
);
|
|
45
|
+
relayClient.connect();
|
|
46
|
+
// Wait for handshake before allowing UI interactions
|
|
47
|
+
setTimeout(() => {
|
|
48
|
+
if (relayClient && relayClient.state !== 'READY') {
|
|
49
|
+
setStatus('Waiting for handshake...', 'var(--muted)');
|
|
50
|
+
}
|
|
51
|
+
}, 1000);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function appendMessage(role, text) {
|
|
55
|
+
const el = document.createElement('div');
|
|
56
|
+
el.className = `message ${role}`;
|
|
57
|
+
el.textContent = text;
|
|
58
|
+
chat.appendChild(el);
|
|
59
|
+
chat.scrollTop = chat.scrollHeight;
|
|
60
|
+
return el;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getToken() {
|
|
64
|
+
// If using Relay, we don't need a token
|
|
65
|
+
if (relayClient) return 'relay-mode';
|
|
66
|
+
|
|
67
|
+
const url = new URL(window.location.href);
|
|
68
|
+
const token = url.searchParams.get('token');
|
|
69
|
+
if (token) {
|
|
70
|
+
localStorage.setItem('termai_token', token);
|
|
71
|
+
url.searchParams.delete('token');
|
|
72
|
+
window.history.replaceState({}, '', url.toString());
|
|
73
|
+
return token;
|
|
74
|
+
}
|
|
75
|
+
return localStorage.getItem('termai_token') || '';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function sha256Hex(text) {
|
|
79
|
+
const enc = new TextEncoder();
|
|
80
|
+
const digest = await crypto.subtle.digest('SHA-256', enc.encode(text));
|
|
81
|
+
return [...new Uint8Array(digest)]
|
|
82
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
83
|
+
.join('');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function hmacSha256Hex(key, payload) {
|
|
87
|
+
const enc = new TextEncoder();
|
|
88
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
89
|
+
'raw',
|
|
90
|
+
enc.encode(key),
|
|
91
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
92
|
+
false,
|
|
93
|
+
['sign'],
|
|
94
|
+
);
|
|
95
|
+
const signature = await crypto.subtle.sign(
|
|
96
|
+
'HMAC',
|
|
97
|
+
cryptoKey,
|
|
98
|
+
enc.encode(payload),
|
|
99
|
+
);
|
|
100
|
+
return [...new Uint8Array(signature)]
|
|
101
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
102
|
+
.join('');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function signedHeaders({ token, method, pathWithQuery, bodyString }) {
|
|
106
|
+
const nonce = crypto.randomUUID();
|
|
107
|
+
const bodyHash = await sha256Hex(bodyString);
|
|
108
|
+
const payload = [method.toUpperCase(), pathWithQuery, bodyHash, nonce].join(
|
|
109
|
+
'\n',
|
|
110
|
+
);
|
|
111
|
+
const signature = await hmacSha256Hex(token, payload);
|
|
112
|
+
return {
|
|
113
|
+
Authorization: `Bearer ${token}`,
|
|
114
|
+
'X-Gemini-Nonce': nonce,
|
|
115
|
+
'X-Gemini-Signature': signature,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function readSse(response, onEvent) {
|
|
120
|
+
const reader = response.body.getReader();
|
|
121
|
+
const decoder = new TextDecoder();
|
|
122
|
+
let buffer = '';
|
|
123
|
+
|
|
124
|
+
while (true) {
|
|
125
|
+
const { value, done } = await reader.read();
|
|
126
|
+
if (done) break;
|
|
127
|
+
buffer += decoder.decode(value, { stream: true });
|
|
128
|
+
const parts = buffer.split('\n\n');
|
|
129
|
+
buffer = parts.pop() || '';
|
|
130
|
+
for (const chunk of parts) {
|
|
131
|
+
const lines = chunk.split('\n').filter((l) => l.startsWith('data:'));
|
|
132
|
+
for (const line of lines) {
|
|
133
|
+
const data = line.slice('data:'.length).trimStart();
|
|
134
|
+
if (!data) continue;
|
|
135
|
+
try {
|
|
136
|
+
onEvent(JSON.parse(data));
|
|
137
|
+
} catch {
|
|
138
|
+
// ignore malformed event
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function ensureAssistantMessage() {
|
|
146
|
+
if (!currentAssistantEl) {
|
|
147
|
+
currentAssistantEl = appendMessage('ai', '');
|
|
148
|
+
}
|
|
149
|
+
return currentAssistantEl;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function clearAssistantMessage() {
|
|
153
|
+
currentAssistantEl = null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function renderPairingPrompt() {
|
|
157
|
+
// Remove any existing pairing prompt
|
|
158
|
+
const existing = document.getElementById('pairing-prompt');
|
|
159
|
+
if (existing) existing.remove();
|
|
160
|
+
|
|
161
|
+
const wrapper = document.createElement('div');
|
|
162
|
+
wrapper.id = 'pairing-prompt';
|
|
163
|
+
wrapper.className = 'message ai';
|
|
164
|
+
wrapper.style.maxWidth = '100%';
|
|
165
|
+
|
|
166
|
+
const title = document.createElement('div');
|
|
167
|
+
title.textContent = 'Pairing Required';
|
|
168
|
+
title.style.fontWeight = '700';
|
|
169
|
+
title.style.marginBottom = '6px';
|
|
170
|
+
|
|
171
|
+
const body = document.createElement('div');
|
|
172
|
+
body.textContent =
|
|
173
|
+
'Enter the 6-digit pairing code shown on the host terminal:';
|
|
174
|
+
body.style.marginBottom = '10px';
|
|
175
|
+
|
|
176
|
+
const codeInput = document.createElement('input');
|
|
177
|
+
codeInput.type = 'text';
|
|
178
|
+
codeInput.placeholder = 'Enter code (6 digits)';
|
|
179
|
+
codeInput.inputMode = 'numeric';
|
|
180
|
+
codeInput.pattern = '\\d*';
|
|
181
|
+
codeInput.maxLength = 6;
|
|
182
|
+
codeInput.style.width = '100%';
|
|
183
|
+
codeInput.style.marginBottom = '10px';
|
|
184
|
+
codeInput.style.padding = '10px 12px';
|
|
185
|
+
codeInput.style.borderRadius = '12px';
|
|
186
|
+
codeInput.style.border = '1px solid var(--border)';
|
|
187
|
+
codeInput.style.background = 'rgba(255,255,255,0.03)';
|
|
188
|
+
codeInput.style.color = 'var(--text)';
|
|
189
|
+
codeInput.style.fontSize = '18px';
|
|
190
|
+
codeInput.style.textAlign = 'center';
|
|
191
|
+
codeInput.style.letterSpacing = '0.3em';
|
|
192
|
+
|
|
193
|
+
const submitBtn = document.createElement('button');
|
|
194
|
+
submitBtn.textContent = 'Pair';
|
|
195
|
+
submitBtn.style.width = '100%';
|
|
196
|
+
submitBtn.disabled = true;
|
|
197
|
+
|
|
198
|
+
const validateCode = () => {
|
|
199
|
+
const val = (codeInput.value || '').replace(/\D/g, '');
|
|
200
|
+
codeInput.value = val.slice(0, 6);
|
|
201
|
+
return codeInput.value.length === 6;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
codeInput.addEventListener('input', () => {
|
|
205
|
+
submitBtn.disabled = !validateCode();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
submitBtn.addEventListener('click', async () => {
|
|
209
|
+
const code = codeInput.value.trim();
|
|
210
|
+
if (code.length !== 6) return;
|
|
211
|
+
submitBtn.disabled = true;
|
|
212
|
+
submitBtn.textContent = 'Pairing...';
|
|
213
|
+
try {
|
|
214
|
+
await relayClient.sendPairingCode(code);
|
|
215
|
+
// Success will be handled by PAIR_ACK message
|
|
216
|
+
wrapper.remove();
|
|
217
|
+
} catch (e) {
|
|
218
|
+
submitBtn.textContent = 'Pair';
|
|
219
|
+
submitBtn.disabled = false;
|
|
220
|
+
console.error('Pairing error:', e);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Allow Enter key to submit
|
|
225
|
+
codeInput.addEventListener('keydown', (e) => {
|
|
226
|
+
if (e.key === 'Enter' && !submitBtn.disabled) {
|
|
227
|
+
submitBtn.click();
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
wrapper.appendChild(title);
|
|
232
|
+
wrapper.appendChild(body);
|
|
233
|
+
wrapper.appendChild(codeInput);
|
|
234
|
+
wrapper.appendChild(submitBtn);
|
|
235
|
+
chat.appendChild(wrapper);
|
|
236
|
+
chat.scrollTop = chat.scrollHeight;
|
|
237
|
+
codeInput.focus();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function renderConfirmation({ callId, prompt, requiresPin, pinLength }) {
|
|
241
|
+
const wrapper = document.createElement('div');
|
|
242
|
+
wrapper.className = 'message ai';
|
|
243
|
+
wrapper.style.maxWidth = '100%';
|
|
244
|
+
|
|
245
|
+
const title = document.createElement('div');
|
|
246
|
+
title.textContent = 'Confirmation required';
|
|
247
|
+
title.style.fontWeight = '700';
|
|
248
|
+
title.style.marginBottom = '6px';
|
|
249
|
+
|
|
250
|
+
const body = document.createElement('div');
|
|
251
|
+
body.textContent = prompt;
|
|
252
|
+
body.style.whiteSpace = 'pre-wrap';
|
|
253
|
+
body.style.marginBottom = '10px';
|
|
254
|
+
|
|
255
|
+
const pinInput = document.createElement('input');
|
|
256
|
+
pinInput.type = 'password';
|
|
257
|
+
pinInput.placeholder = requiresPin ? `PIN (${pinLength} digits)` : '';
|
|
258
|
+
pinInput.inputMode = 'numeric';
|
|
259
|
+
pinInput.pattern = '\\d*';
|
|
260
|
+
pinInput.style.width = '100%';
|
|
261
|
+
pinInput.style.marginBottom = '10px';
|
|
262
|
+
pinInput.style.padding = '10px 12px';
|
|
263
|
+
pinInput.style.borderRadius = '12px';
|
|
264
|
+
pinInput.style.border = '1px solid var(--border)';
|
|
265
|
+
pinInput.style.background = 'rgba(255,255,255,0.03)';
|
|
266
|
+
pinInput.style.color = 'var(--text)';
|
|
267
|
+
pinInput.style.display = requiresPin ? 'block' : 'none';
|
|
268
|
+
|
|
269
|
+
const actions = document.createElement('div');
|
|
270
|
+
actions.style.display = 'flex';
|
|
271
|
+
actions.style.gap = '10px';
|
|
272
|
+
|
|
273
|
+
const yes = document.createElement('button');
|
|
274
|
+
yes.textContent = 'Yes, proceed';
|
|
275
|
+
yes.style.flex = '1';
|
|
276
|
+
|
|
277
|
+
const no = document.createElement('button');
|
|
278
|
+
no.textContent = 'Cancel';
|
|
279
|
+
no.style.flex = '1';
|
|
280
|
+
no.style.background = 'rgba(255,255,255,0.08)';
|
|
281
|
+
no.style.color = 'var(--text)';
|
|
282
|
+
no.style.boxShadow = 'none';
|
|
283
|
+
no.style.border = '1px solid var(--border)';
|
|
284
|
+
|
|
285
|
+
const validatePin = () => {
|
|
286
|
+
if (!requiresPin) return true;
|
|
287
|
+
const val = (pinInput.value || '').replace(/\D/g, '');
|
|
288
|
+
pinInput.value = val.slice(0, pinLength);
|
|
289
|
+
return pinInput.value.length === pinLength;
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
pinInput.addEventListener('input', () => {
|
|
293
|
+
validatePin();
|
|
294
|
+
yes.disabled = requiresPin && !validatePin();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
yes.disabled = requiresPin;
|
|
298
|
+
|
|
299
|
+
yes.addEventListener('click', async () => {
|
|
300
|
+
const pin = requiresPin
|
|
301
|
+
? (pinInput.value || '').replace(/\D/g, '')
|
|
302
|
+
: undefined;
|
|
303
|
+
if (requiresPin && (!pin || pin.length !== pinLength)) return;
|
|
304
|
+
await sendToolConfirmation(callId, true, pin);
|
|
305
|
+
wrapper.remove();
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
no.addEventListener('click', async () => {
|
|
309
|
+
const pin = requiresPin
|
|
310
|
+
? (pinInput.value || '').replace(/\D/g, '')
|
|
311
|
+
: undefined;
|
|
312
|
+
await sendToolConfirmation(callId, false, pin);
|
|
313
|
+
wrapper.remove();
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
actions.appendChild(yes);
|
|
317
|
+
actions.appendChild(no);
|
|
318
|
+
|
|
319
|
+
wrapper.appendChild(title);
|
|
320
|
+
wrapper.appendChild(body);
|
|
321
|
+
wrapper.appendChild(pinInput);
|
|
322
|
+
wrapper.appendChild(actions);
|
|
323
|
+
chat.appendChild(wrapper);
|
|
324
|
+
chat.scrollTop = chat.scrollHeight;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function postStream(body) {
|
|
328
|
+
// If Relay Mode
|
|
329
|
+
if (relayClient) {
|
|
330
|
+
await relayClient.send(body);
|
|
331
|
+
return { ok: true, relayMode: true };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const token = getToken();
|
|
335
|
+
if (!token) {
|
|
336
|
+
setStatus('Missing token (open /ui?token=...)', 'var(--danger)');
|
|
337
|
+
appendMessage(
|
|
338
|
+
'error',
|
|
339
|
+
'Missing token. Start web-remote and open the URL with ?token=...',
|
|
340
|
+
);
|
|
341
|
+
throw new Error('Missing token');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const bodyString = JSON.stringify(body);
|
|
345
|
+
const headers = await signedHeaders({
|
|
346
|
+
token,
|
|
347
|
+
method: 'POST',
|
|
348
|
+
pathWithQuery: '/',
|
|
349
|
+
bodyString,
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
return fetch('/', {
|
|
353
|
+
method: 'POST',
|
|
354
|
+
headers: {
|
|
355
|
+
...headers,
|
|
356
|
+
'Content-Type': 'application/json',
|
|
357
|
+
Accept: 'text/event-stream',
|
|
358
|
+
},
|
|
359
|
+
body: bodyString,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function sendToolConfirmation(callId, approved, pin) {
|
|
364
|
+
if (!activeTaskId) {
|
|
365
|
+
appendMessage('error', 'No active task. Send a message first.');
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
setStatus('Confirming…', 'var(--accent)');
|
|
369
|
+
clearAssistantMessage();
|
|
370
|
+
|
|
371
|
+
const body = {
|
|
372
|
+
jsonrpc: '2.0',
|
|
373
|
+
id: '1',
|
|
374
|
+
method: 'message/stream',
|
|
375
|
+
params: {
|
|
376
|
+
message: {
|
|
377
|
+
kind: 'message',
|
|
378
|
+
role: 'user',
|
|
379
|
+
parts: [
|
|
380
|
+
{
|
|
381
|
+
kind: 'data',
|
|
382
|
+
data: {
|
|
383
|
+
callId,
|
|
384
|
+
outcome: approved ? 'proceed_once' : 'cancel',
|
|
385
|
+
...(pin ? { pin } : {}),
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
],
|
|
389
|
+
messageId: crypto.randomUUID(),
|
|
390
|
+
},
|
|
391
|
+
metadata: {
|
|
392
|
+
coderAgent: { kind: 'agent-settings', workspacePath: '/tmp' },
|
|
393
|
+
},
|
|
394
|
+
taskId: activeTaskId,
|
|
395
|
+
},
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
const response = await postStream(body);
|
|
399
|
+
if (response.relayMode) return;
|
|
400
|
+
|
|
401
|
+
if (!response.ok || !response.body) {
|
|
402
|
+
appendMessage(
|
|
403
|
+
'error',
|
|
404
|
+
`Confirmation failed: ${response.status} ${response.statusText}`,
|
|
405
|
+
);
|
|
406
|
+
setStatus('Offline', 'var(--danger)');
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
await readSse(response, handleA2aEvent);
|
|
411
|
+
setStatus('Connected', 'var(--accent)');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function handleA2aEvent(evt) {
|
|
415
|
+
if (!evt || !evt.result) return;
|
|
416
|
+
const result = evt.result;
|
|
417
|
+
|
|
418
|
+
if (result.kind === 'task' && typeof result.id === 'string') {
|
|
419
|
+
activeTaskId = result.id;
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (result.kind !== 'status-update') return;
|
|
424
|
+
|
|
425
|
+
const coder = result.metadata?.coderAgent;
|
|
426
|
+
const kind = coder?.kind;
|
|
427
|
+
|
|
428
|
+
if (kind === 'text-content') {
|
|
429
|
+
const parts = result.status?.message?.parts || [];
|
|
430
|
+
for (const part of parts) {
|
|
431
|
+
if (part?.kind === 'text' && typeof part.text === 'string') {
|
|
432
|
+
const el = ensureAssistantMessage();
|
|
433
|
+
el.textContent += part.text;
|
|
434
|
+
chat.scrollTop = chat.scrollHeight;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (kind === 'tool-call-confirmation') {
|
|
440
|
+
const part = (result.status?.message?.parts || []).find(
|
|
441
|
+
(p) => p?.kind === 'data' && p.data,
|
|
442
|
+
);
|
|
443
|
+
const tool = part?.data || {};
|
|
444
|
+
const callId = tool?.request?.callId;
|
|
445
|
+
const prompt =
|
|
446
|
+
tool?.confirmationDetails?.prompt ||
|
|
447
|
+
tool?.confirmationDetails?.command ||
|
|
448
|
+
'Confirm tool execution';
|
|
449
|
+
const requiresPin = tool?.confirmationDetails?.requiresPin === true;
|
|
450
|
+
const pinLength =
|
|
451
|
+
typeof tool?.confirmationDetails?.pinLength === 'number'
|
|
452
|
+
? tool.confirmationDetails.pinLength
|
|
453
|
+
: 6;
|
|
454
|
+
if (callId) {
|
|
455
|
+
renderConfirmation({ callId, prompt, requiresPin, pinLength });
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (result.final === true) {
|
|
460
|
+
setStatus('Connected', 'var(--accent)');
|
|
461
|
+
clearAssistantMessage();
|
|
462
|
+
// Re-enable send button if disabled?
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async function sendMessage() {
|
|
467
|
+
const value = input.value.trim();
|
|
468
|
+
if (!value) return;
|
|
469
|
+
appendMessage('user', value);
|
|
470
|
+
input.value = '';
|
|
471
|
+
clearAssistantMessage();
|
|
472
|
+
setStatus('Sending…', 'var(--accent)');
|
|
473
|
+
|
|
474
|
+
const body = {
|
|
475
|
+
jsonrpc: '2.0',
|
|
476
|
+
id: '1',
|
|
477
|
+
method: 'message/stream',
|
|
478
|
+
params: {
|
|
479
|
+
message: {
|
|
480
|
+
kind: 'message',
|
|
481
|
+
role: 'user',
|
|
482
|
+
parts: [{ kind: 'text', text: value }],
|
|
483
|
+
messageId: crypto.randomUUID(),
|
|
484
|
+
},
|
|
485
|
+
metadata: {
|
|
486
|
+
coderAgent: { kind: 'agent-settings', workspacePath: '/tmp' },
|
|
487
|
+
},
|
|
488
|
+
...(activeTaskId ? { taskId: activeTaskId } : {}),
|
|
489
|
+
},
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
try {
|
|
493
|
+
const response = await postStream(body);
|
|
494
|
+
if (response.relayMode) return;
|
|
495
|
+
|
|
496
|
+
if (!response.ok || !response.body) {
|
|
497
|
+
appendMessage(
|
|
498
|
+
'error',
|
|
499
|
+
`Request failed: ${response.status} ${response.statusText}`,
|
|
500
|
+
);
|
|
501
|
+
setStatus('Offline', 'var(--danger)');
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
await readSse(response, handleA2aEvent);
|
|
505
|
+
setStatus('Connected', 'var(--accent)');
|
|
506
|
+
} catch (error) {
|
|
507
|
+
appendMessage('error', error?.message || 'Failed to reach server.');
|
|
508
|
+
setStatus('Offline', 'var(--danger)');
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
sendButton.addEventListener('click', () => {
|
|
513
|
+
void sendMessage();
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
input.addEventListener('keydown', (event) => {
|
|
517
|
+
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
|
|
518
|
+
event.preventDefault();
|
|
519
|
+
void sendMessage();
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
setStatus(
|
|
524
|
+
getToken() ? 'Ready' : 'Missing token',
|
|
525
|
+
getToken() ? 'var(--accent)' : 'var(--danger)',
|
|
526
|
+
);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>TerminaI Remote</title>
|
|
7
|
+
<link rel="stylesheet" href="style.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="app">
|
|
11
|
+
<header>
|
|
12
|
+
<div>
|
|
13
|
+
<p class="eyebrow">TerminaI Remote</p>
|
|
14
|
+
<h1>Stay in flow from your phone</h1>
|
|
15
|
+
<p class="lede">
|
|
16
|
+
Send quick prompts, watch responses stream, and keep the terminal in
|
|
17
|
+
sync.
|
|
18
|
+
</p>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="status">
|
|
21
|
+
<span class="dot" id="status-dot"></span>
|
|
22
|
+
<span id="status-text">Connecting…</span>
|
|
23
|
+
</div>
|
|
24
|
+
</header>
|
|
25
|
+
|
|
26
|
+
<main id="chat"></main>
|
|
27
|
+
|
|
28
|
+
<footer>
|
|
29
|
+
<label for="input">Ask TerminaI</label>
|
|
30
|
+
<textarea
|
|
31
|
+
id="input"
|
|
32
|
+
placeholder="Run tests, summarize logs, or ask for a quick fix…"
|
|
33
|
+
rows="3"
|
|
34
|
+
></textarea>
|
|
35
|
+
<div class="actions">
|
|
36
|
+
<button id="send">Send</button>
|
|
37
|
+
<small>Press Ctrl/Cmd+Enter to send</small>
|
|
38
|
+
</div>
|
|
39
|
+
</footer>
|
|
40
|
+
</div>
|
|
41
|
+
<script src="app.js" type="module"></script>
|
|
42
|
+
</body>
|
|
43
|
+
</html>
|