poke-gate 0.0.9 → 0.1.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 CHANGED
@@ -44,11 +44,11 @@ npx poke-gate
44
44
 
45
45
  ## Setup
46
46
 
47
- 1. Open Poke Gate from your menu bar
48
- 2. Click **Start** (or let auto-start run)
49
- 3. If needed, complete the Poke OAuth sign-in flow in your browser
47
+ 1. Get an API key from [poke.com/kitchen/api-keys](https://poke.com/kitchen/api-keys)
48
+ 2. Open Poke Gate from your menu bar and go to **Settings**
49
+ 3. Paste your API key and save
50
50
 
51
- The app connects automatically after sign-in and shows a green dot when ready.
51
+ The app connects automatically and shows a green dot when ready.
52
52
 
53
53
  ## How it works
54
54
 
@@ -94,10 +94,10 @@ From iMessage or Telegram, ask Poke:
94
94
  The menu bar app manages everything:
95
95
 
96
96
  - **Status** — green dot when connected, yellow when connecting, red on error
97
- - **Personalized** — shows "Connected to your Poke, <name>"
97
+ - **Personalized** — shows "Connected to your Poke, [name]"
98
98
  - **Auto-start** — connects on launch if API key is saved
99
99
  - **Auto-restart** — reconnects automatically if the connection drops
100
- - **Settings** — auth status and reconnect controls
100
+ - **Settings** — paste your API key
101
101
  - **Logs** — view real-time tool calls and connection events
102
102
  - **Screen Recording** — prompts for permission on first launch
103
103
 
@@ -127,19 +127,108 @@ If you prefer the command line over the macOS app:
127
127
  npx poke-gate
128
128
  ```
129
129
 
130
- On first run, if you're not signed in, Poke Gate opens OAuth login automatically. Add `--verbose` to see tool calls in real time:
130
+ On first run, paste your API key when prompted. Add `--verbose` to see tool calls in real time:
131
131
 
132
132
  ```bash
133
133
  npx poke-gate --verbose
134
134
  ```
135
135
 
136
+ Config is stored at `~/.config/poke-gate/config.json`.
137
+
138
+ ## Agents
139
+
140
+ Agents are scheduled scripts that run automatically in the background. They live in `~/.config/poke-gate/agents/` and follow a simple naming convention:
141
+
142
+ ```
143
+ <name>.<interval>.js
144
+ ```
145
+
146
+ | File | Runs |
147
+ |------|------|
148
+ | `beeper.1h.js` | Every hour |
149
+ | `backup.2h.js` | Every 2 hours |
150
+ | `health.10m.js` | Every 10 minutes |
151
+ | `cleanup.30m.js` | Every 30 minutes |
152
+
153
+ Intervals: `Nm` (minutes) or `Nh` (hours). Minimum is 10 minutes.
154
+
155
+ ### Install an agent
156
+
157
+ Download a community agent from the repository:
158
+
159
+ ```bash
160
+ npx poke-gate agent get beeper
161
+ ```
162
+
163
+ This downloads `beeper.1h.js` and `.env.beeper` to `~/.config/poke-gate/agents/`. Edit the env file with your credentials and test it:
164
+
165
+ ```bash
166
+ nano ~/.config/poke-gate/agents/.env.beeper
167
+ npx poke-gate run-agent beeper
168
+ ```
169
+
170
+ ### Per-agent env files
171
+
172
+ Each agent can have a `.env.<name>` file for secrets:
173
+
174
+ ```
175
+ ~/.config/poke-gate/agents/.env.beeper
176
+ ```
177
+
178
+ ```env
179
+ BEEPER_TOKEN=your_token_here
180
+ ```
181
+
182
+ Variables are injected into the agent process automatically.
183
+
184
+ ### Agent frontmatter
185
+
186
+ Each agent file starts with a JSDoc-style frontmatter block:
187
+
188
+ ```javascript
189
+ /**
190
+ * @agent beeper
191
+ * @name Beeper Message Digest
192
+ * @description Fetches messages from the last hour and sends a summary to Poke.
193
+ * @interval 1h
194
+ * @env BEEPER_TOKEN - Beeper Desktop local API token
195
+ * @author f
196
+ */
197
+ ```
198
+
199
+ ### Creating your own agent
200
+
201
+ An agent is just a JS file that runs with Node.js. It has access to:
202
+
203
+ - `process.env` — variables from `.env.<name>`
204
+ - `poke` package — `import { Poke, getToken } from "poke"`
205
+ - Any npm package installed globally or via npx
206
+
207
+ ```javascript
208
+ /**
209
+ * @agent my-agent
210
+ * @name My Custom Agent
211
+ * @description Does something useful every 30 minutes.
212
+ * @interval 30m
213
+ */
214
+
215
+ import { Poke, getToken } from "poke";
216
+
217
+ const poke = new Poke({ apiKey: getToken() });
218
+ await poke.sendMessage("Hello from my agent!");
219
+ ```
220
+
221
+ Save as `~/.config/poke-gate/agents/my-agent.30m.js` and it runs automatically when poke-gate is connected.
222
+
223
+ Agents start running when poke-gate connects and run once immediately on startup.
224
+
136
225
  ## Security
137
226
 
138
227
  **Poke Gate grants full shell access to your Poke agent.** This means:
139
228
 
140
229
  - Any command can be run with your user's permissions
141
230
  - Files can be read and written anywhere your user has access
142
- - Only your authenticated Poke agent can reach the tunnel
231
+ - Only your Poke agent (authenticated by your API key) can reach the tunnel
143
232
 
144
233
  Only run Poke Gate on machines and networks you trust.
145
234
 
@@ -149,11 +238,16 @@ Only run Poke Gate on machines and networks you trust.
149
238
  clients/
150
239
  Poke macOS Gate/ macOS menu bar app (SwiftUI)
151
240
  bin/
152
- poke-gate.js CLI entry point + onboarding
241
+ poke-gate.js CLI entry point, run-agent subcommand
153
242
  src/
154
- app.js Startup: MCP server + tunnel
243
+ app.js Startup: MCP server + tunnel + agent scheduler
244
+ agents.js Agent discovery, scheduling, and runner
155
245
  mcp-server.js JSON-RPC MCP handler with OS tools
156
246
  tunnel.js PokeTunnel wrapper
247
+ examples/
248
+ agents/
249
+ beeper.1h.js Example: Beeper message digest
250
+ .env.beeper Example env file
157
251
  ```
158
252
 
159
253
  ## Credits
package/bin/poke-gate.js CHANGED
@@ -1,7 +1,29 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ const args = process.argv.slice(2);
4
+
3
5
  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
+ }
5
27
  }
6
28
 
7
29
  main();
@@ -264,7 +264,7 @@
264
264
  "$(inherited)",
265
265
  "@executable_path/../Frameworks",
266
266
  );
267
- MARKETING_VERSION = 0.0.8;
267
+ MARKETING_VERSION = 0.1.0;
268
268
  PRODUCT_BUNDLE_IDENTIFIER = "dev.fka.Poke-macOS-Gate";
269
269
  PRODUCT_NAME = "$(TARGET_NAME)";
270
270
  REGISTER_APP_GROUPS = YES;
@@ -296,7 +296,7 @@
296
296
  "$(inherited)",
297
297
  "@executable_path/../Frameworks",
298
298
  );
299
- MARKETING_VERSION = 0.0.8;
299
+ MARKETING_VERSION = 0.1.0;
300
300
  PRODUCT_BUNDLE_IDENTIFIER = "dev.fka.Poke-macOS-Gate";
301
301
  PRODUCT_NAME = "$(TARGET_NAME)";
302
302
  REGISTER_APP_GROUPS = YES;
@@ -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
@@ -0,0 +1,117 @@
1
+ /**
2
+ * @agent beeper
3
+ * @name Beeper Message Digest
4
+ * @description Fetches messages from the last hour via Beeper Desktop and sends a summary to Poke.
5
+ * @interval 1h
6
+ * @env BEEPER_TOKEN - Beeper Desktop local API token (Settings > API)
7
+ * @env BEEPER_BASE_URL - (optional) Override default http://localhost:23373
8
+ * @author f
9
+ */
10
+
11
+ import { Poke, getToken } from "poke";
12
+
13
+ const BEEPER_BASE = process.env.BEEPER_BASE_URL || "http://localhost:23373";
14
+ const BEEPER_TOKEN = process.env.BEEPER_TOKEN;
15
+
16
+ if (!BEEPER_TOKEN) {
17
+ console.error("BEEPER_TOKEN not set. Create ~/.config/poke-gate/agents/.env.beeper");
18
+ process.exit(1);
19
+ }
20
+
21
+ async function beeperRequest(path, params = {}) {
22
+ const url = new URL(BEEPER_BASE + path);
23
+ for (const [key, value] of Object.entries(params)) {
24
+ if (value !== undefined) url.searchParams.set(key, String(value));
25
+ }
26
+ const res = await fetch(url, {
27
+ headers: {
28
+ Authorization: `Bearer ${BEEPER_TOKEN}`,
29
+ Accept: "application/json",
30
+ },
31
+ });
32
+ if (!res.ok) throw new Error(`Beeper API ${res.status}: ${await res.text()}`);
33
+ return res.json();
34
+ }
35
+
36
+ async function getRecentMessages() {
37
+ const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString();
38
+
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
+ }
51
+
52
+ return allMessages;
53
+ }
54
+
55
+ function groupBySender(messages) {
56
+ const groups = {};
57
+ for (const msg of messages) {
58
+ if (msg.isSender) continue;
59
+ const name = msg.senderName || msg.senderID || "Unknown";
60
+ if (!groups[name]) groups[name] = [];
61
+ if (msg.text) groups[name].push(msg.text);
62
+ }
63
+ return groups;
64
+ }
65
+
66
+ function buildSummary(groups) {
67
+ const senders = Object.keys(groups);
68
+ if (senders.length === 0) return null;
69
+
70
+ let summary = `Messages from the last hour (${senders.length} people):\n\n`;
71
+
72
+ for (const [sender, messages] of Object.entries(groups)) {
73
+ summary += `${sender} (${messages.length} messages):\n`;
74
+ for (const text of messages.slice(-3)) {
75
+ const preview = text.length > 100 ? text.slice(0, 100) + "…" : text;
76
+ summary += ` - ${preview}\n`;
77
+ }
78
+ summary += "\n";
79
+ }
80
+
81
+ return summary.trim();
82
+ }
83
+
84
+ async function main() {
85
+ console.log("Fetching messages from the last hour...");
86
+
87
+ const messages = await getRecentMessages();
88
+ console.log(`Found ${messages.length} messages`);
89
+
90
+ const groups = groupBySender(messages);
91
+ const summary = buildSummary(groups);
92
+
93
+ if (!summary) {
94
+ console.log("No new messages from others in the last hour.");
95
+ return;
96
+ }
97
+
98
+ console.log("Sending summary to Poke...");
99
+
100
+ const token = getToken();
101
+ if (!token) {
102
+ console.error("Not logged in to Poke. Run: npx poke login");
103
+ process.exit(1);
104
+ }
105
+
106
+ const poke = new Poke({ apiKey: token });
107
+ await poke.sendMessage(
108
+ `Here's a summary of my Beeper messages from the last hour:\n\n${summary}`
109
+ );
110
+
111
+ console.log("Summary sent to Poke.");
112
+ }
113
+
114
+ main().catch((err) => {
115
+ console.error("Agent error:", err.message);
116
+ process.exit(1);
117
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poke-gate",
3
- "version": "0.0.9",
3
+ "version": "0.1.1",
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 ADDED
@@ -0,0 +1,335 @@
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
+ import { createInterface } from "node:readline";
6
+
7
+ const CONFIG_DIR = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
8
+ const AGENTS_DIR = join(CONFIG_DIR, "poke-gate", "agents");
9
+
10
+ const MIN_INTERVAL_MS = 10 * 60 * 1000;
11
+
12
+ function log(msg) {
13
+ const ts = new Date().toISOString().slice(11, 19);
14
+ console.log(`[${ts}] [agents] ${msg}`);
15
+ }
16
+
17
+ function parseInterval(token) {
18
+ const match = token.match(/^(\d+)(m|h)$/);
19
+ if (!match) return null;
20
+ const value = parseInt(match[1], 10);
21
+ const unit = match[2];
22
+ const ms = unit === "h" ? value * 60 * 60 * 1000 : value * 60 * 1000;
23
+ if (ms < MIN_INTERVAL_MS) return null;
24
+ return ms;
25
+ }
26
+
27
+ function parseFrontmatter(filePath) {
28
+ try {
29
+ const content = readFileSync(filePath, "utf-8");
30
+ const match = content.match(/\/\*\*[\s\S]*?\*\//);
31
+ if (!match) return {};
32
+ const block = match[0];
33
+ const meta = {};
34
+ const lines = block.split("\n");
35
+ for (const line of lines) {
36
+ const m = line.match(/@(\w+)\s+(.*)/);
37
+ if (m) {
38
+ const key = m[1].trim();
39
+ const value = m[2].replace(/\*\/$/, "").trim();
40
+ if (key === "env") {
41
+ if (!meta.env) meta.env = [];
42
+ meta.env.push(value);
43
+ } else {
44
+ meta[key] = value;
45
+ }
46
+ }
47
+ }
48
+ return meta;
49
+ } catch {
50
+ return {};
51
+ }
52
+ }
53
+
54
+ function parseEnvFile(filePath) {
55
+ const env = {};
56
+ if (!existsSync(filePath)) return env;
57
+ const lines = readFileSync(filePath, "utf-8").split("\n");
58
+ for (const line of lines) {
59
+ const trimmed = line.trim();
60
+ if (!trimmed || trimmed.startsWith("#")) continue;
61
+ const eqIdx = trimmed.indexOf("=");
62
+ if (eqIdx === -1) continue;
63
+ const key = trimmed.slice(0, eqIdx).trim();
64
+ let value = trimmed.slice(eqIdx + 1).trim();
65
+ if ((value.startsWith('"') && value.endsWith('"')) ||
66
+ (value.startsWith("'") && value.endsWith("'"))) {
67
+ value = value.slice(1, -1);
68
+ }
69
+ env[key] = value;
70
+ }
71
+ return env;
72
+ }
73
+
74
+ export function discoverAgents() {
75
+ if (!existsSync(AGENTS_DIR)) {
76
+ mkdirSync(AGENTS_DIR, { recursive: true });
77
+ return [];
78
+ }
79
+
80
+ const files = readdirSync(AGENTS_DIR).filter((f) => f.endsWith(".js"));
81
+ const agents = [];
82
+
83
+ for (const file of files) {
84
+ const parts = file.replace(/\.js$/, "").split(".");
85
+ if (parts.length < 2) continue;
86
+
87
+ const intervalToken = parts[parts.length - 1];
88
+ const name = parts.slice(0, -1).join(".");
89
+ const intervalMs = parseInterval(intervalToken);
90
+
91
+ if (!intervalMs) {
92
+ log(`Skipping ${file}: invalid or too short interval (min 10m)`);
93
+ continue;
94
+ }
95
+
96
+ const agentPath = join(AGENTS_DIR, file);
97
+ const meta = parseFrontmatter(agentPath);
98
+
99
+ agents.push({
100
+ name,
101
+ file,
102
+ path: agentPath,
103
+ intervalToken,
104
+ intervalMs,
105
+ envFile: join(AGENTS_DIR, `.env.${name}`),
106
+ meta,
107
+ });
108
+ }
109
+
110
+ return agents;
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
+
132
+ function runAgentProcess(agent) {
133
+ const agentEnv = parseEnvFile(agent.envFile);
134
+ const env = { ...process.env, ...agentEnv };
135
+
136
+ ensureNodeModulesLink();
137
+
138
+ log(`Running agent: ${agent.name} (${agent.file})`);
139
+
140
+ return new Promise((resolve) => {
141
+ exec(`node "${agent.path}"`, {
142
+ env,
143
+ timeout: 5 * 60 * 1000,
144
+ maxBuffer: 1024 * 1024,
145
+ cwd: AGENTS_DIR,
146
+ }, (error, stdout, stderr) => {
147
+ if (stdout.trim()) log(`[${agent.name}] ${stdout.trim()}`);
148
+ if (stderr.trim()) log(`[${agent.name}] stderr: ${stderr.trim()}`);
149
+ if (error) log(`[${agent.name}] exited with code ${error.code ?? 1}`);
150
+ else log(`[${agent.name}] completed`);
151
+ resolve();
152
+ });
153
+ });
154
+ }
155
+
156
+ export async function runAgent(name) {
157
+ const agents = discoverAgents();
158
+ const agent = agents.find((a) => a.name === name);
159
+ if (!agent) {
160
+ const allFiles = readdirSync(AGENTS_DIR).filter((f) => f.endsWith(".js"));
161
+ const match = allFiles.find((f) => f.startsWith(name + "."));
162
+ if (match) {
163
+ const parts = match.replace(/\.js$/, "").split(".");
164
+ const intervalToken = parts[parts.length - 1];
165
+ const intervalMs = parseInterval(intervalToken);
166
+ await runAgentProcess({
167
+ name,
168
+ file: match,
169
+ path: join(AGENTS_DIR, match),
170
+ intervalToken,
171
+ intervalMs: intervalMs || 0,
172
+ envFile: join(AGENTS_DIR, `.env.${name}`),
173
+ });
174
+ return;
175
+ }
176
+ console.error(`Agent "${name}" not found in ${AGENTS_DIR}`);
177
+ console.error("Available agents:", agents.map((a) => a.name).join(", ") || "none");
178
+ process.exit(1);
179
+ }
180
+ await runAgentProcess(agent);
181
+ }
182
+
183
+ const REPO_BASE = "https://raw.githubusercontent.com/f/poke-gate/main/examples/agents";
184
+
185
+ export async function downloadAgent(name) {
186
+ mkdirSync(AGENTS_DIR, { recursive: true });
187
+
188
+ console.log(`Fetching agent "${name}" from GitHub...`);
189
+
190
+ const indexRes = await fetch(`${REPO_BASE}/`).catch(() => null);
191
+
192
+ const jsUrl = `${REPO_BASE}/${name}`;
193
+ const envUrl = `${REPO_BASE}/.env.${name}`;
194
+
195
+ // Try to find the exact file first, or search for name.*.js pattern
196
+ let jsFileName = null;
197
+ let jsContent = null;
198
+
199
+ // Try direct match (user might pass "beeper.1h.js")
200
+ let res = await fetch(`${REPO_BASE}/${name}`).catch(() => null);
201
+ if (res?.ok) {
202
+ jsFileName = name;
203
+ jsContent = await res.text();
204
+ }
205
+
206
+ // Try with .js extension
207
+ if (!jsContent) {
208
+ res = await fetch(`${REPO_BASE}/${name}.js`).catch(() => null);
209
+ if (res?.ok) {
210
+ jsFileName = `${name}.js`;
211
+ jsContent = await res.text();
212
+ }
213
+ }
214
+
215
+ // Try common intervals
216
+ if (!jsContent) {
217
+ for (const interval of ["10m", "30m", "1h", "2h", "6h", "12h", "24h"]) {
218
+ res = await fetch(`${REPO_BASE}/${name}.${interval}.js`).catch(() => null);
219
+ if (res?.ok) {
220
+ jsFileName = `${name}.${interval}.js`;
221
+ jsContent = await res.text();
222
+ break;
223
+ }
224
+ }
225
+ }
226
+
227
+ if (!jsContent) {
228
+ console.error(`Agent "${name}" not found in the repository.`);
229
+ console.error(`Browse available agents: https://github.com/f/poke-gate/tree/main/examples/agents`);
230
+ process.exit(1);
231
+ }
232
+
233
+ const dest = join(AGENTS_DIR, jsFileName);
234
+ writeFileSync(dest, jsContent);
235
+ console.log(` Saved: ${dest}`);
236
+
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
+
246
+ const envRes = await fetch(`${REPO_BASE}/.env.${envName}`).catch(() => null);
247
+ if (envRes?.ok) {
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}`);
261
+ } else {
262
+ writeFileSync(envDest, envTemplate);
263
+ console.log(` Saved: ${envDest}`);
264
+ }
265
+ }
266
+
267
+ console.log(`\n Test it: npx poke-gate run-agent ${envName}`);
268
+ }
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
+ export function startAgentScheduler() {
313
+ const agents = discoverAgents();
314
+
315
+ if (agents.length === 0) {
316
+ log("No agents found. Add scripts to ~/.config/poke-gate/agents/");
317
+ return;
318
+ }
319
+
320
+ log(`Found ${agents.length} agent(s):`);
321
+ for (const agent of agents) {
322
+ const interval = agent.intervalToken;
323
+ const hasEnv = existsSync(agent.envFile);
324
+ const desc = agent.meta.name || agent.name;
325
+ log(` ${desc} (every ${interval}${hasEnv ? ", has .env" : ""})`);
326
+ }
327
+
328
+ for (const agent of agents) {
329
+ runAgentProcess(agent);
330
+
331
+ setInterval(() => {
332
+ runAgentProcess(agent);
333
+ }, agent.intervalMs);
334
+ }
335
+ }
package/src/app.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { startMcpServer, enableLogging } from "./mcp-server.js";
2
2
  import { startTunnel } from "./tunnel.js";
3
+ import { startAgentScheduler } from "./agents.js";
3
4
  import { Poke, isLoggedIn, login, getToken } from "poke";
4
5
 
5
6
  const verbose = process.argv.includes("--verbose") || process.argv.includes("-v");
@@ -44,6 +45,7 @@ async function main() {
44
45
  log(`Tunnel connected (${data.connectionId})`);
45
46
  log("Ready — your Poke agent can now access this machine.");
46
47
  notifyPoke(data.connectionId, token);
48
+ startAgentScheduler();
47
49
  break;
48
50
  case "disconnected":
49
51
  log("Tunnel disconnected. Reconnecting...");