poke-gate 0.1.0 → 0.1.4
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/docs.yml +56 -0
- package/README.md +4 -0
- package/assets/screenshots/agents-editor.png +0 -0
- package/clients/Poke macOS Gate/Poke macOS Gate/AgentsView.swift +485 -0
- package/clients/Poke macOS Gate/Poke macOS Gate/Poke_macOS_GateApp.swift +10 -0
- 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/docs/.vitepress/config.mts +75 -0
- package/docs/agents/beeper.md +107 -0
- package/docs/agents/community.md +77 -0
- package/docs/agents/creating.md +132 -0
- package/docs/agents/index.md +85 -0
- package/docs/agents/installing.md +66 -0
- package/docs/agents/sharing.md +97 -0
- package/docs/cli.md +73 -0
- package/docs/getting-started.md +62 -0
- package/docs/how-it-works.md +56 -0
- package/docs/index.md +63 -0
- package/docs/macos-app.md +74 -0
- package/docs/package-lock.json +3629 -0
- package/docs/package.json +15 -0
- package/docs/public/CNAME +1 -0
- package/docs/public/agents-editor.png +0 -0
- package/docs/public/logo.png +0 -0
- package/docs/security.md +35 -0
- package/docs/tools.md +101 -0
- package/examples/agents/battery.30m.js +78 -0
- package/examples/agents/beeper.1h.js +13 -5
- package/examples/agents/screentime.24h.js +86 -0
- package/examples/agents/wifi.30m.js +85 -0
- package/package.json +1 -1
- package/src/agents.js +107 -10
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"private": true,
|
|
3
|
+
"scripts": {
|
|
4
|
+
"dev": "vitepress dev",
|
|
5
|
+
"build": "vitepress build",
|
|
6
|
+
"preview": "vitepress preview"
|
|
7
|
+
},
|
|
8
|
+
"devDependencies": {
|
|
9
|
+
"vitepress": "^1.6.3"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"mermaid": "^11.13.0",
|
|
13
|
+
"vitepress-plugin-mermaid": "^2.0.17"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
poke-gate.fka.dev
|
|
Binary file
|
|
Binary file
|
package/docs/security.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
::: danger Full shell access
|
|
4
|
+
Poke Gate grants **full shell access** to your Poke agent. Understand the implications before running it.
|
|
5
|
+
:::
|
|
6
|
+
|
|
7
|
+
## What the agent can do
|
|
8
|
+
|
|
9
|
+
When Poke Gate is running, your Poke agent can:
|
|
10
|
+
|
|
11
|
+
- **Run any command** with your user's permissions (`run_command`)
|
|
12
|
+
- **Read any file** your user can access (`read_file`, `read_image`)
|
|
13
|
+
- **Write any file** your user can access (`write_file`)
|
|
14
|
+
- **List any directory** (`list_directory`)
|
|
15
|
+
- **Take screenshots** of your screen (`take_screenshot`)
|
|
16
|
+
- **See system info** — hostname, memory, uptime (`system_info`)
|
|
17
|
+
|
|
18
|
+
## What protects you
|
|
19
|
+
|
|
20
|
+
- **Authentication** — only your Poke agent (authenticated via your Poke OAuth session) can reach the tunnel. No one else can send tool calls.
|
|
21
|
+
- **Tunnel isolation** — the MCP server only listens on `127.0.0.1` (localhost). It's not exposed to the network. The tunnel is the only way to reach it.
|
|
22
|
+
- **No persistent access** — when you quit Poke Gate (Ctrl-C or Quit from menu bar), the tunnel closes and the connection is deleted. Your machine is no longer reachable.
|
|
23
|
+
- **Connection cleanup** — old connections are deleted before new ones are created, preventing stale tunnels.
|
|
24
|
+
|
|
25
|
+
## Best practices
|
|
26
|
+
|
|
27
|
+
1. **Only run on trusted machines** — don't run Poke Gate on shared or public computers.
|
|
28
|
+
2. **Quit when not needed** — close the app when you don't need remote access.
|
|
29
|
+
3. **Review agent scripts** — before installing a community agent, read the code. Agents run with your full user permissions.
|
|
30
|
+
4. **Keep env files secure** — `.env` files in `~/.config/poke-gate/agents/` may contain API tokens. Don't commit them to git.
|
|
31
|
+
5. **Use verbose mode** — run with `--verbose` to see what tools are being called in real time.
|
|
32
|
+
|
|
33
|
+
## Reporting issues
|
|
34
|
+
|
|
35
|
+
If you discover a security vulnerability, please email [security@fka.dev](mailto:security@fka.dev) instead of opening a public issue.
|
package/docs/tools.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Tools
|
|
2
|
+
|
|
3
|
+
Poke Gate exposes 7 tools to your Poke agent via MCP.
|
|
4
|
+
|
|
5
|
+
## run_command
|
|
6
|
+
|
|
7
|
+
Execute any shell command on your machine.
|
|
8
|
+
|
|
9
|
+
| Parameter | Type | Required | Description |
|
|
10
|
+
|-----------|------|----------|-------------|
|
|
11
|
+
| `command` | string | yes | The shell command to execute |
|
|
12
|
+
| `cwd` | string | no | Working directory (defaults to home) |
|
|
13
|
+
|
|
14
|
+
**Returns:** `{ stdout, stderr, exitCode }`
|
|
15
|
+
|
|
16
|
+
**Examples from Poke:**
|
|
17
|
+
- "Run `ls -la` in my home directory"
|
|
18
|
+
- "What's running on port 3000?"
|
|
19
|
+
- "Show me the git log for my project"
|
|
20
|
+
- "Install lodash in my project"
|
|
21
|
+
|
|
22
|
+
::: info
|
|
23
|
+
Commands have a 30-second timeout and 1MB output buffer.
|
|
24
|
+
:::
|
|
25
|
+
|
|
26
|
+
## read_file
|
|
27
|
+
|
|
28
|
+
Read the contents of a text file.
|
|
29
|
+
|
|
30
|
+
| Parameter | Type | Required | Description |
|
|
31
|
+
|-----------|------|----------|-------------|
|
|
32
|
+
| `path` | string | yes | Absolute or relative path (supports `~`) |
|
|
33
|
+
|
|
34
|
+
**Examples:**
|
|
35
|
+
- "Read my ~/.zshrc"
|
|
36
|
+
- "Show me the package.json in my project"
|
|
37
|
+
|
|
38
|
+
## read_image
|
|
39
|
+
|
|
40
|
+
Read an image or binary file and return it as base64.
|
|
41
|
+
|
|
42
|
+
| Parameter | Type | Required | Description |
|
|
43
|
+
|-----------|------|----------|-------------|
|
|
44
|
+
| `path` | string | yes | Path to the image file |
|
|
45
|
+
|
|
46
|
+
Supports: png, jpg, gif, webp, svg, pdf, bmp, ico.
|
|
47
|
+
|
|
48
|
+
For image files, returns MCP `image` content type with base64 data so the agent can "see" the image.
|
|
49
|
+
|
|
50
|
+
## write_file
|
|
51
|
+
|
|
52
|
+
Write content to a file. Creates the file if it doesn't exist, overwrites if it does.
|
|
53
|
+
|
|
54
|
+
| Parameter | Type | Required | Description |
|
|
55
|
+
|-----------|------|----------|-------------|
|
|
56
|
+
| `path` | string | yes | Absolute or relative path (supports `~`) |
|
|
57
|
+
| `content` | string | yes | Content to write |
|
|
58
|
+
|
|
59
|
+
**Examples:**
|
|
60
|
+
- "Create a file called notes.txt on my Desktop"
|
|
61
|
+
- "Write a Python script that..."
|
|
62
|
+
|
|
63
|
+
## list_directory
|
|
64
|
+
|
|
65
|
+
List files and directories at a given path.
|
|
66
|
+
|
|
67
|
+
| Parameter | Type | Required | Description |
|
|
68
|
+
|-----------|------|----------|-------------|
|
|
69
|
+
| `path` | string | no | Directory path (defaults to home, supports `~`) |
|
|
70
|
+
|
|
71
|
+
Returns entries with `d` for directories and `-` for files.
|
|
72
|
+
|
|
73
|
+
## system_info
|
|
74
|
+
|
|
75
|
+
Get system information. No parameters needed.
|
|
76
|
+
|
|
77
|
+
**Returns:**
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"hostname": "MacBook-Pro.local",
|
|
81
|
+
"platform": "darwin",
|
|
82
|
+
"arch": "arm64",
|
|
83
|
+
"uptime": "5h 23m",
|
|
84
|
+
"totalMemory": "16GB",
|
|
85
|
+
"freeMemory": "4GB",
|
|
86
|
+
"homeDir": "/Users/you",
|
|
87
|
+
"nodeVersion": "v22.21.1"
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## take_screenshot
|
|
92
|
+
|
|
93
|
+
Capture the screen and save it to a file.
|
|
94
|
+
|
|
95
|
+
| Parameter | Type | Required | Description |
|
|
96
|
+
|-----------|------|----------|-------------|
|
|
97
|
+
| `path` | string | no | Save path (defaults to `~/Desktop/screenshot-<timestamp>.png`) |
|
|
98
|
+
|
|
99
|
+
::: warning
|
|
100
|
+
Requires **Screen Recording** permission on macOS. The system will prompt you the first time.
|
|
101
|
+
:::
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @agent battery
|
|
3
|
+
* @name Battery Guardian
|
|
4
|
+
* @description Alerts you via Poke when your battery drops below 20%.
|
|
5
|
+
* @interval 30m
|
|
6
|
+
* @author f
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Poke, getToken } from "poke";
|
|
10
|
+
import { execSync } from "node:child_process";
|
|
11
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
|
|
15
|
+
const token = getToken();
|
|
16
|
+
if (!token) {
|
|
17
|
+
console.error("Not signed in. Run: npx poke login");
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const THRESHOLD = parseInt(process.env.BATTERY_THRESHOLD || "20", 10);
|
|
22
|
+
const STATE_FILE = join(homedir(), ".config", "poke-gate", "agents", ".battery-state.json");
|
|
23
|
+
|
|
24
|
+
function getBattery() {
|
|
25
|
+
try {
|
|
26
|
+
const output = execSync("pmset -g batt", { encoding: "utf-8", timeout: 5000 });
|
|
27
|
+
const match = output.match(/(\d+)%/);
|
|
28
|
+
const charging = output.includes("AC Power") || output.includes("charging");
|
|
29
|
+
return {
|
|
30
|
+
level: match ? parseInt(match[1], 10) : null,
|
|
31
|
+
charging,
|
|
32
|
+
};
|
|
33
|
+
} catch {
|
|
34
|
+
return { level: null, charging: false };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function loadState() {
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(readFileSync(STATE_FILE, "utf-8"));
|
|
41
|
+
} catch {
|
|
42
|
+
return { alerted: false };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function saveState(state) {
|
|
47
|
+
writeFileSync(STATE_FILE, JSON.stringify(state));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const { level, charging } = getBattery();
|
|
51
|
+
|
|
52
|
+
if (level === null) {
|
|
53
|
+
console.log("Could not read battery level.");
|
|
54
|
+
process.exit(0);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log(`Battery: ${level}% ${charging ? "(charging)" : "(on battery)"}`);
|
|
58
|
+
|
|
59
|
+
const state = loadState();
|
|
60
|
+
|
|
61
|
+
if (level <= THRESHOLD && !charging && !state.alerted) {
|
|
62
|
+
console.log(`Battery low (${level}%). Alerting Poke...`);
|
|
63
|
+
|
|
64
|
+
const poke = new Poke({ apiKey: token });
|
|
65
|
+
await poke.sendMessage(
|
|
66
|
+
`⚠️ Battery alert: your Mac is at ${level}%. You're not plugged in. Consider charging soon.`
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
saveState({ alerted: true });
|
|
70
|
+
console.log("Alert sent.");
|
|
71
|
+
} else if (level > THRESHOLD || charging) {
|
|
72
|
+
if (state.alerted) {
|
|
73
|
+
saveState({ alerted: false });
|
|
74
|
+
console.log("Battery recovered, reset alert state.");
|
|
75
|
+
} else {
|
|
76
|
+
console.log("Battery OK, no alert needed.");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -36,12 +36,20 @@ async function beeperRequest(path, params = {}) {
|
|
|
36
36
|
async function getRecentMessages() {
|
|
37
37
|
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString();
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
+
}
|
|
43
51
|
|
|
44
|
-
return
|
|
52
|
+
return allMessages;
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
function groupBySender(messages) {
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @agent screentime
|
|
3
|
+
* @name Screen Time Report
|
|
4
|
+
* @description Sends a daily summary of your most-used apps to Poke.
|
|
5
|
+
* @interval 24h
|
|
6
|
+
* @author f
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Poke, getToken } from "poke";
|
|
10
|
+
import { execSync } from "node:child_process";
|
|
11
|
+
|
|
12
|
+
const token = getToken();
|
|
13
|
+
if (!token) {
|
|
14
|
+
console.error("Not signed in. Run: npx poke login");
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getScreenTime() {
|
|
19
|
+
try {
|
|
20
|
+
const result = execSync(
|
|
21
|
+
`defaults read com.apple.ScreenTimeAgent 2>/dev/null || echo "{}"`,
|
|
22
|
+
{ encoding: "utf-8", timeout: 10000 }
|
|
23
|
+
).trim();
|
|
24
|
+
|
|
25
|
+
// Fallback: use process list to estimate active apps
|
|
26
|
+
const ps = execSync(
|
|
27
|
+
`ps -eo etime,comm | grep -i "/Applications/" | sort -rn | head -20`,
|
|
28
|
+
{ encoding: "utf-8", timeout: 10000 }
|
|
29
|
+
).trim();
|
|
30
|
+
|
|
31
|
+
return ps;
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getActiveApps() {
|
|
38
|
+
try {
|
|
39
|
+
const script = `
|
|
40
|
+
tell application "System Events"
|
|
41
|
+
set appList to name of every application process whose background only is false
|
|
42
|
+
end tell
|
|
43
|
+
return appList as text
|
|
44
|
+
`;
|
|
45
|
+
const result = execSync(`osascript -e '${script}'`, {
|
|
46
|
+
encoding: "utf-8",
|
|
47
|
+
timeout: 10000,
|
|
48
|
+
}).trim();
|
|
49
|
+
return result.split(", ");
|
|
50
|
+
} catch {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getUptime() {
|
|
56
|
+
try {
|
|
57
|
+
return execSync("uptime", { encoding: "utf-8", timeout: 5000 }).trim();
|
|
58
|
+
} catch {
|
|
59
|
+
return "unknown";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const apps = getActiveApps();
|
|
64
|
+
const uptimeStr = getUptime();
|
|
65
|
+
const screenData = getScreenTime();
|
|
66
|
+
|
|
67
|
+
let report = `Daily screen report:\n\n`;
|
|
68
|
+
report += `Uptime: ${uptimeStr}\n\n`;
|
|
69
|
+
|
|
70
|
+
if (apps.length > 0) {
|
|
71
|
+
report += `Currently running apps (${apps.length}):\n`;
|
|
72
|
+
for (const app of apps) {
|
|
73
|
+
report += ` - ${app}\n`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (screenData) {
|
|
78
|
+
report += `\nTop processes by runtime:\n${screenData}\n`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log("Sending screen time report...");
|
|
82
|
+
|
|
83
|
+
const poke = new Poke({ apiKey: token });
|
|
84
|
+
await poke.sendMessage(report);
|
|
85
|
+
|
|
86
|
+
console.log("Report sent.");
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @agent wifi
|
|
3
|
+
* @name WiFi Logger
|
|
4
|
+
* @description Logs your current WiFi network to Poke so it knows where you are.
|
|
5
|
+
* @interval 30m
|
|
6
|
+
* @author f
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Poke, getToken } from "poke";
|
|
10
|
+
import { execSync } from "node:child_process";
|
|
11
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
|
|
15
|
+
const token = getToken();
|
|
16
|
+
if (!token) {
|
|
17
|
+
console.error("Not signed in. Run: npx poke login");
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const STATE_FILE = join(homedir(), ".config", "poke-gate", "agents", ".wifi-state.json");
|
|
22
|
+
|
|
23
|
+
function getCurrentNetwork() {
|
|
24
|
+
try {
|
|
25
|
+
const iface = execSync(
|
|
26
|
+
"networksetup -listallhardwareports | awk '/Wi-Fi/{getline; print $2}'",
|
|
27
|
+
{ encoding: "utf-8", timeout: 5000 }
|
|
28
|
+
).trim();
|
|
29
|
+
|
|
30
|
+
const ssid = execSync(
|
|
31
|
+
`networksetup -getairportnetwork ${iface || "en0"} 2>/dev/null | sed 's/Current Wi-Fi Network: //'`,
|
|
32
|
+
{ encoding: "utf-8", timeout: 5000 }
|
|
33
|
+
).trim();
|
|
34
|
+
|
|
35
|
+
if (ssid.includes("not associated") || !ssid) return null;
|
|
36
|
+
return ssid;
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function loadState() {
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(readFileSync(STATE_FILE, "utf-8"));
|
|
45
|
+
} catch {
|
|
46
|
+
return { lastNetwork: null };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function saveState(state) {
|
|
51
|
+
writeFileSync(STATE_FILE, JSON.stringify(state));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const network = getCurrentNetwork();
|
|
55
|
+
const state = loadState();
|
|
56
|
+
|
|
57
|
+
console.log(`Current WiFi: ${network || "not connected"}`);
|
|
58
|
+
|
|
59
|
+
if (network && network !== state.lastNetwork) {
|
|
60
|
+
console.log(`Network changed from "${state.lastNetwork}" to "${network}". Notifying Poke...`);
|
|
61
|
+
|
|
62
|
+
const poke = new Poke({ apiKey: token });
|
|
63
|
+
await poke.sendMessage(
|
|
64
|
+
`I just connected to WiFi network "${network}". ` +
|
|
65
|
+
(state.lastNetwork
|
|
66
|
+
? `Previously I was on "${state.lastNetwork}".`
|
|
67
|
+
: `This is the first network I've logged.`) +
|
|
68
|
+
` Remember this for context about where I am.`
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
saveState({ lastNetwork: network });
|
|
72
|
+
console.log("Poke notified.");
|
|
73
|
+
} else if (!network && state.lastNetwork) {
|
|
74
|
+
console.log("Disconnected from WiFi. Notifying Poke...");
|
|
75
|
+
|
|
76
|
+
const poke = new Poke({ apiKey: token });
|
|
77
|
+
await poke.sendMessage(
|
|
78
|
+
`I've disconnected from WiFi (was on "${state.lastNetwork}"). I might be on the move.`
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
saveState({ lastNetwork: null });
|
|
82
|
+
console.log("Poke notified.");
|
|
83
|
+
} else {
|
|
84
|
+
console.log("No network change.");
|
|
85
|
+
}
|
package/package.json
CHANGED
package/src/agents.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readdirSync, readFileSync, writeFileSync, existsSync, mkdirSync } from
|
|
|
2
2
|
import { join, basename } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { exec } from "node:child_process";
|
|
5
|
+
import { createInterface } from "node:readline";
|
|
5
6
|
|
|
6
7
|
const CONFIG_DIR = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
|
7
8
|
const AGENTS_DIR = join(CONFIG_DIR, "poke-gate", "agents");
|
|
@@ -109,10 +110,31 @@ export function discoverAgents() {
|
|
|
109
110
|
return agents;
|
|
110
111
|
}
|
|
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
|
+
|
|
112
132
|
function runAgentProcess(agent) {
|
|
113
133
|
const agentEnv = parseEnvFile(agent.envFile);
|
|
114
134
|
const env = { ...process.env, ...agentEnv };
|
|
115
135
|
|
|
136
|
+
ensureNodeModulesLink();
|
|
137
|
+
|
|
116
138
|
log(`Running agent: ${agent.name} (${agent.file})`);
|
|
117
139
|
|
|
118
140
|
return new Promise((resolve) => {
|
|
@@ -212,26 +234,91 @@ export async function downloadAgent(name) {
|
|
|
212
234
|
writeFileSync(dest, jsContent);
|
|
213
235
|
console.log(` Saved: ${dest}`);
|
|
214
236
|
|
|
215
|
-
// Try to download matching .env file
|
|
216
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
|
+
|
|
217
246
|
const envRes = await fetch(`${REPO_BASE}/.env.${envName}`).catch(() => null);
|
|
218
247
|
if (envRes?.ok) {
|
|
219
|
-
const
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
console.log(
|
|
224
|
-
|
|
225
|
-
|
|
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}`);
|
|
226
261
|
} else {
|
|
227
|
-
|
|
262
|
+
writeFileSync(envDest, envTemplate);
|
|
263
|
+
console.log(` Saved: ${envDest}`);
|
|
228
264
|
}
|
|
229
265
|
}
|
|
230
266
|
|
|
231
267
|
console.log(`\n Test it: npx poke-gate run-agent ${envName}`);
|
|
232
268
|
}
|
|
233
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
|
+
let schedulerRunning = false;
|
|
313
|
+
const activeTimers = [];
|
|
314
|
+
|
|
234
315
|
export function startAgentScheduler() {
|
|
316
|
+
if (schedulerRunning) {
|
|
317
|
+
log("Agent scheduler already running, skipping.");
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
schedulerRunning = true;
|
|
321
|
+
|
|
235
322
|
const agents = discoverAgents();
|
|
236
323
|
|
|
237
324
|
if (agents.length === 0) {
|
|
@@ -250,8 +337,18 @@ export function startAgentScheduler() {
|
|
|
250
337
|
for (const agent of agents) {
|
|
251
338
|
runAgentProcess(agent);
|
|
252
339
|
|
|
253
|
-
setInterval(() => {
|
|
340
|
+
const timer = setInterval(() => {
|
|
254
341
|
runAgentProcess(agent);
|
|
255
342
|
}, agent.intervalMs);
|
|
343
|
+
activeTimers.push(timer);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function stopAgentScheduler() {
|
|
348
|
+
for (const timer of activeTimers) {
|
|
349
|
+
clearInterval(timer);
|
|
256
350
|
}
|
|
351
|
+
activeTimers.length = 0;
|
|
352
|
+
schedulerRunning = false;
|
|
353
|
+
log("Agent scheduler stopped.");
|
|
257
354
|
}
|