poke-gate 0.0.1

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 ADDED
@@ -0,0 +1,107 @@
1
+ # poke-gate
2
+
3
+ Expose your machine to your [Poke](https://poke.com) AI assistant via MCP tunnel.
4
+
5
+ Run `poke-gate` on your machine, then ask Poke from iMessage, Telegram, or SMS to run commands, read files, check system status — anything you'd do in a terminal.
6
+
7
+ ## Quick start
8
+
9
+ ```bash
10
+ npx poke-gate
11
+ ```
12
+
13
+ On first run, you'll paste your API key from [poke.com/kitchen/api-keys](https://poke.com/kitchen/api-keys).
14
+
15
+ ## How it works
16
+
17
+ ```mermaid
18
+ flowchart TD
19
+ A["You message Poke\nfrom iMessage / Telegram / SMS"] --> B["Poke Agent"]
20
+ B --> C["Agent calls MCP tool"]
21
+ C --> D["MCP Tunnel (WebSocket)"]
22
+ D --> E["poke-gate on your machine"]
23
+ E --> F["Runs command / reads file / etc."]
24
+ F --> D
25
+ D --> B
26
+ B --> A
27
+ ```
28
+
29
+ poke-gate runs a local MCP server and tunnels it to Poke's cloud. When you ask Poke something that needs your machine, the agent calls the tools, poke-gate executes them locally, and the result flows back to your chat.
30
+
31
+ ## Tools
32
+
33
+ | Tool | Description |
34
+ |------|-------------|
35
+ | `run_command` | Execute any shell command (ls, git, brew, python, curl, etc.) |
36
+ | `read_file` | Read a file's contents |
37
+ | `write_file` | Write content to a file |
38
+ | `list_directory` | List files and directories |
39
+ | `system_info` | OS, hostname, architecture, uptime, memory |
40
+
41
+ ## Examples
42
+
43
+ From iMessage/Telegram, ask Poke:
44
+
45
+ - "What's running on port 3000?"
46
+ - "Show me the last 5 git commits in my project"
47
+ - "How much disk space do I have left?"
48
+ - "Read my ~/.zshrc and suggest improvements"
49
+ - "Create a new file called notes.txt with today's meeting notes"
50
+ - "Run the tests in my project"
51
+
52
+ ## Setup
53
+
54
+ ### Option 1: Interactive (recommended)
55
+
56
+ ```bash
57
+ npx poke-gate
58
+ ```
59
+
60
+ ### Option 2: Environment variable
61
+
62
+ ```bash
63
+ export POKE_API_KEY=your_key_here
64
+ npx poke-gate
65
+ ```
66
+
67
+ ## Security
68
+
69
+ **poke-gate grants full shell access to your Poke agent.** This means:
70
+
71
+ - Any command can be run with your user's permissions
72
+ - Files can be read and written anywhere your user has access
73
+ - Only your Poke agent (authenticated by your API key) can reach the tunnel
74
+
75
+ Only run poke-gate on machines and networks you trust. Stop it with `Ctrl-C` when you don't need it.
76
+
77
+ ## Configuration
78
+
79
+ Config is stored at `~/.config/poke-gate/config.json`:
80
+
81
+ ```json
82
+ {
83
+ "apiKey": "your_key_here"
84
+ }
85
+ ```
86
+
87
+ To reset, delete the file and run `npx poke-gate` again.
88
+
89
+ ## Project structure
90
+
91
+ ```
92
+ bin/
93
+ poke-gate.js Entry point, onboarding
94
+ src/
95
+ app.js Startup: MCP server + tunnel
96
+ mcp-server.js JSON-RPC MCP handler with OS tools
97
+ tunnel.js PokeTunnel wrapper
98
+ ```
99
+
100
+ ## Credits
101
+
102
+ - [Poke](https://poke.com) by [The Interaction Company of California](https://interaction.co)
103
+ - [Poke SDK](https://www.npmjs.com/package/poke)
104
+
105
+ ## License
106
+
107
+ MIT
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { homedir } from "node:os";
6
+ import { createInterface } from "node:readline";
7
+
8
+ const CONFIG_DIR = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
9
+ const CONFIG_PATH = join(CONFIG_DIR, "poke-gate", "config.json");
10
+
11
+ function loadConfig() {
12
+ try {
13
+ return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
14
+ } catch {
15
+ return {};
16
+ }
17
+ }
18
+
19
+ function saveConfig(config) {
20
+ mkdirSync(join(CONFIG_DIR, "poke-gate"), { recursive: true });
21
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
22
+ }
23
+
24
+ function resolveToken() {
25
+ if (process.env.POKE_API_KEY) return process.env.POKE_API_KEY;
26
+ const config = loadConfig();
27
+ if (config.apiKey) return config.apiKey;
28
+ return null;
29
+ }
30
+
31
+ function ask(question) {
32
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
33
+ return new Promise((resolve) => {
34
+ rl.question(question, (answer) => {
35
+ rl.close();
36
+ resolve(answer.trim());
37
+ });
38
+ });
39
+ }
40
+
41
+ async function onboarding() {
42
+ console.log();
43
+ console.log(" poke-gate — expose your machine to Poke");
44
+ console.log();
45
+ console.log(" Your Poke agent will be able to run commands,");
46
+ console.log(" read/write files, and access system info on this machine.");
47
+ console.log();
48
+ console.log(" ⚠ This grants full shell access. Only run on trusted networks.");
49
+ console.log();
50
+ console.log(" To get started, you need a Poke API key:");
51
+ console.log();
52
+ console.log(" 1. Go to https://poke.com/kitchen/api-keys");
53
+ console.log(" 2. Generate a new key");
54
+ console.log(" 3. Paste it below");
55
+ console.log();
56
+
57
+ const key = await ask(" API key: ");
58
+
59
+ if (!key) {
60
+ console.log();
61
+ console.log(" No key provided. Set it later:");
62
+ console.log(" export POKE_API_KEY=your_key_here");
63
+ console.log();
64
+ process.exit(1);
65
+ }
66
+
67
+ saveConfig({ apiKey: key });
68
+ console.log();
69
+ console.log(" Saved! Starting poke-gate...");
70
+ console.log();
71
+
72
+ return key;
73
+ }
74
+
75
+ async function main() {
76
+ let token = resolveToken();
77
+
78
+ if (!token) {
79
+ token = await onboarding();
80
+ }
81
+
82
+ process.env.POKE_API_KEY = token;
83
+ await import("../src/app.js");
84
+ }
85
+
86
+ main();
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "poke-gate",
3
+ "version": "0.0.1",
4
+ "description": "Expose your machine to your Poke AI assistant via MCP tunnel",
5
+ "type": "module",
6
+ "bin": {
7
+ "poke-gate": "./bin/poke-gate.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node src/app.js"
11
+ },
12
+ "keywords": [
13
+ "poke",
14
+ "mcp",
15
+ "tunnel",
16
+ "shell",
17
+ "terminal",
18
+ "ai",
19
+ "cli",
20
+ "remote",
21
+ "os"
22
+ ],
23
+ "author": "",
24
+ "license": "MIT",
25
+ "engines": {
26
+ "node": ">=18"
27
+ },
28
+ "dependencies": {
29
+ "poke": "^0.4.2"
30
+ }
31
+ }
package/src/app.js ADDED
@@ -0,0 +1,82 @@
1
+ import { startMcpServer } from "./mcp-server.js";
2
+ import { startTunnel } from "./tunnel.js";
3
+ import { readFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { homedir } from "node:os";
6
+
7
+ function resolveToken() {
8
+ if (process.env.POKE_API_KEY) return process.env.POKE_API_KEY;
9
+
10
+ const configDir = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
11
+
12
+ try {
13
+ const cfg = JSON.parse(readFileSync(join(configDir, "poke-gate", "config.json"), "utf-8"));
14
+ if (cfg.apiKey) return cfg.apiKey;
15
+ } catch {}
16
+
17
+ return null;
18
+ }
19
+
20
+ function log(msg) {
21
+ const ts = new Date().toISOString().slice(11, 19);
22
+ console.log(`[${ts}] ${msg}`);
23
+ }
24
+
25
+ const API_KEY = resolveToken();
26
+
27
+ if (!API_KEY) {
28
+ console.error("No credentials found. Run: npx poke-gate");
29
+ process.exit(1);
30
+ }
31
+
32
+ async function main() {
33
+ log("poke-gate starting...");
34
+
35
+ const { port } = await startMcpServer();
36
+ log(`MCP server on port ${port}`);
37
+
38
+ const mcpUrl = `http://localhost:${port}/mcp`;
39
+
40
+ log("Connecting tunnel to Poke...");
41
+ try {
42
+ const { info } = await startTunnel({
43
+ apiKey: API_KEY,
44
+ mcpUrl,
45
+ onEvent: (type, data) => {
46
+ switch (type) {
47
+ case "connected":
48
+ log(`Tunnel connected (${data.connectionId})`);
49
+ log("Ready — your Poke agent can now access this machine.");
50
+ break;
51
+ case "disconnected":
52
+ log("Tunnel disconnected. Reconnecting...");
53
+ break;
54
+ case "error":
55
+ log(`Tunnel error: ${data}`);
56
+ break;
57
+ case "tools-synced":
58
+ log(`Tools synced: ${data}`);
59
+ break;
60
+ case "oauth-required":
61
+ log(`OAuth required: ${data}`);
62
+ break;
63
+ }
64
+ },
65
+ });
66
+ } catch (err) {
67
+ log(`Failed to connect: ${err.message}`);
68
+ process.exit(1);
69
+ }
70
+ }
71
+
72
+ process.on("SIGINT", () => {
73
+ log("Shutting down...");
74
+ process.exit(0);
75
+ });
76
+
77
+ process.on("SIGTERM", () => {
78
+ log("Shutting down...");
79
+ process.exit(0);
80
+ });
81
+
82
+ main();
@@ -0,0 +1,267 @@
1
+ import http from "node:http";
2
+ import { execSync, exec } from "node:child_process";
3
+ import { readFileSync, writeFileSync, readdirSync, statSync } from "node:fs";
4
+ import { hostname, platform, arch, uptime, totalmem, freemem, homedir } from "node:os";
5
+ import { join, resolve } from "node:path";
6
+
7
+ const SERVER_INFO = { name: "poke-gate", version: "0.0.1" };
8
+
9
+ const COMMAND_TIMEOUT = 30_000;
10
+
11
+ const TOOLS = [
12
+ {
13
+ name: "run_command",
14
+ description:
15
+ "Execute a shell command on the user's machine and return stdout, stderr, and exit code. " +
16
+ "Use this to run any CLI command (ls, cat, git, brew, python, curl, etc.). " +
17
+ "Commands run in a shell with a 30-second timeout.",
18
+ inputSchema: {
19
+ type: "object",
20
+ properties: {
21
+ command: { type: "string", description: "The shell command to execute" },
22
+ cwd: { type: "string", description: "Working directory (optional, defaults to home)" },
23
+ },
24
+ required: ["command"],
25
+ },
26
+ },
27
+ {
28
+ name: "read_file",
29
+ description: "Read the contents of a file on the user's machine.",
30
+ inputSchema: {
31
+ type: "object",
32
+ properties: {
33
+ path: { type: "string", description: "Absolute or relative path to the file" },
34
+ },
35
+ required: ["path"],
36
+ },
37
+ },
38
+ {
39
+ name: "write_file",
40
+ description: "Write content to a file on the user's machine. Creates the file if it doesn't exist, overwrites if it does.",
41
+ inputSchema: {
42
+ type: "object",
43
+ properties: {
44
+ path: { type: "string", description: "Absolute or relative path to the file" },
45
+ content: { type: "string", description: "Content to write" },
46
+ },
47
+ required: ["path", "content"],
48
+ },
49
+ },
50
+ {
51
+ name: "list_directory",
52
+ description: "List files and directories at a given path on the user's machine.",
53
+ inputSchema: {
54
+ type: "object",
55
+ properties: {
56
+ path: { type: "string", description: "Directory path (defaults to home)" },
57
+ },
58
+ },
59
+ },
60
+ {
61
+ name: "system_info",
62
+ description: "Get system information: OS, hostname, architecture, uptime, memory, and home directory.",
63
+ inputSchema: { type: "object", properties: {} },
64
+ },
65
+ ];
66
+
67
+ function runCommand(command, cwd) {
68
+ return new Promise((res) => {
69
+ const dir = cwd || homedir();
70
+ const child = exec(command, {
71
+ cwd: dir,
72
+ timeout: COMMAND_TIMEOUT,
73
+ maxBuffer: 1024 * 1024,
74
+ shell: true,
75
+ }, (error, stdout, stderr) => {
76
+ res({
77
+ stdout: stdout.slice(0, 50_000),
78
+ stderr: stderr.slice(0, 10_000),
79
+ exitCode: error ? (error.code ?? 1) : 0,
80
+ });
81
+ });
82
+ });
83
+ }
84
+
85
+ function handleToolCall(name, args) {
86
+ switch (name) {
87
+ case "run_command": {
88
+ return runCommand(args.command, args.cwd).then((result) => ({
89
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
90
+ }));
91
+ }
92
+
93
+ case "read_file": {
94
+ try {
95
+ const p = resolve(args.path.replace(/^~/, homedir()));
96
+ const text = readFileSync(p, "utf-8");
97
+ return { content: [{ type: "text", text: text.slice(0, 100_000) }] };
98
+ } catch (err) {
99
+ return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
100
+ }
101
+ }
102
+
103
+ case "write_file": {
104
+ try {
105
+ const p = resolve(args.path.replace(/^~/, homedir()));
106
+ writeFileSync(p, args.content);
107
+ return { content: [{ type: "text", text: `Written to ${p}` }] };
108
+ } catch (err) {
109
+ return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
110
+ }
111
+ }
112
+
113
+ case "list_directory": {
114
+ try {
115
+ const dir = resolve((args.path || "~").replace(/^~/, homedir()));
116
+ const entries = readdirSync(dir).map((name) => {
117
+ try {
118
+ const s = statSync(join(dir, name));
119
+ return `${s.isDirectory() ? "d" : "-"} ${name}`;
120
+ } catch {
121
+ return `? ${name}`;
122
+ }
123
+ });
124
+ return { content: [{ type: "text", text: entries.join("\n") }] };
125
+ } catch (err) {
126
+ return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
127
+ }
128
+ }
129
+
130
+ case "system_info": {
131
+ const info = {
132
+ hostname: hostname(),
133
+ platform: platform(),
134
+ arch: arch(),
135
+ uptime: `${Math.floor(uptime() / 3600)}h ${Math.floor((uptime() % 3600) / 60)}m`,
136
+ totalMemory: `${Math.round(totalmem() / 1024 / 1024 / 1024)}GB`,
137
+ freeMemory: `${Math.round(freemem() / 1024 / 1024 / 1024)}GB`,
138
+ homeDir: homedir(),
139
+ nodeVersion: process.version,
140
+ };
141
+ return { content: [{ type: "text", text: JSON.stringify(info, null, 2) }] };
142
+ }
143
+
144
+ default:
145
+ return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
146
+ }
147
+ }
148
+
149
+ function handleJsonRpc(msg) {
150
+ const { id, method, params } = msg;
151
+
152
+ switch (method) {
153
+ case "initialize":
154
+ return {
155
+ jsonrpc: "2.0",
156
+ id,
157
+ result: {
158
+ protocolVersion: params?.protocolVersion || "2024-11-05",
159
+ capabilities: { tools: { listChanged: false } },
160
+ serverInfo: SERVER_INFO,
161
+ instructions:
162
+ "This server gives you access to the user's machine. " +
163
+ "You can run shell commands, read/write files, list directories, and get system info. " +
164
+ "Use these tools to help the user with OS-level tasks.",
165
+ },
166
+ };
167
+
168
+ case "notifications/initialized":
169
+ return null;
170
+
171
+ case "tools/list":
172
+ return { jsonrpc: "2.0", id, result: { tools: TOOLS } };
173
+
174
+ case "tools/call": {
175
+ const result = handleToolCall(params.name, params.arguments || {});
176
+ if (result instanceof Promise) {
177
+ return result.then((r) => ({ jsonrpc: "2.0", id, result: r }));
178
+ }
179
+ return { jsonrpc: "2.0", id, result };
180
+ }
181
+
182
+ case "ping":
183
+ return { jsonrpc: "2.0", id, result: {} };
184
+
185
+ default:
186
+ if (!id) return null;
187
+ return {
188
+ jsonrpc: "2.0",
189
+ id,
190
+ error: { code: -32601, message: `Method not found: ${method}` },
191
+ };
192
+ }
193
+ }
194
+
195
+ function readBody(req) {
196
+ return new Promise((resolve, reject) => {
197
+ const chunks = [];
198
+ req.on("data", (c) => chunks.push(c));
199
+ req.on("end", () => resolve(Buffer.concat(chunks).toString()));
200
+ req.on("error", reject);
201
+ });
202
+ }
203
+
204
+ export function startMcpServer(port = 0) {
205
+ return new Promise((resolve, reject) => {
206
+ const httpServer = http.createServer(async (req, res) => {
207
+ res.setHeader("Access-Control-Allow-Origin", "*");
208
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
209
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, Mcp-Session-Id, Accept");
210
+ res.setHeader("Access-Control-Expose-Headers", "Mcp-Session-Id");
211
+
212
+ if (req.method === "OPTIONS") {
213
+ res.writeHead(204);
214
+ res.end();
215
+ return;
216
+ }
217
+
218
+ const url = new URL(req.url, "http://localhost");
219
+
220
+ if (url.pathname === "/mcp" && req.method === "POST") {
221
+ try {
222
+ const body = await readBody(req);
223
+ const parsed = JSON.parse(body);
224
+
225
+ if (Array.isArray(parsed)) {
226
+ const results = [];
227
+ for (const msg of parsed) {
228
+ const r = handleJsonRpc(msg);
229
+ const resolved = r instanceof Promise ? await r : r;
230
+ if (resolved) results.push(resolved);
231
+ }
232
+ res.writeHead(200, { "Content-Type": "application/json" });
233
+ res.end(JSON.stringify(results));
234
+ } else {
235
+ let result = handleJsonRpc(parsed);
236
+ if (result instanceof Promise) result = await result;
237
+ if (result) {
238
+ res.writeHead(200, { "Content-Type": "application/json" });
239
+ res.end(JSON.stringify(result));
240
+ } else {
241
+ res.writeHead(204);
242
+ res.end();
243
+ }
244
+ }
245
+ } catch (err) {
246
+ res.writeHead(400, { "Content-Type": "application/json" });
247
+ res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32700, message: "Parse error" }, id: null }));
248
+ }
249
+ return;
250
+ }
251
+
252
+ if (url.pathname === "/health") {
253
+ res.writeHead(200, { "Content-Type": "application/json" });
254
+ res.end(JSON.stringify({ status: "ok" }));
255
+ return;
256
+ }
257
+
258
+ res.writeHead(404);
259
+ res.end("Not found");
260
+ });
261
+
262
+ httpServer.on("error", reject);
263
+ httpServer.listen(port, "127.0.0.1", () => {
264
+ resolve({ httpServer, port: httpServer.address().port });
265
+ });
266
+ });
267
+ }
package/src/tunnel.js ADDED
@@ -0,0 +1,20 @@
1
+ import { PokeTunnel, getToken } from "poke";
2
+
3
+ export async function startTunnel({ apiKey, mcpUrl, onEvent }) {
4
+ const token = getToken();
5
+
6
+ const tunnel = new PokeTunnel({
7
+ url: mcpUrl,
8
+ name: "poke-gate",
9
+ token: token || apiKey,
10
+ });
11
+
12
+ tunnel.on("connected", (info) => onEvent("connected", info));
13
+ tunnel.on("disconnected", () => onEvent("disconnected"));
14
+ tunnel.on("error", (err) => onEvent("error", err.message));
15
+ tunnel.on("toolsSynced", ({ toolCount }) => onEvent("tools-synced", toolCount));
16
+ tunnel.on("oauthRequired", ({ authUrl }) => onEvent("oauth-required", authUrl));
17
+
18
+ const info = await tunnel.start();
19
+ return { tunnel, info };
20
+ }