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/.github/workflows/docker.yml +41 -0
- package/.prettierrc +7 -0
- package/.remarkignore +3 -0
- package/.remarkrc.mjs +13 -0
- package/CODE_OF_CONDUCT.md +38 -0
- package/CONTRIBUTING.md +32 -0
- package/Dockerfile +13 -0
- package/README.md +94 -2
- package/SECURITY.md +18 -0
- package/bin/poke-gate.js +21 -93
- package/clients/Poke macOS Gate/Poke macOS Gate/AboutView.swift +54 -0
- package/clients/Poke macOS Gate/Poke macOS Gate/GateService.swift +10 -0
- package/clients/Poke macOS Gate/Poke macOS Gate/LogsView.swift +45 -11
- package/clients/Poke macOS Gate/Poke macOS Gate/Poke_macOS_GateApp.swift +143 -75
- package/clients/Poke macOS Gate/Poke macOS Gate/SettingsView.swift +76 -71
- package/clients/Poke macOS Gate/Poke macOS Gate.xcodeproj/project.pbxproj +2 -2
- package/clients/Poke macOS Gate/Poke macOS Gate.xcodeproj/project.xcworkspace/xcuserdata/fka.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- package/eslint.config.mjs +23 -0
- package/examples/agents/.env.beeper +6 -0
- package/examples/agents/beeper.1h.js +109 -0
- package/package.json +19 -2
- package/src/agents.js +257 -0
- package/src/app.js +24 -34
- package/src/mcp-server.js +3 -3
- package/src/tunnel.js +9 -5
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 {
|
|
4
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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({
|
|
73
|
+
const poke = new Poke({ token });
|
|
85
74
|
await poke.sendMessage(
|
|
86
|
-
`
|
|
87
|
-
`You now
|
|
88
|
-
`
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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(
|
|
22
|
+
async function cleanupOldConnection() {
|
|
23
23
|
const state = loadState();
|
|
24
24
|
if (!state.connectionId) return;
|
|
25
25
|
|
|
26
|
-
const token = getToken()
|
|
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({
|
|
38
|
-
await cleanupOldConnection(
|
|
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
|
|
49
|
+
token,
|
|
46
50
|
cleanupOnStop: false,
|
|
47
51
|
});
|
|
48
52
|
|