pairpod 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.
- package/LICENSE +21 -0
- package/README.md +131 -0
- package/dist/env.js +37 -0
- package/dist/index.js +32 -0
- package/dist/onboard.js +45 -0
- package/dist/start.js +95 -0
- package/package.json +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 solidquant
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# pairpod
|
|
2
|
+
|
|
3
|
+
Run terminals and Claude Code sessions from your phone, through a Telegram bot.
|
|
4
|
+
|
|
5
|
+
Point pairpod at a backend (a Docker container, an SSH host, or the machine the bot runs on) and it gives you a real terminal inside a Telegram mini app. Start a plain shell or a Claude Code session, attach from anywhere, scroll, copy, pinch to zoom. It's xterm over a WebSocket, so it behaves the way you'd expect.
|
|
6
|
+
|
|
7
|
+
## Concepts
|
|
8
|
+
|
|
9
|
+
A **pod** is a backend. A **session** is a terminal running on it.
|
|
10
|
+
|
|
11
|
+
Pods come in three kinds:
|
|
12
|
+
|
|
13
|
+
| | |
|
|
14
|
+
|---|---|
|
|
15
|
+
| π³ Docker | a throwaway, isolated container on the bot host |
|
|
16
|
+
| π SSH | a remote machine reached over SSH |
|
|
17
|
+
| π» Host | a shell on the bot machine itself |
|
|
18
|
+
|
|
19
|
+
Sessions come in three modes:
|
|
20
|
+
|
|
21
|
+
| | |
|
|
22
|
+
|---|---|
|
|
23
|
+
| terminal | a plain shell |
|
|
24
|
+
| regular | `claude` β you answer the permission prompts |
|
|
25
|
+
| skip-perms | `claude --dangerously-skip-permissions` |
|
|
26
|
+
|
|
27
|
+
Docker and SSH pods run all three modes; Host pods are terminal-only. You can name pods and sessions, which keeps a list of `pod-7` / `claude-3` readable once you have a few.
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
You'll need Node 22, pnpm, and [cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/) (`brew install cloudflared`). Docker is optional; only Docker pods touch it.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
nvm use
|
|
35
|
+
pnpm install
|
|
36
|
+
pnpm onboard # asks for a bot token, allowed users, and a port
|
|
37
|
+
pnpm start # opens the tunnel and starts the bot together
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`onboard` points you at [@BotFather](https://t.me/BotFather) to create a bot, then writes everything to `~/.pairpod/.env` and generates a vault key for you. `start` brings up a cloudflared tunnel, points the bot at its URL, and runs both. One command, and Ctrl-C stops everything.
|
|
41
|
+
|
|
42
|
+
Then message your bot `/pods`, create one, and tap βΆ to open a terminal.
|
|
43
|
+
|
|
44
|
+
## Where state lives
|
|
45
|
+
|
|
46
|
+
Everything host-specific sits under `~/.pairpod/` (set `PAIRPOD_HOME` to move it):
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
~/.pairpod/
|
|
50
|
+
.env config
|
|
51
|
+
pairpod.db pods and sessions
|
|
52
|
+
vault/ encrypted SSH credentials
|
|
53
|
+
workspaces/ Docker pod working dirs
|
|
54
|
+
notify-chats.json who gets permission pings
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Nothing gets written into the repo.
|
|
58
|
+
|
|
59
|
+
## Layout
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
packages/
|
|
63
|
+
bot/ the Fastify server, grammy bot, and the mini app
|
|
64
|
+
cli/ the `pairpod` command
|
|
65
|
+
docker/ the image Docker pods run
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
It's a pnpm workspace. `packages/bot` is the whole app: the server, the bot, the pod/session store, the Docker/SSH/PTY backends, and the vault. `packages/cli` is the small `pairpod` command that onboards and launches it.
|
|
69
|
+
|
|
70
|
+
## Commands
|
|
71
|
+
|
|
72
|
+
In Telegram:
|
|
73
|
+
|
|
74
|
+
- `/pods` β your pods; create, rename, delete, open sessions
|
|
75
|
+
- `/sessions` β every session with an open button
|
|
76
|
+
- `/ssh` β add, test, edit, or remove SSH hosts
|
|
77
|
+
- `/whoami` β your id and username, for locking down the allowlist
|
|
78
|
+
|
|
79
|
+
On the command line (`pairpod <cmd>` once installed, or `pnpm <cmd>` in the repo):
|
|
80
|
+
|
|
81
|
+
- `onboard` β write `~/.pairpod/.env`
|
|
82
|
+
- `start` β tunnel plus bot
|
|
83
|
+
- `start --no-tunnel` β bot only, using a `MINIAPP_URL` you set yourself
|
|
84
|
+
- `dev` β hot-reload the bot, no tunnel (for development)
|
|
85
|
+
- `build` β compile both packages to `dist/`
|
|
86
|
+
|
|
87
|
+
## SSH hosts
|
|
88
|
+
|
|
89
|
+
Add one from `/ssh` β Add SSH endpoint, or while creating a pod. The form opens in the mini app, so the secret travels over HTTPS rather than through a chat message. Three ways to authenticate:
|
|
90
|
+
|
|
91
|
+
| Method | What's stored | Use it when |
|
|
92
|
+
|---|---|---|
|
|
93
|
+
| ssh-agent | nothing | the key's already loaded in your agent on the bot host; best for passphrase-protected keys |
|
|
94
|
+
| key file | just the path | the key file lives on the bot host and never leaves it |
|
|
95
|
+
| paste key | the key, encrypted | there's no file on the host; needs the vault |
|
|
96
|
+
|
|
97
|
+
Anything you paste (a key, a passphrase) is encrypted with AES-256-GCM. The master key comes from `PAIRPOD_VAULT_KEY` and only lives in memory, never on disk beside the ciphertext and never in the database. `onboard` generates it.
|
|
98
|
+
|
|
99
|
+
Running Claude on an SSH host needs `claude` installed and logged in over there (open a terminal session and run it once), plus a reachable `MINIAPP_URL`/`PAIRPOD_PUBLIC_URL` so permission prompts can notify you. The first connection to a host pins its key fingerprint and verifies it every time after.
|
|
100
|
+
|
|
101
|
+
## Worth knowing
|
|
102
|
+
|
|
103
|
+
A cloudflared quick tunnel terminates TLS at Cloudflare's edge, so a pasted key or passphrase is briefly in the clear there. If that bothers you, use ssh-agent (nothing leaves the host) or a fixed named tunnel.
|
|
104
|
+
|
|
105
|
+
Docker pods are isolated. SSH pods are a separate machine. A Host pod, though, is an un-sandboxed shell on the bot machine (vault key in memory included), so only enable it somewhere you're comfortable handing out that access. Either way the bot only answers people on your allowlist, and the mini app checks Telegram's signed `initData` on every connection.
|
|
106
|
+
|
|
107
|
+
## Config keys
|
|
108
|
+
|
|
109
|
+
Most people never touch these directly; `onboard` writes the ones that matter. Full list in `.env.example`.
|
|
110
|
+
|
|
111
|
+
| Key | What it does |
|
|
112
|
+
|---|---|
|
|
113
|
+
| `TELEGRAM_BOT_TOKEN` | the bot token |
|
|
114
|
+
| `TELEGRAM_ALLOWED_USERNAMES` / `..._USER_IDS` | who's allowed in (empty means anyone) |
|
|
115
|
+
| `PORT` | server port (default 40002) |
|
|
116
|
+
| `MINIAPP_URL` | public origin for the mini app; `start` fills this from the tunnel |
|
|
117
|
+
| `PAIRPOD_PUBLIC_URL` | where SSH Claude sessions send notifications; defaults to `MINIAPP_URL` |
|
|
118
|
+
| `PAIRPOD_VAULT_KEY` | vault master key |
|
|
119
|
+
| `PAIRPOD_HOME` | where state lives (default `~/.pairpod`) |
|
|
120
|
+
|
|
121
|
+
## Troubleshooting
|
|
122
|
+
|
|
123
|
+
- `ERR_DLOPEN_FAILED` or a `NODE_MODULE_VERSION` mismatch means you're not on Node 22. Run `nvm use`.
|
|
124
|
+
- `posix_spawnp failed` on a Host session is node-pty's helper losing its execute bit. `pnpm install` re-fixes it (a postinstall handles it).
|
|
125
|
+
- If the mini app looks stale after an update, Telegram has cached it. Fully close and reopen the web app.
|
|
126
|
+
- `could not run cloudflared`: install it, or run `pairpod start --no-tunnel` with your own URL.
|
|
127
|
+
- Old "Open" buttons that 404: Telegram bakes the URL into a message when it's sent, so after a new tunnel URL just re-run `/pods` to get fresh buttons.
|
|
128
|
+
|
|
129
|
+
## License
|
|
130
|
+
|
|
131
|
+
MIT
|
package/dist/env.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
export const PAIRPOD_HOME = process.env.PAIRPOD_HOME
|
|
5
|
+
? path.resolve(process.env.PAIRPOD_HOME)
|
|
6
|
+
: path.join(os.homedir(), ".pairpod");
|
|
7
|
+
export const ENV_PATH = path.join(PAIRPOD_HOME, ".env");
|
|
8
|
+
export function readEnv() {
|
|
9
|
+
const out = {};
|
|
10
|
+
let text = "";
|
|
11
|
+
try {
|
|
12
|
+
text = fs.readFileSync(ENV_PATH, "utf8");
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
for (const line of text.split("\n")) {
|
|
18
|
+
const t = line.trim();
|
|
19
|
+
if (!t || t.startsWith("#"))
|
|
20
|
+
continue;
|
|
21
|
+
const eq = t.indexOf("=");
|
|
22
|
+
if (eq === -1)
|
|
23
|
+
continue;
|
|
24
|
+
out[t.slice(0, eq).trim()] = t.slice(eq + 1).trim();
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
// Write the given keys, preserving any other existing ones. Empty values are dropped.
|
|
29
|
+
export function writeEnv(vars) {
|
|
30
|
+
fs.mkdirSync(PAIRPOD_HOME, { recursive: true });
|
|
31
|
+
const merged = { ...readEnv(), ...vars };
|
|
32
|
+
const body = Object.entries(merged)
|
|
33
|
+
.filter(([, v]) => v !== undefined && v !== "")
|
|
34
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
35
|
+
.join("\n");
|
|
36
|
+
fs.writeFileSync(ENV_PATH, body + "\n", { mode: 0o600 });
|
|
37
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { onboard } from "./onboard.js";
|
|
3
|
+
import { start } from "./start.js";
|
|
4
|
+
const HELP = `pairpod β run terminals & Claude sessions from Telegram
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
pairpod onboard Configure the bot (token, allowlist, port, vault key)
|
|
8
|
+
pairpod start Start the tunnel + bot (one command)
|
|
9
|
+
pairpod start --no-tunnel Start the bot only (use the existing MINIAPP_URL)
|
|
10
|
+
|
|
11
|
+
Config lives in ~/.pairpod/ (override with PAIRPOD_HOME).`;
|
|
12
|
+
async function main() {
|
|
13
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
14
|
+
switch (cmd) {
|
|
15
|
+
case "onboard":
|
|
16
|
+
await onboard();
|
|
17
|
+
break;
|
|
18
|
+
case "start":
|
|
19
|
+
await start(!rest.includes("--no-tunnel"));
|
|
20
|
+
break;
|
|
21
|
+
case undefined:
|
|
22
|
+
case "help":
|
|
23
|
+
case "--help":
|
|
24
|
+
case "-h":
|
|
25
|
+
console.log(HELP);
|
|
26
|
+
break;
|
|
27
|
+
default:
|
|
28
|
+
console.error(`Unknown command: ${cmd}\n\n${HELP}`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
main();
|
package/dist/onboard.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import * as p from "@clack/prompts";
|
|
3
|
+
import { readEnv, writeEnv, ENV_PATH } from "./env.js";
|
|
4
|
+
function cancelled(v) {
|
|
5
|
+
if (p.isCancel(v)) {
|
|
6
|
+
p.cancel("Onboarding aborted.");
|
|
7
|
+
process.exit(0);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export async function onboard() {
|
|
11
|
+
p.intro("pairpod onboard");
|
|
12
|
+
const existing = readEnv();
|
|
13
|
+
p.note("1. Open @BotFather in Telegram (https://t.me/BotFather)\n2. Send /newbot and follow the prompts\n3. Copy the token it gives you", "Create a Telegram bot");
|
|
14
|
+
const token = await p.password({
|
|
15
|
+
message: "Telegram bot token",
|
|
16
|
+
validate: (v) => (v && v.includes(":") ? undefined : "Looks off β should look like 123456:AA..."),
|
|
17
|
+
});
|
|
18
|
+
cancelled(token);
|
|
19
|
+
const usernames = await p.text({
|
|
20
|
+
message: "Allowed Telegram @usernames (comma-separated; blank = anyone)",
|
|
21
|
+
placeholder: "alice,bob",
|
|
22
|
+
initialValue: existing.TELEGRAM_ALLOWED_USERNAMES ?? "",
|
|
23
|
+
defaultValue: "",
|
|
24
|
+
});
|
|
25
|
+
cancelled(usernames);
|
|
26
|
+
const port = await p.text({
|
|
27
|
+
message: "Port for the bot/mini-app server",
|
|
28
|
+
initialValue: existing.PORT ?? "40002",
|
|
29
|
+
validate: (v) => (/^\d+$/.test(v) ? undefined : "Must be a number"),
|
|
30
|
+
});
|
|
31
|
+
cancelled(port);
|
|
32
|
+
const hadKey = Boolean(existing.PAIRPOD_VAULT_KEY);
|
|
33
|
+
const vaultKey = existing.PAIRPOD_VAULT_KEY || crypto.randomBytes(32).toString("base64");
|
|
34
|
+
const s = p.spinner();
|
|
35
|
+
s.start("Writing config");
|
|
36
|
+
writeEnv({
|
|
37
|
+
TELEGRAM_BOT_TOKEN: token,
|
|
38
|
+
TELEGRAM_ALLOWED_USERNAMES: usernames.trim(),
|
|
39
|
+
PORT: port,
|
|
40
|
+
PAIRPOD_VAULT_KEY: vaultKey,
|
|
41
|
+
});
|
|
42
|
+
s.stop(`Wrote ${ENV_PATH}`);
|
|
43
|
+
p.note(`${hadKey ? "Kept existing" : "Generated a new"} vault key (AES-256-GCM, stored in your .env).`, "Credential vault");
|
|
44
|
+
p.outro("Done. Start it with: pairpod start");
|
|
45
|
+
}
|
package/dist/start.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { readEnv } from "./env.js";
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
function resolveBotEntry() {
|
|
8
|
+
const botDir = path.dirname(require.resolve("pairpod-bot/package.json"));
|
|
9
|
+
const distMain = path.join(botDir, "dist", "main.js");
|
|
10
|
+
if (fs.existsSync(distMain))
|
|
11
|
+
return { cmd: process.execPath, args: [distMain] };
|
|
12
|
+
return { cmd: "npx", args: ["tsx", path.join(botDir, "src", "main.ts")] };
|
|
13
|
+
}
|
|
14
|
+
function startTunnel(port) {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
const cf = spawn("cloudflared", ["tunnel", "--url", `http://localhost:${port}`], {
|
|
17
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
18
|
+
});
|
|
19
|
+
let done = false;
|
|
20
|
+
const scan = (buf) => {
|
|
21
|
+
const m = String(buf).match(/https:\/\/[a-zA-Z0-9.-]+\.trycloudflare\.com/);
|
|
22
|
+
if (m && !done) {
|
|
23
|
+
done = true;
|
|
24
|
+
resolve({ url: m[0], child: cf });
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
cf.stdout?.on("data", scan);
|
|
28
|
+
cf.stderr?.on("data", scan);
|
|
29
|
+
cf.on("error", (e) => {
|
|
30
|
+
if (!done) {
|
|
31
|
+
done = true;
|
|
32
|
+
reject(new Error(`could not run cloudflared (${e.message}). Install it: brew install cloudflared`));
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
cf.on("exit", (code) => {
|
|
36
|
+
if (!done) {
|
|
37
|
+
done = true;
|
|
38
|
+
reject(new Error(`cloudflared exited (code ${code}) before producing a URL`));
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
setTimeout(() => {
|
|
42
|
+
if (!done) {
|
|
43
|
+
done = true;
|
|
44
|
+
reject(new Error("timed out waiting for the tunnel URL"));
|
|
45
|
+
}
|
|
46
|
+
}, 30000);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
export async function start(useTunnel) {
|
|
50
|
+
const env = { ...process.env, ...readEnv() };
|
|
51
|
+
if (!env.TELEGRAM_BOT_TOKEN) {
|
|
52
|
+
console.error("Not configured yet. Run: pairpod onboard");
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
const port = env.PORT || "40002";
|
|
56
|
+
const children = [];
|
|
57
|
+
let shuttingDown = false;
|
|
58
|
+
const shutdown = () => {
|
|
59
|
+
if (shuttingDown)
|
|
60
|
+
return;
|
|
61
|
+
shuttingDown = true;
|
|
62
|
+
for (const c of children) {
|
|
63
|
+
try {
|
|
64
|
+
c.kill();
|
|
65
|
+
}
|
|
66
|
+
catch { }
|
|
67
|
+
}
|
|
68
|
+
process.exit(0);
|
|
69
|
+
};
|
|
70
|
+
process.on("SIGINT", shutdown);
|
|
71
|
+
process.on("SIGTERM", shutdown);
|
|
72
|
+
if (useTunnel) {
|
|
73
|
+
process.stdout.write("Starting tunnel⦠");
|
|
74
|
+
try {
|
|
75
|
+
const { url, child } = await startTunnel(port);
|
|
76
|
+
children.push(child);
|
|
77
|
+
env.MINIAPP_URL = url;
|
|
78
|
+
console.log(url);
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
console.log("failed");
|
|
82
|
+
console.error(` ${e.message}`);
|
|
83
|
+
console.error(" Continuing without a tunnel β mini-app buttons will be disabled.");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const entry = resolveBotEntry();
|
|
87
|
+
const bot = spawn(entry.cmd, entry.args, { env, stdio: "inherit" });
|
|
88
|
+
children.push(bot);
|
|
89
|
+
bot.on("exit", (code) => {
|
|
90
|
+
if (!shuttingDown) {
|
|
91
|
+
console.error(`bot exited (code ${code ?? "?"})`);
|
|
92
|
+
shutdown();
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pairpod",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"description": "Run terminals and Claude Code sessions from your phone via a Telegram bot, on Docker, SSH, or host backends.",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/solidquant/pairpod.git",
|
|
10
|
+
"directory": "packages/cli"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"telegram-bot",
|
|
14
|
+
"claude-code",
|
|
15
|
+
"claude",
|
|
16
|
+
"terminal",
|
|
17
|
+
"web-terminal",
|
|
18
|
+
"ssh",
|
|
19
|
+
"docker",
|
|
20
|
+
"tmux",
|
|
21
|
+
"pty",
|
|
22
|
+
"mobile",
|
|
23
|
+
"self-hosted",
|
|
24
|
+
"cli"
|
|
25
|
+
],
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=22"
|
|
28
|
+
},
|
|
29
|
+
"bin": {
|
|
30
|
+
"pairpod": "./dist/index.js"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist"
|
|
34
|
+
],
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@clack/prompts": "^0.7.0",
|
|
37
|
+
"pairpod-bot": "0.1.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^22.0.0"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {}
|
|
43
|
+
}
|