poke-gate 0.0.8 → 0.1.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.
@@ -6,21 +6,27 @@ struct Poke_macOS_GateApp: App {
6
6
 
7
7
  var body: some Scene {
8
8
  MenuBarExtra {
9
- MenuBarContent(service: service)
9
+ PopoverContent(service: service)
10
10
  .onAppear { service.autoStartIfNeeded() }
11
11
  } label: {
12
12
  Image(systemName: menuBarIcon)
13
13
  }
14
+ .menuBarExtraStyle(.window)
14
15
 
15
16
  Window("Logs", id: "logs") {
16
17
  LogsView(service: service)
17
18
  }
18
- .defaultSize(width: 480, height: 320)
19
+ .defaultSize(width: 560, height: 400)
19
20
 
20
21
  Window("Settings", id: "settings") {
21
22
  SettingsView(service: service)
22
23
  }
23
24
  .windowResizability(.contentSize)
25
+
26
+ Window("About", id: "about") {
27
+ AboutView()
28
+ }
29
+ .windowResizability(.contentSize)
24
30
  }
25
31
 
26
32
  private var menuBarIcon: String {
@@ -33,77 +39,126 @@ struct Poke_macOS_GateApp: App {
33
39
  }
34
40
  }
35
41
 
36
- struct MenuBarContent: View {
42
+ struct PopoverContent: View {
37
43
  @ObservedObject var service: GateService
38
44
  @Environment(\.openWindow) private var openWindow
39
45
 
40
46
  var body: some View {
41
- Label(statusText, systemImage: statusIcon)
42
- .foregroundStyle(statusColor)
43
-
44
- if service.status == .connected {
45
- Text("This machine is now accessible via Poke.")
46
- .font(.caption2)
47
- .foregroundStyle(.secondary)
48
- Text("Ask your Poke to run commands or read files.")
49
- .font(.caption2)
50
- .foregroundStyle(.secondary)
51
- } else if service.status == .starting {
52
- Text("Establishing connection…")
53
- .font(.caption2)
54
- .foregroundStyle(.secondary)
55
- } else if service.status == .error {
56
- Text("Check Settings or view Logs for details.")
57
- .font(.caption2)
58
- .foregroundStyle(.secondary)
59
- }
60
-
61
- Divider()
62
-
63
- Button("View Logs…") {
64
- NSApp.activate(ignoringOtherApps: true)
65
- openWindow(id: "logs")
66
- }
67
-
68
- Button("Settings…") {
69
- NSApp.activate(ignoringOtherApps: true)
70
- openWindow(id: "settings")
71
- }
72
-
73
- Divider()
74
-
75
- if service.status == .connected || service.status == .starting || service.status == .disconnected {
76
- Button("Restart") {
77
- service.restart()
47
+ VStack(spacing: 0) {
48
+ VStack(spacing: 6) {
49
+ HStack(spacing: 8) {
50
+ Circle()
51
+ .fill(statusColor)
52
+ .frame(width: 10, height: 10)
53
+
54
+ Text(statusText)
55
+ .font(.system(.body, weight: .medium))
56
+
57
+ Spacer()
58
+ }
59
+
60
+ if service.status == .connected {
61
+ Text("This machine is accessible via Poke. Ask your Poke to run commands or read files.")
62
+ .font(.caption)
63
+ .foregroundStyle(.secondary)
64
+ .frame(maxWidth: .infinity, alignment: .leading)
65
+ .fixedSize(horizontal: false, vertical: true)
66
+ } else if service.status == .starting {
67
+ Text("Establishing connection…")
68
+ .font(.caption)
69
+ .foregroundStyle(.secondary)
70
+ .frame(maxWidth: .infinity, alignment: .leading)
71
+ } else if service.status == .error {
72
+ Text("Check Logs for details.")
73
+ .font(.caption)
74
+ .foregroundStyle(.red.opacity(0.8))
75
+ .frame(maxWidth: .infinity, alignment: .leading)
76
+ }
78
77
  }
79
- } else {
80
- Button("Start") {
81
- service.start()
78
+ .padding(12)
79
+
80
+ Divider()
81
+
82
+ VStack(alignment: .leading, spacing: 4) {
83
+ Text("Recent activity")
84
+ .font(.caption2)
85
+ .foregroundStyle(.tertiary)
86
+ .textCase(.uppercase)
87
+
88
+ if service.logs.isEmpty {
89
+ Text("No activity yet")
90
+ .font(.caption)
91
+ .foregroundStyle(.secondary)
92
+ } else {
93
+ ForEach(Array(service.logs.suffix(4).enumerated()), id: \.offset) { _, line in
94
+ Text(line)
95
+ .font(.system(size: 9, design: .monospaced))
96
+ .foregroundStyle(.tertiary)
97
+ .lineLimit(1)
98
+ .truncationMode(.tail)
99
+ }
100
+ }
82
101
  }
83
- }
84
-
85
- Divider()
86
-
87
- Button("Quit Poke Gate") {
88
- service.stop()
89
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
90
- NSApp.terminate(nil)
102
+ .frame(maxWidth: .infinity, alignment: .leading)
103
+ .padding(12)
104
+
105
+ Divider()
106
+
107
+ HStack(spacing: 12) {
108
+ ActionButton(icon: "text.alignleft", label: "Logs") {
109
+ NSApp.activate(ignoringOtherApps: true)
110
+ openWindow(id: "logs")
111
+ }
112
+
113
+ ActionButton(icon: "gearshape", label: "Settings") {
114
+ NSApp.activate(ignoringOtherApps: true)
115
+ openWindow(id: "settings")
116
+ }
117
+
118
+ if service.status == .connected || service.status == .starting || service.status == .disconnected {
119
+ ActionButton(icon: "arrow.counterclockwise", label: "Restart") {
120
+ service.restart()
121
+ }
122
+ } else {
123
+ ActionButton(icon: "play.fill", label: "Start") {
124
+ service.start()
125
+ }
126
+ }
127
+
128
+ Spacer()
129
+
130
+ ActionButton(icon: "xmark.circle", label: "Quit", tint: .secondary) {
131
+ service.stop()
132
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
133
+ NSApp.terminate(nil)
134
+ }
135
+ }
91
136
  }
137
+ .padding(12)
138
+
139
+ Divider()
140
+
141
+ HStack {
142
+ Button {
143
+ NSApp.activate(ignoringOtherApps: true)
144
+ openWindow(id: "about")
145
+ } label: {
146
+ Text("Poke Gate v0.0.8")
147
+ .font(.caption2)
148
+ .foregroundStyle(.tertiary)
149
+ }
150
+ .buttonStyle(.plain)
151
+
152
+ Spacer()
153
+
154
+ Text("Not affiliated with Poke")
155
+ .font(.caption2)
156
+ .foregroundStyle(.quaternary)
157
+ }
158
+ .padding(.horizontal, 12)
159
+ .padding(.vertical, 8)
92
160
  }
93
- .keyboardShortcut("q")
94
-
95
- Divider()
96
-
97
- Text("Poke Gate v0.0.3")
98
- .font(.caption)
99
- .foregroundStyle(.secondary)
100
- Text("Community project — not affiliated with Poke")
101
- .font(.caption2)
102
- .foregroundStyle(.secondary)
103
- Button("GitHub") {
104
- NSWorkspace.shared.open(URL(string: "https://github.com/f/poke-gate")!)
105
- }
106
- .font(.caption)
161
+ .frame(width: 320)
107
162
  }
108
163
 
109
164
  private var statusText: String {
@@ -120,21 +175,34 @@ struct MenuBarContent: View {
120
175
  }
121
176
  }
122
177
 
123
- private var statusIcon: String {
124
- switch service.status {
125
- case .connected: "circle.fill"
126
- case .starting, .disconnected: "circle.dotted"
127
- case .error: "exclamationmark.circle.fill"
128
- case .stopped: "circle"
129
- }
130
- }
131
-
132
178
  private var statusColor: Color {
133
179
  switch service.status {
134
180
  case .connected: .green
135
181
  case .starting, .disconnected: .yellow
136
182
  case .error: .red
137
- case .stopped: .secondary
183
+ case .stopped: .gray.opacity(0.5)
184
+ }
185
+ }
186
+ }
187
+
188
+ struct ActionButton: View {
189
+ let icon: String
190
+ let label: String
191
+ var tint: Color = .primary
192
+ let action: () -> Void
193
+
194
+ var body: some View {
195
+ Button(action: action) {
196
+ VStack(spacing: 2) {
197
+ Image(systemName: icon)
198
+ .font(.system(size: 14))
199
+ Text(label)
200
+ .font(.system(size: 9))
201
+ }
202
+ .foregroundStyle(tint)
203
+ .frame(width: 44, height: 36)
204
+ .contentShape(Rectangle())
138
205
  }
206
+ .buttonStyle(.plain)
139
207
  }
140
208
  }
@@ -2,103 +2,108 @@ import SwiftUI
2
2
 
3
3
  struct SettingsView: View {
4
4
  @ObservedObject var service: GateService
5
- @State private var apiKeyInput: String = ""
6
- @State private var usePokeLogin: Bool = true
7
5
  @Environment(\.dismiss) private var dismiss
8
6
 
9
7
  var body: some View {
10
- VStack(spacing: 16) {
11
- Text("Poke Gate Settings")
12
- .font(.headline)
8
+ VStack(alignment: .leading, spacing: 16) {
9
+ VStack(alignment: .leading, spacing: 8) {
10
+ Text("AUTHENTICATION")
11
+ .font(.caption2)
12
+ .foregroundStyle(.tertiary)
13
+ .textCase(.uppercase)
14
+ .tracking(0.5)
13
15
 
14
- Picker("Authentication", selection: $usePokeLogin) {
15
- Text("Use poke login").tag(true)
16
- Text("Use API key").tag(false)
17
- }
18
- .pickerStyle(.segmented)
16
+ HStack(spacing: 8) {
17
+ Image(systemName: service.hasPokeLoginCredentials
18
+ ? "checkmark.shield.fill" : "shield.slash")
19
+ .foregroundStyle(service.hasPokeLoginCredentials ? .green : .orange)
20
+ .font(.title3)
19
21
 
20
- if usePokeLogin {
21
- VStack(alignment: .leading, spacing: 8) {
22
- if service.hasPokeLoginCredentials {
23
- Label("poke login credentials found", systemImage: "checkmark.circle.fill")
24
- .foregroundStyle(.green)
25
- .font(.subheadline)
26
- } else {
27
- Label("No credentials found", systemImage: "xmark.circle")
28
- .foregroundStyle(.red)
22
+ VStack(alignment: .leading, spacing: 2) {
23
+ Text(service.hasPokeLoginCredentials
24
+ ? "Signed in via Poke"
25
+ : "Not signed in")
29
26
  .font(.subheadline)
27
+ .fontWeight(.medium)
30
28
 
31
- Text("Run this in your terminal:")
32
- .font(.caption)
33
- .foregroundStyle(.secondary)
34
-
35
- HStack {
36
- Text("npx poke login")
37
- .font(.system(.caption, design: .monospaced))
38
- .padding(.horizontal, 8)
39
- .padding(.vertical, 4)
40
- .background(.quaternary)
41
- .cornerRadius(4)
42
-
43
- Button {
44
- NSPasteboard.general.clearContents()
45
- NSPasteboard.general.setString("npx poke login", forType: .string)
46
- } label: {
47
- Image(systemName: "doc.on.doc")
48
- }
49
- .buttonStyle(.plain)
29
+ if service.hasPokeLoginCredentials {
30
+ Text("Your Poke session is active.")
31
+ .font(.caption)
32
+ .foregroundStyle(.secondary)
33
+ } else {
34
+ Text("Run this command in Terminal to sign in:")
35
+ .font(.caption)
36
+ .foregroundStyle(.secondary)
50
37
  }
51
-
52
- Text("Then come back here and click Save.")
53
- .font(.caption)
54
- .foregroundStyle(.secondary)
55
38
  }
56
39
  }
40
+ .padding(10)
57
41
  .frame(maxWidth: .infinity, alignment: .leading)
58
- } else {
59
- VStack(alignment: .leading, spacing: 8) {
60
- Text("API Key")
61
- .font(.subheadline)
62
- .foregroundStyle(.secondary)
42
+ .background(.quaternary.opacity(0.5))
43
+ .cornerRadius(8)
63
44
 
64
- SecureField("Paste your API key", text: $apiKeyInput)
65
- .textFieldStyle(.roundedBorder)
45
+ if !service.hasPokeLoginCredentials {
46
+ Button {
47
+ service.runPokeLogin()
48
+ } label: {
49
+ Label("Sign in with Poke", systemImage: "person.crop.circle.badge.plus")
50
+ }
51
+ .controlSize(.large)
66
52
 
67
- Link("Get your key at poke.com/kitchen/api-keys",
68
- destination: URL(string: "https://poke.com/kitchen/api-keys")!)
53
+ Text("Opens a browser window to sign in.")
69
54
  .font(.caption)
70
- .foregroundStyle(.blue)
55
+ .foregroundStyle(.secondary)
71
56
  }
72
57
  }
73
58
 
74
- HStack {
75
- Button("Cancel") {
76
- dismiss()
77
- }
78
- .keyboardShortcut(.cancelAction)
59
+ VStack(alignment: .leading, spacing: 8) {
60
+ Text("CONNECTION")
61
+ .font(.caption2)
62
+ .foregroundStyle(.tertiary)
63
+ .textCase(.uppercase)
64
+ .tracking(0.5)
79
65
 
80
- Spacer()
66
+ HStack(spacing: 8) {
67
+ Circle()
68
+ .fill(connectionColor)
69
+ .frame(width: 8, height: 8)
70
+
71
+ Text(service.status.rawValue)
72
+ .font(.subheadline)
73
+
74
+ Spacer()
81
75
 
82
- Button("Save") {
83
- if usePokeLogin {
84
- service.authSource = .pokeLogin
85
- } else {
86
- service.apiKey = apiKeyInput
87
- service.authSource = .apiKey
76
+ Button {
77
+ service.restart()
78
+ } label: {
79
+ Label("Reconnect", systemImage: "arrow.counterclockwise")
80
+ .font(.caption)
88
81
  }
82
+ }
83
+ .padding(10)
84
+ .frame(maxWidth: .infinity, alignment: .leading)
85
+ .background(.quaternary.opacity(0.5))
86
+ .cornerRadius(8)
87
+ }
88
+
89
+ HStack {
90
+ Spacer()
91
+ Button("Close") {
89
92
  dismiss()
90
- service.restart()
91
93
  }
92
- .keyboardShortcut(.defaultAction)
93
- .disabled(!usePokeLogin && apiKeyInput.isEmpty)
94
- .disabled(usePokeLogin && !service.hasPokeLoginCredentials)
94
+ .keyboardShortcut(.cancelAction)
95
95
  }
96
96
  }
97
97
  .padding(20)
98
98
  .frame(width: 380)
99
- .onAppear {
100
- usePokeLogin = service.authSource != .apiKey
101
- apiKeyInput = service.apiKey
99
+ }
100
+
101
+ private var connectionColor: Color {
102
+ switch service.status {
103
+ case .connected: .green
104
+ case .starting, .disconnected: .yellow
105
+ case .error: .red
106
+ case .stopped: .gray
102
107
  }
103
108
  }
104
109
  }
@@ -264,7 +264,7 @@
264
264
  "$(inherited)",
265
265
  "@executable_path/../Frameworks",
266
266
  );
267
- MARKETING_VERSION = 0.0.7;
267
+ MARKETING_VERSION = 0.1.0;
268
268
  PRODUCT_BUNDLE_IDENTIFIER = "dev.fka.Poke-macOS-Gate";
269
269
  PRODUCT_NAME = "$(TARGET_NAME)";
270
270
  REGISTER_APP_GROUPS = YES;
@@ -296,7 +296,7 @@
296
296
  "$(inherited)",
297
297
  "@executable_path/../Frameworks",
298
298
  );
299
- MARKETING_VERSION = 0.0.7;
299
+ MARKETING_VERSION = 0.1.0;
300
300
  PRODUCT_BUNDLE_IDENTIFIER = "dev.fka.Poke-macOS-Gate";
301
301
  PRODUCT_NAME = "$(TARGET_NAME)";
302
302
  REGISTER_APP_GROUPS = YES;
@@ -0,0 +1,23 @@
1
+ import js from "@eslint/js";
2
+ import globals from "globals";
3
+
4
+ export default [
5
+ {
6
+ ignores: ["clients/**", "node_modules/**"]
7
+ },
8
+ js.configs.recommended,
9
+ {
10
+ languageOptions: {
11
+ ecmaVersion: "latest",
12
+ sourceType: "module",
13
+ globals: {
14
+ ...globals.node
15
+ }
16
+ },
17
+ rules: {
18
+ "no-unused-vars": ["warn", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }],
19
+ "no-empty": ["error", { allowEmptyCatch: true }],
20
+ "no-console": "off"
21
+ }
22
+ }
23
+ ];
@@ -0,0 +1,6 @@
1
+ # Beeper Desktop local API token
2
+ # Find it in Beeper Desktop > Settings > API
3
+ BEEPER_TOKEN=your_beeper_access_token_here
4
+
5
+ # Optional: override the default Beeper API URL
6
+ # BEEPER_BASE_URL=http://localhost:23373
@@ -0,0 +1,109 @@
1
+ /**
2
+ * @agent beeper
3
+ * @name Beeper Message Digest
4
+ * @description Fetches messages from the last hour via Beeper Desktop and sends a summary to Poke.
5
+ * @interval 1h
6
+ * @env BEEPER_TOKEN - Beeper Desktop local API token (Settings > API)
7
+ * @env BEEPER_BASE_URL - (optional) Override default http://localhost:23373
8
+ * @author f
9
+ */
10
+
11
+ import { Poke, getToken } from "poke";
12
+
13
+ const BEEPER_BASE = process.env.BEEPER_BASE_URL || "http://localhost:23373";
14
+ const BEEPER_TOKEN = process.env.BEEPER_TOKEN;
15
+
16
+ if (!BEEPER_TOKEN) {
17
+ console.error("BEEPER_TOKEN not set. Create ~/.config/poke-gate/agents/.env.beeper");
18
+ process.exit(1);
19
+ }
20
+
21
+ async function beeperRequest(path, params = {}) {
22
+ const url = new URL(BEEPER_BASE + path);
23
+ for (const [key, value] of Object.entries(params)) {
24
+ if (value !== undefined) url.searchParams.set(key, String(value));
25
+ }
26
+ const res = await fetch(url, {
27
+ headers: {
28
+ Authorization: `Bearer ${BEEPER_TOKEN}`,
29
+ Accept: "application/json",
30
+ },
31
+ });
32
+ if (!res.ok) throw new Error(`Beeper API ${res.status}: ${await res.text()}`);
33
+ return res.json();
34
+ }
35
+
36
+ async function getRecentMessages() {
37
+ const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString();
38
+
39
+ const data = await beeperRequest("/v1/messages/search", {
40
+ dateAfter: oneHourAgo,
41
+ limit: 200,
42
+ });
43
+
44
+ return data.items || [];
45
+ }
46
+
47
+ function groupBySender(messages) {
48
+ const groups = {};
49
+ for (const msg of messages) {
50
+ if (msg.isSender) continue;
51
+ const name = msg.senderName || msg.senderID || "Unknown";
52
+ if (!groups[name]) groups[name] = [];
53
+ if (msg.text) groups[name].push(msg.text);
54
+ }
55
+ return groups;
56
+ }
57
+
58
+ function buildSummary(groups) {
59
+ const senders = Object.keys(groups);
60
+ if (senders.length === 0) return null;
61
+
62
+ let summary = `Messages from the last hour (${senders.length} people):\n\n`;
63
+
64
+ for (const [sender, messages] of Object.entries(groups)) {
65
+ summary += `${sender} (${messages.length} messages):\n`;
66
+ for (const text of messages.slice(-3)) {
67
+ const preview = text.length > 100 ? text.slice(0, 100) + "…" : text;
68
+ summary += ` - ${preview}\n`;
69
+ }
70
+ summary += "\n";
71
+ }
72
+
73
+ return summary.trim();
74
+ }
75
+
76
+ async function main() {
77
+ console.log("Fetching messages from the last hour...");
78
+
79
+ const messages = await getRecentMessages();
80
+ console.log(`Found ${messages.length} messages`);
81
+
82
+ const groups = groupBySender(messages);
83
+ const summary = buildSummary(groups);
84
+
85
+ if (!summary) {
86
+ console.log("No new messages from others in the last hour.");
87
+ return;
88
+ }
89
+
90
+ console.log("Sending summary to Poke...");
91
+
92
+ const token = getToken();
93
+ if (!token) {
94
+ console.error("Not logged in to Poke. Run: npx poke login");
95
+ process.exit(1);
96
+ }
97
+
98
+ const poke = new Poke({ apiKey: token });
99
+ await poke.sendMessage(
100
+ `Here's a summary of my Beeper messages from the last hour:\n\n${summary}`
101
+ );
102
+
103
+ console.log("Summary sent to Poke.");
104
+ }
105
+
106
+ main().catch((err) => {
107
+ console.error("Agent error:", err.message);
108
+ process.exit(1);
109
+ });
package/package.json CHANGED
@@ -1,13 +1,18 @@
1
1
  {
2
2
  "name": "poke-gate",
3
- "version": "0.0.8",
3
+ "version": "0.1.0",
4
4
  "description": "Expose your machine to your Poke AI assistant via MCP tunnel",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "poke-gate": "./bin/poke-gate.js"
8
8
  },
9
9
  "scripts": {
10
- "start": "node src/app.js"
10
+ "start": "node src/app.js",
11
+ "lint": "eslint .",
12
+ "lint:fix": "eslint . --fix",
13
+ "lint:md": "remark . --quiet --frail",
14
+ "format": "prettier --write \"{src,bin}/**/*.js\"",
15
+ "format:md": "remark . --output --quiet"
11
16
  },
12
17
  "keywords": [
13
18
  "poke",
@@ -27,5 +32,17 @@
27
32
  },
28
33
  "dependencies": {
29
34
  "poke": "^0.4.2"
35
+ },
36
+ "devDependencies": {
37
+ "@eslint/js": "^9.39.1",
38
+ "eslint": "^9.39.1",
39
+ "globals": "^16.5.0",
40
+ "prettier": "^3.6.2",
41
+ "remark-cli": "^12.0.1",
42
+ "remark-lint-list-item-indent": "^4.0.1",
43
+ "remark-parse": "^11.0.0",
44
+ "remark-preset-lint-consistent": "^6.0.1",
45
+ "remark-preset-lint-recommended": "^7.0.1",
46
+ "remark-stringify": "^11.0.0"
30
47
  }
31
48
  }