pairpod-bot 0.1.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 (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +6 -0
  3. package/dist/access.d.ts +2 -0
  4. package/dist/access.js +14 -0
  5. package/dist/access.js.map +1 -0
  6. package/dist/agents.d.ts +7 -0
  7. package/dist/agents.js +10 -0
  8. package/dist/agents.js.map +1 -0
  9. package/dist/attach.d.ts +2 -0
  10. package/dist/attach.js +65 -0
  11. package/dist/attach.js.map +1 -0
  12. package/dist/bot.d.ts +1 -0
  13. package/dist/bot.js +357 -0
  14. package/dist/bot.js.map +1 -0
  15. package/dist/config.d.ts +18 -0
  16. package/dist/config.js +39 -0
  17. package/dist/config.js.map +1 -0
  18. package/dist/db.d.ts +2 -0
  19. package/dist/db.js +87 -0
  20. package/dist/db.js.map +1 -0
  21. package/dist/docker.d.ts +15 -0
  22. package/dist/docker.js +113 -0
  23. package/dist/docker.js.map +1 -0
  24. package/dist/env.d.ts +2 -0
  25. package/dist/env.js +36 -0
  26. package/dist/env.js.map +1 -0
  27. package/dist/errors.d.ts +7 -0
  28. package/dist/errors.js +25 -0
  29. package/dist/errors.js.map +1 -0
  30. package/dist/local/sessions.d.ts +5 -0
  31. package/dist/local/sessions.js +83 -0
  32. package/dist/local/sessions.js.map +1 -0
  33. package/dist/main.d.ts +1 -0
  34. package/dist/main.js +8 -0
  35. package/dist/main.js.map +1 -0
  36. package/dist/naming.d.ts +3 -0
  37. package/dist/naming.js +19 -0
  38. package/dist/naming.js.map +1 -0
  39. package/dist/network.d.ts +1 -0
  40. package/dist/network.js +11 -0
  41. package/dist/network.js.map +1 -0
  42. package/dist/notifier.d.ts +4 -0
  43. package/dist/notifier.js +47 -0
  44. package/dist/notifier.js.map +1 -0
  45. package/dist/notify.d.ts +2 -0
  46. package/dist/notify.js +19 -0
  47. package/dist/notify.js.map +1 -0
  48. package/dist/paths.d.ts +9 -0
  49. package/dist/paths.js +18 -0
  50. package/dist/paths.js.map +1 -0
  51. package/dist/routes/attach.d.ts +13 -0
  52. package/dist/routes/attach.js +49 -0
  53. package/dist/routes/attach.js.map +1 -0
  54. package/dist/server.d.ts +1 -0
  55. package/dist/server.js +51 -0
  56. package/dist/server.js.map +1 -0
  57. package/dist/ssh.d.ts +2 -0
  58. package/dist/ssh.js +84 -0
  59. package/dist/ssh.js.map +1 -0
  60. package/dist/store.d.ts +65 -0
  61. package/dist/store.js +337 -0
  62. package/dist/store.js.map +1 -0
  63. package/dist/targets/docker.d.ts +7 -0
  64. package/dist/targets/docker.js +14 -0
  65. package/dist/targets/docker.js.map +1 -0
  66. package/dist/targets/index.d.ts +4 -0
  67. package/dist/targets/index.js +33 -0
  68. package/dist/targets/index.js.map +1 -0
  69. package/dist/targets/ssh.d.ts +25 -0
  70. package/dist/targets/ssh.js +121 -0
  71. package/dist/targets/ssh.js.map +1 -0
  72. package/dist/targets/types.d.ts +15 -0
  73. package/dist/targets/types.js +2 -0
  74. package/dist/targets/types.js.map +1 -0
  75. package/dist/telegram-auth.d.ts +7 -0
  76. package/dist/telegram-auth.js +41 -0
  77. package/dist/telegram-auth.js.map +1 -0
  78. package/dist/vault.d.ts +4 -0
  79. package/dist/vault.js +57 -0
  80. package/dist/vault.js.map +1 -0
  81. package/miniapp/index.html +597 -0
  82. package/miniapp/ssh.html +251 -0
  83. package/package.json +44 -0
  84. package/scripts/fix-pty.cjs +15 -0
@@ -0,0 +1,251 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta
6
+ name="viewport"
7
+ content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"
8
+ />
9
+ <title>Add SSH endpoint</title>
10
+ <script src="https://telegram.org/js/telegram-web-app.js"></script>
11
+ <style>
12
+ * { box-sizing: border-box; }
13
+ html, body {
14
+ margin: 0;
15
+ padding: 0;
16
+ min-height: 100%;
17
+ background: #0a0a0a;
18
+ color: #e5e5e5;
19
+ font-family: -apple-system, system-ui, sans-serif;
20
+ }
21
+ #form { max-width: 560px; margin: 0 auto; padding: 16px; }
22
+ h1 { font-size: 18px; margin: 4px 0 16px; }
23
+ label { display: block; font-size: 13px; color: #9a9a9a; margin: 14px 0 5px; }
24
+ input, textarea, select {
25
+ width: 100%;
26
+ padding: 10px;
27
+ font-family: ui-monospace, Menlo, monospace;
28
+ font-size: 15px;
29
+ color: #e5e5e5;
30
+ background: #161616;
31
+ border: 1px solid #3a3a3a;
32
+ border-radius: 6px;
33
+ outline: none;
34
+ }
35
+ textarea { resize: vertical; min-height: 120px; }
36
+ .row { display: flex; gap: 10px; }
37
+ .row > div { flex: 1; }
38
+ fieldset { border: 1px solid #2a2a2a; border-radius: 6px; margin: 14px 0 0; padding: 10px 12px; }
39
+ fieldset legend { font-size: 13px; color: #9a9a9a; padding: 0 6px; }
40
+ .opt { display: flex; align-items: flex-start; gap: 8px; margin: 8px 0; }
41
+ .opt input { width: auto; margin-top: 3px; }
42
+ .opt span { font-size: 14px; }
43
+ .opt small { display: block; color: #777; font-size: 12px; }
44
+ .note { color: #777; font-size: 12px; margin: 6px 0 0; line-height: 1.4; }
45
+ .note code { color: #9a9; }
46
+ .hidden { display: none; }
47
+ #submit {
48
+ width: 100%;
49
+ margin-top: 20px;
50
+ padding: 12px;
51
+ font-size: 15px;
52
+ font-weight: 600;
53
+ color: #0a0a0a;
54
+ background: #e5e5e5;
55
+ border: none;
56
+ border-radius: 6px;
57
+ }
58
+ #submit:disabled { opacity: 0.5; }
59
+ #msg { margin-top: 14px; font-size: 14px; min-height: 18px; white-space: pre-wrap; }
60
+ #msg.err { color: #f08a8a; }
61
+ #msg.ok { color: #8af0a0; }
62
+ </style>
63
+ </head>
64
+ <body>
65
+ <div id="form">
66
+ <h1 id="title">Add SSH endpoint</h1>
67
+
68
+ <label for="label">Name</label>
69
+ <input id="label" autocapitalize="off" autocorrect="off" spellcheck="false" placeholder="prod db, staging…" />
70
+
71
+ <label for="host">Host</label>
72
+ <input id="host" inputmode="url" autocapitalize="off" autocorrect="off" spellcheck="false" placeholder="example.com" />
73
+
74
+ <div class="row">
75
+ <div>
76
+ <label for="port">Port</label>
77
+ <input id="port" inputmode="numeric" value="22" />
78
+ </div>
79
+ <div>
80
+ <label for="username">Username</label>
81
+ <input id="username" autocapitalize="off" autocorrect="off" spellcheck="false" placeholder="ubuntu" />
82
+ </div>
83
+ </div>
84
+
85
+ <label for="remoteCwd">Remote working directory</label>
86
+ <input id="remoteCwd" autocapitalize="off" autocorrect="off" spellcheck="false" value="~" />
87
+
88
+ <fieldset>
89
+ <legend>Auth method</legend>
90
+ <label class="opt">
91
+ <input type="radio" name="auth" value="agent" checked />
92
+ <span>ssh-agent on bot host<small>Uses SSH_AUTH_SOCK. Best for passphrase-protected keys.</small></span>
93
+ </label>
94
+ <label class="opt">
95
+ <input type="radio" name="auth" value="key_path" />
96
+ <span>Key file path on bot host<small>Key already lives on the bot machine.</small></span>
97
+ </label>
98
+ <label class="opt" id="opt-vault">
99
+ <input type="radio" name="auth" value="vault" />
100
+ <span>Paste private key<small>Encrypted at rest (AES-256-GCM) via the vault.</small></span>
101
+ </label>
102
+ </fieldset>
103
+
104
+ <div id="f-keypath" class="hidden">
105
+ <label for="keyPath">Private key path (on bot host)</label>
106
+ <input id="keyPath" autocapitalize="off" autocorrect="off" spellcheck="false" placeholder="/home/bot/.ssh/id_ed25519" />
107
+ </div>
108
+
109
+ <div id="f-vault" class="hidden">
110
+ <label for="privateKey">Private key (PEM / OpenSSH)</label>
111
+ <textarea id="privateKey" autocapitalize="off" autocorrect="off" spellcheck="false" placeholder="-----BEGIN OPENSSH PRIVATE KEY-----"></textarea>
112
+ </div>
113
+
114
+ <div id="f-pass" class="hidden">
115
+ <label for="passphrase">Key passphrase</label>
116
+ <input id="passphrase" type="password" autocapitalize="off" autocorrect="off" spellcheck="false" placeholder="only if the key is encrypted" />
117
+ <p class="note">Stored encrypted in the vault (needs <code>PAIRPOD_VAULT_KEY</code>). For passphrase-protected keys, <b>ssh-agent</b> is safer — it stores nothing.</p>
118
+ </div>
119
+
120
+ <button id="submit">Test &amp; add endpoint</button>
121
+ <div id="msg"></div>
122
+ </div>
123
+
124
+ <script>
125
+ const tg = window.Telegram && window.Telegram.WebApp;
126
+ if (tg) {
127
+ tg.ready();
128
+ tg.expand();
129
+ }
130
+
131
+ const $ = (id) => document.getElementById(id);
132
+ const msg = $("msg");
133
+ const initData = (tg && tg.initData) || "";
134
+ const editId = new URLSearchParams(location.search).get("id");
135
+ let endpointHasKey = false;
136
+
137
+ function authMethod() {
138
+ const el = document.querySelector('input[name="auth"]:checked');
139
+ return el ? el.value : "agent";
140
+ }
141
+
142
+ function syncFields() {
143
+ const m = authMethod();
144
+ $("f-keypath").classList.toggle("hidden", m !== "key_path");
145
+ $("f-vault").classList.toggle("hidden", m !== "vault");
146
+ $("f-pass").classList.toggle("hidden", m !== "key_path" && m !== "vault");
147
+ $("privateKey").placeholder =
148
+ m === "vault" && endpointHasKey
149
+ ? "leave blank to keep the current key"
150
+ : "-----BEGIN OPENSSH PRIVATE KEY-----";
151
+ $("passphrase").placeholder =
152
+ editId && endpointHasKey ? "leave blank to keep stored secret" : "only if the key is encrypted";
153
+ }
154
+
155
+ for (const el of document.querySelectorAll('input[name="auth"]')) {
156
+ el.addEventListener("change", syncFields);
157
+ }
158
+
159
+ fetch("/ssh/config")
160
+ .then((r) => r.json())
161
+ .then((c) => {
162
+ if (!c.vaultEnabled) $("opt-vault").classList.add("hidden");
163
+ })
164
+ .catch(() => {});
165
+
166
+ function setMsg(text, kind) {
167
+ msg.textContent = text;
168
+ msg.className = kind || "";
169
+ }
170
+
171
+ function selectAuth(method) {
172
+ const el = document.querySelector('input[name="auth"][value="' + method + '"]');
173
+ if (el) el.checked = true;
174
+ }
175
+
176
+ async function loadForEdit() {
177
+ $("title").textContent = "Edit SSH endpoint";
178
+ $("submit").textContent = "Test & save";
179
+ try {
180
+ const r = await fetch("/ssh/endpoints/" + encodeURIComponent(editId) + "?tgData=" + encodeURIComponent(initData));
181
+ const data = await r.json();
182
+ if (!data.ok) {
183
+ setMsg(data.error || "Could not load endpoint.", "err");
184
+ return;
185
+ }
186
+ const e = data.endpoint;
187
+ $("label").value = e.label || "";
188
+ $("host").value = e.host || "";
189
+ $("port").value = e.port || 22;
190
+ $("username").value = e.username || "";
191
+ $("remoteCwd").value = e.remoteCwd || "~";
192
+ $("keyPath").value = e.keyPath || "";
193
+ endpointHasKey = !!e.hasKey;
194
+ if (e.auth) selectAuth(e.auth);
195
+ } catch (err) {
196
+ setMsg("Could not load endpoint: " + err.message, "err");
197
+ } finally {
198
+ syncFields();
199
+ }
200
+ }
201
+
202
+ if (editId) loadForEdit();
203
+ else syncFields();
204
+
205
+ $("submit").addEventListener("click", async () => {
206
+ const auth = authMethod();
207
+ const privateKey = auth === "vault" ? $("privateKey").value : "";
208
+ const body = {
209
+ label: $("label").value.trim(),
210
+ host: $("host").value.trim(),
211
+ port: $("port").value.trim() || "22",
212
+ username: $("username").value.trim(),
213
+ remoteCwd: $("remoteCwd").value.trim() || "~",
214
+ auth,
215
+ keyPath: auth === "key_path" ? $("keyPath").value.trim() : "",
216
+ privateKey,
217
+ passphrase: auth === "vault" || auth === "key_path" ? $("passphrase").value : "",
218
+ tgData: initData,
219
+ };
220
+ if (!body.host || !body.username) {
221
+ setMsg("Host and username are required.", "err");
222
+ return;
223
+ }
224
+ if (auth === "vault" && !privateKey && !(editId && endpointHasKey)) {
225
+ setMsg("Paste a private key.", "err");
226
+ return;
227
+ }
228
+ $("submit").disabled = true;
229
+ setMsg("Connecting…", "");
230
+ try {
231
+ const r = await fetch(editId ? "/ssh/endpoints/" + encodeURIComponent(editId) : "/ssh/endpoints", {
232
+ method: editId ? "PUT" : "POST",
233
+ headers: { "Content-Type": "application/json" },
234
+ body: JSON.stringify(body),
235
+ });
236
+ const data = await r.json();
237
+ if (data.ok) {
238
+ setMsg((editId ? "Saved " : "Added ") + data.id + ". Open /ssh or /pods in the bot.", "ok");
239
+ setTimeout(() => tg && tg.close(), 1200);
240
+ } else {
241
+ setMsg(data.error || "Failed to save endpoint.", "err");
242
+ $("submit").disabled = false;
243
+ }
244
+ } catch (e) {
245
+ setMsg("Request failed: " + e.message, "err");
246
+ $("submit").disabled = false;
247
+ }
248
+ });
249
+ </script>
250
+ </body>
251
+ </html>
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "pairpod-bot",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "description": "pairpod bot + mini-app server (installed as a dependency of the pairpod CLI).",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/solidquant/pairpod.git",
10
+ "directory": "packages/bot"
11
+ },
12
+ "engines": {
13
+ "node": ">=22"
14
+ },
15
+ "main": "dist/main.js",
16
+ "files": [
17
+ "dist",
18
+ "miniapp",
19
+ "scripts/fix-pty.cjs"
20
+ ],
21
+ "dependencies": {
22
+ "@fastify/static": "^8.0.0",
23
+ "@fastify/websocket": "^11.0.0",
24
+ "@grammyjs/runner": "^2.0.3",
25
+ "better-sqlite3": "^12.0.0",
26
+ "dockerode": "^4.0.2",
27
+ "fastify": "^5.0.0",
28
+ "grammy": "^1.30.0",
29
+ "node-pty": "^1.1.0",
30
+ "pino": "^9.0.0",
31
+ "pino-pretty": "^11.0.0",
32
+ "ssh2": "^1.17.0",
33
+ "zod": "^3.22.4"
34
+ },
35
+ "devDependencies": {
36
+ "@types/better-sqlite3": "^7.6.8",
37
+ "@types/dockerode": "^3.3.28",
38
+ "@types/node": "^22.0.0",
39
+ "@types/ssh2": "^1.15.5"
40
+ },
41
+ "scripts": {
42
+ "postinstall": "node scripts/fix-pty.cjs"
43
+ }
44
+ }
@@ -0,0 +1,15 @@
1
+ // node-pty ships a `spawn-helper` binary in its prebuilds; some package managers
2
+ // drop its execute bit on extraction, which makes Host PTY sessions fail with
3
+ // "posix_spawnp failed". Re-apply +x wherever node-pty actually landed.
4
+ const fs = require("node:fs");
5
+ const path = require("node:path");
6
+
7
+ try {
8
+ const prebuilds = path.join(path.dirname(require.resolve("node-pty/package.json")), "prebuilds");
9
+ for (const dir of fs.readdirSync(prebuilds)) {
10
+ const helper = path.join(prebuilds, dir, "spawn-helper");
11
+ if (fs.existsSync(helper)) fs.chmodSync(helper, 0o755);
12
+ }
13
+ } catch {
14
+ // node-pty not resolvable / not installed yet — nothing to fix.
15
+ }