poke-gate 0.0.1 → 0.0.3

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/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # poke-gate
1
+ # 🚪 poke-gate
2
2
 
3
3
  Expose your machine to your [Poke](https://poke.com) AI assistant via MCP tunnel.
4
4
 
@@ -0,0 +1,11 @@
1
+ {
2
+ "colors" : [
3
+ {
4
+ "idiom" : "universal"
5
+ }
6
+ ],
7
+ "info" : {
8
+ "author" : "xcode",
9
+ "version" : 1
10
+ }
11
+ }
@@ -0,0 +1,58 @@
1
+ {
2
+ "images" : [
3
+ {
4
+ "idiom" : "mac",
5
+ "scale" : "1x",
6
+ "size" : "16x16"
7
+ },
8
+ {
9
+ "idiom" : "mac",
10
+ "scale" : "2x",
11
+ "size" : "16x16"
12
+ },
13
+ {
14
+ "idiom" : "mac",
15
+ "scale" : "1x",
16
+ "size" : "32x32"
17
+ },
18
+ {
19
+ "idiom" : "mac",
20
+ "scale" : "2x",
21
+ "size" : "32x32"
22
+ },
23
+ {
24
+ "idiom" : "mac",
25
+ "scale" : "1x",
26
+ "size" : "128x128"
27
+ },
28
+ {
29
+ "idiom" : "mac",
30
+ "scale" : "2x",
31
+ "size" : "128x128"
32
+ },
33
+ {
34
+ "idiom" : "mac",
35
+ "scale" : "1x",
36
+ "size" : "256x256"
37
+ },
38
+ {
39
+ "idiom" : "mac",
40
+ "scale" : "2x",
41
+ "size" : "256x256"
42
+ },
43
+ {
44
+ "idiom" : "mac",
45
+ "scale" : "1x",
46
+ "size" : "512x512"
47
+ },
48
+ {
49
+ "idiom" : "mac",
50
+ "scale" : "2x",
51
+ "size" : "512x512"
52
+ }
53
+ ],
54
+ "info" : {
55
+ "author" : "xcode",
56
+ "version" : 1
57
+ }
58
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "info" : {
3
+ "author" : "xcode",
4
+ "version" : 1
5
+ }
6
+ }
@@ -0,0 +1,8 @@
1
+ // Unused — app runs as menu bar only
2
+ import SwiftUI
3
+
4
+ struct ContentView: View {
5
+ var body: some View {
6
+ EmptyView()
7
+ }
8
+ }
@@ -0,0 +1,222 @@
1
+ import Foundation
2
+ import Combine
3
+ import ScreenCaptureKit
4
+
5
+ @MainActor
6
+ class GateService: ObservableObject {
7
+ enum Status: String {
8
+ case stopped = "Stopped"
9
+ case starting = "Starting…"
10
+ case connected = "Connected"
11
+ case disconnected = "Disconnected"
12
+ case error = "Error"
13
+ }
14
+
15
+ @Published var status: Status = .stopped
16
+ @Published var logs: [String] = []
17
+ @Published var userName: String? = nil
18
+
19
+ private var hasAutoStarted = false
20
+ private var process: Process?
21
+ private var outputPipe: Pipe?
22
+ private var shouldRestart = true
23
+ private let maxLogs = 200
24
+
25
+ var apiKey: String {
26
+ get { loadAPIKey() ?? "" }
27
+ set { saveAPIKey(newValue) }
28
+ }
29
+
30
+ var hasAPIKey: Bool {
31
+ let key = loadAPIKey()
32
+ return key != nil && !key!.isEmpty
33
+ }
34
+
35
+ func autoStartIfNeeded() {
36
+ guard !hasAutoStarted else { return }
37
+ hasAutoStarted = true
38
+ requestScreenCapturePermission()
39
+ if hasAPIKey {
40
+ start()
41
+ }
42
+ }
43
+
44
+ private func requestScreenCapturePermission() {
45
+ Task {
46
+ do {
47
+ _ = try await SCShareableContent.current
48
+ } catch {
49
+ appendLog("Screen capture permission not granted yet.")
50
+ }
51
+ }
52
+ }
53
+
54
+ func start() {
55
+ guard hasAPIKey else {
56
+ status = .error
57
+ appendLog("No API key configured.")
58
+ return
59
+ }
60
+ shouldRestart = true
61
+ launchProcess()
62
+ }
63
+
64
+ func stop() {
65
+ shouldRestart = false
66
+ killProcess()
67
+ status = .stopped
68
+ }
69
+
70
+ func restart() {
71
+ stop()
72
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
73
+ self.start()
74
+ }
75
+ }
76
+
77
+ private func shellPath() -> String {
78
+ let loginShell = ProcessInfo.processInfo.environment["SHELL"] ?? "/bin/zsh"
79
+ let pathProc = Process()
80
+ let pathPipe = Pipe()
81
+ pathProc.executableURL = URL(fileURLWithPath: loginShell)
82
+ pathProc.arguments = ["-ilc", "echo $PATH"]
83
+ pathProc.standardOutput = pathPipe
84
+ pathProc.standardError = FileHandle.nullDevice
85
+ pathProc.environment = ["HOME": NSHomeDirectory()]
86
+ try? pathProc.run()
87
+ pathProc.waitUntilExit()
88
+ let data = pathPipe.fileHandleForReading.readDataToEndOfFile()
89
+ return String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
90
+ }
91
+
92
+ private func launchProcess() {
93
+ killProcess()
94
+
95
+ status = .starting
96
+ appendLog("Starting poke-gate…")
97
+
98
+ let fullPath = shellPath()
99
+
100
+ let proc = Process()
101
+ let pipe = Pipe()
102
+
103
+ proc.executableURL = URL(fileURLWithPath: "/bin/zsh")
104
+ proc.arguments = ["-c", "npx -y poke-gate --verbose"]
105
+ proc.environment = ProcessInfo.processInfo.environment.merging(
106
+ [
107
+ "POKE_API_KEY": loadAPIKey() ?? "",
108
+ "PATH": fullPath,
109
+ ],
110
+ uniquingKeysWith: { _, new in new }
111
+ )
112
+ proc.standardOutput = pipe
113
+ proc.standardError = pipe
114
+ proc.currentDirectoryURL = FileManager.default.homeDirectoryForCurrentUser
115
+
116
+ let handle = pipe.fileHandleForReading
117
+ handle.readabilityHandler = { [weak self] fh in
118
+ let data = fh.availableData
119
+ guard !data.isEmpty, let line = String(data: data, encoding: .utf8) else { return }
120
+ DispatchQueue.main.async {
121
+ self?.handleOutput(line)
122
+ }
123
+ }
124
+
125
+ proc.terminationHandler = { [weak self] proc in
126
+ DispatchQueue.main.async {
127
+ self?.handleTermination(exitCode: proc.terminationStatus)
128
+ }
129
+ }
130
+
131
+ do {
132
+ try proc.run()
133
+ self.process = proc
134
+ self.outputPipe = pipe
135
+ } catch {
136
+ status = .error
137
+ appendLog("Failed to start: \(error.localizedDescription)")
138
+ }
139
+ }
140
+
141
+ private func killProcess() {
142
+ if let proc = process, proc.isRunning {
143
+ proc.terminate()
144
+ }
145
+ outputPipe?.fileHandleForReading.readabilityHandler = nil
146
+ process = nil
147
+ outputPipe = nil
148
+ }
149
+
150
+ private func handleOutput(_ raw: String) {
151
+ for line in raw.components(separatedBy: .newlines) where !line.isEmpty {
152
+ appendLog(line)
153
+
154
+ if line.contains("Tunnel connected") || line.contains("Ready") {
155
+ status = .connected
156
+ } else if line.contains("Tunnel disconnected") || line.contains("Reconnecting") {
157
+ status = .disconnected
158
+ } else if line.contains("Failed to connect") || line.contains("error") {
159
+ if status != .connected {
160
+ status = .error
161
+ }
162
+ }
163
+ }
164
+ }
165
+
166
+ private func handleTermination(exitCode: Int32) {
167
+ appendLog("Process exited with code \(exitCode)")
168
+ if shouldRestart {
169
+ status = .disconnected
170
+ appendLog("Restarting in 2 seconds…")
171
+ DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
172
+ if self.shouldRestart {
173
+ self.launchProcess()
174
+ }
175
+ }
176
+ } else {
177
+ status = .stopped
178
+ }
179
+ }
180
+
181
+ private func appendLog(_ line: String) {
182
+ let ts = DateFormatter.localizedString(from: Date(), dateStyle: .none, timeStyle: .medium)
183
+ logs.append("[\(ts)] \(line)")
184
+ if logs.count > maxLogs {
185
+ logs.removeFirst(logs.count - maxLogs)
186
+ }
187
+ }
188
+
189
+ // MARK: - Config
190
+
191
+ private var configURL: URL {
192
+ let configDir: URL
193
+ if let xdg = ProcessInfo.processInfo.environment["XDG_CONFIG_HOME"] {
194
+ configDir = URL(fileURLWithPath: xdg)
195
+ } else {
196
+ configDir = FileManager.default.homeDirectoryForCurrentUser
197
+ .appendingPathComponent(".config")
198
+ }
199
+ return configDir
200
+ .appendingPathComponent("poke-gate")
201
+ .appendingPathComponent("config.json")
202
+ }
203
+
204
+ private func loadAPIKey() -> String? {
205
+ guard let data = try? Data(contentsOf: configURL),
206
+ let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
207
+ let key = json["apiKey"] as? String else {
208
+ return nil
209
+ }
210
+ return key
211
+ }
212
+
213
+ private func saveAPIKey(_ key: String) {
214
+ let dir = configURL.deletingLastPathComponent()
215
+ try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
216
+ let json: [String: Any] = ["apiKey": key]
217
+ if let data = try? JSONSerialization.data(withJSONObject: json, options: [.prettyPrinted]) {
218
+ try? data.write(to: configURL)
219
+ }
220
+ objectWillChange.send()
221
+ }
222
+ }
@@ -0,0 +1,8 @@
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>LSUIElement</key>
6
+ <true/>
7
+ </dict>
8
+ </plist>
@@ -0,0 +1,2 @@
1
+ // Unused — kept for Xcode project reference compatibility
2
+ import Foundation
@@ -0,0 +1,47 @@
1
+ import SwiftUI
2
+
3
+ struct LogsView: View {
4
+ @ObservedObject var service: GateService
5
+
6
+ var body: some View {
7
+ VStack(spacing: 0) {
8
+ HStack {
9
+ Text("Logs")
10
+ .font(.headline)
11
+ Spacer()
12
+ Button("Clear") {
13
+ service.logs.removeAll()
14
+ }
15
+ .buttonStyle(.plain)
16
+ .foregroundStyle(.secondary)
17
+ .font(.caption)
18
+ }
19
+ .padding(.horizontal, 12)
20
+ .padding(.vertical, 8)
21
+
22
+ Divider()
23
+
24
+ ScrollViewReader { proxy in
25
+ ScrollView {
26
+ LazyVStack(alignment: .leading, spacing: 2) {
27
+ ForEach(Array(service.logs.enumerated()), id: \.offset) { index, line in
28
+ Text(line)
29
+ .font(.system(.caption, design: .monospaced))
30
+ .foregroundStyle(.primary)
31
+ .textSelection(.enabled)
32
+ .id(index)
33
+ }
34
+ }
35
+ .padding(.horizontal, 12)
36
+ .padding(.vertical, 8)
37
+ }
38
+ .onChange(of: service.logs.count) { _, _ in
39
+ if let last = service.logs.indices.last {
40
+ proxy.scrollTo(last, anchor: .bottom)
41
+ }
42
+ }
43
+ }
44
+ }
45
+ .frame(width: 480, height: 320)
46
+ }
47
+ }
@@ -0,0 +1,109 @@
1
+ import SwiftUI
2
+
3
+ @main
4
+ struct Poke_macOS_GateApp: App {
5
+ @StateObject private var service = GateService()
6
+
7
+ var body: some Scene {
8
+ MenuBarExtra {
9
+ MenuBarContent(service: service)
10
+ .onAppear { service.autoStartIfNeeded() }
11
+ } label: {
12
+ Image(systemName: menuBarIcon)
13
+ }
14
+
15
+ Window("Logs", id: "logs") {
16
+ LogsView(service: service)
17
+ }
18
+ .defaultSize(width: 480, height: 320)
19
+
20
+ Window("Settings", id: "settings") {
21
+ SettingsView(service: service)
22
+ }
23
+ .windowResizability(.contentSize)
24
+ }
25
+
26
+ private var menuBarIcon: String {
27
+ switch service.status {
28
+ case .connected: "door.left.hand.open"
29
+ case .starting, .disconnected: "door.left.hand.closed"
30
+ case .error: "exclamationmark.triangle"
31
+ case .stopped: "door.left.hand.closed"
32
+ }
33
+ }
34
+ }
35
+
36
+ struct MenuBarContent: View {
37
+ @ObservedObject var service: GateService
38
+ @Environment(\.openWindow) private var openWindow
39
+
40
+ var body: some View {
41
+ Label(service.status.rawValue, systemImage: statusIcon)
42
+ .foregroundStyle(statusColor)
43
+
44
+ Divider()
45
+
46
+ Button("View Logs…") {
47
+ NSApp.activate(ignoringOtherApps: true)
48
+ openWindow(id: "logs")
49
+ }
50
+
51
+ Button("Settings…") {
52
+ NSApp.activate(ignoringOtherApps: true)
53
+ openWindow(id: "settings")
54
+ }
55
+
56
+ Divider()
57
+
58
+ if service.status == .connected || service.status == .starting || service.status == .disconnected {
59
+ Button("Restart") {
60
+ service.restart()
61
+ }
62
+ } else {
63
+ Button("Start") {
64
+ service.start()
65
+ }
66
+ }
67
+
68
+ Divider()
69
+
70
+ Button("Quit Poke Gate") {
71
+ service.stop()
72
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
73
+ NSApp.terminate(nil)
74
+ }
75
+ }
76
+ .keyboardShortcut("q")
77
+
78
+ Divider()
79
+
80
+ Text("Poke Gate v0.0.3")
81
+ .font(.caption)
82
+ .foregroundStyle(.secondary)
83
+ Text("Community project — not affiliated with Poke")
84
+ .font(.caption2)
85
+ .foregroundStyle(.secondary)
86
+ Button("GitHub") {
87
+ NSWorkspace.shared.open(URL(string: "https://github.com/f/poke-gate")!)
88
+ }
89
+ .font(.caption)
90
+ }
91
+
92
+ private var statusIcon: String {
93
+ switch service.status {
94
+ case .connected: "circle.fill"
95
+ case .starting, .disconnected: "circle.dotted"
96
+ case .error: "exclamationmark.circle.fill"
97
+ case .stopped: "circle"
98
+ }
99
+ }
100
+
101
+ private var statusColor: Color {
102
+ switch service.status {
103
+ case .connected: .green
104
+ case .starting, .disconnected: .yellow
105
+ case .error: .red
106
+ case .stopped: .secondary
107
+ }
108
+ }
109
+ }
@@ -0,0 +1,50 @@
1
+ import SwiftUI
2
+
3
+ struct SettingsView: View {
4
+ @ObservedObject var service: GateService
5
+ @State private var apiKeyInput: String = ""
6
+ @Environment(\.dismiss) private var dismiss
7
+
8
+ var body: some View {
9
+ VStack(spacing: 16) {
10
+ Text("Poke Gate Settings")
11
+ .font(.headline)
12
+
13
+ VStack(alignment: .leading, spacing: 8) {
14
+ Text("API Key")
15
+ .font(.subheadline)
16
+ .foregroundStyle(.secondary)
17
+
18
+ SecureField("Paste your API key", text: $apiKeyInput)
19
+ .textFieldStyle(.roundedBorder)
20
+
21
+ Link("Get your key at poke.com/kitchen/api-keys",
22
+ destination: URL(string: "https://poke.com/kitchen/api-keys")!)
23
+ .font(.caption)
24
+ .foregroundStyle(.blue)
25
+ }
26
+
27
+ HStack {
28
+ Button("Cancel") {
29
+ dismiss()
30
+ }
31
+ .keyboardShortcut(.cancelAction)
32
+
33
+ Spacer()
34
+
35
+ Button("Save") {
36
+ service.apiKey = apiKeyInput
37
+ dismiss()
38
+ service.restart()
39
+ }
40
+ .keyboardShortcut(.defaultAction)
41
+ .disabled(apiKeyInput.isEmpty)
42
+ }
43
+ }
44
+ .padding(20)
45
+ .frame(width: 360)
46
+ .onAppear {
47
+ apiKeyInput = service.apiKey
48
+ }
49
+ }
50
+ }
@@ -0,0 +1,336 @@
1
+ // !$*UTF8*$!
2
+ {
3
+ archiveVersion = 1;
4
+ classes = {
5
+ };
6
+ objectVersion = 77;
7
+ objects = {
8
+
9
+ /* Begin PBXFileReference section */
10
+ 442DE4462F71DCD9009BF9EF /* Poke macOS Gate.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Poke macOS Gate.app"; sourceTree = BUILT_PRODUCTS_DIR; };
11
+ /* End PBXFileReference section */
12
+
13
+ /* Begin PBXFileSystemSynchronizedRootGroup section */
14
+ 442DE4482F71DCD9009BF9EF /* Poke macOS Gate */ = {
15
+ isa = PBXFileSystemSynchronizedRootGroup;
16
+ path = "Poke macOS Gate";
17
+ sourceTree = "<group>";
18
+ };
19
+ /* End PBXFileSystemSynchronizedRootGroup section */
20
+
21
+ /* Begin PBXFrameworksBuildPhase section */
22
+ 442DE4432F71DCD9009BF9EF /* Frameworks */ = {
23
+ isa = PBXFrameworksBuildPhase;
24
+ buildActionMask = 2147483647;
25
+ files = (
26
+ );
27
+ runOnlyForDeploymentPostprocessing = 0;
28
+ };
29
+ /* End PBXFrameworksBuildPhase section */
30
+
31
+ /* Begin PBXGroup section */
32
+ 442DE43D2F71DCD9009BF9EF = {
33
+ isa = PBXGroup;
34
+ children = (
35
+ 442DE4482F71DCD9009BF9EF /* Poke macOS Gate */,
36
+ 442DE4472F71DCD9009BF9EF /* Products */,
37
+ );
38
+ sourceTree = "<group>";
39
+ };
40
+ 442DE4472F71DCD9009BF9EF /* Products */ = {
41
+ isa = PBXGroup;
42
+ children = (
43
+ 442DE4462F71DCD9009BF9EF /* Poke macOS Gate.app */,
44
+ );
45
+ name = Products;
46
+ sourceTree = "<group>";
47
+ };
48
+ /* End PBXGroup section */
49
+
50
+ /* Begin PBXNativeTarget section */
51
+ 442DE4452F71DCD9009BF9EF /* Poke macOS Gate */ = {
52
+ isa = PBXNativeTarget;
53
+ buildConfigurationList = 442DE4532F71DCDA009BF9EF /* Build configuration list for PBXNativeTarget "Poke macOS Gate" */;
54
+ buildPhases = (
55
+ 442DE4422F71DCD9009BF9EF /* Sources */,
56
+ 442DE4432F71DCD9009BF9EF /* Frameworks */,
57
+ 442DE4442F71DCD9009BF9EF /* Resources */,
58
+ );
59
+ buildRules = (
60
+ );
61
+ dependencies = (
62
+ );
63
+ fileSystemSynchronizedGroups = (
64
+ 442DE4482F71DCD9009BF9EF /* Poke macOS Gate */,
65
+ );
66
+ name = "Poke macOS Gate";
67
+ packageProductDependencies = (
68
+ );
69
+ productName = "Poke macOS Gate";
70
+ productReference = 442DE4462F71DCD9009BF9EF /* Poke macOS Gate.app */;
71
+ productType = "com.apple.product-type.application";
72
+ };
73
+ /* End PBXNativeTarget section */
74
+
75
+ /* Begin PBXProject section */
76
+ 442DE43E2F71DCD9009BF9EF /* Project object */ = {
77
+ isa = PBXProject;
78
+ attributes = {
79
+ BuildIndependentTargetsInParallel = 1;
80
+ LastSwiftUpdateCheck = 2620;
81
+ LastUpgradeCheck = 2620;
82
+ TargetAttributes = {
83
+ 442DE4452F71DCD9009BF9EF = {
84
+ CreatedOnToolsVersion = 26.2;
85
+ };
86
+ };
87
+ };
88
+ buildConfigurationList = 442DE4412F71DCD9009BF9EF /* Build configuration list for PBXProject "Poke macOS Gate" */;
89
+ developmentRegion = en;
90
+ hasScannedForEncodings = 0;
91
+ knownRegions = (
92
+ en,
93
+ Base,
94
+ );
95
+ mainGroup = 442DE43D2F71DCD9009BF9EF;
96
+ minimizedProjectReferenceProxies = 1;
97
+ preferredProjectObjectVersion = 77;
98
+ productRefGroup = 442DE4472F71DCD9009BF9EF /* Products */;
99
+ projectDirPath = "";
100
+ projectRoot = "";
101
+ targets = (
102
+ 442DE4452F71DCD9009BF9EF /* Poke macOS Gate */,
103
+ );
104
+ };
105
+ /* End PBXProject section */
106
+
107
+ /* Begin PBXResourcesBuildPhase section */
108
+ 442DE4442F71DCD9009BF9EF /* Resources */ = {
109
+ isa = PBXResourcesBuildPhase;
110
+ buildActionMask = 2147483647;
111
+ files = (
112
+ );
113
+ runOnlyForDeploymentPostprocessing = 0;
114
+ };
115
+ /* End PBXResourcesBuildPhase section */
116
+
117
+ /* Begin PBXSourcesBuildPhase section */
118
+ 442DE4422F71DCD9009BF9EF /* Sources */ = {
119
+ isa = PBXSourcesBuildPhase;
120
+ buildActionMask = 2147483647;
121
+ files = (
122
+ );
123
+ runOnlyForDeploymentPostprocessing = 0;
124
+ };
125
+ /* End PBXSourcesBuildPhase section */
126
+
127
+ /* Begin XCBuildConfiguration section */
128
+ 442DE4512F71DCDA009BF9EF /* Debug */ = {
129
+ isa = XCBuildConfiguration;
130
+ buildSettings = {
131
+ ALWAYS_SEARCH_USER_PATHS = NO;
132
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
133
+ CLANG_ANALYZER_NONNULL = YES;
134
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
135
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
136
+ CLANG_ENABLE_MODULES = YES;
137
+ CLANG_ENABLE_OBJC_ARC = YES;
138
+ CLANG_ENABLE_OBJC_WEAK = YES;
139
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
140
+ CLANG_WARN_BOOL_CONVERSION = YES;
141
+ CLANG_WARN_COMMA = YES;
142
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
143
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
144
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
145
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
146
+ CLANG_WARN_EMPTY_BODY = YES;
147
+ CLANG_WARN_ENUM_CONVERSION = YES;
148
+ CLANG_WARN_INFINITE_RECURSION = YES;
149
+ CLANG_WARN_INT_CONVERSION = YES;
150
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
151
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
152
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
153
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
154
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
155
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
156
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
157
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
158
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
159
+ CLANG_WARN_UNREACHABLE_CODE = YES;
160
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
161
+ COPY_PHASE_STRIP = NO;
162
+ DEBUG_INFORMATION_FORMAT = dwarf;
163
+ DEVELOPMENT_TEAM = RJA7656U34;
164
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
165
+ ENABLE_TESTABILITY = YES;
166
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
167
+ GCC_C_LANGUAGE_STANDARD = gnu17;
168
+ GCC_DYNAMIC_NO_PIC = NO;
169
+ GCC_NO_COMMON_BLOCKS = YES;
170
+ GCC_OPTIMIZATION_LEVEL = 0;
171
+ GCC_PREPROCESSOR_DEFINITIONS = (
172
+ "DEBUG=1",
173
+ "$(inherited)",
174
+ );
175
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
176
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
177
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
178
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
179
+ GCC_WARN_UNUSED_FUNCTION = YES;
180
+ GCC_WARN_UNUSED_VARIABLE = YES;
181
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
182
+ MACOSX_DEPLOYMENT_TARGET = 26.2;
183
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
184
+ MTL_FAST_MATH = YES;
185
+ ONLY_ACTIVE_ARCH = YES;
186
+ SDKROOT = macosx;
187
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
188
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
189
+ };
190
+ name = Debug;
191
+ };
192
+ 442DE4522F71DCDA009BF9EF /* Release */ = {
193
+ isa = XCBuildConfiguration;
194
+ buildSettings = {
195
+ ALWAYS_SEARCH_USER_PATHS = NO;
196
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
197
+ CLANG_ANALYZER_NONNULL = YES;
198
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
199
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
200
+ CLANG_ENABLE_MODULES = YES;
201
+ CLANG_ENABLE_OBJC_ARC = YES;
202
+ CLANG_ENABLE_OBJC_WEAK = YES;
203
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
204
+ CLANG_WARN_BOOL_CONVERSION = YES;
205
+ CLANG_WARN_COMMA = YES;
206
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
207
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
208
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
209
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
210
+ CLANG_WARN_EMPTY_BODY = YES;
211
+ CLANG_WARN_ENUM_CONVERSION = YES;
212
+ CLANG_WARN_INFINITE_RECURSION = YES;
213
+ CLANG_WARN_INT_CONVERSION = YES;
214
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
215
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
216
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
217
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
218
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
219
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
220
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
221
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
222
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
223
+ CLANG_WARN_UNREACHABLE_CODE = YES;
224
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
225
+ COPY_PHASE_STRIP = NO;
226
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
227
+ DEVELOPMENT_TEAM = RJA7656U34;
228
+ ENABLE_NS_ASSERTIONS = NO;
229
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
230
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
231
+ GCC_C_LANGUAGE_STANDARD = gnu17;
232
+ GCC_NO_COMMON_BLOCKS = YES;
233
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
234
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
235
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
236
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
237
+ GCC_WARN_UNUSED_FUNCTION = YES;
238
+ GCC_WARN_UNUSED_VARIABLE = YES;
239
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
240
+ MACOSX_DEPLOYMENT_TARGET = 26.2;
241
+ MTL_ENABLE_DEBUG_INFO = NO;
242
+ MTL_FAST_MATH = YES;
243
+ SDKROOT = macosx;
244
+ SWIFT_COMPILATION_MODE = wholemodule;
245
+ };
246
+ name = Release;
247
+ };
248
+ 442DE4542F71DCDA009BF9EF /* Debug */ = {
249
+ isa = XCBuildConfiguration;
250
+ buildSettings = {
251
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
252
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
253
+ CODE_SIGN_STYLE = Automatic;
254
+ COMBINE_HIDPI_IMAGES = YES;
255
+ CURRENT_PROJECT_VERSION = 1;
256
+ DEVELOPMENT_TEAM = RJA7656U34;
257
+ ENABLE_APP_SANDBOX = NO;
258
+ ENABLE_HARDENED_RUNTIME = NO;
259
+ ENABLE_PREVIEWS = YES;
260
+ GENERATE_INFOPLIST_FILE = YES;
261
+ INFOPLIST_FILE = "Poke macOS Gate/Info.plist";
262
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
263
+ LD_RUNPATH_SEARCH_PATHS = (
264
+ "$(inherited)",
265
+ "@executable_path/../Frameworks",
266
+ );
267
+ MARKETING_VERSION = 1.0;
268
+ PRODUCT_BUNDLE_IDENTIFIER = "dev.fka.Poke-macOS-Gate";
269
+ PRODUCT_NAME = "$(TARGET_NAME)";
270
+ REGISTER_APP_GROUPS = YES;
271
+ STRING_CATALOG_GENERATE_SYMBOLS = YES;
272
+ SWIFT_APPROACHABLE_CONCURRENCY = YES;
273
+ SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
274
+ SWIFT_EMIT_LOC_STRINGS = YES;
275
+ SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
276
+ SWIFT_VERSION = 5.0;
277
+ };
278
+ name = Debug;
279
+ };
280
+ 442DE4552F71DCDA009BF9EF /* Release */ = {
281
+ isa = XCBuildConfiguration;
282
+ buildSettings = {
283
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
284
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
285
+ CODE_SIGN_STYLE = Automatic;
286
+ COMBINE_HIDPI_IMAGES = YES;
287
+ CURRENT_PROJECT_VERSION = 1;
288
+ DEVELOPMENT_TEAM = RJA7656U34;
289
+ ENABLE_APP_SANDBOX = NO;
290
+ ENABLE_HARDENED_RUNTIME = NO;
291
+ ENABLE_PREVIEWS = YES;
292
+ GENERATE_INFOPLIST_FILE = YES;
293
+ INFOPLIST_FILE = "Poke macOS Gate/Info.plist";
294
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
295
+ LD_RUNPATH_SEARCH_PATHS = (
296
+ "$(inherited)",
297
+ "@executable_path/../Frameworks",
298
+ );
299
+ MARKETING_VERSION = 1.0;
300
+ PRODUCT_BUNDLE_IDENTIFIER = "dev.fka.Poke-macOS-Gate";
301
+ PRODUCT_NAME = "$(TARGET_NAME)";
302
+ REGISTER_APP_GROUPS = YES;
303
+ STRING_CATALOG_GENERATE_SYMBOLS = YES;
304
+ SWIFT_APPROACHABLE_CONCURRENCY = YES;
305
+ SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
306
+ SWIFT_EMIT_LOC_STRINGS = YES;
307
+ SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
308
+ SWIFT_VERSION = 5.0;
309
+ };
310
+ name = Release;
311
+ };
312
+ /* End XCBuildConfiguration section */
313
+
314
+ /* Begin XCConfigurationList section */
315
+ 442DE4412F71DCD9009BF9EF /* Build configuration list for PBXProject "Poke macOS Gate" */ = {
316
+ isa = XCConfigurationList;
317
+ buildConfigurations = (
318
+ 442DE4512F71DCDA009BF9EF /* Debug */,
319
+ 442DE4522F71DCDA009BF9EF /* Release */,
320
+ );
321
+ defaultConfigurationIsVisible = 0;
322
+ defaultConfigurationName = Release;
323
+ };
324
+ 442DE4532F71DCDA009BF9EF /* Build configuration list for PBXNativeTarget "Poke macOS Gate" */ = {
325
+ isa = XCConfigurationList;
326
+ buildConfigurations = (
327
+ 442DE4542F71DCDA009BF9EF /* Debug */,
328
+ 442DE4552F71DCDA009BF9EF /* Release */,
329
+ );
330
+ defaultConfigurationIsVisible = 0;
331
+ defaultConfigurationName = Release;
332
+ };
333
+ /* End XCConfigurationList section */
334
+ };
335
+ rootObject = 442DE43E2F71DCD9009BF9EF /* Project object */;
336
+ }
@@ -0,0 +1,7 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <Workspace
3
+ version = "1.0">
4
+ <FileRef
5
+ location = "self:">
6
+ </FileRef>
7
+ </Workspace>
@@ -0,0 +1,14 @@
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>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poke-gate",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
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,9 +1,12 @@
1
- import { startMcpServer } from "./mcp-server.js";
1
+ import { startMcpServer, enableLogging } from "./mcp-server.js";
2
2
  import { startTunnel } from "./tunnel.js";
3
3
  import { readFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { homedir } from "node:os";
6
6
 
7
+ const verbose = process.argv.includes("--verbose") || process.argv.includes("-v");
8
+ enableLogging(verbose);
9
+
7
10
  function resolveToken() {
8
11
  if (process.env.POKE_API_KEY) return process.env.POKE_API_KEY;
9
12
 
package/src/mcp-server.js CHANGED
@@ -2,12 +2,29 @@ import http from "node:http";
2
2
  import { execSync, exec } from "node:child_process";
3
3
  import { readFileSync, writeFileSync, readdirSync, statSync } from "node:fs";
4
4
  import { hostname, platform, arch, uptime, totalmem, freemem, homedir } from "node:os";
5
- import { join, resolve } from "node:path";
5
+ import { join, resolve, extname } from "node:path";
6
6
 
7
7
  const SERVER_INFO = { name: "poke-gate", version: "0.0.1" };
8
8
 
9
9
  const COMMAND_TIMEOUT = 30_000;
10
10
 
11
+ let logEnabled = false;
12
+
13
+ export function enableLogging(enabled) {
14
+ logEnabled = enabled;
15
+ }
16
+
17
+ function logTool(name, args, result) {
18
+ if (!logEnabled) return;
19
+ const ts = new Date().toISOString().slice(11, 19);
20
+ console.log(`[${ts}] tool: ${name}`);
21
+ if (name === "run_command") console.log(`[${ts}] $ ${args.command}${args.cwd ? ` (in ${args.cwd})` : ""}`);
22
+ else if (name === "read_file") console.log(`[${ts}] read: ${args.path}`);
23
+ else if (name === "write_file") console.log(`[${ts}] write: ${args.path}`);
24
+ else if (name === "list_directory") console.log(`[${ts}] ls: ${args.path || "~"}`);
25
+ if (result?.isError) console.log(`[${ts}] error`);
26
+ }
27
+
11
28
  const TOOLS = [
12
29
  {
13
30
  name: "run_command",
@@ -62,6 +79,30 @@ const TOOLS = [
62
79
  description: "Get system information: OS, hostname, architecture, uptime, memory, and home directory.",
63
80
  inputSchema: { type: "object", properties: {} },
64
81
  },
82
+ {
83
+ name: "read_image",
84
+ description:
85
+ "Read an image or binary file and return it as base64-encoded data. " +
86
+ "Supports png, jpg, jpeg, gif, webp, pdf, and any other binary file. " +
87
+ "Returns the base64 string and MIME type.",
88
+ inputSchema: {
89
+ type: "object",
90
+ properties: {
91
+ path: { type: "string", description: "Absolute or relative path to the image/binary file" },
92
+ },
93
+ required: ["path"],
94
+ },
95
+ },
96
+ {
97
+ name: "take_screenshot",
98
+ 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.",
99
+ inputSchema: {
100
+ type: "object",
101
+ properties: {
102
+ path: { type: "string", description: "File path to save the screenshot (optional, defaults to ~/Desktop/screenshot-<timestamp>.png)" },
103
+ },
104
+ },
105
+ },
65
106
  ];
66
107
 
67
108
  function runCommand(command, cwd) {
@@ -85,18 +126,26 @@ function runCommand(command, cwd) {
85
126
  function handleToolCall(name, args) {
86
127
  switch (name) {
87
128
  case "run_command": {
88
- return runCommand(args.command, args.cwd).then((result) => ({
89
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
90
- }));
129
+ logTool(name, args);
130
+ return runCommand(args.command, args.cwd).then((result) => {
131
+ const r = { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
132
+ if (result.exitCode !== 0) r.isError = true;
133
+ logTool(name, args, r);
134
+ return r;
135
+ });
91
136
  }
92
137
 
93
138
  case "read_file": {
94
139
  try {
95
140
  const p = resolve(args.path.replace(/^~/, homedir()));
96
141
  const text = readFileSync(p, "utf-8");
97
- return { content: [{ type: "text", text: text.slice(0, 100_000) }] };
142
+ const r = { content: [{ type: "text", text: text.slice(0, 100_000) }] };
143
+ logTool(name, args, r);
144
+ return r;
98
145
  } catch (err) {
99
- return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
146
+ const r = { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
147
+ logTool(name, args, r);
148
+ return r;
100
149
  }
101
150
  }
102
151
 
@@ -104,26 +153,34 @@ function handleToolCall(name, args) {
104
153
  try {
105
154
  const p = resolve(args.path.replace(/^~/, homedir()));
106
155
  writeFileSync(p, args.content);
107
- return { content: [{ type: "text", text: `Written to ${p}` }] };
156
+ const r = { content: [{ type: "text", text: `Written to ${p}` }] };
157
+ logTool(name, args, r);
158
+ return r;
108
159
  } catch (err) {
109
- return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
160
+ const r = { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
161
+ logTool(name, args, r);
162
+ return r;
110
163
  }
111
164
  }
112
165
 
113
166
  case "list_directory": {
114
167
  try {
115
168
  const dir = resolve((args.path || "~").replace(/^~/, homedir()));
116
- const entries = readdirSync(dir).map((name) => {
169
+ const entries = readdirSync(dir).map((entry) => {
117
170
  try {
118
- const s = statSync(join(dir, name));
119
- return `${s.isDirectory() ? "d" : "-"} ${name}`;
171
+ const s = statSync(join(dir, entry));
172
+ return `${s.isDirectory() ? "d" : "-"} ${entry}`;
120
173
  } catch {
121
- return `? ${name}`;
174
+ return `? ${entry}`;
122
175
  }
123
176
  });
124
- return { content: [{ type: "text", text: entries.join("\n") }] };
177
+ const r = { content: [{ type: "text", text: entries.join("\n") }] };
178
+ logTool(name, args, r);
179
+ return r;
125
180
  } catch (err) {
126
- return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
181
+ const r = { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
182
+ logTool(name, args, r);
183
+ return r;
127
184
  }
128
185
  }
129
186
 
@@ -138,9 +195,58 @@ function handleToolCall(name, args) {
138
195
  homeDir: homedir(),
139
196
  nodeVersion: process.version,
140
197
  };
198
+ logTool(name, args);
141
199
  return { content: [{ type: "text", text: JSON.stringify(info, null, 2) }] };
142
200
  }
143
201
 
202
+ case "read_image": {
203
+ try {
204
+ const p = resolve(args.path.replace(/^~/, homedir()));
205
+ const ext = extname(p).toLowerCase().slice(1);
206
+ const mimeMap = {
207
+ png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg",
208
+ gif: "image/gif", webp: "image/webp", svg: "image/svg+xml",
209
+ pdf: "application/pdf", ico: "image/x-icon", bmp: "image/bmp",
210
+ };
211
+ const mimeType = mimeMap[ext] || "application/octet-stream";
212
+ const buf = readFileSync(p);
213
+ const base64 = buf.toString("base64");
214
+ logTool(name, args);
215
+
216
+ if (mimeType.startsWith("image/")) {
217
+ return {
218
+ content: [
219
+ { type: "image", data: base64, mimeType },
220
+ { type: "text", text: `Image: ${p} (${mimeType}, ${buf.length} bytes)` },
221
+ ],
222
+ };
223
+ }
224
+ return {
225
+ content: [
226
+ { type: "text", text: `File: ${p} (${mimeType}, ${buf.length} bytes)\nBase64: ${base64.slice(0, 200)}${base64.length > 200 ? "..." : ""}` },
227
+ ],
228
+ };
229
+ } catch (err) {
230
+ const r = { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
231
+ logTool(name, args, r);
232
+ return r;
233
+ }
234
+ }
235
+
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}` }] };
245
+ }
246
+ return { content: [{ type: "text", text: `Screenshot failed: ${result.stderr || "unknown error"}` }], isError: true };
247
+ });
248
+ }
249
+
144
250
  default:
145
251
  return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
146
252
  }