Skip to content

Commit 858621a

Browse files
committed
v0.1.0 — Agents: scheduled scripts with per-agent env files
- Agent scheduler: scans ~/.config/poke-gate/agents/ for *.js files - Naming convention: name.10m.js, name.1h.js, name.2h.js (min 10 minutes) - Per-agent .env files: .env.beeper, .env.backup etc. - Manual trigger: npx poke-gate run-agent <name> - Download agents: npx poke-gate agent get <name> - Example: beeper.1h.js fetches last hour messages and sends summary to Poke - Agents start automatically when tunnel connects Made-with: Cursor
1 parent 785427d commit 858621a

7 files changed

Lines changed: 372 additions & 2 deletions

File tree

bin/poke-gate.js

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,29 @@
11
#!/usr/bin/env node
22

3+
const args = process.argv.slice(2);
4+
35
async function main() {
4-
await import("../src/app.js");
6+
if (args[0] === "run-agent") {
7+
const name = args[1];
8+
if (!name) {
9+
console.error("Usage: poke-gate run-agent <name>");
10+
console.error("Example: poke-gate run-agent beeper");
11+
process.exit(1);
12+
}
13+
const { runAgent } = await import("../src/agents.js");
14+
await runAgent(name);
15+
} else if (args[0] === "agent" && args[1] === "get") {
16+
const name = args[2];
17+
if (!name) {
18+
console.error("Usage: poke-gate agent get <name>");
19+
console.error("Example: poke-gate agent get beeper");
20+
process.exit(1);
21+
}
22+
const { downloadAgent } = await import("../src/agents.js");
23+
await downloadAgent(name);
24+
} else {
25+
await import("../src/app.js");
26+
}
527
}
628

729
main();

examples/agents/.env.beeper

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Beeper Desktop local API token
2+
# Find it in Beeper Desktop > Settings > API
3+
BEEPER_TOKEN=your_beeper_access_token_here
4+
5+
# Optional: override the default Beeper API URL
6+
# BEEPER_BASE_URL=http://localhost:23373

examples/agents/beeper.1h.js

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/**
2+
* Beeper Agent — runs every 1 hour
3+
*
4+
* Fetches messages from the last hour via Beeper Desktop's local API,
5+
* groups them by sender, and sends a summary to your Poke agent.
6+
*
7+
* Setup:
8+
* 1. Copy this file to ~/.config/poke-gate/agents/beeper.1h.js
9+
* 2. Create ~/.config/poke-gate/agents/.env.beeper with:
10+
* BEEPER_TOKEN=your_beeper_access_token
11+
* 3. Run: npx poke-gate run-agent beeper (to test)
12+
*
13+
* The token is from Beeper Desktop's local API (localhost:23373).
14+
* You can find it in Beeper Desktop > Settings > API.
15+
*/
16+
17+
import { Poke, getToken } from "poke";
18+
19+
const BEEPER_BASE = process.env.BEEPER_BASE_URL || "http://localhost:23373";
20+
const BEEPER_TOKEN = process.env.BEEPER_TOKEN;
21+
22+
if (!BEEPER_TOKEN) {
23+
console.error("BEEPER_TOKEN not set. Create ~/.config/poke-gate/agents/.env.beeper");
24+
process.exit(1);
25+
}
26+
27+
async function beeperRequest(path, params = {}) {
28+
const url = new URL(BEEPER_BASE + path);
29+
for (const [key, value] of Object.entries(params)) {
30+
if (value !== undefined) url.searchParams.set(key, String(value));
31+
}
32+
const res = await fetch(url, {
33+
headers: {
34+
Authorization: `Bearer ${BEEPER_TOKEN}`,
35+
Accept: "application/json",
36+
},
37+
});
38+
if (!res.ok) throw new Error(`Beeper API ${res.status}: ${await res.text()}`);
39+
return res.json();
40+
}
41+
42+
async function getRecentMessages() {
43+
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString();
44+
45+
const data = await beeperRequest("/v1/messages/search", {
46+
dateAfter: oneHourAgo,
47+
limit: 200,
48+
});
49+
50+
return data.items || [];
51+
}
52+
53+
function groupBySender(messages) {
54+
const groups = {};
55+
for (const msg of messages) {
56+
if (msg.isSender) continue;
57+
const name = msg.senderName || msg.senderID || "Unknown";
58+
if (!groups[name]) groups[name] = [];
59+
if (msg.text) groups[name].push(msg.text);
60+
}
61+
return groups;
62+
}
63+
64+
function buildSummary(groups) {
65+
const senders = Object.keys(groups);
66+
if (senders.length === 0) return null;
67+
68+
let summary = `Messages from the last hour (${senders.length} people):\n\n`;
69+
70+
for (const [sender, messages] of Object.entries(groups)) {
71+
summary += `${sender} (${messages.length} messages):\n`;
72+
for (const text of messages.slice(-3)) {
73+
const preview = text.length > 100 ? text.slice(0, 100) + "…" : text;
74+
summary += ` - ${preview}\n`;
75+
}
76+
summary += "\n";
77+
}
78+
79+
return summary.trim();
80+
}
81+
82+
async function main() {
83+
console.log("Fetching messages from the last hour...");
84+
85+
const messages = await getRecentMessages();
86+
console.log(`Found ${messages.length} messages`);
87+
88+
const groups = groupBySender(messages);
89+
const summary = buildSummary(groups);
90+
91+
if (!summary) {
92+
console.log("No new messages from others in the last hour.");
93+
return;
94+
}
95+
96+
console.log("Sending summary to Poke...");
97+
98+
const token = getToken();
99+
if (!token) {
100+
console.error("Not logged in to Poke. Run: npx poke login");
101+
process.exit(1);
102+
}
103+
104+
const poke = new Poke({ apiKey: token });
105+
await poke.sendMessage(
106+
`Here's a summary of my Beeper messages from the last hour:\n\n${summary}`
107+
);
108+
109+
console.log("Summary sent to Poke.");
110+
}
111+
112+
main().catch((err) => {
113+
console.error("Agent error:", err.message);
114+
process.exit(1);
115+
});

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "poke-gate",
3-
"version": "0.0.9",
3+
"version": "0.1.0",
44
"description": "Expose your machine to your Poke AI assistant via MCP tunnel",
55
"type": "module",
66
"bin": {

src/agents.js

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
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 parseEnvFile(filePath) {
27+
const env = {};
28+
if (!existsSync(filePath)) return env;
29+
const lines = readFileSync(filePath, "utf-8").split("\n");
30+
for (const line of lines) {
31+
const trimmed = line.trim();
32+
if (!trimmed || trimmed.startsWith("#")) continue;
33+
const eqIdx = trimmed.indexOf("=");
34+
if (eqIdx === -1) continue;
35+
const key = trimmed.slice(0, eqIdx).trim();
36+
let value = trimmed.slice(eqIdx + 1).trim();
37+
if ((value.startsWith('"') && value.endsWith('"')) ||
38+
(value.startsWith("'") && value.endsWith("'"))) {
39+
value = value.slice(1, -1);
40+
}
41+
env[key] = value;
42+
}
43+
return env;
44+
}
45+
46+
export function discoverAgents() {
47+
if (!existsSync(AGENTS_DIR)) {
48+
mkdirSync(AGENTS_DIR, { recursive: true });
49+
return [];
50+
}
51+
52+
const files = readdirSync(AGENTS_DIR).filter((f) => f.endsWith(".js"));
53+
const agents = [];
54+
55+
for (const file of files) {
56+
const parts = file.replace(/\.js$/, "").split(".");
57+
if (parts.length < 2) continue;
58+
59+
const intervalToken = parts[parts.length - 1];
60+
const name = parts.slice(0, -1).join(".");
61+
const intervalMs = parseInterval(intervalToken);
62+
63+
if (!intervalMs) {
64+
log(`Skipping ${file}: invalid or too short interval (min 10m)`);
65+
continue;
66+
}
67+
68+
agents.push({
69+
name,
70+
file,
71+
path: join(AGENTS_DIR, file),
72+
intervalToken,
73+
intervalMs,
74+
envFile: join(AGENTS_DIR, `.env.${name}`),
75+
});
76+
}
77+
78+
return agents;
79+
}
80+
81+
function runAgentProcess(agent) {
82+
const agentEnv = parseEnvFile(agent.envFile);
83+
const env = { ...process.env, ...agentEnv };
84+
85+
log(`Running agent: ${agent.name} (${agent.file})`);
86+
87+
return new Promise((resolve) => {
88+
exec(`node "${agent.path}"`, {
89+
env,
90+
timeout: 5 * 60 * 1000,
91+
maxBuffer: 1024 * 1024,
92+
cwd: AGENTS_DIR,
93+
}, (error, stdout, stderr) => {
94+
if (stdout.trim()) log(`[${agent.name}] ${stdout.trim()}`);
95+
if (stderr.trim()) log(`[${agent.name}] stderr: ${stderr.trim()}`);
96+
if (error) log(`[${agent.name}] exited with code ${error.code ?? 1}`);
97+
else log(`[${agent.name}] completed`);
98+
resolve();
99+
});
100+
});
101+
}
102+
103+
export async function runAgent(name) {
104+
const agents = discoverAgents();
105+
const agent = agents.find((a) => a.name === name);
106+
if (!agent) {
107+
const allFiles = readdirSync(AGENTS_DIR).filter((f) => f.endsWith(".js"));
108+
const match = allFiles.find((f) => f.startsWith(name + "."));
109+
if (match) {
110+
const parts = match.replace(/\.js$/, "").split(".");
111+
const intervalToken = parts[parts.length - 1];
112+
const intervalMs = parseInterval(intervalToken);
113+
await runAgentProcess({
114+
name,
115+
file: match,
116+
path: join(AGENTS_DIR, match),
117+
intervalToken,
118+
intervalMs: intervalMs || 0,
119+
envFile: join(AGENTS_DIR, `.env.${name}`),
120+
});
121+
return;
122+
}
123+
console.error(`Agent "${name}" not found in ${AGENTS_DIR}`);
124+
console.error("Available agents:", agents.map((a) => a.name).join(", ") || "none");
125+
process.exit(1);
126+
}
127+
await runAgentProcess(agent);
128+
}
129+
130+
const REPO_BASE = "https://raw.githubusercontent.com/f/poke-gate/main/examples/agents";
131+
132+
export async function downloadAgent(name) {
133+
mkdirSync(AGENTS_DIR, { recursive: true });
134+
135+
console.log(`Fetching agent "${name}" from GitHub...`);
136+
137+
const indexRes = await fetch(`${REPO_BASE}/`).catch(() => null);
138+
139+
const jsUrl = `${REPO_BASE}/${name}`;
140+
const envUrl = `${REPO_BASE}/.env.${name}`;
141+
142+
// Try to find the exact file first, or search for name.*.js pattern
143+
let jsFileName = null;
144+
let jsContent = null;
145+
146+
// Try direct match (user might pass "beeper.1h.js")
147+
let res = await fetch(`${REPO_BASE}/${name}`).catch(() => null);
148+
if (res?.ok) {
149+
jsFileName = name;
150+
jsContent = await res.text();
151+
}
152+
153+
// Try with .js extension
154+
if (!jsContent) {
155+
res = await fetch(`${REPO_BASE}/${name}.js`).catch(() => null);
156+
if (res?.ok) {
157+
jsFileName = `${name}.js`;
158+
jsContent = await res.text();
159+
}
160+
}
161+
162+
// Try common intervals
163+
if (!jsContent) {
164+
for (const interval of ["10m", "30m", "1h", "2h", "6h", "12h", "24h"]) {
165+
res = await fetch(`${REPO_BASE}/${name}.${interval}.js`).catch(() => null);
166+
if (res?.ok) {
167+
jsFileName = `${name}.${interval}.js`;
168+
jsContent = await res.text();
169+
break;
170+
}
171+
}
172+
}
173+
174+
if (!jsContent) {
175+
console.error(`Agent "${name}" not found in the repository.`);
176+
console.error(`Browse available agents: https://github.com/f/poke-gate/tree/main/examples/agents`);
177+
process.exit(1);
178+
}
179+
180+
const dest = join(AGENTS_DIR, jsFileName);
181+
writeFileSync(dest, jsContent);
182+
console.log(` Saved: ${dest}`);
183+
184+
// Try to download matching .env file
185+
const envName = name.split(".")[0];
186+
const envRes = await fetch(`${REPO_BASE}/.env.${envName}`).catch(() => null);
187+
if (envRes?.ok) {
188+
const envContent = await envRes.text();
189+
const envDest = join(AGENTS_DIR, `.env.${envName}`);
190+
if (!existsSync(envDest)) {
191+
writeFileSync(envDest, envContent);
192+
console.log(` Saved: ${envDest}`);
193+
console.log(`\n Edit the env file with your credentials:`);
194+
console.log(` nano ${envDest}`);
195+
} else {
196+
console.log(` .env.${envName} already exists, skipped.`);
197+
}
198+
}
199+
200+
console.log(`\n Test it: npx poke-gate run-agent ${envName}`);
201+
}
202+
203+
export function startAgentScheduler() {
204+
const agents = discoverAgents();
205+
206+
if (agents.length === 0) {
207+
log("No agents found. Add scripts to ~/.config/poke-gate/agents/");
208+
return;
209+
}
210+
211+
log(`Found ${agents.length} agent(s):`);
212+
for (const agent of agents) {
213+
const interval = agent.intervalToken;
214+
const hasEnv = existsSync(agent.envFile);
215+
log(` ${agent.name} (every ${interval}${hasEnv ? ", has .env" : ""})`);
216+
}
217+
218+
for (const agent of agents) {
219+
runAgentProcess(agent);
220+
221+
setInterval(() => {
222+
runAgentProcess(agent);
223+
}, agent.intervalMs);
224+
}
225+
}

0 commit comments

Comments
 (0)