poke-gate 0.0.7 → 0.0.9

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.
@@ -0,0 +1,41 @@
1
+ name: Build and push Docker image
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ build-and-push:
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ contents: read
13
+ packages: write
14
+
15
+ steps:
16
+ - name: Checkout repo
17
+ uses: actions/checkout@v4
18
+
19
+ - name: Log in to GHCR
20
+ uses: docker/login-action@v3
21
+ with:
22
+ registry: ghcr.io
23
+ username: ${{ github.actor }}
24
+ password: ${{ secrets.GITHUB_TOKEN }}
25
+
26
+ - name: Get tag
27
+ id: vars
28
+ run: echo "tag=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
29
+
30
+ - name: Build and push
31
+ uses: docker/build-push-action@v5
32
+ with:
33
+ context: .
34
+ push: true
35
+ tags: |
36
+ ghcr.io/f/poke-gate:latest
37
+ ghcr.io/f/poke-gate:${{ steps.vars.outputs.tag }}
38
+ labels: |
39
+ org.opencontainers.image.source=https://github.com/f/poke-gate
40
+ org.opencontainers.image.description=Expose your machine to your Poke AI assistant via MCP tunnel
41
+ org.opencontainers.image.licenses=MIT
package/.prettierrc ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "printWidth": 100,
3
+ "singleQuote": true,
4
+ "trailingComma": "all",
5
+ "endOfLine": "auto",
6
+ "bracketSpacing": true
7
+ }
package/.remarkignore ADDED
@@ -0,0 +1,3 @@
1
+ node_modules/
2
+ dist/
3
+ coverage/
package/.remarkrc.mjs ADDED
@@ -0,0 +1,13 @@
1
+ import remarkParse from "remark-parse";
2
+ import remarkStringify from "remark-stringify";
3
+ import presetConsistent from "remark-preset-lint-consistent";
4
+ import presetRecommended from "remark-preset-lint-recommended";
5
+ import listItemIndent from "remark-lint-list-item-indent";
6
+
7
+ export default {
8
+ settings: {
9
+ bullet: "*",
10
+ listItemIndent: "one"
11
+ },
12
+ plugins: [remarkParse, remarkStringify, presetConsistent, presetRecommended, [listItemIndent, "one"]]
13
+ };
@@ -0,0 +1,38 @@
1
+ # Code of Conduct
2
+
3
+ ## Our Commitment
4
+
5
+ We are committed to a respectful, inclusive, and harassment-free community.
6
+
7
+ ## Expected Behavior
8
+
9
+ * Be respectful and constructive.
10
+ * Critique ideas, not people.
11
+ * Give and accept feedback professionally.
12
+ * Prioritize the health of the project and community.
13
+
14
+ ## Unacceptable Behavior
15
+
16
+ * Harassment, discrimination, threats, or personal attacks
17
+ * Insults, intimidation, or hostile behavior
18
+ * Publishing private information without consent
19
+ * Any conduct that creates an unsafe environment
20
+
21
+ ## Enforcement
22
+
23
+ Project maintainers are responsible for interpreting and enforcing this Code of Conduct. They may remove or reject comments, commits, issues, pull requests, or other contributions that violate these rules.
24
+
25
+ Possible actions include:
26
+
27
+ 1. Warning
28
+ 2. Temporary restriction
29
+ 3. Permanent ban
30
+
31
+ ## Reporting
32
+
33
+ Report unacceptable behavior to maintainers through:
34
+
35
+ * https://github.com/f/poke-gate/issues
36
+ * https://github.com/f/poke-gate/security/advisories/new
37
+
38
+ Reports will be reviewed in good faith and handled as privately as possible.
@@ -0,0 +1,32 @@
1
+ # Contributing
2
+
3
+ Thanks for contributing to `poke-gate`.
4
+
5
+ ## How To Contribute
6
+
7
+ 1. Fork the repository.
8
+ 2. Create a branch from `main`.
9
+ 3. Keep changes small and focused.
10
+ 4. Use clear commit messages.
11
+ 5. Open a pull request with a short technical summary.
12
+
13
+ ## Local Validation
14
+
15
+ Before opening a PR, run:
16
+
17
+ 1. `npm install`
18
+ 2. `npm run lint`
19
+ 3. `npm run lint:md`
20
+ 4. `npm run start`
21
+
22
+ ## Pull Request Notes
23
+
24
+ Include these in the PR description:
25
+
26
+ 1. What changed
27
+ 2. Why it changed
28
+ 3. How you validated it
29
+
30
+ ## Community Rules
31
+
32
+ Please read [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md) before contributing.
package/Dockerfile ADDED
@@ -0,0 +1,13 @@
1
+ FROM node:20-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY . .
6
+
7
+ RUN npm install -g .
8
+
9
+ LABEL org.opencontainers.image.source="https://github.com/f/poke-gate"
10
+ LABEL org.opencontainers.image.description="Expose your machine to your Poke AI assistant via MCP tunnel"
11
+ LABEL org.opencontainers.image.licenses="MIT"
12
+
13
+ ENTRYPOINT ["poke-gate"]
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fatih Kadir Akın
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -44,11 +44,11 @@ npx poke-gate
44
44
 
45
45
  ## Setup
46
46
 
47
- 1. Get an API key from [poke.com/kitchen/api-keys](https://poke.com/kitchen/api-keys)
48
- 2. Open Poke Gate from your menu bar and go to **Settings**
49
- 3. Paste your API key and save
47
+ 1. Open Poke Gate from your menu bar
48
+ 2. Click **Start** (or let auto-start run)
49
+ 3. If needed, complete the Poke OAuth sign-in flow in your browser
50
50
 
51
- The app connects automatically and shows a green dot when ready.
51
+ The app connects automatically after sign-in and shows a green dot when ready.
52
52
 
53
53
  ## How it works
54
54
 
@@ -94,10 +94,10 @@ From iMessage or Telegram, ask Poke:
94
94
  The menu bar app manages everything:
95
95
 
96
96
  - **Status** — green dot when connected, yellow when connecting, red on error
97
- - **Personalized** — shows "Connected to your Poke, [name]"
97
+ - **Personalized** — shows "Connected to your Poke, <name>"
98
98
  - **Auto-start** — connects on launch if API key is saved
99
99
  - **Auto-restart** — reconnects automatically if the connection drops
100
- - **Settings** — paste your API key
100
+ - **Settings** — auth status and reconnect controls
101
101
  - **Logs** — view real-time tool calls and connection events
102
102
  - **Screen Recording** — prompts for permission on first launch
103
103
 
@@ -127,21 +127,19 @@ If you prefer the command line over the macOS app:
127
127
  npx poke-gate
128
128
  ```
129
129
 
130
- On first run, paste your API key when prompted. Add `--verbose` to see tool calls in real time:
130
+ On first run, if you're not signed in, Poke Gate opens OAuth login automatically. Add `--verbose` to see tool calls in real time:
131
131
 
132
132
  ```bash
133
133
  npx poke-gate --verbose
134
134
  ```
135
135
 
136
- Config is stored at `~/.config/poke-gate/config.json`.
137
-
138
136
  ## Security
139
137
 
140
138
  **Poke Gate grants full shell access to your Poke agent.** This means:
141
139
 
142
140
  - Any command can be run with your user's permissions
143
141
  - Files can be read and written anywhere your user has access
144
- - Only your Poke agent (authenticated by your API key) can reach the tunnel
142
+ - Only your authenticated Poke agent can reach the tunnel
145
143
 
146
144
  Only run Poke Gate on machines and networks you trust.
147
145
 
package/SECURITY.md ADDED
@@ -0,0 +1,18 @@
1
+ # Security Policy
2
+
3
+ ## Reporting a Vulnerability
4
+
5
+ If you discover a security vulnerability in this project, please report it privately through GitHub Security Advisories:
6
+
7
+ https://github.com/f/poke-gate/security/advisories/new
8
+
9
+ Please do not disclose vulnerabilities publicly until maintainers have had a chance to investigate and patch.
10
+
11
+ ## What to Include
12
+
13
+ 1. A clear description of the issue.
14
+ 2. Reproduction steps or a proof of concept.
15
+ 3. The expected impact.
16
+ 4. Any suggested mitigation (optional).
17
+
18
+ We will aim to acknowledge reports within 72 hours and provide updates as the investigation progresses.
package/bin/poke-gate.js CHANGED
@@ -1,100 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
4
- import { join } from "node:path";
5
- import { homedir } from "node:os";
6
- import { createInterface } from "node:readline";
7
-
8
- const CONFIG_DIR = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
9
- const CONFIG_PATH = join(CONFIG_DIR, "poke-gate", "config.json");
10
-
11
- function loadConfig() {
12
- try {
13
- return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
14
- } catch {
15
- return {};
16
- }
17
- }
18
-
19
- function saveConfig(config) {
20
- mkdirSync(join(CONFIG_DIR, "poke-gate"), { recursive: true });
21
- writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
22
- }
23
-
24
- function loadPokeCredentials() {
25
- try {
26
- const creds = JSON.parse(readFileSync(join(CONFIG_DIR, "poke", "credentials.json"), "utf-8"));
27
- if (creds.token) return creds.token;
28
- } catch {}
29
- return null;
30
- }
31
-
32
- function resolveToken() {
33
- if (process.env.POKE_API_KEY) return process.env.POKE_API_KEY;
34
- const config = loadConfig();
35
- if (config.apiKey) return config.apiKey;
36
- const pokeCreds = loadPokeCredentials();
37
- if (pokeCreds) return pokeCreds;
38
- return null;
39
- }
40
-
41
- function ask(question) {
42
- const rl = createInterface({ input: process.stdin, output: process.stdout });
43
- return new Promise((resolve) => {
44
- rl.question(question, (answer) => {
45
- rl.close();
46
- resolve(answer.trim());
47
- });
48
- });
49
- }
50
-
51
- async function onboarding() {
52
- console.log();
53
- console.log(" poke-gate — expose your machine to Poke");
54
- console.log();
55
- console.log(" Your Poke agent will be able to run commands,");
56
- console.log(" read/write files, and access system info on this machine.");
57
- console.log();
58
- console.log(" ⚠ This grants full shell access. Only run on trusted networks.");
59
- console.log();
60
- console.log(" To get started, either:");
61
- console.log();
62
- console.log(" Option 1: Run 'npx poke login' (recommended)");
63
- console.log(" Option 2: Paste an API key from https://poke.com/kitchen/api-keys");
64
- console.log();
65
-
66
- const key = await ask(" API key (or press Enter if you ran poke login): ");
67
-
68
- if (!key) {
69
- const pokeCreds = loadPokeCredentials();
70
- if (pokeCreds) {
71
- console.log();
72
- console.log(" Found poke login credentials! Starting...");
73
- console.log();
74
- return pokeCreds;
75
- }
76
- console.log();
77
- console.log(" No credentials found. Run: npx poke login");
78
- console.log();
79
- process.exit(1);
80
- }
81
-
82
- saveConfig({ apiKey: key });
83
- console.log();
84
- console.log(" Saved! Starting poke-gate...");
85
- console.log();
86
-
87
- return key;
88
- }
89
-
90
3
  async function main() {
91
- let token = resolveToken();
92
-
93
- if (!token) {
94
- token = await onboarding();
95
- }
96
-
97
- process.env.POKE_API_KEY = token;
98
4
  await import("../src/app.js");
99
5
  }
100
6
 
@@ -0,0 +1,54 @@
1
+ import SwiftUI
2
+
3
+ struct AboutView: View {
4
+ @Environment(\.dismiss) private var dismiss
5
+
6
+ var body: some View {
7
+ VStack(spacing: 16) {
8
+ Image(nsImage: NSApp.applicationIconImage)
9
+ .resizable()
10
+ .frame(width: 80, height: 80)
11
+
12
+ Text("Poke Gate")
13
+ .font(.title2)
14
+ .fontWeight(.semibold)
15
+
16
+ Text("Version 0.0.8")
17
+ .font(.caption)
18
+ .foregroundStyle(.secondary)
19
+
20
+ Text("Let your Poke AI assistant access your machine.\nRun commands, read files, take screenshots — from anywhere.")
21
+ .font(.caption)
22
+ .foregroundStyle(.secondary)
23
+ .multilineTextAlignment(.center)
24
+ .fixedSize(horizontal: false, vertical: true)
25
+
26
+ Divider()
27
+
28
+ VStack(spacing: 4) {
29
+ Text("A community project — not affiliated with Poke")
30
+ .font(.caption2)
31
+ .foregroundStyle(.tertiary)
32
+
33
+ Text("or The Interaction Company of California.")
34
+ .font(.caption2)
35
+ .foregroundStyle(.tertiary)
36
+ }
37
+
38
+ HStack(spacing: 16) {
39
+ Link("GitHub", destination: URL(string: "https://github.com/f/poke-gate")!)
40
+ .font(.caption)
41
+
42
+ Link("poke.com", destination: URL(string: "https://poke.com")!)
43
+ .font(.caption)
44
+ }
45
+
46
+ Button("Close") {
47
+ dismiss()
48
+ }
49
+ .keyboardShortcut(.cancelAction)
50
+ }
51
+ .padding(24)
52
+ .frame(width: 300)
53
+ }
54
+ }
@@ -30,6 +30,16 @@ class GateService: ObservableObject {
30
30
  resolveToken() != nil
31
31
  }
32
32
 
33
+ func runPokeLogin() {
34
+ let fullPath = shellPath()
35
+ let proc = Process()
36
+ proc.executableURL = URL(fileURLWithPath: "/bin/zsh")
37
+ proc.arguments = ["-c", "npx -y poke@latest login"]
38
+ proc.environment = ["HOME": NSHomeDirectory(), "PATH": fullPath]
39
+ try? proc.run()
40
+ appendLog("Launched poke login — check your browser.")
41
+ }
42
+
33
43
  func autoStartIfNeeded() {
34
44
  guard !hasAutoStarted else { return }
35
45
  hasAutoStarted = true
@@ -6,42 +6,76 @@ struct LogsView: View {
6
6
  var body: some View {
7
7
  VStack(spacing: 0) {
8
8
  HStack {
9
- Text("Logs")
10
- .font(.headline)
9
+ Text("\(service.logs.count) entries")
10
+ .font(.caption)
11
+ .foregroundStyle(.tertiary)
12
+
11
13
  Spacer()
12
- Button("Clear") {
14
+
15
+ Button {
16
+ let text = service.logs.joined(separator: "\n")
17
+ NSPasteboard.general.clearContents()
18
+ NSPasteboard.general.setString(text, forType: .string)
19
+ } label: {
20
+ Image(systemName: "doc.on.doc")
21
+ .font(.caption)
22
+ }
23
+ .buttonStyle(.plain)
24
+ .foregroundStyle(.secondary)
25
+ .help("Copy all logs")
26
+
27
+ Button {
13
28
  service.logs.removeAll()
29
+ } label: {
30
+ Image(systemName: "trash")
31
+ .font(.caption)
14
32
  }
15
33
  .buttonStyle(.plain)
16
34
  .foregroundStyle(.secondary)
17
- .font(.caption)
35
+ .help("Clear logs")
18
36
  }
19
- .padding(.horizontal, 12)
37
+ .padding(.horizontal, 16)
20
38
  .padding(.vertical, 8)
21
39
 
22
40
  Divider()
23
41
 
24
42
  ScrollViewReader { proxy in
25
43
  ScrollView {
26
- LazyVStack(alignment: .leading, spacing: 2) {
44
+ LazyVStack(alignment: .leading, spacing: 1) {
27
45
  ForEach(Array(service.logs.enumerated()), id: \.offset) { index, line in
28
46
  Text(line)
29
47
  .font(.system(.caption, design: .monospaced))
30
- .foregroundStyle(.primary)
48
+ .foregroundStyle(lineColor(line))
31
49
  .textSelection(.enabled)
50
+ .fixedSize(horizontal: false, vertical: true)
51
+ .padding(.horizontal, 16)
52
+ .padding(.vertical, 2)
53
+ .frame(maxWidth: .infinity, alignment: .leading)
54
+ .background(index % 2 == 0 ? Color.clear : Color.primary.opacity(0.02))
32
55
  .id(index)
33
56
  }
34
57
  }
35
- .padding(.horizontal, 12)
36
- .padding(.vertical, 8)
58
+ .padding(.vertical, 4)
37
59
  }
38
60
  .onChange(of: service.logs.count) { _, _ in
39
61
  if let last = service.logs.indices.last {
40
- proxy.scrollTo(last, anchor: .bottom)
62
+ withAnimation(.easeOut(duration: 0.15)) {
63
+ proxy.scrollTo(last, anchor: .bottom)
64
+ }
41
65
  }
42
66
  }
43
67
  }
44
68
  }
45
- .frame(width: 480, height: 320)
69
+ .frame(minWidth: 480, minHeight: 300)
70
+ }
71
+
72
+ private func lineColor(_ line: String) -> Color {
73
+ if line.contains("error") || line.contains("Error") || line.contains("failed") {
74
+ return .red
75
+ }
76
+ if line.contains("tool:") || line.contains("$") {
77
+ return .primary
78
+ }
79
+ return .secondary
46
80
  }
47
81
  }
@@ -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.5;
267
+ MARKETING_VERSION = 0.0.8;
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.5;
299
+ MARKETING_VERSION = 0.0.8;
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
+ ];
package/package.json CHANGED
@@ -1,13 +1,18 @@
1
1
  {
2
2
  "name": "poke-gate",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
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
  }
package/src/app.js CHANGED
@@ -1,46 +1,34 @@
1
1
  import { startMcpServer, enableLogging } from "./mcp-server.js";
2
2
  import { startTunnel } from "./tunnel.js";
3
- import { Poke } from "poke";
4
- import { readFileSync } from "node:fs";
5
- import { join } from "node:path";
6
- import { homedir } from "node:os";
3
+ import { Poke, isLoggedIn, login, getToken } from "poke";
7
4
 
8
5
  const verbose = process.argv.includes("--verbose") || process.argv.includes("-v");
9
6
  enableLogging(verbose);
10
7
 
11
- function resolveToken() {
12
- if (process.env.POKE_API_KEY) return process.env.POKE_API_KEY;
13
-
14
- const configDir = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
15
-
16
- try {
17
- const cfg = JSON.parse(readFileSync(join(configDir, "poke-gate", "config.json"), "utf-8"));
18
- if (cfg.apiKey) return cfg.apiKey;
19
- } catch {}
20
-
21
- try {
22
- const creds = JSON.parse(readFileSync(join(configDir, "poke", "credentials.json"), "utf-8"));
23
- if (creds.token) return creds.token;
24
- } catch {}
25
-
26
- return null;
27
- }
28
-
29
8
  function log(msg) {
30
9
  const ts = new Date().toISOString().slice(11, 19);
31
10
  console.log(`[${ts}] ${msg}`);
32
11
  }
33
12
 
34
- const API_KEY = resolveToken();
13
+ async function ensureAuthenticated() {
14
+ if (!isLoggedIn()) {
15
+ log("Signing in to Poke...");
16
+ await login();
17
+ }
18
+
19
+ const token = getToken();
20
+ if (!token) {
21
+ throw new Error("Authentication failed: no token returned by Poke SDK.");
22
+ }
35
23
 
36
- if (!API_KEY) {
37
- console.error("No credentials found. Run: npx poke-gate");
38
- process.exit(1);
24
+ return token;
39
25
  }
40
26
 
41
27
  async function main() {
42
28
  log("poke-gate starting...");
43
29
 
30
+ const token = await ensureAuthenticated();
31
+
44
32
  const { port } = await startMcpServer();
45
33
  log(`MCP server on port ${port}`);
46
34
 
@@ -48,15 +36,14 @@ async function main() {
48
36
 
49
37
  log("Connecting tunnel to Poke...");
50
38
  try {
51
- const { info } = await startTunnel({
52
- apiKey: API_KEY,
39
+ await startTunnel({
53
40
  mcpUrl,
54
41
  onEvent: (type, data) => {
55
42
  switch (type) {
56
43
  case "connected":
57
44
  log(`Tunnel connected (${data.connectionId})`);
58
45
  log("Ready — your Poke agent can now access this machine.");
59
- notifyPoke(data.connectionId);
46
+ notifyPoke(data.connectionId, token);
60
47
  break;
61
48
  case "disconnected":
62
49
  log("Tunnel disconnected. Reconnecting...");
@@ -79,13 +66,14 @@ async function main() {
79
66
  }
80
67
  }
81
68
 
82
- async function notifyPoke(connectionId) {
69
+ async function notifyPoke(connectionId, token) {
83
70
  try {
84
- const poke = new Poke({ apiKey: API_KEY });
71
+ const poke = new Poke({ token });
85
72
  await poke.sendMessage(
86
- `Poke macOS Gate is connected. Tunnel ID: ${connectionId}. ` +
87
- `You now have access to this machine's terminal, files, and screen. ` +
88
- `Use the available tools (run_command, read_file, write_file, list_directory, system_info, read_image, take_screenshot) to help the user.`
73
+ `Hey! I've connected my computer to you via Poke Gate (tunnel: ${connectionId}). ` +
74
+ `You can now run commands, read and write files, list directories, take screenshots, and check system info on my machine. ` +
75
+ `Just use the tools whenever I ask you to do something on my computer.` +
76
+ `Now reply me in my language "now I am connected to your computer".`
89
77
  );
90
78
  log("Notified Poke agent about connection.");
91
79
  } catch (err) {
package/src/mcp-server.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import http from "node:http";
2
- import { execSync, exec } from "node:child_process";
2
+ import { 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
5
  import { join, resolve, extname } from "node:path";
@@ -108,7 +108,7 @@ const TOOLS = [
108
108
  function runCommand(command, cwd) {
109
109
  return new Promise((res) => {
110
110
  const dir = cwd || homedir();
111
- const child = exec(command, {
111
+ exec(command, {
112
112
  cwd: dir,
113
113
  timeout: COMMAND_TIMEOUT,
114
114
  maxBuffer: 1024 * 1024,
@@ -348,7 +348,7 @@ export function startMcpServer(port = 0) {
348
348
  res.end();
349
349
  }
350
350
  }
351
- } catch (err) {
351
+ } catch {
352
352
  res.writeHead(400, { "Content-Type": "application/json" });
353
353
  res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32700, message: "Parse error" }, id: null }));
354
354
  }
package/src/tunnel.js CHANGED
@@ -1,15 +1,59 @@
1
1
  import { PokeTunnel, getToken } from "poke";
2
+ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { homedir } from "node:os";
5
+
6
+ const CONFIG_DIR = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
7
+ const STATE_PATH = join(CONFIG_DIR, "poke-gate", "state.json");
8
+
9
+ function loadState() {
10
+ try {
11
+ return JSON.parse(readFileSync(STATE_PATH, "utf-8"));
12
+ } catch {
13
+ return {};
14
+ }
15
+ }
16
+
17
+ function saveState(state) {
18
+ mkdirSync(join(CONFIG_DIR, "poke-gate"), { recursive: true });
19
+ writeFileSync(STATE_PATH, JSON.stringify(state, null, 2));
20
+ }
21
+
22
+ async function cleanupOldConnection() {
23
+ const state = loadState();
24
+ if (!state.connectionId) return;
2
25
 
3
- export async function startTunnel({ apiKey, mcpUrl, onEvent }) {
4
26
  const token = getToken();
27
+ if (!token) return;
28
+ const base = process.env.POKE_API ?? "https://poke.com/api/v1";
29
+
30
+ try {
31
+ await fetch(`${base}/mcp/connections/${state.connectionId}`, {
32
+ method: "DELETE",
33
+ headers: { Authorization: `Bearer ${token}` },
34
+ });
35
+ } catch {}
36
+ }
37
+
38
+ export async function startTunnel({ mcpUrl, onEvent }) {
39
+ await cleanupOldConnection();
40
+
41
+ const token = getToken();
42
+ if (!token) {
43
+ throw new Error("No Poke auth token available for tunnel.");
44
+ }
5
45
 
6
46
  const tunnel = new PokeTunnel({
7
47
  url: mcpUrl,
8
48
  name: "poke-gate",
9
- token: token || apiKey,
49
+ token,
50
+ cleanupOnStop: false,
10
51
  });
11
52
 
12
- tunnel.on("connected", (info) => onEvent("connected", info));
53
+ tunnel.on("connected", (info) => {
54
+ saveState({ connectionId: info.connectionId });
55
+ onEvent("connected", info);
56
+ });
13
57
  tunnel.on("disconnected", () => onEvent("disconnected"));
14
58
  tunnel.on("error", (err) => onEvent("error", err.message));
15
59
  tunnel.on("toolsSynced", ({ toolCount }) => onEvent("tools-synced", toolCount));