poke-gate 0.1.0 → 0.1.4

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.
@@ -0,0 +1,15 @@
1
+ {
2
+ "private": true,
3
+ "scripts": {
4
+ "dev": "vitepress dev",
5
+ "build": "vitepress build",
6
+ "preview": "vitepress preview"
7
+ },
8
+ "devDependencies": {
9
+ "vitepress": "^1.6.3"
10
+ },
11
+ "dependencies": {
12
+ "mermaid": "^11.13.0",
13
+ "vitepress-plugin-mermaid": "^2.0.17"
14
+ }
15
+ }
@@ -0,0 +1 @@
1
+ poke-gate.fka.dev
Binary file
Binary file
@@ -0,0 +1,35 @@
1
+ # Security
2
+
3
+ ::: danger Full shell access
4
+ Poke Gate grants **full shell access** to your Poke agent. Understand the implications before running it.
5
+ :::
6
+
7
+ ## What the agent can do
8
+
9
+ When Poke Gate is running, your Poke agent can:
10
+
11
+ - **Run any command** with your user's permissions (`run_command`)
12
+ - **Read any file** your user can access (`read_file`, `read_image`)
13
+ - **Write any file** your user can access (`write_file`)
14
+ - **List any directory** (`list_directory`)
15
+ - **Take screenshots** of your screen (`take_screenshot`)
16
+ - **See system info** — hostname, memory, uptime (`system_info`)
17
+
18
+ ## What protects you
19
+
20
+ - **Authentication** — only your Poke agent (authenticated via your Poke OAuth session) can reach the tunnel. No one else can send tool calls.
21
+ - **Tunnel isolation** — the MCP server only listens on `127.0.0.1` (localhost). It's not exposed to the network. The tunnel is the only way to reach it.
22
+ - **No persistent access** — when you quit Poke Gate (Ctrl-C or Quit from menu bar), the tunnel closes and the connection is deleted. Your machine is no longer reachable.
23
+ - **Connection cleanup** — old connections are deleted before new ones are created, preventing stale tunnels.
24
+
25
+ ## Best practices
26
+
27
+ 1. **Only run on trusted machines** — don't run Poke Gate on shared or public computers.
28
+ 2. **Quit when not needed** — close the app when you don't need remote access.
29
+ 3. **Review agent scripts** — before installing a community agent, read the code. Agents run with your full user permissions.
30
+ 4. **Keep env files secure** — `.env` files in `~/.config/poke-gate/agents/` may contain API tokens. Don't commit them to git.
31
+ 5. **Use verbose mode** — run with `--verbose` to see what tools are being called in real time.
32
+
33
+ ## Reporting issues
34
+
35
+ If you discover a security vulnerability, please email [security@fka.dev](mailto:security@fka.dev) instead of opening a public issue.
package/docs/tools.md ADDED
@@ -0,0 +1,101 @@
1
+ # Tools
2
+
3
+ Poke Gate exposes 7 tools to your Poke agent via MCP.
4
+
5
+ ## run_command
6
+
7
+ Execute any shell command on your machine.
8
+
9
+ | Parameter | Type | Required | Description |
10
+ |-----------|------|----------|-------------|
11
+ | `command` | string | yes | The shell command to execute |
12
+ | `cwd` | string | no | Working directory (defaults to home) |
13
+
14
+ **Returns:** `{ stdout, stderr, exitCode }`
15
+
16
+ **Examples from Poke:**
17
+ - "Run `ls -la` in my home directory"
18
+ - "What's running on port 3000?"
19
+ - "Show me the git log for my project"
20
+ - "Install lodash in my project"
21
+
22
+ ::: info
23
+ Commands have a 30-second timeout and 1MB output buffer.
24
+ :::
25
+
26
+ ## read_file
27
+
28
+ Read the contents of a text file.
29
+
30
+ | Parameter | Type | Required | Description |
31
+ |-----------|------|----------|-------------|
32
+ | `path` | string | yes | Absolute or relative path (supports `~`) |
33
+
34
+ **Examples:**
35
+ - "Read my ~/.zshrc"
36
+ - "Show me the package.json in my project"
37
+
38
+ ## read_image
39
+
40
+ Read an image or binary file and return it as base64.
41
+
42
+ | Parameter | Type | Required | Description |
43
+ |-----------|------|----------|-------------|
44
+ | `path` | string | yes | Path to the image file |
45
+
46
+ Supports: png, jpg, gif, webp, svg, pdf, bmp, ico.
47
+
48
+ For image files, returns MCP `image` content type with base64 data so the agent can "see" the image.
49
+
50
+ ## write_file
51
+
52
+ Write content to a file. Creates the file if it doesn't exist, overwrites if it does.
53
+
54
+ | Parameter | Type | Required | Description |
55
+ |-----------|------|----------|-------------|
56
+ | `path` | string | yes | Absolute or relative path (supports `~`) |
57
+ | `content` | string | yes | Content to write |
58
+
59
+ **Examples:**
60
+ - "Create a file called notes.txt on my Desktop"
61
+ - "Write a Python script that..."
62
+
63
+ ## list_directory
64
+
65
+ List files and directories at a given path.
66
+
67
+ | Parameter | Type | Required | Description |
68
+ |-----------|------|----------|-------------|
69
+ | `path` | string | no | Directory path (defaults to home, supports `~`) |
70
+
71
+ Returns entries with `d` for directories and `-` for files.
72
+
73
+ ## system_info
74
+
75
+ Get system information. No parameters needed.
76
+
77
+ **Returns:**
78
+ ```json
79
+ {
80
+ "hostname": "MacBook-Pro.local",
81
+ "platform": "darwin",
82
+ "arch": "arm64",
83
+ "uptime": "5h 23m",
84
+ "totalMemory": "16GB",
85
+ "freeMemory": "4GB",
86
+ "homeDir": "/Users/you",
87
+ "nodeVersion": "v22.21.1"
88
+ }
89
+ ```
90
+
91
+ ## take_screenshot
92
+
93
+ Capture the screen and save it to a file.
94
+
95
+ | Parameter | Type | Required | Description |
96
+ |-----------|------|----------|-------------|
97
+ | `path` | string | no | Save path (defaults to `~/Desktop/screenshot-<timestamp>.png`) |
98
+
99
+ ::: warning
100
+ Requires **Screen Recording** permission on macOS. The system will prompt you the first time.
101
+ :::
@@ -0,0 +1,78 @@
1
+ /**
2
+ * @agent battery
3
+ * @name Battery Guardian
4
+ * @description Alerts you via Poke when your battery drops below 20%.
5
+ * @interval 30m
6
+ * @author f
7
+ */
8
+
9
+ import { Poke, getToken } from "poke";
10
+ import { execSync } from "node:child_process";
11
+ import { readFileSync, writeFileSync, existsSync } from "node:fs";
12
+ import { join } from "node:path";
13
+ import { homedir } from "node:os";
14
+
15
+ const token = getToken();
16
+ if (!token) {
17
+ console.error("Not signed in. Run: npx poke login");
18
+ process.exit(1);
19
+ }
20
+
21
+ const THRESHOLD = parseInt(process.env.BATTERY_THRESHOLD || "20", 10);
22
+ const STATE_FILE = join(homedir(), ".config", "poke-gate", "agents", ".battery-state.json");
23
+
24
+ function getBattery() {
25
+ try {
26
+ const output = execSync("pmset -g batt", { encoding: "utf-8", timeout: 5000 });
27
+ const match = output.match(/(\d+)%/);
28
+ const charging = output.includes("AC Power") || output.includes("charging");
29
+ return {
30
+ level: match ? parseInt(match[1], 10) : null,
31
+ charging,
32
+ };
33
+ } catch {
34
+ return { level: null, charging: false };
35
+ }
36
+ }
37
+
38
+ function loadState() {
39
+ try {
40
+ return JSON.parse(readFileSync(STATE_FILE, "utf-8"));
41
+ } catch {
42
+ return { alerted: false };
43
+ }
44
+ }
45
+
46
+ function saveState(state) {
47
+ writeFileSync(STATE_FILE, JSON.stringify(state));
48
+ }
49
+
50
+ const { level, charging } = getBattery();
51
+
52
+ if (level === null) {
53
+ console.log("Could not read battery level.");
54
+ process.exit(0);
55
+ }
56
+
57
+ console.log(`Battery: ${level}% ${charging ? "(charging)" : "(on battery)"}`);
58
+
59
+ const state = loadState();
60
+
61
+ if (level <= THRESHOLD && !charging && !state.alerted) {
62
+ console.log(`Battery low (${level}%). Alerting Poke...`);
63
+
64
+ const poke = new Poke({ apiKey: token });
65
+ await poke.sendMessage(
66
+ `⚠️ Battery alert: your Mac is at ${level}%. You're not plugged in. Consider charging soon.`
67
+ );
68
+
69
+ saveState({ alerted: true });
70
+ console.log("Alert sent.");
71
+ } else if (level > THRESHOLD || charging) {
72
+ if (state.alerted) {
73
+ saveState({ alerted: false });
74
+ console.log("Battery recovered, reset alert state.");
75
+ } else {
76
+ console.log("Battery OK, no alert needed.");
77
+ }
78
+ }
@@ -36,12 +36,20 @@ async function beeperRequest(path, params = {}) {
36
36
  async function getRecentMessages() {
37
37
  const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString();
38
38
 
39
- const data = await beeperRequest("/v1/messages/search", {
40
- dateAfter: oneHourAgo,
41
- limit: 200,
42
- });
39
+ let allMessages = [];
40
+ let cursor = null;
41
+
42
+ while (true) {
43
+ const params = { dateAfter: oneHourAgo, limit: 20 };
44
+ if (cursor) params.cursor = cursor;
45
+ const data = await beeperRequest("/v1/messages/search", params);
46
+ const items = data.items || [];
47
+ allMessages.push(...items);
48
+ if (!data.hasMore || !data.oldestCursor) break;
49
+ cursor = data.oldestCursor;
50
+ }
43
51
 
44
- return data.items || [];
52
+ return allMessages;
45
53
  }
46
54
 
47
55
  function groupBySender(messages) {
@@ -0,0 +1,86 @@
1
+ /**
2
+ * @agent screentime
3
+ * @name Screen Time Report
4
+ * @description Sends a daily summary of your most-used apps to Poke.
5
+ * @interval 24h
6
+ * @author f
7
+ */
8
+
9
+ import { Poke, getToken } from "poke";
10
+ import { execSync } from "node:child_process";
11
+
12
+ const token = getToken();
13
+ if (!token) {
14
+ console.error("Not signed in. Run: npx poke login");
15
+ process.exit(1);
16
+ }
17
+
18
+ function getScreenTime() {
19
+ try {
20
+ const result = execSync(
21
+ `defaults read com.apple.ScreenTimeAgent 2>/dev/null || echo "{}"`,
22
+ { encoding: "utf-8", timeout: 10000 }
23
+ ).trim();
24
+
25
+ // Fallback: use process list to estimate active apps
26
+ const ps = execSync(
27
+ `ps -eo etime,comm | grep -i "/Applications/" | sort -rn | head -20`,
28
+ { encoding: "utf-8", timeout: 10000 }
29
+ ).trim();
30
+
31
+ return ps;
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ function getActiveApps() {
38
+ try {
39
+ const script = `
40
+ tell application "System Events"
41
+ set appList to name of every application process whose background only is false
42
+ end tell
43
+ return appList as text
44
+ `;
45
+ const result = execSync(`osascript -e '${script}'`, {
46
+ encoding: "utf-8",
47
+ timeout: 10000,
48
+ }).trim();
49
+ return result.split(", ");
50
+ } catch {
51
+ return [];
52
+ }
53
+ }
54
+
55
+ function getUptime() {
56
+ try {
57
+ return execSync("uptime", { encoding: "utf-8", timeout: 5000 }).trim();
58
+ } catch {
59
+ return "unknown";
60
+ }
61
+ }
62
+
63
+ const apps = getActiveApps();
64
+ const uptimeStr = getUptime();
65
+ const screenData = getScreenTime();
66
+
67
+ let report = `Daily screen report:\n\n`;
68
+ report += `Uptime: ${uptimeStr}\n\n`;
69
+
70
+ if (apps.length > 0) {
71
+ report += `Currently running apps (${apps.length}):\n`;
72
+ for (const app of apps) {
73
+ report += ` - ${app}\n`;
74
+ }
75
+ }
76
+
77
+ if (screenData) {
78
+ report += `\nTop processes by runtime:\n${screenData}\n`;
79
+ }
80
+
81
+ console.log("Sending screen time report...");
82
+
83
+ const poke = new Poke({ apiKey: token });
84
+ await poke.sendMessage(report);
85
+
86
+ console.log("Report sent.");
@@ -0,0 +1,85 @@
1
+ /**
2
+ * @agent wifi
3
+ * @name WiFi Logger
4
+ * @description Logs your current WiFi network to Poke so it knows where you are.
5
+ * @interval 30m
6
+ * @author f
7
+ */
8
+
9
+ import { Poke, getToken } from "poke";
10
+ import { execSync } from "node:child_process";
11
+ import { readFileSync, writeFileSync } from "node:fs";
12
+ import { join } from "node:path";
13
+ import { homedir } from "node:os";
14
+
15
+ const token = getToken();
16
+ if (!token) {
17
+ console.error("Not signed in. Run: npx poke login");
18
+ process.exit(1);
19
+ }
20
+
21
+ const STATE_FILE = join(homedir(), ".config", "poke-gate", "agents", ".wifi-state.json");
22
+
23
+ function getCurrentNetwork() {
24
+ try {
25
+ const iface = execSync(
26
+ "networksetup -listallhardwareports | awk '/Wi-Fi/{getline; print $2}'",
27
+ { encoding: "utf-8", timeout: 5000 }
28
+ ).trim();
29
+
30
+ const ssid = execSync(
31
+ `networksetup -getairportnetwork ${iface || "en0"} 2>/dev/null | sed 's/Current Wi-Fi Network: //'`,
32
+ { encoding: "utf-8", timeout: 5000 }
33
+ ).trim();
34
+
35
+ if (ssid.includes("not associated") || !ssid) return null;
36
+ return ssid;
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ function loadState() {
43
+ try {
44
+ return JSON.parse(readFileSync(STATE_FILE, "utf-8"));
45
+ } catch {
46
+ return { lastNetwork: null };
47
+ }
48
+ }
49
+
50
+ function saveState(state) {
51
+ writeFileSync(STATE_FILE, JSON.stringify(state));
52
+ }
53
+
54
+ const network = getCurrentNetwork();
55
+ const state = loadState();
56
+
57
+ console.log(`Current WiFi: ${network || "not connected"}`);
58
+
59
+ if (network && network !== state.lastNetwork) {
60
+ console.log(`Network changed from "${state.lastNetwork}" to "${network}". Notifying Poke...`);
61
+
62
+ const poke = new Poke({ apiKey: token });
63
+ await poke.sendMessage(
64
+ `I just connected to WiFi network "${network}". ` +
65
+ (state.lastNetwork
66
+ ? `Previously I was on "${state.lastNetwork}".`
67
+ : `This is the first network I've logged.`) +
68
+ ` Remember this for context about where I am.`
69
+ );
70
+
71
+ saveState({ lastNetwork: network });
72
+ console.log("Poke notified.");
73
+ } else if (!network && state.lastNetwork) {
74
+ console.log("Disconnected from WiFi. Notifying Poke...");
75
+
76
+ const poke = new Poke({ apiKey: token });
77
+ await poke.sendMessage(
78
+ `I've disconnected from WiFi (was on "${state.lastNetwork}"). I might be on the move.`
79
+ );
80
+
81
+ saveState({ lastNetwork: null });
82
+ console.log("Poke notified.");
83
+ } else {
84
+ console.log("No network change.");
85
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poke-gate",
3
- "version": "0.1.0",
3
+ "version": "0.1.4",
4
4
  "description": "Expose your machine to your Poke AI assistant via MCP tunnel",
5
5
  "type": "module",
6
6
  "bin": {
package/src/agents.js CHANGED
@@ -2,6 +2,7 @@ import { readdirSync, readFileSync, writeFileSync, existsSync, mkdirSync } from
2
2
  import { join, basename } from "node:path";
3
3
  import { homedir } from "node:os";
4
4
  import { exec } from "node:child_process";
5
+ import { createInterface } from "node:readline";
5
6
 
6
7
  const CONFIG_DIR = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
7
8
  const AGENTS_DIR = join(CONFIG_DIR, "poke-gate", "agents");
@@ -109,10 +110,31 @@ export function discoverAgents() {
109
110
  return agents;
110
111
  }
111
112
 
113
+ import { symlinkSync, lstatSync } from "node:fs";
114
+
115
+ function ensureNodeModulesLink() {
116
+ const pkgRoot = join(new URL(".", import.meta.url).pathname, "..");
117
+ const source = join(pkgRoot, "node_modules");
118
+ const target = join(AGENTS_DIR, "node_modules");
119
+
120
+ if (!existsSync(source)) return;
121
+
122
+ try {
123
+ const stat = lstatSync(target);
124
+ if (stat.isSymbolicLink()) return;
125
+ } catch {}
126
+
127
+ try {
128
+ symlinkSync(source, target, "junction");
129
+ } catch {}
130
+ }
131
+
112
132
  function runAgentProcess(agent) {
113
133
  const agentEnv = parseEnvFile(agent.envFile);
114
134
  const env = { ...process.env, ...agentEnv };
115
135
 
136
+ ensureNodeModulesLink();
137
+
116
138
  log(`Running agent: ${agent.name} (${agent.file})`);
117
139
 
118
140
  return new Promise((resolve) => {
@@ -212,26 +234,91 @@ export async function downloadAgent(name) {
212
234
  writeFileSync(dest, jsContent);
213
235
  console.log(` Saved: ${dest}`);
214
236
 
215
- // Try to download matching .env file
216
237
  const envName = name.split(".")[0];
238
+ const envDest = join(AGENTS_DIR, `.env.${envName}`);
239
+
240
+ if (existsSync(envDest)) {
241
+ console.log(` .env.${envName} already exists, skipped.`);
242
+ console.log(`\n Test it: npx poke-gate run-agent ${envName}`);
243
+ return;
244
+ }
245
+
217
246
  const envRes = await fetch(`${REPO_BASE}/.env.${envName}`).catch(() => null);
218
247
  if (envRes?.ok) {
219
- const envContent = await envRes.text();
220
- const envDest = join(AGENTS_DIR, `.env.${envName}`);
221
- if (!existsSync(envDest)) {
222
- writeFileSync(envDest, envContent);
223
- console.log(` Saved: ${envDest}`);
224
- console.log(`\n Edit the env file with your credentials:`);
225
- console.log(` nano ${envDest}`);
248
+ const envTemplate = await envRes.text();
249
+ const keys = parseEnvKeys(envTemplate);
250
+
251
+ if (keys.length > 0) {
252
+ console.log(`\n This agent needs ${keys.length} env variable(s):\n`);
253
+ const values = await promptEnvKeys(keys);
254
+ let content = "";
255
+ for (const { key, comment } of keys) {
256
+ if (comment) content += `# ${comment}\n`;
257
+ content += `${key}=${values[key] || ""}\n`;
258
+ }
259
+ writeFileSync(envDest, content);
260
+ console.log(`\n Saved: ${envDest}`);
226
261
  } else {
227
- console.log(` .env.${envName} already exists, skipped.`);
262
+ writeFileSync(envDest, envTemplate);
263
+ console.log(` Saved: ${envDest}`);
228
264
  }
229
265
  }
230
266
 
231
267
  console.log(`\n Test it: npx poke-gate run-agent ${envName}`);
232
268
  }
233
269
 
270
+ function parseEnvKeys(template) {
271
+ const keys = [];
272
+ const lines = template.split("\n");
273
+ let lastComment = null;
274
+ for (const line of lines) {
275
+ const trimmed = line.trim();
276
+ if (trimmed.startsWith("#")) {
277
+ lastComment = trimmed.slice(1).trim();
278
+ continue;
279
+ }
280
+ const eqIdx = trimmed.indexOf("=");
281
+ if (eqIdx === -1) { lastComment = null; continue; }
282
+ const key = trimmed.slice(0, eqIdx).trim();
283
+ const value = trimmed.slice(eqIdx + 1).trim();
284
+ const isPlaceholder = !value || value.includes("your_") || value.includes("_here");
285
+ if (isPlaceholder) {
286
+ keys.push({ key, comment: lastComment });
287
+ }
288
+ lastComment = null;
289
+ }
290
+ return keys;
291
+ }
292
+
293
+ function ask(question) {
294
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
295
+ return new Promise((resolve) => {
296
+ rl.question(question, (answer) => {
297
+ rl.close();
298
+ resolve(answer.trim());
299
+ });
300
+ });
301
+ }
302
+
303
+ async function promptEnvKeys(keys) {
304
+ const values = {};
305
+ for (const { key, comment } of keys) {
306
+ const hint = comment ? ` (${comment})` : "";
307
+ values[key] = await ask(` ${key}${hint}: `);
308
+ }
309
+ return values;
310
+ }
311
+
312
+ let schedulerRunning = false;
313
+ const activeTimers = [];
314
+
234
315
  export function startAgentScheduler() {
316
+ if (schedulerRunning) {
317
+ log("Agent scheduler already running, skipping.");
318
+ return;
319
+ }
320
+ schedulerRunning = true;
321
+
235
322
  const agents = discoverAgents();
236
323
 
237
324
  if (agents.length === 0) {
@@ -250,8 +337,18 @@ export function startAgentScheduler() {
250
337
  for (const agent of agents) {
251
338
  runAgentProcess(agent);
252
339
 
253
- setInterval(() => {
340
+ const timer = setInterval(() => {
254
341
  runAgentProcess(agent);
255
342
  }, agent.intervalMs);
343
+ activeTimers.push(timer);
344
+ }
345
+ }
346
+
347
+ export function stopAgentScheduler() {
348
+ for (const timer of activeTimers) {
349
+ clearInterval(timer);
256
350
  }
351
+ activeTimers.length = 0;
352
+ schedulerRunning = false;
353
+ log("Agent scheduler stopped.");
257
354
  }