poke-gate 0.3.0 → 0.3.2

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 CHANGED
@@ -42,6 +42,9 @@ async function main() {
42
42
  } else if (args[0] === "download-macos") {
43
43
  const { downloadMacOSApp } = await import("../src/download-macos.js");
44
44
  await downloadMacOSApp();
45
+ } else if (args[0] === "take-screenshot") {
46
+ const { takeScreenshot } = await import("../src/take-screenshot.js");
47
+ await takeScreenshot();
45
48
  } else {
46
49
  const mode = parseMode();
47
50
  if (mode) {
@@ -4,7 +4,7 @@ struct AccessibilityPermissionView: View {
4
4
  @ObservedObject var service: GateService
5
5
 
6
6
  var body: some View {
7
- let granted = service.hasSystemPermissionsGranted
7
+ let granted = service.isPermissionGranted(.accessibility)
8
8
 
9
9
  VStack(alignment: .leading, spacing: 10) {
10
10
  HStack(spacing: 8) {
@@ -1,6 +1,5 @@
1
1
  import Foundation
2
2
  import Combine
3
- import ScreenCaptureKit
4
3
  import AppKit
5
4
  import CoreGraphics
6
5
  import ApplicationServices
@@ -41,30 +40,35 @@ class GateService: ObservableObject {
41
40
 
42
41
  enum SystemPermission: String, CaseIterable, Identifiable {
43
42
  case accessibility
43
+ case screenRecording
44
44
 
45
45
  var id: String { rawValue }
46
46
 
47
47
  var title: String {
48
48
  switch self {
49
49
  case .accessibility: return "Accessibility"
50
+ case .screenRecording: return "Screen Recording"
50
51
  }
51
52
  }
52
53
 
53
54
  var subtitle: String {
54
55
  switch self {
55
56
  case .accessibility: return "Needed for keyboard, mouse, and automation-style control."
57
+ case .screenRecording: return "Needed for the take_screenshot tool."
56
58
  }
57
59
  }
58
60
 
59
61
  var settingsURL: String {
60
62
  switch self {
61
63
  case .accessibility: return "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"
64
+ case .screenRecording: return "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture"
62
65
  }
63
66
  }
64
67
 
65
68
  var systemImageName: String {
66
69
  switch self {
67
70
  case .accessibility: return "figure.wave"
71
+ case .screenRecording: return "camera.viewfinder"
68
72
  }
69
73
  }
70
74
  }
@@ -239,68 +243,12 @@ class GateService: ObservableObject {
239
243
  case .accessibility:
240
244
  let options = [kAXTrustedCheckOptionPrompt.takeUnretainedValue(): true] as CFDictionary
241
245
  _ = AXIsProcessTrustedWithOptions(options)
246
+ case .screenRecording:
247
+ guard let url = URL(string: permission.settingsURL) else { return }
248
+ NSWorkspace.shared.open(url)
242
249
  }
243
250
  }
244
251
 
245
- func captureAndSend() {
246
- appendLog("Screenshot requested via deeplink.")
247
-
248
- Task {
249
- do {
250
- let content = try await SCShareableContent.current
251
- guard let display = content.displays.first else {
252
- appendLog("No display found for screenshot.")
253
- return
254
- }
255
-
256
- let filter = SCContentFilter(display: display, excludingWindows: [])
257
- let config = SCStreamConfiguration()
258
- config.width = display.width * 2
259
- config.height = display.height * 2
260
- config.capturesAudio = false
261
-
262
- let image = try await SCScreenshotManager.captureImage(
263
- contentFilter: filter,
264
- configuration: config
265
- )
266
-
267
- let rep = NSBitmapImageRep(cgImage: image)
268
- guard let pngData = rep.representation(using: .png, properties: [:]) else {
269
- appendLog("Failed to encode screenshot as PNG.")
270
- return
271
- }
272
-
273
- let tempPath = NSTemporaryDirectory() + "poke-gate-screenshot.png"
274
- let tempURL = URL(fileURLWithPath: tempPath)
275
- try pngData.write(to: tempURL)
276
- appendLog("Screenshot saved to \(tempPath) (\(pngData.count) bytes)")
277
-
278
- guard let token = loadPokeLoginToken() else {
279
- appendLog("Cannot send screenshot: not signed in to Poke.")
280
- return
281
- }
282
-
283
- let url = URL(string: "https://poke.com/api/v1/inbound/api-message")!
284
- var request = URLRequest(url: url)
285
- request.httpMethod = "POST"
286
- request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
287
- request.setValue("application/json", forHTTPHeaderField: "Content-Type")
288
-
289
- let message = "Here's a screenshot of my screen right now. [Image attached as base64 PNG, \(pngData.count) bytes, \(display.width)x\(display.height)]"
290
- let body: [String: Any] = ["message": message]
291
- request.httpBody = try JSONSerialization.data(withJSONObject: body)
292
-
293
- let (_, response) = try await URLSession.shared.data(for: request)
294
- if let httpResp = response as? HTTPURLResponse, httpResp.statusCode == 200 {
295
- appendLog("Screenshot sent to Poke.")
296
- } else {
297
- appendLog("Failed to send screenshot to Poke.")
298
- }
299
- } catch {
300
- appendLog("Screenshot error: \(error.localizedDescription)")
301
- }
302
- }
303
- }
304
252
 
305
253
  func autoStartIfNeeded() {
306
254
  guard !hasAutoStarted else { return }
@@ -608,7 +556,7 @@ class GateService: ObservableObject {
608
556
  private func killOrphanedProcesses() {
609
557
  let task = Process()
610
558
  task.executableURL = URL(fileURLWithPath: "/usr/bin/pkill")
611
- task.arguments = ["-f", "poke-gate"]
559
+ task.arguments = ["-f", "node.*poke-gate.*app\\.js"]
612
560
  try? task.run()
613
561
  task.waitUntilExit()
614
562
  }
@@ -789,14 +737,10 @@ class GateService: ObservableObject {
789
737
  writeConfig(json)
790
738
  }
791
739
 
792
- private func isPermissionGranted(_ permission: SystemPermission) -> Bool {
740
+ func isPermissionGranted(_ permission: SystemPermission) -> Bool {
793
741
  switch permission {
794
742
  case .accessibility:
795
743
  if AXIsProcessTrusted() { return true }
796
- // AXIsProcessTrusted() can return false despite the toggle being ON
797
- // in System Settings when the TCC entry's code signature doesn't match
798
- // the running binary (ad-hoc signing, rebuild from Xcode, etc.).
799
- // Probe the API directly as a fallback.
800
744
  let systemWide = AXUIElementCreateSystemWide()
801
745
  var value: AnyObject?
802
746
  let result = AXUIElementCopyAttributeValue(
@@ -805,6 +749,8 @@ class GateService: ObservableObject {
805
749
  &value
806
750
  )
807
751
  return result == .success || result == .noValue || result == .attributeUnsupported
752
+ case .screenRecording:
753
+ return CGPreflightScreenCaptureAccess()
808
754
  }
809
755
  }
810
756
 
@@ -6,16 +6,5 @@
6
6
  <string>dev.fka.Poke-macOS-Gate</string>
7
7
  <key>LSUIElement</key>
8
8
  <true/>
9
- <key>CFBundleURLTypes</key>
10
- <array>
11
- <dict>
12
- <key>CFBundleURLName</key>
13
- <string>dev.fka.Poke-macOS-Gate</string>
14
- <key>CFBundleURLSchemes</key>
15
- <array>
16
- <string>poke-gate</string>
17
- </array>
18
- </dict>
19
- </array>
20
9
  </dict>
21
10
  </plist>
@@ -3,15 +3,6 @@ import ServiceManagement
3
3
 
4
4
  class AppDelegate: NSObject, NSApplicationDelegate {
5
5
  var service: GateService?
6
-
7
- func application(_ application: NSApplication, open urls: [URL]) {
8
- for url in urls {
9
- guard url.scheme == "poke-gate" else { continue }
10
- if url.host == "screenshot" {
11
- service?.captureAndSend()
12
- }
13
- }
14
- }
15
6
  }
16
7
 
17
8
  @main
@@ -122,8 +113,8 @@ struct PopoverContent: View {
122
113
  }
123
114
  .frame(width: 320)
124
115
  .padding(10)
125
- .onChange(of: service.hasSystemPermissionsGranted) { _, granted in
126
- if granted && pendingFullMode {
116
+ .onChange(of: service.systemPermissionStatuses) { _, _ in
117
+ if service.isPermissionGranted(.accessibility) && pendingFullMode {
127
118
  pendingFullMode = false
128
119
  service.setPermissionMode(.full)
129
120
  }
@@ -436,7 +427,7 @@ struct PopoverContent: View {
436
427
  private func handleModeSelection(_ mode: GateService.PermissionMode) {
437
428
  guard mode != service.permissionMode else { return }
438
429
 
439
- if mode == .full && !service.hasSystemPermissionsGranted {
430
+ if mode == .full && !service.isPermissionGranted(.accessibility) {
440
431
  pendingFullMode = true
441
432
  service.openSystemPermission(.accessibility)
442
433
  } else {
@@ -286,7 +286,7 @@
286
286
  "@executable_path/../Frameworks",
287
287
  );
288
288
  MACOSX_DEPLOYMENT_TARGET = 15.0;
289
- MARKETING_VERSION = 0.2.2;
289
+ MARKETING_VERSION = 0.3.1;
290
290
  PRODUCT_BUNDLE_IDENTIFIER = "dev.fka.Poke-macOS-Gate";
291
291
  PRODUCT_NAME = "$(TARGET_NAME)";
292
292
  REGISTER_APP_GROUPS = YES;
@@ -322,7 +322,7 @@
322
322
  "@executable_path/../Frameworks",
323
323
  );
324
324
  MACOSX_DEPLOYMENT_TARGET = 15.0;
325
- MARKETING_VERSION = 0.2.2;
325
+ MARKETING_VERSION = 0.3.1;
326
326
  PRODUCT_BUNDLE_IDENTIFIER = "dev.fka.Poke-macOS-Gate";
327
327
  PRODUCT_NAME = "$(TARGET_NAME)";
328
328
  REGISTER_APP_GROUPS = YES;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poke-gate",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Expose your machine to your Poke AI assistant via MCP tunnel",
5
5
  "type": "module",
6
6
  "bin": {
package/src/app.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { startMcpServer, enableLogging, getPermissionMode } from "./mcp-server.js";
2
2
  import { startTunnel } from "./tunnel.js";
3
3
  import { startAgentScheduler, stopAgentScheduler } from "./agents.js";
4
- import { Poke, isLoggedIn, login, getToken } from "poke";
4
+ import { sendToWebhook } from "./webhook.js";
5
+ import { isLoggedIn, login, getToken } from "poke";
5
6
  import { execSync } from "node:child_process";
6
7
 
7
8
  const verbose = process.argv.includes("--verbose") || process.argv.includes("-v");
@@ -9,9 +10,10 @@ enableLogging(verbose);
9
10
 
10
11
  function killExistingInstances() {
11
12
  const myPid = process.pid;
13
+ const ppid = process.ppid;
12
14
  try {
13
- const out = execSync("pgrep -f 'poke-gate'", { encoding: "utf-8" }).trim();
14
- const pids = out.split("\n").map(Number).filter((p) => p && p !== myPid);
15
+ const out = execSync("pgrep -f 'node.*poke-gate.*app\\.js'", { encoding: "utf-8" }).trim();
16
+ const pids = out.split("\n").map(Number).filter((p) => p && p !== myPid && p !== ppid);
15
17
  for (const pid of pids) {
16
18
  try { process.kill(pid, "SIGTERM"); } catch {}
17
19
  }
@@ -66,7 +68,7 @@ async function connectWithRetry(mcpUrl, token) {
66
68
  reconnectWatchdog = null;
67
69
  log(`Tunnel connected (${data.connectionId})`);
68
70
  log("Ready — your Poke agent can now access this machine.");
69
- notifyPoke(data.connectionId, token);
71
+ notifyPoke(data.connectionId);
70
72
  startAgentScheduler();
71
73
  break;
72
74
  case "disconnected":
@@ -152,11 +154,10 @@ function buildAccessModeMessage(mode) {
152
154
  }
153
155
  }
154
156
 
155
- async function notifyPoke(connectionId, token) {
157
+ async function notifyPoke(connectionId) {
156
158
  try {
157
159
  const mode = getPermissionMode();
158
- const poke = new Poke({ token });
159
- await poke.sendMessage(
160
+ await sendToWebhook(
160
161
  `Hey! I've connected my computer to you via Poke Gate (tunnel: ${connectionId}). ` +
161
162
  `${buildAccessModeMessage(mode)} ` +
162
163
  `Just use the tools whenever I ask you to do something on my computer. ` +
package/src/mcp-server.js CHANGED
@@ -779,23 +779,11 @@ function handleToolCall(name, args, context = {}) {
779
779
  case "take_screenshot": {
780
780
  logTool(name, cleanArgs);
781
781
 
782
- return runCommand('open -Ra "Poke macOS Gate" 2>/dev/null', homedir()).then((appCheck) => {
783
- if (appCheck.exitCode === 0) {
784
- return runCommand('open "poke-gate://screenshot"', homedir()).then(() => {
785
- return { content: [{ type: "text", text: "Screenshot captured and sent to Poke via the macOS app." }] };
786
- });
782
+ return runCommand("npx -y poke-gate@latest take-screenshot", homedir(), { permissionMode: "full" }).then((result) => {
783
+ if (result.exitCode === 0) {
784
+ return { content: [{ type: "text", text: "Screenshot captured and sent to Poke." }] };
787
785
  }
788
-
789
- const ts = new Date().toISOString().replace(/[:.]/g, "-");
790
- const dest = cleanArgs.path
791
- ? resolve(cleanArgs.path.replace(/^~/, homedir()))
792
- : join(homedir(), "Desktop", `screenshot-${ts}.png`);
793
- return runCommand(`/usr/sbin/screencapture -x "${dest}"`, homedir()).then((result) => {
794
- if (result.exitCode === 0) {
795
- return { content: [{ type: "text", text: `Screenshot saved to ${dest}` }] };
796
- }
797
- return { content: [{ type: "text", text: `Screenshot failed: ${result.stderr || "unknown error"}. Grant Screen Recording permission to Terminal or install the Poke macOS Gate app.` }], isError: true };
798
- });
786
+ return { content: [{ type: "text", text: `Screenshot failed: ${result.stderr || result.stdout || "unknown error"}` }], isError: true };
799
787
  });
800
788
  }
801
789
 
@@ -0,0 +1,42 @@
1
+ import { execSync } from "node:child_process";
2
+ import { readFileSync, unlinkSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { tmpdir, platform } from "node:os";
5
+ import { isLoggedIn, login } from "poke";
6
+ import { sendToWebhook } from "./webhook.js";
7
+
8
+ export async function takeScreenshot() {
9
+ if (platform() !== "darwin") {
10
+ console.error("Screenshots are only supported on macOS.");
11
+ process.exit(1);
12
+ }
13
+
14
+ if (!isLoggedIn()) {
15
+ console.log("Signing in to Poke...");
16
+ await login();
17
+ }
18
+
19
+ const dest = join(tmpdir(), `poke-gate-screenshot-${Date.now()}.png`);
20
+
21
+ console.log("Capturing screenshot...");
22
+ try {
23
+ execSync(`/usr/sbin/screencapture -x "${dest}"`, { stdio: "pipe" });
24
+ } catch {
25
+ console.error("Screenshot failed. Grant Screen Recording permission in System Settings > Privacy & Security > Screen Recording.");
26
+ process.exit(1);
27
+ }
28
+
29
+ const png = readFileSync(dest);
30
+ const base64 = png.toString("base64");
31
+
32
+ console.log(`Screenshot captured (${(png.length / 1024).toFixed(0)} KB). Sending to Poke...`);
33
+
34
+ try {
35
+ await sendToWebhook(
36
+ `Here is a screenshot of my screen right now. Reply me with the image.\n\n\`\`\`\ndata:image/png;base64,${base64}\n\`\`\``
37
+ );
38
+ console.log("Screenshot sent to Poke.");
39
+ } finally {
40
+ try { unlinkSync(dest); } catch {}
41
+ }
42
+ }
package/src/tunnel.js CHANGED
@@ -1,29 +1,11 @@
1
1
  import { PokeTunnel, getToken } from "poke";
2
- import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
3
- import { join } from "node:path";
4
- import { homedir } from "node:os";
5
-
6
- const CONFIG_DIR = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
7
- const STATE_PATH = join(CONFIG_DIR, "poke-gate", "state.json");
2
+ import { loadState, saveState } from "./webhook.js";
8
3
 
9
4
  function log(msg) {
10
5
  const ts = new Date().toISOString().slice(11, 19);
11
6
  console.log(`[${ts}] ${msg}`);
12
7
  }
13
8
 
14
- function loadState() {
15
- try {
16
- return JSON.parse(readFileSync(STATE_PATH, "utf-8"));
17
- } catch {
18
- return {};
19
- }
20
- }
21
-
22
- function saveState(state) {
23
- mkdirSync(join(CONFIG_DIR, "poke-gate"), { recursive: true });
24
- writeFileSync(STATE_PATH, JSON.stringify(state, null, 2));
25
- }
26
-
27
9
  async function cleanupStaleConnections() {
28
10
  const token = getToken();
29
11
  if (!token) return;
@@ -49,7 +31,8 @@ async function cleanupStaleConnections() {
49
31
  } catch {}
50
32
  }
51
33
 
52
- saveState({});
34
+ const { webhookUrl, webhookToken } = loadState();
35
+ saveState({ webhookUrl, webhookToken });
53
36
  }
54
37
 
55
38
  export async function startTunnel({ mcpUrl, onEvent }) {
@@ -64,7 +47,7 @@ export async function startTunnel({ mcpUrl, onEvent }) {
64
47
  url: mcpUrl,
65
48
  name: "poke-gate",
66
49
  token,
67
- cleanupOnStop: false,
50
+ cleanupOnStop: true,
68
51
  });
69
52
 
70
53
  tunnel.on("connected", (info) => {
@@ -72,6 +55,7 @@ export async function startTunnel({ mcpUrl, onEvent }) {
72
55
  const history = state.connectionHistory || [];
73
56
  history.push(info.connectionId);
74
57
  saveState({
58
+ ...state,
75
59
  connectionId: info.connectionId,
76
60
  connectionHistory: history.slice(-10),
77
61
  });
package/src/webhook.js ADDED
@@ -0,0 +1,43 @@
1
+ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { Poke, getToken } from "poke";
5
+
6
+ const CONFIG_DIR = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
7
+ const STATE_PATH = join(CONFIG_DIR, "poke-gate", "state.json");
8
+
9
+ export function loadState() {
10
+ try {
11
+ return JSON.parse(readFileSync(STATE_PATH, "utf-8"));
12
+ } catch {
13
+ return {};
14
+ }
15
+ }
16
+
17
+ export function saveState(state) {
18
+ mkdirSync(join(CONFIG_DIR, "poke-gate"), { recursive: true });
19
+ writeFileSync(STATE_PATH, JSON.stringify(state, null, 2));
20
+ }
21
+
22
+ export async function getWebhook() {
23
+ const state = loadState();
24
+ if (state.webhookUrl && state.webhookToken) {
25
+ return { webhookUrl: state.webhookUrl, webhookToken: state.webhookToken };
26
+ }
27
+
28
+ const token = getToken();
29
+ if (!token) throw new Error("No Poke auth token available.");
30
+
31
+ const poke = new Poke({ token });
32
+ const result = await poke.createWebhook({ condition: "poke-gate", action: "poke-gate" });
33
+
34
+ const webhook = { webhookUrl: result.webhookUrl, webhookToken: result.webhookToken };
35
+ saveState({ ...state, ...webhook });
36
+ return webhook;
37
+ }
38
+
39
+ export async function sendToWebhook(message) {
40
+ const { webhookUrl, webhookToken } = await getWebhook();
41
+ const poke = new Poke({ token: getToken() });
42
+ return poke.sendWebhook({ webhookUrl, webhookToken, data: { message } });
43
+ }
package/Gate.app DELETED
File without changes