appback-remoteagent 0.13.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.
Files changed (46) hide show
  1. package/.env.example +39 -0
  2. package/LICENSE +21 -0
  3. package/README.md +371 -0
  4. package/bin/remoteagent.js +2 -0
  5. package/dist/adapters/claude-adapter.js +78 -0
  6. package/dist/adapters/codex-adapter.js +241 -0
  7. package/dist/adapters/provider-adapter.js +1 -0
  8. package/dist/adapters/shell-adapter.js +44 -0
  9. package/dist/adapters/windows-shell.js +111 -0
  10. package/dist/bot.js +2135 -0
  11. package/dist/config.js +170 -0
  12. package/dist/index.js +534 -0
  13. package/dist/secret-helper.js +24 -0
  14. package/dist/services/agent-memory-service.js +737 -0
  15. package/dist/services/bot-management-service.js +626 -0
  16. package/dist/services/bridge-service.js +807 -0
  17. package/dist/services/local-ui-service.js +533 -0
  18. package/dist/services/provider-setup-service.js +284 -0
  19. package/dist/services/remote-shell-service.js +97 -0
  20. package/dist/store/file-store.js +690 -0
  21. package/dist/telegram-fetch.js +85 -0
  22. package/dist/types.js +1 -0
  23. package/docs/ARCHITECTURE.md +170 -0
  24. package/docs/COKACDIR_NOTES.md +79 -0
  25. package/docs/ERROR_NORMALIZATION.md +46 -0
  26. package/docs/MINI_APP.md +112 -0
  27. package/docs/MVP.md +108 -0
  28. package/docs/OPERATIONS.md +181 -0
  29. package/docs/RELEASING.md +87 -0
  30. package/docs/SESSION_DIRECTORY_PLAN.md +506 -0
  31. package/package.json +47 -0
  32. package/scripts/bump-version.sh +23 -0
  33. package/scripts/finish-claude-login.sh +48 -0
  34. package/scripts/install-claude.sh +6 -0
  35. package/scripts/install-codex.sh +8 -0
  36. package/scripts/install.ps1 +51 -0
  37. package/scripts/install.sh +101 -0
  38. package/scripts/mock-adapter.sh +7 -0
  39. package/scripts/restart-after-bot-op.sh +118 -0
  40. package/scripts/selftest-telegram-update.mjs +359 -0
  41. package/scripts/start-claude-login.sh +4 -0
  42. package/scripts/start.ps1 +39 -0
  43. package/scripts/start.sh +54 -0
  44. package/scripts/stop.ps1 +40 -0
  45. package/scripts/stop.sh +39 -0
  46. package/tsconfig.json +20 -0
@@ -0,0 +1,533 @@
1
+ import http from "node:http";
2
+ import { URL } from "node:url";
3
+ export class LocalUiService {
4
+ bridge;
5
+ host;
6
+ port;
7
+ server;
8
+ constructor(bridge, host, port) {
9
+ this.bridge = bridge;
10
+ this.host = host;
11
+ this.port = port;
12
+ }
13
+ async start() {
14
+ if (this.server) {
15
+ return;
16
+ }
17
+ this.server = http.createServer((request, response) => {
18
+ void this.handleRequest(request, response).catch((error) => {
19
+ console.error("Local UI request failed:", error);
20
+ if (!response.headersSent) {
21
+ this.sendJson(response, 500, { error: "Internal server error." });
22
+ }
23
+ });
24
+ });
25
+ await new Promise((resolve, reject) => {
26
+ this.server.once("error", reject);
27
+ this.server.listen(this.port, this.host, () => {
28
+ this.server.off("error", reject);
29
+ resolve();
30
+ });
31
+ });
32
+ }
33
+ async handleRequest(request, response) {
34
+ const method = request.method ?? "GET";
35
+ const url = new URL(request.url ?? "/", `http://${this.host}:${this.port}`);
36
+ const pathname = url.pathname;
37
+ if (method === "GET" && pathname === "/") {
38
+ this.sendHtml(response, INDEX_HTML);
39
+ return;
40
+ }
41
+ if (method === "GET" && pathname === "/api/sessions") {
42
+ const sessions = await this.bridge.listSessions();
43
+ this.sendJson(response, 200, { sessions });
44
+ return;
45
+ }
46
+ const eventsMatch = pathname.match(/^\/api\/sessions\/([^/]+)\/events$/);
47
+ if (method === "GET" && eventsMatch) {
48
+ const sessionId = decodeURIComponent(eventsMatch[1]);
49
+ const limitParam = url.searchParams.get("limit");
50
+ const limit = limitParam ? Number.parseInt(limitParam, 10) : 200;
51
+ const events = await this.bridge.sessionEvents(sessionId, Number.isFinite(limit) ? limit : 200);
52
+ this.sendJson(response, 200, { events });
53
+ return;
54
+ }
55
+ const messageMatch = pathname.match(/^\/api\/sessions\/([^/]+)\/messages$/);
56
+ if (method === "POST" && messageMatch) {
57
+ const sessionId = decodeURIComponent(messageMatch[1]);
58
+ const body = await this.readJsonBody(request);
59
+ const message = typeof body.message === "string" ? body.message.trim() : "";
60
+ if (!message) {
61
+ this.sendJson(response, 400, { error: "Message is required." });
62
+ return;
63
+ }
64
+ const responses = await this.bridge.routeSessionMessage(sessionId, message);
65
+ this.sendJson(response, 200, {
66
+ responses: responses.map((item) => ({
67
+ provider: item.provider,
68
+ sessionId: item.sessionId,
69
+ cwd: item.cwd,
70
+ output: item.output,
71
+ })),
72
+ });
73
+ return;
74
+ }
75
+ this.sendJson(response, 404, { error: "Not found." });
76
+ }
77
+ async readJsonBody(request) {
78
+ const chunks = [];
79
+ await new Promise((resolve, reject) => {
80
+ request.on("data", (chunk) => {
81
+ chunks.push(chunk);
82
+ });
83
+ request.on("end", resolve);
84
+ request.on("error", reject);
85
+ });
86
+ const raw = Buffer.concat(chunks).toString("utf8").trim();
87
+ if (!raw) {
88
+ return {};
89
+ }
90
+ try {
91
+ const parsed = JSON.parse(raw);
92
+ return parsed && typeof parsed === "object" ? parsed : {};
93
+ }
94
+ catch {
95
+ return {};
96
+ }
97
+ }
98
+ sendHtml(response, body) {
99
+ response.statusCode = 200;
100
+ response.setHeader("Content-Type", "text/html; charset=utf-8");
101
+ response.end(body);
102
+ }
103
+ sendJson(response, statusCode, body) {
104
+ response.statusCode = statusCode;
105
+ response.setHeader("Content-Type", "application/json; charset=utf-8");
106
+ response.end(JSON.stringify(body));
107
+ }
108
+ }
109
+ const INDEX_HTML = String.raw `<!doctype html>
110
+ <html lang="ko">
111
+ <head>
112
+ <meta charset="utf-8" />
113
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
114
+ <title>remoteagent</title>
115
+ <style>
116
+ :root {
117
+ color-scheme: dark;
118
+ --bg: #101318;
119
+ --panel: #171b22;
120
+ --panel-2: #1e2430;
121
+ --line: #2b3342;
122
+ --text: #f5f7fb;
123
+ --muted: #9ca7b8;
124
+ --accent: #4ec9b0;
125
+ --accent-2: #6fb1ff;
126
+ --danger: #ff7a7a;
127
+ }
128
+
129
+ * { box-sizing: border-box; }
130
+ html, body { height: 100%; }
131
+ body {
132
+ margin: 0;
133
+ background: var(--bg);
134
+ color: var(--text);
135
+ font: 14px/1.5 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
136
+ }
137
+
138
+ .app {
139
+ display: grid;
140
+ grid-template-columns: 320px 1fr;
141
+ min-height: 100vh;
142
+ }
143
+
144
+ .sidebar {
145
+ border-right: 1px solid var(--line);
146
+ background: var(--panel);
147
+ padding: 16px;
148
+ }
149
+
150
+ .content {
151
+ display: grid;
152
+ grid-template-rows: auto 1fr auto;
153
+ min-height: 100vh;
154
+ }
155
+
156
+ .header {
157
+ padding: 16px 20px;
158
+ border-bottom: 1px solid var(--line);
159
+ background: rgba(23, 27, 34, 0.9);
160
+ position: sticky;
161
+ top: 0;
162
+ backdrop-filter: blur(8px);
163
+ }
164
+
165
+ h1, h2, h3, p { margin: 0; }
166
+ h1 { font-size: 18px; }
167
+ .subtle { color: var(--muted); }
168
+
169
+ .session-list {
170
+ margin-top: 16px;
171
+ display: grid;
172
+ gap: 8px;
173
+ }
174
+
175
+ button, textarea {
176
+ font: inherit;
177
+ }
178
+
179
+ .session-item,
180
+ .send-button,
181
+ .refresh-button {
182
+ border: 1px solid var(--line);
183
+ border-radius: 8px;
184
+ }
185
+
186
+ .session-item {
187
+ width: 100%;
188
+ background: var(--panel-2);
189
+ color: var(--text);
190
+ text-align: left;
191
+ padding: 12px;
192
+ cursor: pointer;
193
+ }
194
+
195
+ .session-item.active {
196
+ border-color: var(--accent-2);
197
+ box-shadow: inset 0 0 0 1px var(--accent-2);
198
+ }
199
+
200
+ .session-item strong {
201
+ display: block;
202
+ margin-bottom: 2px;
203
+ }
204
+
205
+ .session-item .meta {
206
+ color: var(--muted);
207
+ font-size: 12px;
208
+ word-break: break-word;
209
+ }
210
+
211
+ .toolbar {
212
+ display: flex;
213
+ gap: 8px;
214
+ align-items: center;
215
+ margin-top: 12px;
216
+ }
217
+
218
+ .refresh-button,
219
+ .send-button {
220
+ background: var(--panel-2);
221
+ color: var(--text);
222
+ padding: 9px 12px;
223
+ cursor: pointer;
224
+ }
225
+
226
+ .send-button {
227
+ background: var(--accent);
228
+ border-color: var(--accent);
229
+ color: #08120f;
230
+ font-weight: 600;
231
+ }
232
+
233
+ .refresh-button:disabled,
234
+ .send-button:disabled,
235
+ .session-item:disabled {
236
+ opacity: 0.6;
237
+ cursor: default;
238
+ }
239
+
240
+ .events {
241
+ padding: 20px;
242
+ display: grid;
243
+ gap: 12px;
244
+ align-content: start;
245
+ overflow: auto;
246
+ }
247
+
248
+ .event {
249
+ border: 1px solid var(--line);
250
+ border-radius: 8px;
251
+ padding: 12px;
252
+ background: var(--panel);
253
+ }
254
+
255
+ .event .meta {
256
+ margin-bottom: 8px;
257
+ color: var(--muted);
258
+ font-size: 12px;
259
+ }
260
+
261
+ .event pre {
262
+ margin: 0;
263
+ white-space: pre-wrap;
264
+ word-break: break-word;
265
+ font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
266
+ }
267
+
268
+ .composer {
269
+ border-top: 1px solid var(--line);
270
+ padding: 16px 20px;
271
+ background: var(--panel);
272
+ }
273
+
274
+ textarea {
275
+ width: 100%;
276
+ min-height: 120px;
277
+ resize: vertical;
278
+ border: 1px solid var(--line);
279
+ border-radius: 8px;
280
+ padding: 12px;
281
+ background: #0f141c;
282
+ color: var(--text);
283
+ }
284
+
285
+ .composer-row {
286
+ margin-top: 10px;
287
+ display: flex;
288
+ justify-content: space-between;
289
+ gap: 12px;
290
+ align-items: center;
291
+ }
292
+
293
+ .status {
294
+ color: var(--muted);
295
+ min-height: 20px;
296
+ }
297
+
298
+ .status.error {
299
+ color: var(--danger);
300
+ }
301
+
302
+ .empty {
303
+ color: var(--muted);
304
+ padding: 8px 0;
305
+ }
306
+
307
+ @media (max-width: 900px) {
308
+ .app {
309
+ grid-template-columns: 1fr;
310
+ }
311
+
312
+ .sidebar {
313
+ border-right: 0;
314
+ border-bottom: 1px solid var(--line);
315
+ }
316
+ }
317
+ </style>
318
+ </head>
319
+ <body>
320
+ <div class="app">
321
+ <aside class="sidebar">
322
+ <h1>remoteagent</h1>
323
+ <p class="subtle">Local session console</p>
324
+ <div class="toolbar">
325
+ <button id="refreshSessions" class="refresh-button" type="button">Refresh</button>
326
+ </div>
327
+ <div id="sessionList" class="session-list"></div>
328
+ </aside>
329
+ <main class="content">
330
+ <header class="header">
331
+ <h2 id="sessionTitle">No session selected</h2>
332
+ <p id="sessionMeta" class="subtle">Choose a session from the left.</p>
333
+ </header>
334
+ <section id="events" class="events">
335
+ <div class="empty">No events yet.</div>
336
+ </section>
337
+ <section class="composer">
338
+ <textarea id="messageInput" placeholder="Send a message to the selected session"></textarea>
339
+ <div class="composer-row">
340
+ <div id="status" class="status"></div>
341
+ <button id="sendMessage" class="send-button" type="button">Send</button>
342
+ </div>
343
+ </section>
344
+ </main>
345
+ </div>
346
+ <script>
347
+ const state = {
348
+ sessions: [],
349
+ selectedSessionId: null,
350
+ sending: false,
351
+ };
352
+
353
+ const elements = {
354
+ sessionList: document.getElementById("sessionList"),
355
+ sessionTitle: document.getElementById("sessionTitle"),
356
+ sessionMeta: document.getElementById("sessionMeta"),
357
+ events: document.getElementById("events"),
358
+ messageInput: document.getElementById("messageInput"),
359
+ sendMessage: document.getElementById("sendMessage"),
360
+ refreshSessions: document.getElementById("refreshSessions"),
361
+ status: document.getElementById("status"),
362
+ };
363
+
364
+ elements.refreshSessions.addEventListener("click", () => {
365
+ void loadSessions();
366
+ });
367
+
368
+ elements.sendMessage.addEventListener("click", () => {
369
+ void sendMessage();
370
+ });
371
+
372
+ async function fetchJson(url, options) {
373
+ const response = await fetch(url, options);
374
+ const data = await response.json().catch(() => ({}));
375
+ if (!response.ok) {
376
+ throw new Error(data.error || "Request failed.");
377
+ }
378
+ return data;
379
+ }
380
+
381
+ function setStatus(message, isError = false) {
382
+ elements.status.textContent = message || "";
383
+ elements.status.classList.toggle("error", isError);
384
+ }
385
+
386
+ function renderSessions() {
387
+ const sessions = state.sessions;
388
+ elements.sessionList.innerHTML = "";
389
+
390
+ if (sessions.length === 0) {
391
+ const empty = document.createElement("div");
392
+ empty.className = "empty";
393
+ empty.textContent = "No sessions found.";
394
+ elements.sessionList.appendChild(empty);
395
+ return;
396
+ }
397
+
398
+ for (const session of sessions) {
399
+ const button = document.createElement("button");
400
+ button.type = "button";
401
+ button.className = "session-item";
402
+ if (session.sessionId === state.selectedSessionId) {
403
+ button.classList.add("active");
404
+ }
405
+ button.innerHTML = [
406
+ "<strong>" + escapeHtml(session.publicId) + " · " + escapeHtml(session.mode) + "</strong>",
407
+ "<div class=\"meta\">" + escapeHtml(session.workspace) + "</div>",
408
+ "<div class=\"meta\">updated " + escapeHtml(session.updatedAt) + "</div>",
409
+ ].join("");
410
+ button.addEventListener("click", () => {
411
+ state.selectedSessionId = session.sessionId;
412
+ renderSessions();
413
+ renderSessionHeader();
414
+ void loadEvents();
415
+ });
416
+ elements.sessionList.appendChild(button);
417
+ }
418
+ }
419
+
420
+ function renderSessionHeader() {
421
+ const session = state.sessions.find((item) => item.sessionId === state.selectedSessionId);
422
+ if (!session) {
423
+ elements.sessionTitle.textContent = "No session selected";
424
+ elements.sessionMeta.textContent = "Choose a session from the left.";
425
+ return;
426
+ }
427
+
428
+ elements.sessionTitle.textContent = session.publicId + " · " + session.mode;
429
+ elements.sessionMeta.textContent = session.workspace;
430
+ }
431
+
432
+ function renderEvents(events) {
433
+ elements.events.innerHTML = "";
434
+ if (!events.length) {
435
+ const empty = document.createElement("div");
436
+ empty.className = "empty";
437
+ empty.textContent = "No events yet.";
438
+ elements.events.appendChild(empty);
439
+ return;
440
+ }
441
+
442
+ for (const event of events) {
443
+ const card = document.createElement("article");
444
+ card.className = "event";
445
+ const meta = document.createElement("div");
446
+ meta.className = "meta";
447
+ meta.textContent = event.timestamp + " · " + event.provider + " · " + event.direction;
448
+ const body = document.createElement("pre");
449
+ body.textContent = event.text || "";
450
+ card.appendChild(meta);
451
+ card.appendChild(body);
452
+ elements.events.appendChild(card);
453
+ }
454
+
455
+ elements.events.scrollTop = elements.events.scrollHeight;
456
+ }
457
+
458
+ async function loadSessions() {
459
+ setStatus("Loading sessions...");
460
+ const data = await fetchJson("/api/sessions");
461
+ state.sessions = data.sessions || [];
462
+
463
+ if (!state.selectedSessionId && state.sessions.length > 0) {
464
+ state.selectedSessionId = state.sessions[0].sessionId;
465
+ }
466
+ if (state.selectedSessionId && !state.sessions.some((item) => item.sessionId === state.selectedSessionId)) {
467
+ state.selectedSessionId = state.sessions[0] ? state.sessions[0].sessionId : null;
468
+ }
469
+
470
+ renderSessions();
471
+ renderSessionHeader();
472
+ await loadEvents();
473
+ setStatus("");
474
+ }
475
+
476
+ async function loadEvents() {
477
+ if (!state.selectedSessionId) {
478
+ renderEvents([]);
479
+ return;
480
+ }
481
+
482
+ const data = await fetchJson("/api/sessions/" + encodeURIComponent(state.selectedSessionId) + "/events?limit=200");
483
+ renderEvents(data.events || []);
484
+ }
485
+
486
+ async function sendMessage() {
487
+ if (!state.selectedSessionId || state.sending) {
488
+ return;
489
+ }
490
+
491
+ const message = elements.messageInput.value.trim();
492
+ if (!message) {
493
+ setStatus("Write a message first.", true);
494
+ return;
495
+ }
496
+
497
+ state.sending = true;
498
+ elements.sendMessage.disabled = true;
499
+ setStatus("Sending...");
500
+
501
+ try {
502
+ await fetchJson("/api/sessions/" + encodeURIComponent(state.selectedSessionId) + "/messages", {
503
+ method: "POST",
504
+ headers: { "Content-Type": "application/json" },
505
+ body: JSON.stringify({ message }),
506
+ });
507
+ elements.messageInput.value = "";
508
+ await loadSessions();
509
+ setStatus("Sent.");
510
+ } catch (error) {
511
+ setStatus(error instanceof Error ? error.message : "Failed to send.", true);
512
+ } finally {
513
+ state.sending = false;
514
+ elements.sendMessage.disabled = false;
515
+ }
516
+ }
517
+
518
+ function escapeHtml(value) {
519
+ return String(value)
520
+ .replaceAll("&", "&amp;")
521
+ .replaceAll("<", "&lt;")
522
+ .replaceAll(">", "&gt;")
523
+ .replaceAll("\"", "&quot;")
524
+ .replaceAll("'", "&#39;");
525
+ }
526
+
527
+ void loadSessions().catch((error) => {
528
+ setStatus(error instanceof Error ? error.message : "Failed to load sessions.", true);
529
+ });
530
+ </script>
531
+ </body>
532
+ </html>
533
+ `;