Skip to content

Commit 465074c

Browse files
committed
v0.1.5 — smarter npx discovery, run button in agents editor, SwiftUI fix
Made-with: Cursor
1 parent 5663b6b commit 465074c

5 files changed

Lines changed: 150 additions & 23 deletions

File tree

clients/Poke macOS Gate/Poke macOS Gate.xcodeproj/project.pbxproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,11 +259,14 @@
259259
ENABLE_PREVIEWS = YES;
260260
GENERATE_INFOPLIST_FILE = YES;
261261
INFOPLIST_FILE = "Poke macOS Gate/Info.plist";
262+
INFOPLIST_KEY_CFBundleDisplayName = "Poke macOS Gate";
263+
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
262264
INFOPLIST_KEY_NSHumanReadableCopyright = "";
263265
LD_RUNPATH_SEARCH_PATHS = (
264266
"$(inherited)",
265267
"@executable_path/../Frameworks",
266268
);
269+
MACOSX_DEPLOYMENT_TARGET = 26.0;
267270
MARKETING_VERSION = 0.1.3;
268271
PRODUCT_BUNDLE_IDENTIFIER = "dev.fka.Poke-macOS-Gate";
269272
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -291,11 +294,14 @@
291294
ENABLE_PREVIEWS = YES;
292295
GENERATE_INFOPLIST_FILE = YES;
293296
INFOPLIST_FILE = "Poke macOS Gate/Info.plist";
297+
INFOPLIST_KEY_CFBundleDisplayName = "Poke macOS Gate";
298+
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
294299
INFOPLIST_KEY_NSHumanReadableCopyright = "";
295300
LD_RUNPATH_SEARCH_PATHS = (
296301
"$(inherited)",
297302
"@executable_path/../Frameworks",
298303
);
304+
MACOSX_DEPLOYMENT_TARGET = 26.0;
299305
MARKETING_VERSION = 0.1.3;
300306
PRODUCT_BUNDLE_IDENTIFIER = "dev.fka.Poke-macOS-Gate";
301307
PRODUCT_NAME = "$(TARGET_NAME)";

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

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ class AgentsViewModel: ObservableObject {
2525
@Published var selectedAgent: AgentFile?
2626
@Published var editorContent: String = ""
2727
@Published var showingEnv: Bool = false
28+
@Published var isRunning: Bool = false
29+
@Published var lastRunOutput: String = ""
2830

2931
private var fileWatcher: DispatchSourceFileSystemObject?
3032
private var dirFD: Int32 = -1
@@ -108,15 +110,20 @@ class AgentsViewModel: ObservableObject {
108110
}
109111

110112
func select(_ agent: AgentFile) {
111-
selectedAgent = agent
112-
showingEnv = false
113-
loadContent()
113+
DispatchQueue.main.async {
114+
self.selectedAgent = agent
115+
self.showingEnv = false
116+
self.loadContent()
117+
}
114118
}
115119

116120
func loadContent() {
117121
guard let agent = selectedAgent else { return }
118122
let url = showingEnv ? agent.envPath : agent.path
119-
editorContent = (try? String(contentsOf: url, encoding: .utf8)) ?? (showingEnv ? "# No .env file yet\n" : "")
123+
let content = (try? String(contentsOf: url, encoding: .utf8)) ?? (showingEnv ? "# No .env file yet\n" : "")
124+
DispatchQueue.main.async {
125+
self.editorContent = content
126+
}
120127
}
121128

122129
func save() {
@@ -169,6 +176,45 @@ class AgentsViewModel: ObservableObject {
169176
}
170177
}
171178

179+
func runAgent(_ agent: AgentFile) {
180+
guard !isRunning else { return }
181+
isRunning = true
182+
lastRunOutput = ""
183+
184+
let fullPath = GateService().shellPath()
185+
let proc = Process()
186+
let pipe = Pipe()
187+
188+
proc.executableURL = URL(fileURLWithPath: "/bin/zsh")
189+
proc.arguments = ["-c", "npx -y poke-gate run-agent \(agent.agentId)"]
190+
proc.environment = ["HOME": NSHomeDirectory(), "PATH": fullPath]
191+
proc.standardOutput = pipe
192+
proc.standardError = pipe
193+
proc.currentDirectoryURL = FileManager.default.homeDirectoryForCurrentUser
194+
195+
let handle = pipe.fileHandleForReading
196+
handle.readabilityHandler = { [weak self] fh in
197+
let data = fh.availableData
198+
guard !data.isEmpty, let text = String(data: data, encoding: .utf8) else { return }
199+
DispatchQueue.main.async {
200+
self?.lastRunOutput += text
201+
}
202+
}
203+
204+
proc.terminationHandler = { [weak self] _ in
205+
DispatchQueue.main.async {
206+
self?.isRunning = false
207+
}
208+
}
209+
210+
do {
211+
try proc.run()
212+
} catch {
213+
isRunning = false
214+
lastRunOutput = "Failed to run: \(error.localizedDescription)"
215+
}
216+
}
217+
172218
func deleteAgent(_ agent: AgentFile) {
173219
try? FileManager.default.removeItem(at: agent.path)
174220
if agent.hasEnv {
@@ -301,6 +347,15 @@ struct AgentDetailView: View {
301347
}
302348
}
303349
}
350+
351+
Button {
352+
viewModel.runAgent(agent)
353+
} label: {
354+
Label(viewModel.isRunning ? "Running…" : "Run", systemImage: "play.fill")
355+
.font(.caption)
356+
}
357+
.disabled(viewModel.isRunning)
358+
.padding(.leading, 4)
304359
}
305360
.padding(.horizontal, 12)
306361
.padding(.vertical, 8)

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

Lines changed: 83 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,13 @@ class GateService: ObservableObject {
3232

3333
func runPokeLogin() {
3434
let fullPath = shellPath()
35+
let npxBin = findNpx()
3536
let proc = Process()
3637
proc.executableURL = URL(fileURLWithPath: "/bin/zsh")
37-
proc.arguments = ["-c", "npx -y poke@latest login"]
38+
proc.arguments = ["-c", "\(npxBin) -y poke@latest login"]
3839
proc.environment = ["HOME": NSHomeDirectory(), "PATH": fullPath]
3940
try? proc.run()
40-
appendLog("Launched poke login — check your browser.")
41+
appendLog("Launched poke login (npx: \(npxBin)) — check your browser.")
4142
}
4243

4344
func autoStartIfNeeded() {
@@ -90,19 +91,82 @@ class GateService: ObservableObject {
9091
}
9192
}
9293

93-
private func shellPath() -> String {
94-
let loginShell = ProcessInfo.processInfo.environment["SHELL"] ?? "/bin/zsh"
95-
let pathProc = Process()
96-
let pathPipe = Pipe()
97-
pathProc.executableURL = URL(fileURLWithPath: loginShell)
98-
pathProc.arguments = ["-ilc", "echo $PATH"]
99-
pathProc.standardOutput = pathPipe
100-
pathProc.standardError = FileHandle.nullDevice
101-
pathProc.environment = ["HOME": NSHomeDirectory()]
102-
try? pathProc.run()
103-
pathProc.waitUntilExit()
104-
let data = pathPipe.fileHandleForReading.readDataToEndOfFile()
105-
return String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
94+
func shellPath() -> String {
95+
let home = NSHomeDirectory()
96+
let fallback = "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin"
97+
98+
// Try multiple shells/strategies to get PATH
99+
let strategies: [(String, [String])] = [
100+
("/bin/zsh", ["-ilc", "echo $PATH"]),
101+
("/bin/zsh", ["-lc", "echo $PATH"]),
102+
("/bin/bash", ["-lc", "echo $PATH"]),
103+
]
104+
105+
for (shell, args) in strategies {
106+
let proc = Process()
107+
let pipe = Pipe()
108+
proc.executableURL = URL(fileURLWithPath: shell)
109+
proc.arguments = args
110+
proc.standardOutput = pipe
111+
proc.standardError = FileHandle.nullDevice
112+
proc.environment = ["HOME": home]
113+
do {
114+
try proc.run()
115+
proc.waitUntilExit()
116+
if proc.terminationStatus == 0 {
117+
let data = pipe.fileHandleForReading.readDataToEndOfFile()
118+
if let path = String(data: data, encoding: .utf8)?
119+
.trimmingCharacters(in: .whitespacesAndNewlines),
120+
!path.isEmpty {
121+
return path
122+
}
123+
}
124+
} catch {
125+
continue
126+
}
127+
}
128+
129+
// Fallback: build PATH from common locations
130+
var paths = fallback.split(separator: ":").map(String.init)
131+
132+
let commonDirs = [
133+
"\(home)/.nvm/versions/node",
134+
"\(home)/.volta/bin",
135+
"\(home)/.fnm/aliases/default/bin",
136+
"\(home)/.local/bin",
137+
"\(home)/.cargo/bin",
138+
"/opt/homebrew/bin",
139+
"/usr/local/bin",
140+
]
141+
142+
for dir in commonDirs {
143+
if FileManager.default.fileExists(atPath: dir) {
144+
if dir.contains(".nvm") {
145+
// Find the latest node version in nvm
146+
if let versions = try? FileManager.default.contentsOfDirectory(atPath: dir) {
147+
if let latest = versions.sorted().last {
148+
let binPath = "\(dir)/\(latest)/bin"
149+
if !paths.contains(binPath) { paths.insert(binPath, at: 0) }
150+
}
151+
}
152+
} else if !paths.contains(dir) {
153+
paths.insert(dir, at: 0)
154+
}
155+
}
156+
}
157+
158+
return paths.joined(separator: ":")
159+
}
160+
161+
private func findNpx() -> String {
162+
let path = shellPath()
163+
for dir in path.split(separator: ":") {
164+
let npxPath = "\(dir)/npx"
165+
if FileManager.default.isExecutableFile(atPath: npxPath) {
166+
return npxPath
167+
}
168+
}
169+
return "npx"
106170
}
107171

108172
private func launchProcess() {
@@ -112,15 +176,17 @@ class GateService: ObservableObject {
112176
appendLog("Starting poke-gate…")
113177

114178
let fullPath = shellPath()
179+
let npxBin = findNpx()
180+
181+
appendLog("Using npx at: \(npxBin)")
115182

116183
let proc = Process()
117184
let pipe = Pipe()
118185

119186
proc.executableURL = URL(fileURLWithPath: "/bin/zsh")
120-
proc.arguments = ["-c", "npx -y poke-gate --verbose"]
187+
proc.arguments = ["-c", "\(npxBin) -y poke-gate --verbose"]
121188
proc.environment = ProcessInfo.processInfo.environment.merging(
122189
[
123-
"POKE_API_KEY": resolveToken() ?? "",
124190
"PATH": fullPath,
125191
],
126192
uniquingKeysWith: { _, new in new }

docs/index.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ layout: home
33

44
hero:
55
name: Poke Gate
6-
text: Your Mac, controlled by AI — from anywhere.
7-
tagline: Give your Poke AI assistant access to your machine. Run commands, read files, take screenshots — all from iMessage, Telegram, or SMS.
6+
text: A two-way bridge between your Mac and your AI.
7+
tagline: Poke pulls from your Mac when you ask. Your Mac pushes to Poke when something happens. Run commands, read files, take screenshots — and automate it all with Agents.
88
image:
99
src: /logo.png
1010
alt: Poke Gate

0 commit comments

Comments
 (0)