poke-gate 0.1.7 → 0.1.9
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/bin/poke-gate.js +5 -0
- package/clients/Poke macOS Gate/Poke macOS Gate/AgentsView.swift +166 -7
- package/clients/Poke macOS Gate/Poke macOS Gate/GateService.swift +1 -1
- package/clients/Poke macOS Gate/Poke macOS Gate/Poke_macOS_GateApp.swift +33 -1
- package/clients/Poke macOS Gate/Poke macOS Gate/SettingsView.swift +24 -0
- package/clients/Poke macOS Gate/Poke macOS Gate.xcodeproj/project.pbxproj +2 -2
- package/docs/agents/creating.md +29 -1
- package/docs/cli.md +24 -0
- package/package.json +1 -1
- package/src/agent-create.js +100 -0
- package/src/mcp-server.js +48 -1
- package/src/tunnel.js +35 -12
package/bin/poke-gate.js
CHANGED
|
@@ -21,6 +21,11 @@ async function main() {
|
|
|
21
21
|
}
|
|
22
22
|
const { downloadAgent } = await import("../src/agents.js");
|
|
23
23
|
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);
|
|
24
29
|
} else {
|
|
25
30
|
await import("../src/app.js");
|
|
26
31
|
}
|
|
@@ -247,6 +247,8 @@ class AgentsViewModel: ObservableObject {
|
|
|
247
247
|
|
|
248
248
|
struct AgentsView: View {
|
|
249
249
|
@StateObject private var viewModel = AgentsViewModel()
|
|
250
|
+
@State private var showingGenerate = false
|
|
251
|
+
@State private var agentToDelete: AgentFile? = nil
|
|
250
252
|
|
|
251
253
|
var body: some View {
|
|
252
254
|
NavigationSplitView {
|
|
@@ -282,20 +284,30 @@ struct AgentsView: View {
|
|
|
282
284
|
.tag(agent)
|
|
283
285
|
.contextMenu {
|
|
284
286
|
Button("Delete", role: .destructive) {
|
|
285
|
-
|
|
287
|
+
agentToDelete = agent
|
|
286
288
|
}
|
|
287
289
|
}
|
|
288
290
|
}
|
|
289
291
|
.listStyle(.sidebar)
|
|
290
292
|
.navigationSplitViewColumnWidth(min: 180, ideal: 220)
|
|
291
293
|
.safeAreaInset(edge: .bottom) {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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)
|
|
297
310
|
}
|
|
298
|
-
.buttonStyle(.plain)
|
|
299
311
|
.padding(8)
|
|
300
312
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
301
313
|
}
|
|
@@ -315,6 +327,153 @@ struct AgentsView: View {
|
|
|
315
327
|
.onDisappear {
|
|
316
328
|
viewModel.stopWatching()
|
|
317
329
|
}
|
|
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
|
+
}
|
|
318
477
|
}
|
|
319
478
|
}
|
|
320
479
|
|
|
@@ -225,7 +225,7 @@ class GateService: ObservableObject {
|
|
|
225
225
|
return paths.joined(separator: ":")
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
-
|
|
228
|
+
func findNpx() -> String {
|
|
229
229
|
let path = shellPath()
|
|
230
230
|
for dir in path.split(separator: ":") {
|
|
231
231
|
let npxPath = "\(dir)/npx"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import SwiftUI
|
|
2
|
+
import ServiceManagement
|
|
2
3
|
|
|
3
4
|
class AppDelegate: NSObject, NSApplicationDelegate {
|
|
4
5
|
var service: GateService?
|
|
@@ -17,13 +18,13 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|
|
17
18
|
struct Poke_macOS_GateApp: App {
|
|
18
19
|
@StateObject private var service = GateService()
|
|
19
20
|
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
|
20
|
-
|
|
21
21
|
var body: some Scene {
|
|
22
22
|
MenuBarExtra {
|
|
23
23
|
PopoverContent(service: service)
|
|
24
24
|
.onAppear {
|
|
25
25
|
service.autoStartIfNeeded()
|
|
26
26
|
appDelegate.service = service
|
|
27
|
+
checkLoginItemPrompt()
|
|
27
28
|
}
|
|
28
29
|
} label: {
|
|
29
30
|
Image(systemName: menuBarIcon)
|
|
@@ -51,6 +52,37 @@ struct Poke_macOS_GateApp: App {
|
|
|
51
52
|
.windowResizability(.contentSize)
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
private func checkLoginItemPrompt() {
|
|
56
|
+
let dismissed = UserDefaults.standard.bool(forKey: "loginItemPromptDismissed")
|
|
57
|
+
let alreadyEnabled = SMAppService.mainApp.status == .enabled
|
|
58
|
+
if dismissed || alreadyEnabled { return }
|
|
59
|
+
|
|
60
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
|
|
61
|
+
let alert = NSAlert()
|
|
62
|
+
alert.messageText = "Start on login?"
|
|
63
|
+
alert.informativeText = "Would you like Poke Gate to start automatically when you log in?"
|
|
64
|
+
alert.addButton(withTitle: "Enable")
|
|
65
|
+
alert.addButton(withTitle: "Not now")
|
|
66
|
+
alert.addButton(withTitle: "Don't ask again")
|
|
67
|
+
alert.alertStyle = .informational
|
|
68
|
+
|
|
69
|
+
NSApp.activate(ignoringOtherApps: true)
|
|
70
|
+
let response = alert.runModal()
|
|
71
|
+
|
|
72
|
+
switch response {
|
|
73
|
+
case .alertFirstButtonReturn:
|
|
74
|
+
try? SMAppService.mainApp.register()
|
|
75
|
+
UserDefaults.standard.set(true, forKey: "loginItemPromptDismissed")
|
|
76
|
+
case .alertSecondButtonReturn:
|
|
77
|
+
break
|
|
78
|
+
case .alertThirdButtonReturn:
|
|
79
|
+
UserDefaults.standard.set(true, forKey: "loginItemPromptDismissed")
|
|
80
|
+
default:
|
|
81
|
+
break
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
54
86
|
private var menuBarIcon: String {
|
|
55
87
|
switch service.status {
|
|
56
88
|
case .connected: "door.left.hand.open"
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import SwiftUI
|
|
2
|
+
import ServiceManagement
|
|
2
3
|
|
|
3
4
|
struct SettingsView: View {
|
|
4
5
|
@ObservedObject var service: GateService
|
|
5
6
|
@Environment(\.dismiss) private var dismiss
|
|
7
|
+
@State private var launchAtLogin = SMAppService.mainApp.status == .enabled
|
|
6
8
|
|
|
7
9
|
var body: some View {
|
|
8
10
|
VStack(alignment: .leading, spacing: 16) {
|
|
@@ -86,6 +88,28 @@ struct SettingsView: View {
|
|
|
86
88
|
.cornerRadius(8)
|
|
87
89
|
}
|
|
88
90
|
|
|
91
|
+
VStack(alignment: .leading, spacing: 8) {
|
|
92
|
+
Text("GENERAL")
|
|
93
|
+
.font(.caption2)
|
|
94
|
+
.foregroundStyle(.tertiary)
|
|
95
|
+
.textCase(.uppercase)
|
|
96
|
+
.tracking(0.5)
|
|
97
|
+
|
|
98
|
+
Toggle("Start Poke Gate on login", isOn: $launchAtLogin)
|
|
99
|
+
.font(.subheadline)
|
|
100
|
+
.onChange(of: launchAtLogin) { _, newValue in
|
|
101
|
+
do {
|
|
102
|
+
if newValue {
|
|
103
|
+
try SMAppService.mainApp.register()
|
|
104
|
+
} else {
|
|
105
|
+
try SMAppService.mainApp.unregister()
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
launchAtLogin = !newValue
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
89
113
|
HStack {
|
|
90
114
|
Spacer()
|
|
91
115
|
Button("Close") {
|
|
@@ -267,7 +267,7 @@
|
|
|
267
267
|
"@executable_path/../Frameworks",
|
|
268
268
|
);
|
|
269
269
|
MACOSX_DEPLOYMENT_TARGET = 26.0;
|
|
270
|
-
MARKETING_VERSION = 0.1.
|
|
270
|
+
MARKETING_VERSION = 0.1.7;
|
|
271
271
|
PRODUCT_BUNDLE_IDENTIFIER = "dev.fka.Poke-macOS-Gate";
|
|
272
272
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
273
273
|
REGISTER_APP_GROUPS = YES;
|
|
@@ -302,7 +302,7 @@
|
|
|
302
302
|
"@executable_path/../Frameworks",
|
|
303
303
|
);
|
|
304
304
|
MACOSX_DEPLOYMENT_TARGET = 26.0;
|
|
305
|
-
MARKETING_VERSION = 0.1.
|
|
305
|
+
MARKETING_VERSION = 0.1.7;
|
|
306
306
|
PRODUCT_BUNDLE_IDENTIFIER = "dev.fka.Poke-macOS-Gate";
|
|
307
307
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
308
308
|
REGISTER_APP_GROUPS = YES;
|
package/docs/agents/creating.md
CHANGED
|
@@ -1,6 +1,34 @@
|
|
|
1
1
|
# Creating Agents
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
|
4
32
|
|
|
5
33
|
## Step 1: Create the file
|
|
6
34
|
|
package/docs/cli.md
CHANGED
|
@@ -41,6 +41,30 @@ npx poke-gate run-agent beeper
|
|
|
41
41
|
|
|
42
42
|
Finds `~/.config/poke-gate/agents/beeper.*.js` and runs it with the env from `.env.beeper`.
|
|
43
43
|
|
|
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
|
+
|
|
44
68
|
## Install an agent
|
|
45
69
|
|
|
46
70
|
```bash
|
package/package.json
CHANGED
|
@@ -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
|
+
}
|
package/src/mcp-server.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
2
|
import { exec } from "node:child_process";
|
|
3
|
-
import { readFileSync, writeFileSync, readdirSync, statSync } from "node:fs";
|
|
3
|
+
import { readFileSync, writeFileSync, readdirSync, statSync, existsSync, symlinkSync, lstatSync } from "node:fs";
|
|
4
4
|
import { hostname, platform, arch, uptime, totalmem, freemem, homedir } from "node:os";
|
|
5
5
|
import { join, resolve, extname } from "node:path";
|
|
6
6
|
|
|
@@ -93,6 +93,19 @@ const TOOLS = [
|
|
|
93
93
|
required: ["path"],
|
|
94
94
|
},
|
|
95
95
|
},
|
|
96
|
+
{
|
|
97
|
+
name: "run_agent",
|
|
98
|
+
description:
|
|
99
|
+
"Run a Poke Gate agent by name. Agents are scheduled scripts in ~/.config/poke-gate/agents/. " +
|
|
100
|
+
"Use this to manually trigger an agent — it will execute and send its results to you.",
|
|
101
|
+
inputSchema: {
|
|
102
|
+
type: "object",
|
|
103
|
+
properties: {
|
|
104
|
+
name: { type: "string", description: "Agent name (e.g. 'beeper', 'battery', 'context')" },
|
|
105
|
+
},
|
|
106
|
+
required: ["name"],
|
|
107
|
+
},
|
|
108
|
+
},
|
|
96
109
|
{
|
|
97
110
|
name: "take_screenshot",
|
|
98
111
|
description: "Take a screenshot of the user's screen and save it to a file. Returns the file path. Requires screen recording permission on macOS.",
|
|
@@ -233,6 +246,40 @@ function handleToolCall(name, args) {
|
|
|
233
246
|
}
|
|
234
247
|
}
|
|
235
248
|
|
|
249
|
+
case "run_agent": {
|
|
250
|
+
const agentName = args.name;
|
|
251
|
+
logTool(name, { name: agentName });
|
|
252
|
+
const agentsDir = join(homedir(), ".config", "poke-gate", "agents");
|
|
253
|
+
|
|
254
|
+
// Ensure node_modules symlink so agents can import poke
|
|
255
|
+
const pkgNodeModules = join(new URL(".", import.meta.url).pathname, "..", "node_modules");
|
|
256
|
+
const agentNodeModules = join(agentsDir, "node_modules");
|
|
257
|
+
if (existsSync(pkgNodeModules)) {
|
|
258
|
+
try {
|
|
259
|
+
const s = lstatSync(agentNodeModules);
|
|
260
|
+
if (!s.isSymbolicLink()) throw new Error();
|
|
261
|
+
} catch {
|
|
262
|
+
try { symlinkSync(pkgNodeModules, agentNodeModules, "junction"); } catch {}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
let files;
|
|
267
|
+
try { files = readdirSync(agentsDir).filter((f) => f.endsWith(".js") && f.startsWith(agentName + ".")); } catch { files = []; }
|
|
268
|
+
if (files.length === 0) {
|
|
269
|
+
let available = [];
|
|
270
|
+
try { available = readdirSync(agentsDir).filter(f => f.endsWith(".js")).map(f => f.split(".")[0]); } catch {}
|
|
271
|
+
return { content: [{ type: "text", text: `Agent "${agentName}" not found. Available: ${available.join(", ") || "none"}` }], isError: true };
|
|
272
|
+
}
|
|
273
|
+
const agentFile = join(agentsDir, files[0]);
|
|
274
|
+
return runCommand(`node "${agentFile}"`, agentsDir).then((result) => {
|
|
275
|
+
const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
276
|
+
if (result.exitCode === 0) {
|
|
277
|
+
return { content: [{ type: "text", text: `Agent "${agentName}" completed.\n${output || "No output."}` }] };
|
|
278
|
+
}
|
|
279
|
+
return { content: [{ type: "text", text: `Agent "${agentName}" failed (exit ${result.exitCode}).\n${output}` }], isError: true };
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
236
283
|
case "take_screenshot": {
|
|
237
284
|
logTool(name, args);
|
|
238
285
|
|
package/src/tunnel.js
CHANGED
|
@@ -6,6 +6,11 @@ import { homedir } from "node:os";
|
|
|
6
6
|
const CONFIG_DIR = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
|
7
7
|
const STATE_PATH = join(CONFIG_DIR, "poke-gate", "state.json");
|
|
8
8
|
|
|
9
|
+
function log(msg) {
|
|
10
|
+
const ts = new Date().toISOString().slice(11, 19);
|
|
11
|
+
console.log(`[${ts}] ${msg}`);
|
|
12
|
+
}
|
|
13
|
+
|
|
9
14
|
function loadState() {
|
|
10
15
|
try {
|
|
11
16
|
return JSON.parse(readFileSync(STATE_PATH, "utf-8"));
|
|
@@ -19,24 +24,36 @@ function saveState(state) {
|
|
|
19
24
|
writeFileSync(STATE_PATH, JSON.stringify(state, null, 2));
|
|
20
25
|
}
|
|
21
26
|
|
|
22
|
-
async function
|
|
23
|
-
const state = loadState();
|
|
24
|
-
if (!state.connectionId) return;
|
|
25
|
-
|
|
27
|
+
async function cleanupStaleConnections() {
|
|
26
28
|
const token = getToken();
|
|
27
29
|
if (!token) return;
|
|
28
30
|
const base = process.env.POKE_API ?? "https://poke.com/api/v1";
|
|
31
|
+
const state = loadState();
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
const ids = new Set();
|
|
34
|
+
if (state.connectionId) ids.add(state.connectionId);
|
|
35
|
+
if (Array.isArray(state.connectionHistory)) {
|
|
36
|
+
for (const id of state.connectionHistory) ids.add(id);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (ids.size === 0) return;
|
|
40
|
+
|
|
41
|
+
log(`Cleaning up ${ids.size} old connection(s)…`);
|
|
42
|
+
|
|
43
|
+
for (const id of ids) {
|
|
44
|
+
try {
|
|
45
|
+
await fetch(`${base}/mcp/connections/${id}`, {
|
|
46
|
+
method: "DELETE",
|
|
47
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
48
|
+
});
|
|
49
|
+
} catch {}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
saveState({});
|
|
36
53
|
}
|
|
37
54
|
|
|
38
55
|
export async function startTunnel({ mcpUrl, onEvent }) {
|
|
39
|
-
await
|
|
56
|
+
await cleanupStaleConnections();
|
|
40
57
|
|
|
41
58
|
const token = getToken();
|
|
42
59
|
if (!token) {
|
|
@@ -51,7 +68,13 @@ export async function startTunnel({ mcpUrl, onEvent }) {
|
|
|
51
68
|
});
|
|
52
69
|
|
|
53
70
|
tunnel.on("connected", (info) => {
|
|
54
|
-
|
|
71
|
+
const state = loadState();
|
|
72
|
+
const history = state.connectionHistory || [];
|
|
73
|
+
history.push(info.connectionId);
|
|
74
|
+
saveState({
|
|
75
|
+
connectionId: info.connectionId,
|
|
76
|
+
connectionHistory: history.slice(-10),
|
|
77
|
+
});
|
|
55
78
|
onEvent("connected", info);
|
|
56
79
|
});
|
|
57
80
|
tunnel.on("disconnected", () => onEvent("disconnected"));
|