poke-gate 0.1.5 → 0.1.6

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.
@@ -1,5 +1,6 @@
1
1
  import Foundation
2
2
  import Combine
3
+ import ScreenCaptureKit
3
4
 
4
5
  @MainActor
5
6
  class GateService: ObservableObject {
@@ -20,6 +21,8 @@ class GateService: ObservableObject {
20
21
  private var outputPipe: Pipe?
21
22
  private var shouldRestart = true
22
23
  private let maxLogs = 200
24
+ private var restartAttempts = 0
25
+ private var healthCheckTimer: Timer?
23
26
 
24
27
  var apiKey: String {
25
28
  get { loadAPIKey() ?? "" }
@@ -41,6 +44,68 @@ class GateService: ObservableObject {
41
44
  appendLog("Launched poke login (npx: \(npxBin)) — check your browser.")
42
45
  }
43
46
 
47
+ func captureAndSend() {
48
+ appendLog("Screenshot requested via deeplink.")
49
+
50
+ Task {
51
+ do {
52
+ let content = try await SCShareableContent.current
53
+ guard let display = content.displays.first else {
54
+ appendLog("No display found for screenshot.")
55
+ return
56
+ }
57
+
58
+ let filter = SCContentFilter(display: display, excludingWindows: [])
59
+ let config = SCStreamConfiguration()
60
+ config.width = display.width * 2
61
+ config.height = display.height * 2
62
+ config.capturesAudio = false
63
+
64
+ let image = try await SCScreenshotManager.captureImage(
65
+ contentFilter: filter,
66
+ configuration: config
67
+ )
68
+
69
+ let rep = NSBitmapImageRep(cgImage: image)
70
+ guard let pngData = rep.representation(using: .png, properties: [:]) else {
71
+ appendLog("Failed to encode screenshot as PNG.")
72
+ return
73
+ }
74
+
75
+ let tempPath = NSTemporaryDirectory() + "poke-gate-screenshot.png"
76
+ let tempURL = URL(fileURLWithPath: tempPath)
77
+ try pngData.write(to: tempURL)
78
+ appendLog("Screenshot saved to \(tempPath) (\(pngData.count) bytes)")
79
+
80
+ let base64 = pngData.base64EncodedString()
81
+
82
+ guard let token = loadPokeLoginToken() else {
83
+ appendLog("Cannot send screenshot: not signed in to Poke.")
84
+ return
85
+ }
86
+
87
+ let url = URL(string: "https://poke.com/api/v1/inbound/api-message")!
88
+ var request = URLRequest(url: url)
89
+ request.httpMethod = "POST"
90
+ request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
91
+ request.setValue("application/json", forHTTPHeaderField: "Content-Type")
92
+
93
+ let message = "Here's a screenshot of my screen right now. [Image attached as base64 PNG, \(pngData.count) bytes, \(display.width)x\(display.height)]"
94
+ let body: [String: Any] = ["message": message]
95
+ request.httpBody = try JSONSerialization.data(withJSONObject: body)
96
+
97
+ let (_, response) = try await URLSession.shared.data(for: request)
98
+ if let httpResp = response as? HTTPURLResponse, httpResp.statusCode == 200 {
99
+ appendLog("Screenshot sent to Poke.")
100
+ } else {
101
+ appendLog("Failed to send screenshot to Poke.")
102
+ }
103
+ } catch {
104
+ appendLog("Screenshot error: \(error.localizedDescription)")
105
+ }
106
+ }
107
+ }
108
+
44
109
  func autoStartIfNeeded() {
45
110
  guard !hasAutoStarted else { return }
46
111
  hasAutoStarted = true
@@ -80,7 +145,9 @@ class GateService: ObservableObject {
80
145
 
81
146
  func stop() {
82
147
  shouldRestart = false
148
+ stopHealthCheck()
83
149
  killProcess()
150
+ restartAttempts = 0
84
151
  status = .stopped
85
152
  }
86
153
 
@@ -235,6 +302,8 @@ class GateService: ObservableObject {
235
302
 
236
303
  if line.contains("Tunnel connected") || line.contains("Ready") {
237
304
  status = .connected
305
+ restartAttempts = 0
306
+ startHealthCheck()
238
307
  } else if line.contains("Tunnel disconnected") || line.contains("Reconnecting") {
239
308
  status = .disconnected
240
309
  } else if line.contains("Failed to connect") || line.contains("error") {
@@ -247,10 +316,14 @@ class GateService: ObservableObject {
247
316
 
248
317
  private func handleTermination(exitCode: Int32) {
249
318
  appendLog("Process exited with code \(exitCode)")
319
+ stopHealthCheck()
320
+
250
321
  if shouldRestart {
322
+ restartAttempts += 1
323
+ let delay = min(Double(2 * (1 << min(restartAttempts - 1, 5))), 60.0)
251
324
  status = .disconnected
252
- appendLog("Restarting in 2 seconds…")
253
- DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
325
+ appendLog("Restarting in \(Int(delay))s (attempt \(restartAttempts))…")
326
+ DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
254
327
  if self.shouldRestart {
255
328
  self.launchProcess()
256
329
  }
@@ -260,6 +333,24 @@ class GateService: ObservableObject {
260
333
  }
261
334
  }
262
335
 
336
+ private func startHealthCheck() {
337
+ stopHealthCheck()
338
+ healthCheckTimer = Timer.scheduledTimer(withTimeInterval: 30, repeats: true) { [weak self] _ in
339
+ guard let self else { return }
340
+ Task { @MainActor in
341
+ if let proc = self.process, !proc.isRunning {
342
+ self.appendLog("Health check: process died, restarting.")
343
+ self.handleTermination(exitCode: -1)
344
+ }
345
+ }
346
+ }
347
+ }
348
+
349
+ private func stopHealthCheck() {
350
+ healthCheckTimer?.invalidate()
351
+ healthCheckTimer = nil
352
+ }
353
+
263
354
  private func appendLog(_ line: String) {
264
355
  let ts = DateFormatter.localizedString(from: Date(), dateStyle: .none, timeStyle: .medium)
265
356
  logs.append("[\(ts)] \(line)")
@@ -4,5 +4,16 @@
4
4
  <dict>
5
5
  <key>LSUIElement</key>
6
6
  <true/>
7
+ <key>CFBundleURLTypes</key>
8
+ <array>
9
+ <dict>
10
+ <key>CFBundleURLName</key>
11
+ <string>dev.fka.Poke-macOS-Gate</string>
12
+ <key>CFBundleURLSchemes</key>
13
+ <array>
14
+ <string>poke-gate</string>
15
+ </array>
16
+ </dict>
17
+ </array>
7
18
  </dict>
8
19
  </plist>
@@ -1,13 +1,30 @@
1
1
  import SwiftUI
2
2
 
3
+ class AppDelegate: NSObject, NSApplicationDelegate {
4
+ var service: GateService?
5
+
6
+ func application(_ application: NSApplication, open urls: [URL]) {
7
+ for url in urls {
8
+ guard url.scheme == "poke-gate" else { continue }
9
+ if url.host == "screenshot" {
10
+ service?.captureAndSend()
11
+ }
12
+ }
13
+ }
14
+ }
15
+
3
16
  @main
4
17
  struct Poke_macOS_GateApp: App {
5
18
  @StateObject private var service = GateService()
19
+ @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
6
20
 
7
21
  var body: some Scene {
8
22
  MenuBarExtra {
9
23
  PopoverContent(service: service)
10
- .onAppear { service.autoStartIfNeeded() }
24
+ .onAppear {
25
+ service.autoStartIfNeeded()
26
+ appDelegate.service = service
27
+ }
11
28
  } label: {
12
29
  Image(systemName: menuBarIcon)
13
30
  }
@@ -0,0 +1,7 @@
1
+ .mermaid svg {
2
+ line-height: normal !important;
3
+ }
4
+
5
+ .mermaid svg * {
6
+ line-height: normal !important;
7
+ }
@@ -0,0 +1,4 @@
1
+ import DefaultTheme from 'vitepress/theme'
2
+ import './custom.css'
3
+
4
+ export default DefaultTheme
@@ -9,15 +9,16 @@ Agents are **push-only** — they send data to Poke, but Poke cannot reach back
9
9
  :::
10
10
 
11
11
  ```mermaid
12
- flowchart LR
13
- subgraph YourMac ["Your Mac"]
14
- Agent["Agent script"]
15
- Local["Local data source"]
16
- Local -->|reads| Agent
12
+ flowchart TB
13
+ subgraph mac ["Your Mac"]
14
+ direction TB
15
+ Source["Local data source\nBeeper, files, APIs, logs"]
16
+ Script["Agent script\nruns on a schedule"]
17
+ Source -->|reads| Script
17
18
  end
18
19
 
19
- Agent -->|sendMessage| Poke["Poke Agent"]
20
- Poke -->|replies| You["You"]
20
+ Script -->|sendMessage| Poke["Poke Agent"]
21
+ Poke -->|replies via iMessage,\nTelegram, or SMS| You["You"]
21
22
  ```
22
23
 
23
24
  **Example:** A Beeper agent runs every hour, fetches your unread messages, and sends a digest to Poke. Now Poke knows who messaged you — and can answer "did anyone text me?" without needing your machine in real time.
@@ -5,14 +5,15 @@ Poke Gate bridges your machine to Poke's cloud so your AI assistant can execute
5
5
  ## Architecture
6
6
 
7
7
  ```mermaid
8
- flowchart LR
9
- A["You (iMessage)"] --> B["Poke Agent"]
10
- B --> C["MCP Tunnel"]
11
- C --> D["Poke Gate"]
12
- D --> E["Executes locally"]
13
- E --> C
14
- C --> B
15
- B --> A
8
+ flowchart TB
9
+ You["You"] -->|message via iMessage,\nTelegram, or SMS| Agent["Poke Agent"]
10
+ Agent -->|tool call| Tunnel["MCP Tunnel"]
11
+ Tunnel -->|WebSocket| Gate["Poke Gate"]
12
+ Gate -->|execute| Mac["Your Mac"]
13
+ Mac -->|result| Gate
14
+ Gate -->|response| Tunnel
15
+ Tunnel -->|result| Agent
16
+ Agent -->|reply| You
16
17
  ```
17
18
 
18
19
  ## Step by step
@@ -0,0 +1,148 @@
1
+ /**
2
+ * @agent context
3
+ * @name Context Fingerprint
4
+ * @description Sends a tiny snapshot of your Mac's state to Poke — volume, battery, WiFi, displays, camera. Maximum context, zero private data.
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", ".context-state.json");
22
+
23
+ function loadState() {
24
+ try { return JSON.parse(readFileSync(STATE_FILE, "utf-8")); }
25
+ catch { return {}; }
26
+ }
27
+
28
+ function saveState(state) {
29
+ writeFileSync(STATE_FILE, JSON.stringify(state));
30
+ }
31
+
32
+ function run(cmd) {
33
+ try { return execSync(cmd, { encoding: "utf-8", timeout: 5000 }).trim(); }
34
+ catch { return null; }
35
+ }
36
+
37
+ function getVolume() {
38
+ const raw = run("osascript -e 'get volume settings'");
39
+ if (!raw) return { volume: null, muted: null };
40
+ const vol = raw.match(/output volume:(\d+)/);
41
+ const muted = raw.includes("output muted:true");
42
+ return { volume: vol ? parseInt(vol[1]) : null, muted };
43
+ }
44
+
45
+ function getBattery() {
46
+ const raw = run("pmset -g batt");
47
+ if (!raw) return { level: null, charging: null };
48
+ const match = raw.match(/(\d+)%/);
49
+ const charging = raw.includes("AC Power") || raw.includes("charging");
50
+ return { level: match ? parseInt(match[1]) : null, charging };
51
+ }
52
+
53
+ function getWifi() {
54
+ const raw = run("networksetup -getairportnetwork en0");
55
+ if (!raw || raw.includes("not associated") || raw.includes("off")) return null;
56
+ return raw.replace("Current Wi-Fi Network: ", "").trim();
57
+ }
58
+
59
+ function getDisplayCount() {
60
+ const raw = run("system_profiler SPDisplaysDataType");
61
+ if (!raw) return 0;
62
+ return (raw.match(/Resolution:/g) || []).length;
63
+ }
64
+
65
+ function getCameraInUse() {
66
+ const raw = run("lsof 2>/dev/null | grep -c 'AppleCamera\\|VDC\\|iSight'");
67
+ return raw && parseInt(raw) > 0;
68
+ }
69
+
70
+ function getBluetoothDevices() {
71
+ const raw = run("system_profiler SPBluetoothDataType 2>/dev/null");
72
+ if (!raw) return [];
73
+ const devices = [];
74
+ const lines = raw.split("\n");
75
+ for (let i = 0; i < lines.length; i++) {
76
+ if (lines[i].includes("Connected: Yes")) {
77
+ for (let j = i - 1; j >= Math.max(0, i - 5); j--) {
78
+ const nameMatch = lines[j].match(/^\s{8}(\S.+):$/);
79
+ if (nameMatch) {
80
+ devices.push(nameMatch[1]);
81
+ break;
82
+ }
83
+ }
84
+ }
85
+ }
86
+ return devices;
87
+ }
88
+
89
+ function getUptime() {
90
+ const raw = run("uptime");
91
+ if (!raw) return null;
92
+ const match = raw.match(/up\s+(.+?),\s+\d+ user/);
93
+ return match ? match[1].trim() : null;
94
+ }
95
+
96
+ // Gather all signals
97
+ const volume = getVolume();
98
+ const battery = getBattery();
99
+ const wifi = getWifi();
100
+ const displays = getDisplayCount();
101
+ const camera = getCameraInUse();
102
+ const bluetooth = getBluetoothDevices();
103
+ const uptime = getUptime();
104
+
105
+ const now = new Date();
106
+ const hour = now.getHours();
107
+ const timeOfDay = hour < 6 ? "night" : hour < 12 ? "morning" : hour < 18 ? "afternoon" : "evening";
108
+
109
+ const fingerprint = {
110
+ time: now.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" }),
111
+ timeOfDay,
112
+ volume: volume.muted ? "muted" : `${volume.volume}%`,
113
+ battery: `${battery.level}%${battery.charging ? " (charging)" : " (on battery)"}`,
114
+ wifi: wifi || "not connected",
115
+ displays,
116
+ camera: camera ? "in use" : "off",
117
+ bluetooth: bluetooth.length > 0 ? bluetooth.join(", ") : "none",
118
+ uptime: uptime || "unknown",
119
+ };
120
+
121
+ console.log("Context fingerprint:", JSON.stringify(fingerprint, null, 2));
122
+
123
+ // Check if anything meaningful changed
124
+ const state = loadState();
125
+ const key = `${fingerprint.volume}|${fingerprint.wifi}|${fingerprint.displays}|${fingerprint.camera}|${fingerprint.battery}`;
126
+
127
+ if (key === state.lastKey) {
128
+ console.log("No meaningful change, skipping.");
129
+ process.exit(0);
130
+ }
131
+
132
+ saveState({ lastKey: key });
133
+
134
+ // Build a natural language summary
135
+ let summary = `Context update (${fingerprint.time}, ${timeOfDay}):\n`;
136
+ summary += `• Volume: ${fingerprint.volume}\n`;
137
+ summary += `• Battery: ${fingerprint.battery}\n`;
138
+ summary += `• WiFi: ${fingerprint.wifi}\n`;
139
+ summary += `• Displays: ${fingerprint.displays}\n`;
140
+ summary += `• Camera: ${fingerprint.camera}\n`;
141
+ summary += `• Bluetooth: ${fingerprint.bluetooth}\n`;
142
+ summary += `• Uptime: ${fingerprint.uptime}\n`;
143
+ summary += `\nUse this to understand my current situation without asking.`;
144
+
145
+ const poke = new Poke({ apiKey: token });
146
+ await poke.sendMessage(summary);
147
+
148
+ console.log("Sent context to Poke.");
@@ -0,0 +1,89 @@
1
+ /**
2
+ * @agent music
3
+ * @name Music Log
4
+ * @description Tracks what you're listening to and sends a log to Poke. Supports Apple Music and Spotify.
5
+ * @interval 10m
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", ".music-state.json");
22
+
23
+ function loadState() {
24
+ try { return JSON.parse(readFileSync(STATE_FILE, "utf-8")); }
25
+ catch { return { lastTrack: null }; }
26
+ }
27
+
28
+ function saveState(state) {
29
+ writeFileSync(STATE_FILE, JSON.stringify(state));
30
+ }
31
+
32
+ function getNowPlaying() {
33
+ // Try Spotify first
34
+ try {
35
+ const result = execSync(`osascript -e '
36
+ if application "Spotify" is running then
37
+ tell application "Spotify"
38
+ if player state is playing then
39
+ return name of current track & " — " & artist of current track & " (" & album of current track & ")"
40
+ end if
41
+ end tell
42
+ end if
43
+ return "not_playing"
44
+ '`, { encoding: "utf-8", timeout: 5000 }).trim();
45
+ if (result && result !== "not_playing") return { source: "Spotify", track: result };
46
+ } catch {}
47
+
48
+ // Try Apple Music
49
+ try {
50
+ const result = execSync(`osascript -e '
51
+ if application "Music" is running then
52
+ tell application "Music"
53
+ if player state is playing then
54
+ return name of current track & " — " & artist of current track & " (" & album of current track & ")"
55
+ end if
56
+ end tell
57
+ end if
58
+ return "not_playing"
59
+ '`, { encoding: "utf-8", timeout: 5000 }).trim();
60
+ if (result && result !== "not_playing") return { source: "Apple Music", track: result };
61
+ } catch {}
62
+
63
+ return null;
64
+ }
65
+
66
+ const playing = getNowPlaying();
67
+ const state = loadState();
68
+
69
+ if (!playing) {
70
+ console.log("Nothing playing right now.");
71
+ process.exit(0);
72
+ }
73
+
74
+ console.log(`Now playing (${playing.source}): ${playing.track}`);
75
+
76
+ if (playing.track === state.lastTrack) {
77
+ console.log("Same track as last check, skipping.");
78
+ process.exit(0);
79
+ }
80
+
81
+ saveState({ lastTrack: playing.track });
82
+
83
+ const poke = new Poke({ apiKey: token });
84
+ await poke.sendMessage(
85
+ `I'm currently listening to: ${playing.track} (on ${playing.source}). ` +
86
+ `Remember this for context about my mood and taste.`
87
+ );
88
+
89
+ console.log("Sent to Poke.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poke-gate",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
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
@@ -11,6 +11,10 @@ function log(msg) {
11
11
  console.log(`[${ts}] ${msg}`);
12
12
  }
13
13
 
14
+ function sleep(ms) {
15
+ return new Promise((resolve) => setTimeout(resolve, ms));
16
+ }
17
+
14
18
  async function ensureAuthenticated() {
15
19
  if (!isLoggedIn()) {
16
20
  log("Signing in to Poke...");
@@ -25,6 +29,53 @@ async function ensureAuthenticated() {
25
29
  return token;
26
30
  }
27
31
 
32
+ async function connectTunnel(mcpUrl, token) {
33
+ let attempt = 0;
34
+ const maxDelay = 60_000;
35
+
36
+ while (true) {
37
+ attempt++;
38
+ const delay = Math.min(2000 * Math.pow(2, attempt - 1), maxDelay);
39
+
40
+ try {
41
+ log(attempt > 1 ? `Reconnecting tunnel (attempt ${attempt})…` : "Connecting tunnel to Poke...");
42
+
43
+ await startTunnel({
44
+ mcpUrl,
45
+ onEvent: (type, data) => {
46
+ switch (type) {
47
+ case "connected":
48
+ attempt = 0;
49
+ log(`Tunnel connected (${data.connectionId})`);
50
+ log("Ready — your Poke agent can now access this machine.");
51
+ notifyPoke(data.connectionId, token);
52
+ startAgentScheduler();
53
+ break;
54
+ case "disconnected":
55
+ log("Tunnel disconnected. PokeTunnel will reconnect automatically.");
56
+ break;
57
+ case "error":
58
+ log(`Tunnel error: ${data}`);
59
+ break;
60
+ case "tools-synced":
61
+ log(`Tools synced: ${data}`);
62
+ break;
63
+ case "oauth-required":
64
+ log(`OAuth required: ${data}`);
65
+ break;
66
+ }
67
+ },
68
+ });
69
+
70
+ break;
71
+ } catch (err) {
72
+ log(`Tunnel failed: ${err.message}`);
73
+ log(`Retrying in ${Math.round(delay / 1000)}s…`);
74
+ await sleep(delay);
75
+ }
76
+ }
77
+ }
78
+
28
79
  async function main() {
29
80
  log("poke-gate starting...");
30
81
 
@@ -35,37 +86,7 @@ async function main() {
35
86
 
36
87
  const mcpUrl = `http://localhost:${port}/mcp`;
37
88
 
38
- log("Connecting tunnel to Poke...");
39
- try {
40
- await startTunnel({
41
- mcpUrl,
42
- onEvent: (type, data) => {
43
- switch (type) {
44
- case "connected":
45
- log(`Tunnel connected (${data.connectionId})`);
46
- log("Ready — your Poke agent can now access this machine.");
47
- notifyPoke(data.connectionId, token);
48
- startAgentScheduler();
49
- break;
50
- case "disconnected":
51
- log("Tunnel disconnected. Reconnecting...");
52
- break;
53
- case "error":
54
- log(`Tunnel error: ${data}`);
55
- break;
56
- case "tools-synced":
57
- log(`Tools synced: ${data}`);
58
- break;
59
- case "oauth-required":
60
- log(`OAuth required: ${data}`);
61
- break;
62
- }
63
- },
64
- });
65
- } catch (err) {
66
- log(`Failed to connect: ${err.message}`);
67
- process.exit(1);
68
- }
89
+ await connectTunnel(mcpUrl, token);
69
90
  }
70
91
 
71
92
  async function notifyPoke(connectionId, token) {
@@ -93,4 +114,12 @@ process.on("SIGTERM", () => {
93
114
  process.exit(0);
94
115
  });
95
116
 
117
+ process.on("uncaughtException", (err) => {
118
+ log(`Uncaught exception: ${err.message}`);
119
+ });
120
+
121
+ process.on("unhandledRejection", (err) => {
122
+ log(`Unhandled rejection: ${err instanceof Error ? err.message : String(err)}`);
123
+ });
124
+
96
125
  main();
package/src/mcp-server.js CHANGED
@@ -234,16 +234,25 @@ function handleToolCall(name, args) {
234
234
  }
235
235
 
236
236
  case "take_screenshot": {
237
- const ts = new Date().toISOString().replace(/[:.]/g, "-");
238
- const dest = args.path
239
- ? resolve(args.path.replace(/^~/, homedir()))
240
- : join(homedir(), "Desktop", `screenshot-${ts}.png`);
241
- logTool(name, { path: dest });
242
- return runCommand(`/usr/sbin/screencapture -x "${dest}"`, homedir()).then((result) => {
243
- if (result.exitCode === 0) {
244
- return { content: [{ type: "text", text: `Screenshot saved to ${dest}` }] };
237
+ logTool(name, args);
238
+
239
+ return runCommand('open -Ra "Poke macOS Gate" 2>/dev/null', homedir()).then((appCheck) => {
240
+ if (appCheck.exitCode === 0) {
241
+ return runCommand('open "poke-gate://screenshot"', homedir()).then(() => {
242
+ return { content: [{ type: "text", text: "Screenshot captured and sent to Poke via the macOS app." }] };
243
+ });
245
244
  }
246
- return { content: [{ type: "text", text: `Screenshot failed: ${result.stderr || "unknown error"}` }], isError: true };
245
+
246
+ const ts = new Date().toISOString().replace(/[:.]/g, "-");
247
+ const dest = args.path
248
+ ? resolve(args.path.replace(/^~/, homedir()))
249
+ : join(homedir(), "Desktop", `screenshot-${ts}.png`);
250
+ return runCommand(`/usr/sbin/screencapture -x "${dest}"`, homedir()).then((result) => {
251
+ if (result.exitCode === 0) {
252
+ return { content: [{ type: "text", text: `Screenshot saved to ${dest}` }] };
253
+ }
254
+ 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 };
255
+ });
247
256
  });
248
257
  }
249
258
 
@@ -1,14 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
- <plist version="1.0">
4
- <dict>
5
- <key>SchemeUserState</key>
6
- <dict>
7
- <key>Poke macOS Gate.xcscheme_^#shared#^_</key>
8
- <dict>
9
- <key>orderHint</key>
10
- <integer>0</integer>
11
- </dict>
12
- </dict>
13
- </dict>
14
- </plist>