poke-gate 0.3.2 → 0.3.5

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,8 @@
1
+ scripts:
2
+ - name: xcode-build
3
+ command: cd "clients/Poke macOS Gate" && xcodebuild -scheme "Poke macOS Gate" -configuration Debug -destination 'platform=macOS' build | xcpretty || xcodebuild -scheme "Poke macOS Gate" -configuration Debug -destination 'platform=macOS' build
4
+ - name: xcode-run-simulator
5
+ command: cd "clients/Poke macOS Gate" && xcodebuild -scheme "Poke macOS Gate" -configuration Debug -destination 'platform=macOS' build && APP_PATH=$(find ~/Library/Developer/Xcode/DerivedData -name "Poke macOS Gate.app" -type d 2>/dev/null | head -1) && [ -n "$APP_PATH" ] && open "$APP_PATH"
6
+ automation:
7
+ auto_pr_review: false
8
+ auto_issue_session: true
@@ -5,6 +5,10 @@ on:
5
5
  tags:
6
6
  - 'v*'
7
7
 
8
+ permissions:
9
+ contents: read
10
+ id-token: write
11
+
8
12
  jobs:
9
13
  publish:
10
14
  runs-on: ubuntu-latest
@@ -18,7 +22,14 @@ jobs:
18
22
  node-version: 22
19
23
  registry-url: https://registry.npmjs.org
20
24
 
25
+ - name: Verify tag matches package version
26
+ run: |
27
+ TAG_VERSION="${GITHUB_REF_NAME#v}"
28
+ PACKAGE_VERSION="$(node -p "require('./package.json').version")"
29
+ if [[ "${TAG_VERSION}" != "${PACKAGE_VERSION}" ]]; then
30
+ echo "Tag version ${TAG_VERSION} does not match package.json version ${PACKAGE_VERSION}."
31
+ exit 1
32
+ fi
33
+
21
34
  - name: Publish
22
- run: npm publish --access public
23
- env:
24
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
35
+ run: npm publish --access public --provenance
@@ -139,7 +139,10 @@ jobs:
139
139
  fi
140
140
 
141
141
  VERSION_NUM="${VERSION#v}"
142
- git clone https://x-access-token:${TAP_REPO_TOKEN_HOMEBREW}@github.com/f/homebrew-tap.git tap
142
+ if ! git clone https://x-access-token:${TAP_REPO_TOKEN_HOMEBREW}@github.com/f/homebrew-tap.git tap; then
143
+ echo "::warning::Skipping Homebrew tap update because cloning f/homebrew-tap failed."
144
+ exit 0
145
+ fi
143
146
  mkdir -p tap/Casks
144
147
  cat > tap/Casks/poke-gate.rb << EOF
145
148
  cask "poke-gate" do
@@ -170,5 +173,12 @@ jobs:
170
173
  git config user.name "github-actions[bot]"
171
174
  git config user.email "github-actions[bot]@users.noreply.github.com"
172
175
  git add .
176
+ if git diff --cached --quiet; then
177
+ echo "Homebrew cask is already up to date."
178
+ exit 0
179
+ fi
173
180
  git commit -m "Update poke-gate to ${VERSION}"
174
- git push
181
+ if ! git push; then
182
+ echo "::warning::Skipping Homebrew tap update because pushing to f/homebrew-tap failed."
183
+ exit 0
184
+ fi
@@ -507,13 +507,17 @@ class GateService: ObservableObject {
507
507
 
508
508
  proc.executableURL = URL(fileURLWithPath: "/bin/zsh")
509
509
  proc.arguments = ["-c", "\(npxBin) -y poke-gate@latest --verbose"]
510
- proc.environment = ProcessInfo.processInfo.environment.merging(
510
+ var environment = ProcessInfo.processInfo.environment.merging(
511
511
  [
512
512
  "PATH": fullPath,
513
513
  "POKE_GATE_PERMISSION_MODE": permissionMode.rawValue,
514
514
  ],
515
515
  uniquingKeysWith: { _, new in new }
516
516
  )
517
+ if let token = resolveToken() {
518
+ environment["POKE_API_KEY"] = token
519
+ }
520
+ proc.environment = environment
517
521
  proc.standardOutput = pipe
518
522
  proc.standardError = pipe
519
523
  proc.currentDirectoryURL = FileManager.default.homeDirectoryForCurrentUser
@@ -556,7 +560,7 @@ class GateService: ObservableObject {
556
560
  private func killOrphanedProcesses() {
557
561
  let task = Process()
558
562
  task.executableURL = URL(fileURLWithPath: "/usr/bin/pkill")
559
- task.arguments = ["-f", "node.*poke-gate.*app\\.js"]
563
+ task.arguments = ["-f", "node .*((poke-gate.*app\\.js)|(\\.bin/poke-gate))"]
560
564
  try? task.run()
561
565
  task.waitUntilExit()
562
566
  }
@@ -286,7 +286,7 @@
286
286
  "@executable_path/../Frameworks",
287
287
  );
288
288
  MACOSX_DEPLOYMENT_TARGET = 15.0;
289
- MARKETING_VERSION = 0.3.1;
289
+ MARKETING_VERSION = 0.3.4;
290
290
  PRODUCT_BUNDLE_IDENTIFIER = "dev.fka.Poke-macOS-Gate";
291
291
  PRODUCT_NAME = "$(TARGET_NAME)";
292
292
  REGISTER_APP_GROUPS = YES;
@@ -322,7 +322,7 @@
322
322
  "@executable_path/../Frameworks",
323
323
  );
324
324
  MACOSX_DEPLOYMENT_TARGET = 15.0;
325
- MARKETING_VERSION = 0.3.1;
325
+ MARKETING_VERSION = 0.3.4;
326
326
  PRODUCT_BUNDLE_IDENTIFIER = "dev.fka.Poke-macOS-Gate";
327
327
  PRODUCT_NAME = "$(TARGET_NAME)";
328
328
  REGISTER_APP_GROUPS = YES;
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "poke-gate",
3
- "version": "0.3.2",
3
+ "version": "0.3.5",
4
4
  "description": "Expose your machine to your Poke AI assistant via MCP tunnel",
5
5
  "type": "module",
6
6
  "bin": {
7
- "poke-gate": "./bin/poke-gate.js"
7
+ "poke-gate": "bin/poke-gate.js"
8
8
  },
9
9
  "scripts": {
10
10
  "start": "node src/app.js",
package/src/app.js CHANGED
@@ -2,7 +2,7 @@ import { startMcpServer, enableLogging, getPermissionMode } from "./mcp-server.j
2
2
  import { startTunnel } from "./tunnel.js";
3
3
  import { startAgentScheduler, stopAgentScheduler } from "./agents.js";
4
4
  import { sendToWebhook } from "./webhook.js";
5
- import { isLoggedIn, login, getToken } from "poke";
5
+ import { ensurePokeAuthenticated } from "./poke-auth.js";
6
6
  import { execSync } from "node:child_process";
7
7
 
8
8
  const verbose = process.argv.includes("--verbose") || process.argv.includes("-v");
@@ -12,8 +12,28 @@ function killExistingInstances() {
12
12
  const myPid = process.pid;
13
13
  const ppid = process.ppid;
14
14
  try {
15
- const out = execSync("pgrep -f 'node.*poke-gate.*app\\.js'", { encoding: "utf-8" }).trim();
16
- const pids = out.split("\n").map(Number).filter((p) => p && p !== myPid && p !== ppid);
15
+ const out = execSync("ps -axo pid=,ppid=,command=", { encoding: "utf-8" }).trim();
16
+ const pids = out
17
+ .split("\n")
18
+ .map((line) => {
19
+ const match = line.trim().match(/^(\d+)\s+(\d+)\s+(.+)$/);
20
+ if (!match) return null;
21
+ const [, pid, parentPid, command] = match;
22
+ return { pid: Number(pid), parentPid: Number(parentPid), command };
23
+ })
24
+ .filter((processInfo) => {
25
+ if (!processInfo || processInfo.pid === myPid || processInfo.pid === ppid || processInfo.parentPid === myPid) return false;
26
+ return (
27
+ processInfo.command.includes("node ") &&
28
+ processInfo.command.includes("poke-gate") &&
29
+ (
30
+ processInfo.command.includes("app.js") ||
31
+ processInfo.command.includes(".bin/poke-gate") ||
32
+ processInfo.command.includes("/bin/poke-gate")
33
+ )
34
+ );
35
+ })
36
+ .map(({ pid }) => pid);
17
37
  for (const pid of pids) {
18
38
  try { process.kill(pid, "SIGTERM"); } catch {}
19
39
  }
@@ -31,17 +51,7 @@ function sleep(ms) {
31
51
  }
32
52
 
33
53
  async function ensureAuthenticated() {
34
- if (!isLoggedIn()) {
35
- log("Signing in to Poke...");
36
- await login();
37
- }
38
-
39
- const token = getToken();
40
- if (!token) {
41
- throw new Error("Authentication failed: no token returned by Poke SDK.");
42
- }
43
-
44
- return token;
54
+ return ensurePokeAuthenticated({ onLogin: () => log("Signing in to Poke...") });
45
55
  }
46
56
 
47
57
  let currentTunnel = null;
@@ -60,6 +70,7 @@ async function connectWithRetry(mcpUrl, token) {
60
70
 
61
71
  const { tunnel } = await startTunnel({
62
72
  mcpUrl,
73
+ token,
63
74
  onEvent: (type, data) => {
64
75
  switch (type) {
65
76
  case "connected":
package/src/mcp-server.js CHANGED
@@ -847,12 +847,29 @@ function readBody(req) {
847
847
  });
848
848
  }
849
849
 
850
+ function writeMcpEventStream(req, res) {
851
+ const sessionId = extractSessionId(req);
852
+ res.writeHead(200, {
853
+ "Content-Type": "text/event-stream",
854
+ "Cache-Control": "no-cache, no-transform",
855
+ Connection: "keep-alive",
856
+ "Mcp-Session-Id": sessionId,
857
+ });
858
+ res.write(": connected\n\n");
859
+
860
+ const keepAlive = setInterval(() => {
861
+ res.write(": keepalive\n\n");
862
+ }, 25_000);
863
+
864
+ req.on("close", () => clearInterval(keepAlive));
865
+ }
866
+
850
867
  export function startMcpServer(port = 0) {
851
868
  return new Promise((resolve, reject) => {
852
869
  const httpServer = http.createServer(async (req, res) => {
853
870
  res.setHeader("Access-Control-Allow-Origin", "*");
854
871
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
855
- res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, Mcp-Session-Id, Accept");
872
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, Mcp-Session-Id, Accept, X-Poke-User-Id");
856
873
  res.setHeader("Access-Control-Expose-Headers", "Mcp-Session-Id");
857
874
 
858
875
  if (req.method === "OPTIONS") {
@@ -863,12 +880,27 @@ export function startMcpServer(port = 0) {
863
880
 
864
881
  const url = new URL(req.url, "http://localhost");
865
882
 
883
+ if (url.pathname === "/mcp" && req.method === "GET") {
884
+ const accept = req.headers.accept || "";
885
+ if (accept.includes("text/event-stream")) {
886
+ writeMcpEventStream(req, res);
887
+ } else {
888
+ res.writeHead(405, { "Content-Type": "text/plain", Allow: "POST, OPTIONS" });
889
+ res.end("MCP endpoint expects POST, or GET with Accept: text/event-stream");
890
+ }
891
+ return;
892
+ }
893
+
866
894
  if (url.pathname === "/mcp" && req.method === "POST") {
867
895
  try {
868
896
  const body = await readBody(req);
869
897
  const parsed = JSON.parse(body);
870
898
 
871
899
  const sessionId = extractSessionId(req);
900
+ const responseHeaders = {
901
+ "Content-Type": "application/json",
902
+ "Mcp-Session-Id": sessionId,
903
+ };
872
904
 
873
905
  if (Array.isArray(parsed)) {
874
906
  const results = [];
@@ -878,17 +910,17 @@ export function startMcpServer(port = 0) {
878
910
  const resolved = r instanceof Promise ? await r : r;
879
911
  if (resolved) results.push(resolved);
880
912
  }
881
- res.writeHead(200, { "Content-Type": "application/json" });
913
+ res.writeHead(200, responseHeaders);
882
914
  res.end(JSON.stringify(results));
883
915
  } else {
884
916
  const m = { ...parsed, __context: { sessionId } };
885
917
  let result = handleJsonRpc(m);
886
918
  if (result instanceof Promise) result = await result;
887
919
  if (result) {
888
- res.writeHead(200, { "Content-Type": "application/json" });
920
+ res.writeHead(200, responseHeaders);
889
921
  res.end(JSON.stringify(result));
890
922
  } else {
891
- res.writeHead(204);
923
+ res.writeHead(204, { "Mcp-Session-Id": sessionId });
892
924
  res.end();
893
925
  }
894
926
  }
@@ -0,0 +1,25 @@
1
+ import { getToken, isLoggedIn, login } from "poke";
2
+
3
+ export function resolvePokeToken(options = {}) {
4
+ const { env = process.env } = options;
5
+ const loginToken = Object.hasOwn(options, "token") ? options.token : getToken();
6
+ return env.POKE_API_KEY || loginToken;
7
+ }
8
+
9
+ export function getPokeAuthToken() {
10
+ return resolvePokeToken();
11
+ }
12
+
13
+ export async function ensurePokeAuthenticated({ onLogin } = {}) {
14
+ if (!getPokeAuthToken() && !isLoggedIn()) {
15
+ onLogin?.();
16
+ await login();
17
+ }
18
+
19
+ const token = getPokeAuthToken();
20
+ if (!token) {
21
+ throw new Error("Authentication failed: no token returned by Poke SDK.");
22
+ }
23
+
24
+ return token;
25
+ }
package/src/tunnel.js CHANGED
@@ -1,4 +1,4 @@
1
- import { PokeTunnel, getToken } from "poke";
1
+ import { PokeTunnel } from "poke";
2
2
  import { loadState, saveState } from "./webhook.js";
3
3
 
4
4
  function log(msg) {
@@ -6,8 +6,7 @@ function log(msg) {
6
6
  console.log(`[${ts}] ${msg}`);
7
7
  }
8
8
 
9
- async function cleanupStaleConnections() {
10
- const token = getToken();
9
+ async function cleanupStaleConnections(token) {
11
10
  if (!token) return;
12
11
  const base = process.env.POKE_API ?? "https://poke.com/api/v1";
13
12
  const state = loadState();
@@ -35,10 +34,9 @@ async function cleanupStaleConnections() {
35
34
  saveState({ webhookUrl, webhookToken });
36
35
  }
37
36
 
38
- export async function startTunnel({ mcpUrl, onEvent }) {
39
- await cleanupStaleConnections();
37
+ export async function startTunnel({ mcpUrl, token, onEvent }) {
38
+ await cleanupStaleConnections(token);
40
39
 
41
- const token = getToken();
42
40
  if (!token) {
43
41
  throw new Error("No Poke auth token available for tunnel.");
44
42
  }
package/src/webhook.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { homedir } from "node:os";
4
- import { Poke, getToken } from "poke";
4
+ import { Poke } from "poke";
5
+ import { getPokeAuthToken } from "./poke-auth.js";
5
6
 
6
7
  const CONFIG_DIR = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
7
8
  const STATE_PATH = join(CONFIG_DIR, "poke-gate", "state.json");
@@ -25,10 +26,10 @@ export async function getWebhook() {
25
26
  return { webhookUrl: state.webhookUrl, webhookToken: state.webhookToken };
26
27
  }
27
28
 
28
- const token = getToken();
29
+ const token = getPokeAuthToken();
29
30
  if (!token) throw new Error("No Poke auth token available.");
30
31
 
31
- const poke = new Poke({ token });
32
+ const poke = new Poke({ apiKey: token });
32
33
  const result = await poke.createWebhook({ condition: "poke-gate", action: "poke-gate" });
33
34
 
34
35
  const webhook = { webhookUrl: result.webhookUrl, webhookToken: result.webhookToken };
@@ -38,6 +39,9 @@ export async function getWebhook() {
38
39
 
39
40
  export async function sendToWebhook(message) {
40
41
  const { webhookUrl, webhookToken } = await getWebhook();
41
- const poke = new Poke({ token: getToken() });
42
+ const token = getPokeAuthToken();
43
+ if (!token) throw new Error("No Poke auth token available.");
44
+
45
+ const poke = new Poke({ apiKey: token });
42
46
  return poke.sendWebhook({ webhookUrl, webhookToken, data: { message } });
43
47
  }
@@ -0,0 +1,70 @@
1
+ import assert from "node:assert/strict";
2
+ import http from "node:http";
3
+ import test from "node:test";
4
+
5
+ import { startMcpServer } from "../src/mcp-server.js";
6
+
7
+ function request({ port, method = "GET", path = "/", headers = {}, body }) {
8
+ return new Promise((resolve, reject) => {
9
+ const req = http.request({ hostname: "127.0.0.1", port, method, path, headers }, (res) => {
10
+ let data = "";
11
+ res.on("data", (chunk) => {
12
+ data += chunk;
13
+ if (headers.Accept === "text/event-stream") {
14
+ req.destroy();
15
+ }
16
+ });
17
+ res.on("end", () => resolve({ res, body: data }));
18
+ res.on("close", () => resolve({ res, body: data }));
19
+ });
20
+ req.on("error", (error) => {
21
+ if (headers.Accept === "text/event-stream") return;
22
+ reject(error);
23
+ });
24
+ if (body) req.write(body);
25
+ req.end();
26
+ });
27
+ }
28
+
29
+ test("MCP POST responses include session id header", async () => {
30
+ const { httpServer, port } = await startMcpServer();
31
+ try {
32
+ const { res, body } = await request({
33
+ port,
34
+ method: "POST",
35
+ path: "/mcp",
36
+ headers: {
37
+ "Content-Type": "application/json",
38
+ "Mcp-Session-Id": "session-1",
39
+ },
40
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list", params: {} }),
41
+ });
42
+
43
+ assert.equal(res.statusCode, 200);
44
+ assert.equal(res.headers["mcp-session-id"], "session-1");
45
+ assert.equal(JSON.parse(body).result.tools.length > 0, true);
46
+ } finally {
47
+ httpServer.close();
48
+ }
49
+ });
50
+
51
+ test("MCP GET supports event stream transport", async () => {
52
+ const { httpServer, port } = await startMcpServer();
53
+ try {
54
+ const { res, body } = await request({
55
+ port,
56
+ path: "/mcp",
57
+ headers: {
58
+ Accept: "text/event-stream",
59
+ "Mcp-Session-Id": "session-2",
60
+ },
61
+ });
62
+
63
+ assert.equal(res.statusCode, 200);
64
+ assert.equal(res.headers["content-type"], "text/event-stream");
65
+ assert.equal(res.headers["mcp-session-id"], "session-2");
66
+ assert.match(body, /: connected/);
67
+ } finally {
68
+ httpServer.close();
69
+ }
70
+ });
@@ -0,0 +1,16 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import { resolvePokeToken } from "../src/poke-auth.js";
5
+
6
+ test("POKE_API_KEY takes precedence over login token", () => {
7
+ assert.equal(resolvePokeToken({ env: { POKE_API_KEY: "from-env" }, token: "from-login" }), "from-env");
8
+ });
9
+
10
+ test("login token is used when POKE_API_KEY is not set", () => {
11
+ assert.equal(resolvePokeToken({ env: {}, token: "from-login" }), "from-login");
12
+ });
13
+
14
+ test("missing env and login token resolves to undefined", () => {
15
+ assert.equal(resolvePokeToken({ env: {}, token: undefined }), undefined);
16
+ });