Skip to content

Commit d237000

Browse files
committed
v0.1.8 — run_agent tool, AI agent generation, stale connection cleanup
- New MCP tool: run_agent — trigger agents from Poke via iMessage - agent create: sends prompt to Poke, writes code via write_file tool - macOS app: Generate with Poke UI, direct API call instead of npx - Stale connection cleanup via connection history tracking - Delete agent confirmation dialog - Docs: AI agent generation guide, CLI reference update Made-with: Cursor
1 parent 7197d26 commit d237000

9 files changed

Lines changed: 409 additions & 23 deletions

File tree

bin/poke-gate.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ async function main() {
2121
}
2222
const { downloadAgent } = await import("../src/agents.js");
2323
await downloadAgent(name);
24+
} else if (args[0] === "agent" && args[1] === "create") {
25+
const promptIdx = args.indexOf("--prompt");
26+
const prompt = promptIdx !== -1 ? args.slice(promptIdx + 1).join(" ") : args.slice(2).join(" ") || null;
27+
const { createAgent } = await import("../src/agent-create.js");
28+
await createAgent(prompt);
2429
} else {
2530
await import("../src/app.js");
2631
}

clients/Poke macOS Gate/Poke macOS Gate/AgentsView.swift

Lines changed: 166 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,8 @@ class AgentsViewModel: ObservableObject {
247247

248248
struct AgentsView: View {
249249
@StateObject private var viewModel = AgentsViewModel()
250+
@State private var showingGenerate = false
251+
@State private var agentToDelete: AgentFile? = nil
250252

251253
var body: some View {
252254
NavigationSplitView {
@@ -282,20 +284,30 @@ struct AgentsView: View {
282284
.tag(agent)
283285
.contextMenu {
284286
Button("Delete", role: .destructive) {
285-
viewModel.deleteAgent(agent)
287+
agentToDelete = agent
286288
}
287289
}
288290
}
289291
.listStyle(.sidebar)
290292
.navigationSplitViewColumnWidth(min: 180, ideal: 220)
291293
.safeAreaInset(edge: .bottom) {
292-
Button {
293-
viewModel.addAgent()
294-
} label: {
295-
Label("New Agent", systemImage: "plus")
296-
.font(.caption)
294+
HStack(spacing: 12) {
295+
Button {
296+
viewModel.addAgent()
297+
} label: {
298+
Label("New", systemImage: "plus")
299+
.font(.caption)
300+
}
301+
.buttonStyle(.plain)
302+
303+
Button {
304+
showingGenerate = true
305+
} label: {
306+
Label("Generate with Poke", systemImage: "sparkles")
307+
.font(.caption)
308+
}
309+
.buttonStyle(.plain)
297310
}
298-
.buttonStyle(.plain)
299311
.padding(8)
300312
.frame(maxWidth: .infinity, alignment: .leading)
301313
}
@@ -315,6 +327,153 @@ struct AgentsView: View {
315327
.onDisappear {
316328
viewModel.stopWatching()
317329
}
330+
.sheet(isPresented: $showingGenerate) {
331+
GenerateAgentView(viewModel: viewModel, isPresented: $showingGenerate)
332+
}
333+
.alert("Delete Agent", isPresented: Binding(
334+
get: { agentToDelete != nil },
335+
set: { if !$0 { agentToDelete = nil } }
336+
)) {
337+
Button("Cancel", role: .cancel) { agentToDelete = nil }
338+
Button("Delete", role: .destructive) {
339+
if let agent = agentToDelete {
340+
viewModel.deleteAgent(agent)
341+
agentToDelete = nil
342+
}
343+
}
344+
} message: {
345+
if let agent = agentToDelete {
346+
Text("Are you sure you want to delete \"\(agent.name)\"? This will remove the script\(agent.hasEnv ? " and its .env file" : "").")
347+
}
348+
}
349+
}
350+
}
351+
352+
struct GenerateAgentView: View {
353+
@ObservedObject var viewModel: AgentsViewModel
354+
@Binding var isPresented: Bool
355+
@State private var prompt: String = ""
356+
@State private var isSending: Bool = false
357+
@State private var sent: Bool = false
358+
359+
var body: some View {
360+
VStack(spacing: 16) {
361+
if sent {
362+
Spacer()
363+
364+
Image(systemName: "checkmark.circle.fill")
365+
.font(.system(size: 40))
366+
.foregroundStyle(.green)
367+
368+
Text("Request sent to Poke!")
369+
.font(.headline)
370+
371+
Text("Poke is generating your agent and will save it directly to your Mac. You'll see it appear in the sidebar when it's ready.")
372+
.font(.caption)
373+
.foregroundStyle(.secondary)
374+
.multilineTextAlignment(.center)
375+
376+
Text("Keep Poke Gate running while Poke works.")
377+
.font(.caption)
378+
.foregroundStyle(.secondary)
379+
.fontWeight(.medium)
380+
381+
Spacer()
382+
383+
Button("Done") {
384+
isPresented = false
385+
}
386+
.keyboardShortcut(.defaultAction)
387+
} else {
388+
HStack {
389+
Image(systemName: "sparkles")
390+
.foregroundStyle(.purple)
391+
Text("Generate Agent with Poke")
392+
.font(.headline)
393+
}
394+
395+
Text("Describe what you want the agent to do. Poke will generate the code and save it to your agents folder.")
396+
.font(.caption)
397+
.foregroundStyle(.secondary)
398+
.multilineTextAlignment(.center)
399+
400+
TextEditor(text: $prompt)
401+
.font(.system(.body, design: .default))
402+
.padding(8)
403+
.frame(minHeight: 100)
404+
.scrollContentBackground(.hidden)
405+
.background(.quaternary.opacity(0.3))
406+
.cornerRadius(6)
407+
.overlay(
408+
RoundedRectangle(cornerRadius: 6)
409+
.stroke(.quaternary)
410+
)
411+
412+
HStack {
413+
Button("Cancel") {
414+
isPresented = false
415+
}
416+
.keyboardShortcut(.cancelAction)
417+
418+
Spacer()
419+
420+
Button("Generate") {
421+
sendPrompt()
422+
}
423+
.keyboardShortcut(.defaultAction)
424+
.disabled(prompt.isEmpty || isSending)
425+
}
426+
}
427+
}
428+
.padding(20)
429+
.frame(width: 420, height: sent ? 250 : nil)
430+
}
431+
432+
private func sendPrompt() {
433+
isSending = true
434+
435+
Task {
436+
do {
437+
guard let token = GateService().loadPokeLoginToken() else {
438+
isSending = false
439+
return
440+
}
441+
442+
let message = """
443+
Generate a Poke Gate agent based on my description below.
444+
445+
Write the COMPLETE JavaScript code using the write_file tool to save it directly to ~/.config/poke-gate/agents/<name>.<interval>.js
446+
447+
RULES:
448+
- Valid ES module with imports.
449+
- Start with JSDoc frontmatter: @agent, @name, @description, @interval, @author.
450+
- Use: import { Poke, getToken } from "poke";
451+
- Keep under 100 lines. Intervals: 10m, 30m, 1h, 2h, 6h, 12h, 24h.
452+
- Handle errors with try/catch.
453+
- Use state files to avoid duplicate sends.
454+
455+
IMPORTANT: Use the write_file tool to save the agent code now.
456+
IMPORTANT: When done, tell me the file name.
457+
458+
My request: \(prompt)
459+
"""
460+
461+
let url = URL(string: "https://poke.com/api/v1/inbound/api-message")!
462+
var request = URLRequest(url: url)
463+
request.httpMethod = "POST"
464+
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
465+
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
466+
request.httpBody = try JSONSerialization.data(withJSONObject: ["message": message])
467+
468+
let (_, response) = try await URLSession.shared.data(for: request)
469+
let httpResp = response as? HTTPURLResponse
470+
471+
isSending = false
472+
sent = httpResp?.statusCode == 200
473+
} catch {
474+
isSending = false
475+
}
476+
}
318477
}
319478
}
320479

clients/Poke macOS Gate/Poke macOS Gate/GateService.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ class GateService: ObservableObject {
225225
return paths.joined(separator: ":")
226226
}
227227

228-
private func findNpx() -> String {
228+
func findNpx() -> String {
229229
let path = shellPath()
230230
for dir in path.split(separator: ":") {
231231
let npxPath = "\(dir)/npx"

docs/agents/creating.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,34 @@
11
# Creating Agents
22

3-
This guide walks you through creating an agent from scratch.
3+
There are two ways to create agents: **ask Poke to generate one** (recommended) or write one manually.
4+
5+
## Generate with AI
6+
7+
The fastest way — describe what you want and Poke writes the code for you:
8+
9+
```bash
10+
npx poke-gate agent create --prompt "monitor disk space and alert when above 85%"
11+
```
12+
13+
Or interactively:
14+
15+
```bash
16+
npx poke-gate agent create
17+
> Describe the agent you want to create:
18+
> track my git repos for uncommitted changes
19+
```
20+
21+
Poke generates the complete agent code and saves it directly to your agents folder using the `write_file` tool (requires poke-gate to be running). You'll get a confirmation in your chat when it's done.
22+
23+
You can also generate agents from the **macOS app** — open Agents, click "Generate with AI", type your description, and Poke does the rest.
24+
25+
::: tip
26+
Poke may ask clarifying questions before writing the code. Just reply in your chat and it will proceed.
27+
:::
28+
29+
## Write manually
30+
31+
If you prefer to write agents yourself, follow the steps below.
432

533
## Step 1: Create the file
634

docs/cli.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,30 @@ npx poke-gate run-agent beeper
4141

4242
Finds `~/.config/poke-gate/agents/beeper.*.js` and runs it with the env from `.env.beeper`.
4343

44+
## Generate an agent with AI
45+
46+
```bash
47+
npx poke-gate agent create --prompt "<description>"
48+
```
49+
50+
Sends your description to Poke with detailed instructions and examples. Poke generates the agent code and saves it directly to `~/.config/poke-gate/agents/` using the `write_file` tool.
51+
52+
**Requires poke-gate to be running** (so Poke can use the `write_file` tool through the tunnel).
53+
54+
**Interactive mode:**
55+
56+
```bash
57+
npx poke-gate agent create
58+
```
59+
60+
**Examples:**
61+
62+
```bash
63+
npx poke-gate agent create --prompt "alert me when disk space is above 85%"
64+
npx poke-gate agent create --prompt "send me a daily git commit summary across all repos"
65+
npx poke-gate agent create --prompt "track Spotify listening and log my music taste"
66+
```
67+
4468
## Install an agent
4569

4670
```bash

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.1.7",
3+
"version": "0.1.8",
44
"description": "Expose your machine to your Poke AI assistant via MCP tunnel",
55
"type": "module",
66
"bin": {

src/agent-create.js

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { Poke, getToken, isLoggedIn, login } from "poke";
2+
import { join } from "node:path";
3+
import { homedir } from "node:os";
4+
import { createInterface } from "node:readline";
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 SYSTEM_PROMPT = `Generate a Poke Gate agent based on my description below.
10+
11+
Write the COMPLETE JavaScript code using the write_file tool to save it directly to the agents folder.
12+
13+
RULES:
14+
- Save to: ~/.config/poke-gate/agents/<name>.<interval>.js
15+
- Valid ES module with imports.
16+
- Start with JSDoc frontmatter: @agent, @name, @description, @interval, @author.
17+
- Use: import { Poke, getToken } from "poke";
18+
- Auth: const token = getToken(); const poke = new Poke({ apiKey: token });
19+
- Send results: await poke.sendMessage("...");
20+
- Shell commands: import { execSync } from "node:child_process";
21+
- State files: ~/.config/poke-gate/agents/.<agent-name>-state.json
22+
- Only send to Poke when something changed (use state files).
23+
- Handle errors with try/catch. Log with console.log().
24+
- Keep under 100 lines. Intervals: 10m, 30m, 1h, 2h, 6h, 12h, 24h.
25+
- If secrets needed, read from process.env (from .env.<name> file).
26+
27+
EXAMPLE agent (battery monitor):
28+
/**
29+
* @agent battery
30+
* @name Battery Guardian
31+
* @description Alerts via Poke when battery drops below 20%.
32+
* @interval 30m
33+
*/
34+
import { Poke, getToken } from "poke";
35+
import { execSync } from "node:child_process";
36+
import { readFileSync, writeFileSync } from "node:fs";
37+
import { join } from "node:path";
38+
import { homedir } from "node:os";
39+
const token = getToken();
40+
if (!token) { console.error("Not signed in."); process.exit(1); }
41+
const STATE = join(homedir(), ".config", "poke-gate", "agents", ".battery-state.json");
42+
function load() { try { return JSON.parse(readFileSync(STATE, "utf-8")); } catch { return {}; } }
43+
function save(s) { writeFileSync(STATE, JSON.stringify(s)); }
44+
const out = execSync("pmset -g batt", { encoding: "utf-8" });
45+
const level = parseInt(out.match(/(\\d+)%/)?.[1] || "100");
46+
const charging = out.includes("AC Power");
47+
const state = load();
48+
if (level <= 20 && !charging && !state.alerted) {
49+
await new Poke({ apiKey: token }).sendMessage("Battery: " + level + "%, not charging.");
50+
save({ alerted: true });
51+
} else if (level > 20 || charging) { if (state.alerted) save({ alerted: false }); }
52+
53+
Now use the write_file tool via Poke Gate to save the generated agent code. After writing, tell me the file name and what the agent does.
54+
If you cannot reach out the tunnel, you can send the code via iMessage, Telegram, or SMS.
55+
56+
IMPORTANT: Now write me immediately, before starting that you will write the agent code now and save to your file.
57+
IMPORTANT: If you have questions to clarify, ask me first.
58+
IMPORTANT: When you finish writing the agent code, tell user that you created the agent and saved to the file.
59+
60+
My request: `;
61+
62+
function ask(question) {
63+
const rl = createInterface({ input: process.stdin, output: process.stdout });
64+
return new Promise((resolve) => {
65+
rl.question(question, (answer) => {
66+
rl.close();
67+
resolve(answer.trim());
68+
});
69+
});
70+
}
71+
72+
export async function createAgent(promptArg) {
73+
if (!isLoggedIn()) {
74+
console.log(" Signing in to Poke...");
75+
await login();
76+
}
77+
78+
const token = getToken();
79+
if (!token) {
80+
console.error(" Not signed in. Run: npx poke login");
81+
process.exit(1);
82+
}
83+
84+
const prompt = promptArg || await ask("\n Describe the agent you want to create:\n > ");
85+
if (!prompt) {
86+
console.error(" No description provided.");
87+
process.exit(1);
88+
}
89+
90+
console.log("\n Sending request to Poke...");
91+
console.log(" Poke will generate the code and save it using the write_file tool.\n");
92+
93+
const poke = new Poke({ apiKey: token });
94+
await poke.sendMessage(SYSTEM_PROMPT + prompt);
95+
96+
console.log(" Request sent! Poke will write the agent file to:");
97+
console.log(` ${AGENTS_DIR}/<name>.<interval>.js\n`);
98+
console.log(" Watch for Poke's confirmation in your chat.");
99+
console.log(" Once created, test it: npx poke-gate run-agent <name>\n");
100+
}

0 commit comments

Comments
 (0)