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 +104 -10
- package/bin/poke-gate.js +23 -1
- 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/examples/agents/.env.beeper +6 -0
- package/examples/agents/beeper.1h.js +117 -0
- package/package.json +1 -1
- package/src/agents.js +335 -0
- package/src/app.js +2 -0
package/README.md
CHANGED
|
@@ -44,11 +44,11 @@ npx poke-gate
|
|
|
44
44
|
|
|
45
45
|
## Setup
|
|
46
46
|
|
|
47
|
-
1.
|
|
48
|
-
2.
|
|
49
|
-
3.
|
|
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
|
|
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,
|
|
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** —
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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;
|
|
Binary file
|
|
@@ -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
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...");
|