poke-gate 0.1.9 → 0.2.0

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.
Files changed (30) hide show
  1. package/.github/workflows/release.yml +53 -3
  2. package/README.md +48 -14
  3. package/bin/poke-gate.js +17 -0
  4. package/clients/Poke macOS Gate/Poke macOS Gate/AboutView.swift +7 -1
  5. package/clients/Poke macOS Gate/Poke macOS Gate/AccessibilityPermissionView.swift +58 -0
  6. package/clients/Poke macOS Gate/Poke macOS Gate/GateService.swift +354 -23
  7. package/clients/Poke macOS Gate/Poke macOS Gate/Info.plist +2 -0
  8. package/clients/Poke macOS Gate/Poke macOS Gate/LogsView.swift +1 -1
  9. package/clients/Poke macOS Gate/Poke macOS Gate/MacVisualStyle.swift +89 -0
  10. package/clients/Poke macOS Gate/Poke macOS Gate/PermissionRowView.swift +55 -0
  11. package/clients/Poke macOS Gate/Poke macOS Gate/Poke_macOS_GateApp.swift +209 -91
  12. package/clients/Poke macOS Gate/Poke macOS Gate/SettingsView.swift +123 -81
  13. package/clients/Poke macOS Gate/Poke macOS Gate/SetupView.swift +157 -0
  14. package/clients/Poke macOS Gate/Poke macOS Gate.xcodeproj/project.pbxproj +28 -8
  15. package/docs/cli.md +19 -0
  16. package/docs/getting-started.md +9 -6
  17. package/docs/index.md +23 -18
  18. package/docs/macos-app.md +39 -4
  19. package/docs/security.md +62 -18
  20. package/examples/agents/battery.30m.js +1 -1
  21. package/examples/agents/screentime.24h.js +5 -6
  22. package/package.json +3 -1
  23. package/src/agents.js +5 -8
  24. package/src/app.js +3 -3
  25. package/src/mcp-server.js +502 -27
  26. package/src/permission-service.js +128 -0
  27. package/test/mcp-server-access-policy.test.js +40 -0
  28. package/test/mcp-server-loop-guard.test.js +57 -0
  29. package/test/mcp-server-sandbox-command.test.js +18 -0
  30. package/test/permission-service.test.js +97 -0
@@ -24,7 +24,10 @@ struct Poke_macOS_GateApp: App {
24
24
  .onAppear {
25
25
  service.autoStartIfNeeded()
26
26
  appDelegate.service = service
27
- checkLoginItemPrompt()
27
+ service.startPermissionPolling()
28
+ }
29
+ .onDisappear {
30
+ service.stopPermissionPolling()
28
31
  }
29
32
  } label: {
30
33
  Image(systemName: menuBarIcon)
@@ -36,6 +39,11 @@ struct Poke_macOS_GateApp: App {
36
39
  }
37
40
  .defaultSize(width: 560, height: 400)
38
41
 
42
+ Window("Setup", id: "setup") {
43
+ SetupView(service: service)
44
+ }
45
+ .windowResizability(.contentSize)
46
+
39
47
  Window("Settings", id: "settings") {
40
48
  SettingsView(service: service)
41
49
  }
@@ -96,56 +104,89 @@ struct Poke_macOS_GateApp: App {
96
104
  struct PopoverContent: View {
97
105
  @ObservedObject var service: GateService
98
106
  @Environment(\.openWindow) private var openWindow
107
+ @State private var pendingFullMode = false
99
108
 
100
109
  var body: some View {
101
- VStack(spacing: 0) {
102
- VStack(spacing: 6) {
103
- HStack(spacing: 8) {
104
- Circle()
105
- .fill(statusColor)
106
- .frame(width: 10, height: 10)
110
+ if !service.hasCompletedSetup {
111
+ SetupView(service: service)
112
+ } else {
113
+ VStack(spacing: 10) {
114
+ statusSection
115
+ recentActivitySection
116
+ accessModeSection
117
+ actionsSection
118
+ footerSection
119
+ }
120
+ .frame(width: 320)
121
+ .padding(10)
122
+ .onChange(of: service.hasSystemPermissionsGranted) { _, granted in
123
+ if granted && pendingFullMode {
124
+ pendingFullMode = false
125
+ service.setPermissionMode(.full)
126
+ }
127
+ }
128
+ }
129
+ }
107
130
 
108
- Text(statusText)
109
- .font(.system(.body, weight: .medium))
131
+ private var statusSection: some View {
132
+ VStack(alignment: .leading, spacing: 6) {
133
+ HStack(spacing: 8) {
134
+ Circle()
135
+ .fill(statusColor)
136
+ .frame(width: 10, height: 10)
110
137
 
111
- Spacer()
112
- }
138
+ Text(statusText)
139
+ .font(.system(.body, weight: .medium))
113
140
 
114
- if service.status == .connected {
115
- Text("This machine is accessible via Poke. Ask your Poke to run commands or read files.")
116
- .font(.caption)
117
- .foregroundStyle(.secondary)
118
- .frame(maxWidth: .infinity, alignment: .leading)
119
- .fixedSize(horizontal: false, vertical: true)
120
- } else if service.status == .starting {
121
- Text("Establishing connection…")
122
- .font(.caption)
123
- .foregroundStyle(.secondary)
124
- .frame(maxWidth: .infinity, alignment: .leading)
125
- } else if service.status == .error {
126
- Text("Check Logs for details.")
127
- .font(.caption)
128
- .foregroundStyle(.red.opacity(0.8))
129
- .frame(maxWidth: .infinity, alignment: .leading)
130
- }
141
+ Spacer()
131
142
  }
132
- .padding(12)
133
143
 
134
- Divider()
144
+ statusMessage
145
+ }
146
+ .frame(maxWidth: .infinity, alignment: .leading)
147
+ .padding(12)
148
+ .macPanelStyle(.neutral, cornerRadius: 12)
149
+ }
135
150
 
136
- VStack(alignment: .leading, spacing: 4) {
137
- Text("Recent activity")
138
- .font(.caption2)
139
- .foregroundStyle(.tertiary)
140
- .textCase(.uppercase)
141
-
142
- if service.logs.isEmpty {
143
- Text("No activity yet")
144
- .font(.caption)
145
- .foregroundStyle(.secondary)
146
- } else {
147
- ForEach(Array(service.logs.suffix(4).enumerated()), id: \.offset) { _, line in
148
- Text(line)
151
+ @ViewBuilder
152
+ private var statusMessage: some View {
153
+ switch service.status {
154
+ case .connected:
155
+ Text("This machine is accessible via Poke. Ask your Poke to run commands or read files.")
156
+ .font(.caption)
157
+ .foregroundStyle(.secondary)
158
+ .frame(maxWidth: .infinity, alignment: .leading)
159
+ .fixedSize(horizontal: false, vertical: true)
160
+ case .starting:
161
+ Text("Establishing connection…")
162
+ .font(.caption)
163
+ .foregroundStyle(.secondary)
164
+ .frame(maxWidth: .infinity, alignment: .leading)
165
+ case .error:
166
+ Text("Check Logs for details.")
167
+ .font(.caption)
168
+ .foregroundStyle(.red.opacity(0.8))
169
+ .frame(maxWidth: .infinity, alignment: .leading)
170
+ case .disconnected, .stopped:
171
+ EmptyView()
172
+ }
173
+ }
174
+
175
+ private var recentActivitySection: some View {
176
+ VStack(alignment: .leading, spacing: 6) {
177
+ sectionTitle("Recent activity")
178
+
179
+ if service.terminalPreviews.isEmpty {
180
+ Text("No activity yet")
181
+ .font(.caption)
182
+ .foregroundStyle(.secondary)
183
+ } else {
184
+ ForEach(Array(service.terminalPreviews.suffix(4).enumerated()), id: \.element.id) { _, entry in
185
+ HStack(spacing: 6) {
186
+ Circle()
187
+ .fill(entry.exitCode == 0 ? Color.green : (entry.exitCode == nil ? Color.gray : Color.red))
188
+ .frame(width: 5, height: 5)
189
+ Text("$ \(entry.command)")
149
190
  .font(.system(size: 9, design: .monospaced))
150
191
  .foregroundStyle(.tertiary)
151
192
  .lineLimit(1)
@@ -153,71 +194,117 @@ struct PopoverContent: View {
153
194
  }
154
195
  }
155
196
  }
156
- .frame(maxWidth: .infinity, alignment: .leading)
157
- .padding(12)
197
+ }
198
+ .frame(maxWidth: .infinity, alignment: .leading)
199
+ .padding(12)
200
+ .macPanelStyle(.neutral, cornerRadius: 12)
201
+ }
158
202
 
159
- Divider()
203
+ private var accessModeSection: some View {
204
+ VStack(alignment: .leading, spacing: 8) {
205
+ HStack {
206
+ sectionTitle("Access mode")
207
+ Spacer()
208
+ Text(service.permissionMode.title)
209
+ .font(.caption2)
210
+ .foregroundStyle(.secondary)
211
+ }
160
212
 
161
- HStack(spacing: 12) {
162
- ActionButton(icon: "text.alignleft", label: "Logs") {
163
- NSApp.activate(ignoringOtherApps: true)
164
- openWindow(id: "logs")
213
+ HStack(spacing: 6) {
214
+ ForEach(GateService.PermissionMode.allCases) { mode in
215
+ let isActive = service.permissionMode == mode || (mode == .full && pendingFullMode)
216
+ Button {
217
+ handleModeSelection(mode)
218
+ } label: {
219
+ Text(modeChipTitle(mode))
220
+ .font(.system(size: 10, weight: .semibold))
221
+ .foregroundStyle(isActive ? .white : .secondary)
222
+ .padding(.vertical, 5)
223
+ .padding(.horizontal, 8)
224
+ .background(isActive ? MacVisualStyle.chipActiveFill : MacVisualStyle.chipInactiveFill)
225
+ .clipShape(Capsule())
226
+ }
227
+ .buttonStyle(.plain)
165
228
  }
229
+ }
166
230
 
167
- ActionButton(icon: "bolt.fill", label: "Agents") {
168
- NSApp.activate(ignoringOtherApps: true)
169
- openWindow(id: "agents")
170
- }
231
+ if service.permissionMode == .full || pendingFullMode {
232
+ AccessibilityPermissionView(service: service)
233
+ }
234
+ }
235
+ .frame(maxWidth: .infinity, alignment: .leading)
236
+ .padding(12)
237
+ .macPanelStyle(.neutral, cornerRadius: 12)
238
+ }
171
239
 
172
- ActionButton(icon: "gearshape", label: "Settings") {
173
- NSApp.activate(ignoringOtherApps: true)
174
- openWindow(id: "settings")
175
- }
240
+ private var actionsSection: some View {
241
+ HStack(spacing: 12) {
242
+ ActionButton(icon: "text.alignleft", label: "Logs") {
243
+ NSApp.activate(ignoringOtherApps: true)
244
+ openWindow(id: "logs")
245
+ }
176
246
 
177
- if service.status == .connected || service.status == .starting || service.status == .disconnected {
178
- ActionButton(icon: "arrow.counterclockwise", label: "Restart") {
179
- service.restart()
180
- }
181
- } else {
182
- ActionButton(icon: "play.fill", label: "Start") {
183
- service.start()
184
- }
185
- }
247
+ ActionButton(icon: "bolt.fill", label: "Agents") {
248
+ NSApp.activate(ignoringOtherApps: true)
249
+ openWindow(id: "agents")
250
+ }
186
251
 
187
- Spacer()
252
+ ActionButton(icon: "gearshape", label: "Settings") {
253
+ NSApp.activate(ignoringOtherApps: true)
254
+ openWindow(id: "settings")
255
+ }
188
256
 
189
- ActionButton(icon: "xmark.circle", label: "Quit", tint: .secondary) {
190
- service.stop()
191
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
192
- NSApp.terminate(nil)
193
- }
257
+ if service.status == .connected || service.status == .starting || service.status == .disconnected {
258
+ ActionButton(icon: "arrow.counterclockwise", label: "Restart") {
259
+ service.restart()
260
+ }
261
+ } else {
262
+ ActionButton(icon: "play.fill", label: "Start") {
263
+ service.start()
194
264
  }
195
265
  }
196
- .padding(12)
197
266
 
198
- Divider()
267
+ Spacer()
199
268
 
200
- HStack {
201
- Button {
202
- NSApp.activate(ignoringOtherApps: true)
203
- openWindow(id: "about")
204
- } label: {
205
- Text("Poke Gate v0.0.8")
206
- .font(.caption2)
207
- .foregroundStyle(.tertiary)
269
+ ActionButton(icon: "xmark.circle", label: "Quit", tint: .secondary) {
270
+ service.stop()
271
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
272
+ NSApp.terminate(nil)
208
273
  }
209
- .buttonStyle(.plain)
210
-
211
- Spacer()
274
+ }
275
+ }
276
+ .padding(12)
277
+ .frame(maxWidth: .infinity, alignment: .leading)
278
+ .macPanelStyle(.neutral, cornerRadius: 12)
279
+ }
212
280
 
213
- Text("Not affiliated with Poke")
281
+ private var footerSection: some View {
282
+ HStack {
283
+ Button {
284
+ NSApp.activate(ignoringOtherApps: true)
285
+ openWindow(id: "about")
286
+ } label: {
287
+ Text(appVersionText)
214
288
  .font(.caption2)
215
- .foregroundStyle(.quaternary)
289
+ .foregroundStyle(MacVisualStyle.sectionTitleColor)
216
290
  }
217
- .padding(.horizontal, 12)
218
- .padding(.vertical, 8)
291
+ .buttonStyle(.plain)
292
+
293
+ Spacer()
294
+
295
+ Text("Not affiliated with Poke")
296
+ .font(.caption2)
297
+ .foregroundStyle(MacVisualStyle.sectionTitleColor.opacity(0.7))
219
298
  }
220
- .frame(width: 320)
299
+ .padding(.horizontal, 6)
300
+ }
301
+
302
+ private func sectionTitle(_ text: String) -> some View {
303
+ Text(text)
304
+ .font(.caption2)
305
+ .foregroundStyle(MacVisualStyle.sectionTitleColor)
306
+ .textCase(.uppercase)
307
+ .tracking(0.5)
221
308
  }
222
309
 
223
310
  private var statusText: String {
@@ -242,6 +329,37 @@ struct PopoverContent: View {
242
329
  case .stopped: .gray.opacity(0.5)
243
330
  }
244
331
  }
332
+
333
+ private func modeChipTitle(_ mode: GateService.PermissionMode) -> String {
334
+ switch mode {
335
+ case .full: return "Full"
336
+ case .limited: return "Limited"
337
+ case .sandbox: return "Sandbox"
338
+ }
339
+ }
340
+
341
+ private func handleModeSelection(_ mode: GateService.PermissionMode) {
342
+ guard mode != service.permissionMode else { return }
343
+
344
+ if mode == .full && !service.hasSystemPermissionsGranted {
345
+ pendingFullMode = true
346
+ service.openSystemPermission(.accessibility)
347
+ } else {
348
+ pendingFullMode = false
349
+ service.setPermissionMode(mode)
350
+ }
351
+ }
352
+
353
+ private var appVersionText: String {
354
+ let short = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "0.0.0"
355
+ let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String
356
+
357
+ if let build, !build.isEmpty, build != short {
358
+ return "Poke Gate v\(short) (\(build))"
359
+ }
360
+
361
+ return "Poke Gate v\(short)"
362
+ }
245
363
  }
246
364
 
247
365
  struct ActionButton: View {
@@ -8,90 +8,14 @@ struct SettingsView: View {
8
8
 
9
9
  var body: some View {
10
10
  VStack(alignment: .leading, spacing: 16) {
11
- VStack(alignment: .leading, spacing: 8) {
12
- Text("AUTHENTICATION")
13
- .font(.caption2)
14
- .foregroundStyle(.tertiary)
15
- .textCase(.uppercase)
16
- .tracking(0.5)
17
-
18
- HStack(spacing: 8) {
19
- Image(systemName: service.hasPokeLoginCredentials
20
- ? "checkmark.shield.fill" : "shield.slash")
21
- .foregroundStyle(service.hasPokeLoginCredentials ? .green : .orange)
22
- .font(.title3)
23
-
24
- VStack(alignment: .leading, spacing: 2) {
25
- Text(service.hasPokeLoginCredentials
26
- ? "Signed in via Poke"
27
- : "Not signed in")
28
- .font(.subheadline)
29
- .fontWeight(.medium)
30
-
31
- if service.hasPokeLoginCredentials {
32
- Text("Your Poke session is active.")
33
- .font(.caption)
34
- .foregroundStyle(.secondary)
35
- } else {
36
- Text("Run this command in Terminal to sign in:")
37
- .font(.caption)
38
- .foregroundStyle(.secondary)
39
- }
40
- }
41
- }
42
- .padding(10)
43
- .frame(maxWidth: .infinity, alignment: .leading)
44
- .background(.quaternary.opacity(0.5))
45
- .cornerRadius(8)
46
-
47
- if !service.hasPokeLoginCredentials {
48
- Button {
49
- service.runPokeLogin()
50
- } label: {
51
- Label("Sign in with Poke", systemImage: "person.crop.circle.badge.plus")
52
- }
53
- .controlSize(.large)
54
-
55
- Text("Opens a browser window to sign in.")
56
- .font(.caption)
57
- .foregroundStyle(.secondary)
58
- }
59
- }
60
-
61
- VStack(alignment: .leading, spacing: 8) {
62
- Text("CONNECTION")
63
- .font(.caption2)
64
- .foregroundStyle(.tertiary)
65
- .textCase(.uppercase)
66
- .tracking(0.5)
67
-
68
- HStack(spacing: 8) {
69
- Circle()
70
- .fill(connectionColor)
71
- .frame(width: 8, height: 8)
72
-
73
- Text(service.status.rawValue)
74
- .font(.subheadline)
75
-
76
- Spacer()
77
-
78
- Button {
79
- service.restart()
80
- } label: {
81
- Label("Reconnect", systemImage: "arrow.counterclockwise")
82
- .font(.caption)
83
- }
84
- }
85
- .padding(10)
86
- .frame(maxWidth: .infinity, alignment: .leading)
87
- .background(.quaternary.opacity(0.5))
88
- .cornerRadius(8)
89
- }
11
+ authenticationSection
12
+ accessModeSection
13
+ connectionSection
90
14
 
91
15
  VStack(alignment: .leading, spacing: 8) {
92
16
  Text("GENERAL")
93
17
  .font(.caption2)
94
- .foregroundStyle(.tertiary)
18
+ .foregroundStyle(MacVisualStyle.sectionTitleColor)
95
19
  .textCase(.uppercase)
96
20
  .tracking(0.5)
97
21
 
@@ -119,7 +43,125 @@ struct SettingsView: View {
119
43
  }
120
44
  }
121
45
  .padding(20)
122
- .frame(width: 380)
46
+ .frame(width: 430)
47
+ }
48
+
49
+ private var authenticationSection: some View {
50
+ VStack(alignment: .leading, spacing: 8) {
51
+ sectionTitle("AUTHENTICATION")
52
+
53
+ HStack(spacing: 8) {
54
+ Image(systemName: service.hasPokeLoginCredentials ? "checkmark.shield.fill" : "shield.slash")
55
+ .foregroundStyle(service.hasPokeLoginCredentials ? .green : .orange)
56
+ .font(.title3)
57
+
58
+ VStack(alignment: .leading, spacing: 2) {
59
+ Text(service.hasPokeLoginCredentials ? "Signed in via Poke" : "Not signed in")
60
+ .font(.subheadline)
61
+ .fontWeight(.medium)
62
+
63
+ Text(service.hasPokeLoginCredentials ? "Your Poke session is active." : "Run this command in Terminal to sign in:")
64
+ .font(.caption)
65
+ .foregroundStyle(.secondary)
66
+ }
67
+ }
68
+ .padding(10)
69
+ .frame(maxWidth: .infinity, alignment: .leading)
70
+ .macPanelStyle(.neutral, cornerRadius: 8)
71
+
72
+ if !service.hasPokeLoginCredentials {
73
+ Button {
74
+ service.runPokeLogin()
75
+ } label: {
76
+ Label("Sign in with Poke", systemImage: "person.crop.circle.badge.plus")
77
+ }
78
+ .controlSize(.large)
79
+
80
+ Text("Opens a browser window to sign in.")
81
+ .font(.caption)
82
+ .foregroundStyle(.secondary)
83
+ }
84
+ }
85
+ }
86
+
87
+ private var accessModeSection: some View {
88
+ VStack(alignment: .leading, spacing: 10) {
89
+ sectionTitle("ACCESS MODE")
90
+
91
+ VStack(spacing: 8) {
92
+ ForEach(GateService.PermissionMode.allCases) { mode in
93
+ permissionModeRow(mode)
94
+ }
95
+ }
96
+
97
+ if service.permissionMode == .full {
98
+ AccessibilityPermissionView(service: service)
99
+ }
100
+ }
101
+ }
102
+
103
+ private func permissionModeRow(_ mode: GateService.PermissionMode) -> some View {
104
+ Button {
105
+ service.setPermissionMode(mode)
106
+ } label: {
107
+ HStack(alignment: .top, spacing: 10) {
108
+ Image(systemName: service.permissionMode == mode ? "checkmark.circle.fill" : "circle")
109
+ .font(.headline)
110
+ .foregroundStyle(service.permissionMode == mode ? .green : .secondary)
111
+
112
+ VStack(alignment: .leading, spacing: 2) {
113
+ Text(mode.title)
114
+ .font(.subheadline)
115
+ .fontWeight(.medium)
116
+ .foregroundStyle(.primary)
117
+
118
+ Text(mode.subtitle)
119
+ .font(.caption)
120
+ .foregroundStyle(.secondary)
121
+ }
122
+
123
+ Spacer()
124
+ }
125
+ .padding(10)
126
+ .frame(maxWidth: .infinity, alignment: .leading)
127
+ .macPanelStyle(service.permissionMode == mode ? .selected : .neutral)
128
+ }
129
+ .buttonStyle(.plain)
130
+ }
131
+
132
+ private var connectionSection: some View {
133
+ VStack(alignment: .leading, spacing: 8) {
134
+ sectionTitle("CONNECTION")
135
+
136
+ HStack(spacing: 8) {
137
+ Circle()
138
+ .fill(connectionColor)
139
+ .frame(width: 8, height: 8)
140
+
141
+ Text(service.status.rawValue)
142
+ .font(.subheadline)
143
+
144
+ Spacer()
145
+
146
+ Button {
147
+ service.restart()
148
+ } label: {
149
+ Label("Reconnect", systemImage: "arrow.counterclockwise")
150
+ .font(.caption)
151
+ }
152
+ }
153
+ .padding(10)
154
+ .frame(maxWidth: .infinity, alignment: .leading)
155
+ .macPanelStyle(.neutral, cornerRadius: 8)
156
+ }
157
+ }
158
+
159
+ private func sectionTitle(_ text: String) -> some View {
160
+ Text(text)
161
+ .font(.caption2)
162
+ .foregroundStyle(MacVisualStyle.sectionTitleColor)
163
+ .textCase(.uppercase)
164
+ .tracking(0.5)
123
165
  }
124
166
 
125
167
  private var connectionColor: Color {