poke-gate 0.0.8 → 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/src/agents.js ADDED
@@ -0,0 +1,257 @@
1
+ import { readdirSync, readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
2
+ import { join, basename } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { exec } from "node:child_process";
5
+
6
+ const CONFIG_DIR = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
7
+ const AGENTS_DIR = join(CONFIG_DIR, "poke-gate", "agents");
8
+
9
+ const MIN_INTERVAL_MS = 10 * 60 * 1000;
10
+
11
+ function log(msg) {
12
+ const ts = new Date().toISOString().slice(11, 19);
13
+ console.log(`[${ts}] [agents] ${msg}`);
14
+ }
15
+
16
+ function parseInterval(token) {
17
+ const match = token.match(/^(\d+)(m|h)$/);
18
+ if (!match) return null;
19
+ const value = parseInt(match[1], 10);
20
+ const unit = match[2];
21
+ const ms = unit === "h" ? value * 60 * 60 * 1000 : value * 60 * 1000;
22
+ if (ms < MIN_INTERVAL_MS) return null;
23
+ return ms;
24
+ }
25
+
26
+ function parseFrontmatter(filePath) {
27
+ try {
28
+ const content = readFileSync(filePath, "utf-8");
29
+ const match = content.match(/\/\*\*[\s\S]*?\*\//);
30
+ if (!match) return {};
31
+ const block = match[0];
32
+ const meta = {};
33
+ const lines = block.split("\n");
34
+ for (const line of lines) {
35
+ const m = line.match(/@(\w+)\s+(.*)/);
36
+ if (m) {
37
+ const key = m[1].trim();
38
+ const value = m[2].replace(/\*\/$/, "").trim();
39
+ if (key === "env") {
40
+ if (!meta.env) meta.env = [];
41
+ meta.env.push(value);
42
+ } else {
43
+ meta[key] = value;
44
+ }
45
+ }
46
+ }
47
+ return meta;
48
+ } catch {
49
+ return {};
50
+ }
51
+ }
52
+
53
+ function parseEnvFile(filePath) {
54
+ const env = {};
55
+ if (!existsSync(filePath)) return env;
56
+ const lines = readFileSync(filePath, "utf-8").split("\n");
57
+ for (const line of lines) {
58
+ const trimmed = line.trim();
59
+ if (!trimmed || trimmed.startsWith("#")) continue;
60
+ const eqIdx = trimmed.indexOf("=");
61
+ if (eqIdx === -1) continue;
62
+ const key = trimmed.slice(0, eqIdx).trim();
63
+ let value = trimmed.slice(eqIdx + 1).trim();
64
+ if ((value.startsWith('"') && value.endsWith('"')) ||
65
+ (value.startsWith("'") && value.endsWith("'"))) {
66
+ value = value.slice(1, -1);
67
+ }
68
+ env[key] = value;
69
+ }
70
+ return env;
71
+ }
72
+
73
+ export function discoverAgents() {
74
+ if (!existsSync(AGENTS_DIR)) {
75
+ mkdirSync(AGENTS_DIR, { recursive: true });
76
+ return [];
77
+ }
78
+
79
+ const files = readdirSync(AGENTS_DIR).filter((f) => f.endsWith(".js"));
80
+ const agents = [];
81
+
82
+ for (const file of files) {
83
+ const parts = file.replace(/\.js$/, "").split(".");
84
+ if (parts.length < 2) continue;
85
+
86
+ const intervalToken = parts[parts.length - 1];
87
+ const name = parts.slice(0, -1).join(".");
88
+ const intervalMs = parseInterval(intervalToken);
89
+
90
+ if (!intervalMs) {
91
+ log(`Skipping ${file}: invalid or too short interval (min 10m)`);
92
+ continue;
93
+ }
94
+
95
+ const agentPath = join(AGENTS_DIR, file);
96
+ const meta = parseFrontmatter(agentPath);
97
+
98
+ agents.push({
99
+ name,
100
+ file,
101
+ path: agentPath,
102
+ intervalToken,
103
+ intervalMs,
104
+ envFile: join(AGENTS_DIR, `.env.${name}`),
105
+ meta,
106
+ });
107
+ }
108
+
109
+ return agents;
110
+ }
111
+
112
+ function runAgentProcess(agent) {
113
+ const agentEnv = parseEnvFile(agent.envFile);
114
+ const env = { ...process.env, ...agentEnv };
115
+
116
+ log(`Running agent: ${agent.name} (${agent.file})`);
117
+
118
+ return new Promise((resolve) => {
119
+ exec(`node "${agent.path}"`, {
120
+ env,
121
+ timeout: 5 * 60 * 1000,
122
+ maxBuffer: 1024 * 1024,
123
+ cwd: AGENTS_DIR,
124
+ }, (error, stdout, stderr) => {
125
+ if (stdout.trim()) log(`[${agent.name}] ${stdout.trim()}`);
126
+ if (stderr.trim()) log(`[${agent.name}] stderr: ${stderr.trim()}`);
127
+ if (error) log(`[${agent.name}] exited with code ${error.code ?? 1}`);
128
+ else log(`[${agent.name}] completed`);
129
+ resolve();
130
+ });
131
+ });
132
+ }
133
+
134
+ export async function runAgent(name) {
135
+ const agents = discoverAgents();
136
+ const agent = agents.find((a) => a.name === name);
137
+ if (!agent) {
138
+ const allFiles = readdirSync(AGENTS_DIR).filter((f) => f.endsWith(".js"));
139
+ const match = allFiles.find((f) => f.startsWith(name + "."));
140
+ if (match) {
141
+ const parts = match.replace(/\.js$/, "").split(".");
142
+ const intervalToken = parts[parts.length - 1];
143
+ const intervalMs = parseInterval(intervalToken);
144
+ await runAgentProcess({
145
+ name,
146
+ file: match,
147
+ path: join(AGENTS_DIR, match),
148
+ intervalToken,
149
+ intervalMs: intervalMs || 0,
150
+ envFile: join(AGENTS_DIR, `.env.${name}`),
151
+ });
152
+ return;
153
+ }
154
+ console.error(`Agent "${name}" not found in ${AGENTS_DIR}`);
155
+ console.error("Available agents:", agents.map((a) => a.name).join(", ") || "none");
156
+ process.exit(1);
157
+ }
158
+ await runAgentProcess(agent);
159
+ }
160
+
161
+ const REPO_BASE = "https://raw.githubusercontent.com/f/poke-gate/main/examples/agents";
162
+
163
+ export async function downloadAgent(name) {
164
+ mkdirSync(AGENTS_DIR, { recursive: true });
165
+
166
+ console.log(`Fetching agent "${name}" from GitHub...`);
167
+
168
+ const indexRes = await fetch(`${REPO_BASE}/`).catch(() => null);
169
+
170
+ const jsUrl = `${REPO_BASE}/${name}`;
171
+ const envUrl = `${REPO_BASE}/.env.${name}`;
172
+
173
+ // Try to find the exact file first, or search for name.*.js pattern
174
+ let jsFileName = null;
175
+ let jsContent = null;
176
+
177
+ // Try direct match (user might pass "beeper.1h.js")
178
+ let res = await fetch(`${REPO_BASE}/${name}`).catch(() => null);
179
+ if (res?.ok) {
180
+ jsFileName = name;
181
+ jsContent = await res.text();
182
+ }
183
+
184
+ // Try with .js extension
185
+ if (!jsContent) {
186
+ res = await fetch(`${REPO_BASE}/${name}.js`).catch(() => null);
187
+ if (res?.ok) {
188
+ jsFileName = `${name}.js`;
189
+ jsContent = await res.text();
190
+ }
191
+ }
192
+
193
+ // Try common intervals
194
+ if (!jsContent) {
195
+ for (const interval of ["10m", "30m", "1h", "2h", "6h", "12h", "24h"]) {
196
+ res = await fetch(`${REPO_BASE}/${name}.${interval}.js`).catch(() => null);
197
+ if (res?.ok) {
198
+ jsFileName = `${name}.${interval}.js`;
199
+ jsContent = await res.text();
200
+ break;
201
+ }
202
+ }
203
+ }
204
+
205
+ if (!jsContent) {
206
+ console.error(`Agent "${name}" not found in the repository.`);
207
+ console.error(`Browse available agents: https://github.com/f/poke-gate/tree/main/examples/agents`);
208
+ process.exit(1);
209
+ }
210
+
211
+ const dest = join(AGENTS_DIR, jsFileName);
212
+ writeFileSync(dest, jsContent);
213
+ console.log(` Saved: ${dest}`);
214
+
215
+ // Try to download matching .env file
216
+ const envName = name.split(".")[0];
217
+ const envRes = await fetch(`${REPO_BASE}/.env.${envName}`).catch(() => null);
218
+ 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}`);
226
+ } else {
227
+ console.log(` .env.${envName} already exists, skipped.`);
228
+ }
229
+ }
230
+
231
+ console.log(`\n Test it: npx poke-gate run-agent ${envName}`);
232
+ }
233
+
234
+ export function startAgentScheduler() {
235
+ const agents = discoverAgents();
236
+
237
+ if (agents.length === 0) {
238
+ log("No agents found. Add scripts to ~/.config/poke-gate/agents/");
239
+ return;
240
+ }
241
+
242
+ log(`Found ${agents.length} agent(s):`);
243
+ for (const agent of agents) {
244
+ const interval = agent.intervalToken;
245
+ const hasEnv = existsSync(agent.envFile);
246
+ const desc = agent.meta.name || agent.name;
247
+ log(` ${desc} (every ${interval}${hasEnv ? ", has .env" : ""})`);
248
+ }
249
+
250
+ for (const agent of agents) {
251
+ runAgentProcess(agent);
252
+
253
+ setInterval(() => {
254
+ runAgentProcess(agent);
255
+ }, agent.intervalMs);
256
+ }
257
+ }
package/src/app.js CHANGED
@@ -1,46 +1,35 @@
1
1
  import { startMcpServer, enableLogging } from "./mcp-server.js";
2
2
  import { startTunnel } from "./tunnel.js";
3
- import { Poke } from "poke";
4
- import { readFileSync } from "node:fs";
5
- import { join } from "node:path";
6
- import { homedir } from "node:os";
3
+ import { startAgentScheduler } from "./agents.js";
4
+ import { Poke, isLoggedIn, login, getToken } from "poke";
7
5
 
8
6
  const verbose = process.argv.includes("--verbose") || process.argv.includes("-v");
9
7
  enableLogging(verbose);
10
8
 
11
- function resolveToken() {
12
- if (process.env.POKE_API_KEY) return process.env.POKE_API_KEY;
13
-
14
- const configDir = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
15
-
16
- try {
17
- const cfg = JSON.parse(readFileSync(join(configDir, "poke-gate", "config.json"), "utf-8"));
18
- if (cfg.apiKey) return cfg.apiKey;
19
- } catch {}
20
-
21
- try {
22
- const creds = JSON.parse(readFileSync(join(configDir, "poke", "credentials.json"), "utf-8"));
23
- if (creds.token) return creds.token;
24
- } catch {}
25
-
26
- return null;
27
- }
28
-
29
9
  function log(msg) {
30
10
  const ts = new Date().toISOString().slice(11, 19);
31
11
  console.log(`[${ts}] ${msg}`);
32
12
  }
33
13
 
34
- const API_KEY = resolveToken();
14
+ async function ensureAuthenticated() {
15
+ if (!isLoggedIn()) {
16
+ log("Signing in to Poke...");
17
+ await login();
18
+ }
19
+
20
+ const token = getToken();
21
+ if (!token) {
22
+ throw new Error("Authentication failed: no token returned by Poke SDK.");
23
+ }
35
24
 
36
- if (!API_KEY) {
37
- console.error("No credentials found. Run: npx poke-gate");
38
- process.exit(1);
25
+ return token;
39
26
  }
40
27
 
41
28
  async function main() {
42
29
  log("poke-gate starting...");
43
30
 
31
+ const token = await ensureAuthenticated();
32
+
44
33
  const { port } = await startMcpServer();
45
34
  log(`MCP server on port ${port}`);
46
35
 
@@ -48,15 +37,15 @@ async function main() {
48
37
 
49
38
  log("Connecting tunnel to Poke...");
50
39
  try {
51
- const { info } = await startTunnel({
52
- apiKey: API_KEY,
40
+ await startTunnel({
53
41
  mcpUrl,
54
42
  onEvent: (type, data) => {
55
43
  switch (type) {
56
44
  case "connected":
57
45
  log(`Tunnel connected (${data.connectionId})`);
58
46
  log("Ready — your Poke agent can now access this machine.");
59
- notifyPoke(data.connectionId);
47
+ notifyPoke(data.connectionId, token);
48
+ startAgentScheduler();
60
49
  break;
61
50
  case "disconnected":
62
51
  log("Tunnel disconnected. Reconnecting...");
@@ -79,13 +68,14 @@ async function main() {
79
68
  }
80
69
  }
81
70
 
82
- async function notifyPoke(connectionId) {
71
+ async function notifyPoke(connectionId, token) {
83
72
  try {
84
- const poke = new Poke({ apiKey: API_KEY });
73
+ const poke = new Poke({ token });
85
74
  await poke.sendMessage(
86
- `Poke macOS Gate is connected. Tunnel ID: ${connectionId}. ` +
87
- `You now have access to this machine's terminal, files, and screen. ` +
88
- `Use the available tools (run_command, read_file, write_file, list_directory, system_info, read_image, take_screenshot) to help the user.`
75
+ `Hey! I've connected my computer to you via Poke Gate (tunnel: ${connectionId}). ` +
76
+ `You can now run commands, read and write files, list directories, take screenshots, and check system info on my machine. ` +
77
+ `Just use the tools whenever I ask you to do something on my computer.` +
78
+ `Now reply me in my language "now I am connected to your computer".`
89
79
  );
90
80
  log("Notified Poke agent about connection.");
91
81
  } catch (err) {
package/src/mcp-server.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import http from "node:http";
2
- import { execSync, exec } from "node:child_process";
2
+ import { exec } from "node:child_process";
3
3
  import { readFileSync, writeFileSync, readdirSync, statSync } from "node:fs";
4
4
  import { hostname, platform, arch, uptime, totalmem, freemem, homedir } from "node:os";
5
5
  import { join, resolve, extname } from "node:path";
@@ -108,7 +108,7 @@ const TOOLS = [
108
108
  function runCommand(command, cwd) {
109
109
  return new Promise((res) => {
110
110
  const dir = cwd || homedir();
111
- const child = exec(command, {
111
+ exec(command, {
112
112
  cwd: dir,
113
113
  timeout: COMMAND_TIMEOUT,
114
114
  maxBuffer: 1024 * 1024,
@@ -348,7 +348,7 @@ export function startMcpServer(port = 0) {
348
348
  res.end();
349
349
  }
350
350
  }
351
- } catch (err) {
351
+ } catch {
352
352
  res.writeHead(400, { "Content-Type": "application/json" });
353
353
  res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32700, message: "Parse error" }, id: null }));
354
354
  }
package/src/tunnel.js CHANGED
@@ -19,11 +19,12 @@ function saveState(state) {
19
19
  writeFileSync(STATE_PATH, JSON.stringify(state, null, 2));
20
20
  }
21
21
 
22
- async function cleanupOldConnection(apiKey) {
22
+ async function cleanupOldConnection() {
23
23
  const state = loadState();
24
24
  if (!state.connectionId) return;
25
25
 
26
- const token = getToken() || apiKey;
26
+ const token = getToken();
27
+ if (!token) return;
27
28
  const base = process.env.POKE_API ?? "https://poke.com/api/v1";
28
29
 
29
30
  try {
@@ -34,15 +35,18 @@ async function cleanupOldConnection(apiKey) {
34
35
  } catch {}
35
36
  }
36
37
 
37
- export async function startTunnel({ apiKey, mcpUrl, onEvent }) {
38
- await cleanupOldConnection(apiKey);
38
+ export async function startTunnel({ mcpUrl, onEvent }) {
39
+ await cleanupOldConnection();
39
40
 
40
41
  const token = getToken();
42
+ if (!token) {
43
+ throw new Error("No Poke auth token available for tunnel.");
44
+ }
41
45
 
42
46
  const tunnel = new PokeTunnel({
43
47
  url: mcpUrl,
44
48
  name: "poke-gate",
45
- token: token || apiKey,
49
+ token,
46
50
  cleanupOnStop: false,
47
51
  });
48
52