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.
- package/.github/workflows/docker.yml +41 -0
- package/.prettierrc +7 -0
- package/.remarkignore +3 -0
- package/.remarkrc.mjs +13 -0
- package/CODE_OF_CONDUCT.md +38 -0
- package/CONTRIBUTING.md +32 -0
- package/Dockerfile +13 -0
- package/LICENSE +21 -0
- package/README.md +8 -10
- package/SECURITY.md +18 -0
- package/bin/poke-gate.js +0 -94
- package/clients/Poke macOS Gate/Poke macOS Gate/AboutView.swift +54 -0
- package/clients/Poke macOS Gate/Poke macOS Gate/GateService.swift +10 -0
- package/clients/Poke macOS Gate/Poke macOS Gate/LogsView.swift +45 -11
- package/clients/Poke macOS Gate/Poke macOS Gate/Poke_macOS_GateApp.swift +143 -75
- package/clients/Poke macOS Gate/Poke macOS Gate/SettingsView.swift +76 -71
- package/clients/Poke macOS Gate/Poke macOS Gate.xcodeproj/project.pbxproj +2 -2
- package/clients/Poke macOS Gate/Poke macOS Gate.xcodeproj/project.xcworkspace/xcuserdata/fka.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- package/eslint.config.mjs +23 -0
- package/package.json +19 -2
- package/src/app.js +22 -34
- package/src/mcp-server.js +3 -3
- package/src/tunnel.js +47 -3
|
@@ -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
package/.remarkignore
ADDED
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.
|
package/CONTRIBUTING.md
ADDED
|
@@ -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.
|
|
48
|
-
2.
|
|
49
|
-
3.
|
|
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,
|
|
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** —
|
|
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,
|
|
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
|
|
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("
|
|
10
|
-
.font(.
|
|
9
|
+
Text("\(service.logs.count) entries")
|
|
10
|
+
.font(.caption)
|
|
11
|
+
.foregroundStyle(.tertiary)
|
|
12
|
+
|
|
11
13
|
Spacer()
|
|
12
|
-
|
|
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
|
-
.
|
|
35
|
+
.help("Clear logs")
|
|
18
36
|
}
|
|
19
|
-
.padding(.horizontal,
|
|
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:
|
|
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(
|
|
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(.
|
|
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
|
-
|
|
62
|
+
withAnimation(.easeOut(duration: 0.15)) {
|
|
63
|
+
proxy.scrollTo(last, anchor: .bottom)
|
|
64
|
+
}
|
|
41
65
|
}
|
|
42
66
|
}
|
|
43
67
|
}
|
|
44
68
|
}
|
|
45
|
-
.frame(
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
.
|
|
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: .
|
|
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
|
-
|
|
12
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
Text("
|
|
37
|
-
.font(.
|
|
38
|
-
.
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
Text("API Key")
|
|
61
|
-
.font(.subheadline)
|
|
62
|
-
.foregroundStyle(.secondary)
|
|
42
|
+
.background(.quaternary.opacity(0.5))
|
|
43
|
+
.cornerRadius(8)
|
|
63
44
|
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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(.
|
|
55
|
+
.foregroundStyle(.secondary)
|
|
71
56
|
}
|
|
72
57
|
}
|
|
73
58
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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(.
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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.
|
|
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.
|
|
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;
|
|
Binary file
|
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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({
|
|
71
|
+
const poke = new Poke({ token });
|
|
85
72
|
await poke.sendMessage(
|
|
86
|
-
`
|
|
87
|
-
`You now
|
|
88
|
-
`
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
|
49
|
+
token,
|
|
50
|
+
cleanupOnStop: false,
|
|
10
51
|
});
|
|
11
52
|
|
|
12
|
-
tunnel.on("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));
|