poke-gate 0.3.3 → 0.3.6
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/npm.yml +14 -3
- package/.github/workflows/release.yml +12 -2
- package/clients/Poke macOS Gate/Poke macOS Gate/AgentsView.swift +121 -4
- package/clients/Poke macOS Gate/Poke macOS Gate.xcodeproj/project.pbxproj +2 -2
- package/package.json +2 -2
- package/src/mcp-server.js +9 -2
- package/test/mcp-server-transport.test.js +62 -0
- package/.github/copilot-desktop.yml +0 -8
|
@@ -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
|
|
@@ -481,6 +481,7 @@ struct AgentDetailView: View {
|
|
|
481
481
|
@ObservedObject var viewModel: AgentsViewModel
|
|
482
482
|
let agent: AgentFile
|
|
483
483
|
@State private var intervalInput: String = ""
|
|
484
|
+
@State private var showOutput: Bool = false
|
|
484
485
|
|
|
485
486
|
var body: some View {
|
|
486
487
|
VStack(spacing: 0) {
|
|
@@ -508,6 +509,18 @@ struct AgentDetailView: View {
|
|
|
508
509
|
}
|
|
509
510
|
|
|
510
511
|
Button {
|
|
512
|
+
withAnimation(.easeInOut(duration: 0.15)) { showOutput.toggle() }
|
|
513
|
+
} label: {
|
|
514
|
+
Image(systemName: "terminal")
|
|
515
|
+
.font(.caption)
|
|
516
|
+
.foregroundStyle(showOutput ? Color.accentColor : .secondary)
|
|
517
|
+
}
|
|
518
|
+
.buttonStyle(.plain)
|
|
519
|
+
.help(showOutput ? "Hide output panel" : "Show output panel")
|
|
520
|
+
.padding(.leading, 4)
|
|
521
|
+
|
|
522
|
+
Button {
|
|
523
|
+
showOutput = true
|
|
511
524
|
viewModel.runAgent(agent)
|
|
512
525
|
} label: {
|
|
513
526
|
Label(viewModel.isRunning ? "Running…" : "Run", systemImage: "play.fill")
|
|
@@ -563,16 +576,120 @@ struct AgentDetailView: View {
|
|
|
563
576
|
|
|
564
577
|
Divider()
|
|
565
578
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
579
|
+
if showOutput {
|
|
580
|
+
VSplitView {
|
|
581
|
+
HighlightedCodeEditor(
|
|
582
|
+
text: $viewModel.editorContent,
|
|
583
|
+
language: viewModel.showingEnv ? "env" : "javascript"
|
|
584
|
+
)
|
|
585
|
+
.frame(minHeight: 100)
|
|
586
|
+
|
|
587
|
+
AgentOutputPanel(
|
|
588
|
+
output: viewModel.lastRunOutput,
|
|
589
|
+
isRunning: viewModel.isRunning,
|
|
590
|
+
onClose: {
|
|
591
|
+
withAnimation(.easeInOut(duration: 0.15)) { showOutput = false }
|
|
592
|
+
},
|
|
593
|
+
onClear: {
|
|
594
|
+
viewModel.lastRunOutput = ""
|
|
595
|
+
}
|
|
596
|
+
)
|
|
597
|
+
.frame(minHeight: 80, idealHeight: 180)
|
|
598
|
+
}
|
|
599
|
+
} else {
|
|
600
|
+
HighlightedCodeEditor(
|
|
601
|
+
text: $viewModel.editorContent,
|
|
602
|
+
language: viewModel.showingEnv ? "env" : "javascript"
|
|
603
|
+
)
|
|
604
|
+
}
|
|
570
605
|
}
|
|
571
606
|
.onAppear {
|
|
572
607
|
intervalInput = agent.interval
|
|
573
608
|
}
|
|
574
609
|
.onChange(of: agent.id) { _, _ in
|
|
575
610
|
intervalInput = agent.interval
|
|
611
|
+
showOutput = false
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
struct AgentOutputPanel: View {
|
|
617
|
+
let output: String
|
|
618
|
+
let isRunning: Bool
|
|
619
|
+
let onClose: () -> Void
|
|
620
|
+
let onClear: () -> Void
|
|
621
|
+
|
|
622
|
+
var body: some View {
|
|
623
|
+
VStack(spacing: 0) {
|
|
624
|
+
HStack {
|
|
625
|
+
HStack(spacing: 6) {
|
|
626
|
+
if isRunning {
|
|
627
|
+
ProgressView()
|
|
628
|
+
.scaleEffect(0.6)
|
|
629
|
+
.frame(width: 12, height: 12)
|
|
630
|
+
} else {
|
|
631
|
+
Image(systemName: "checkmark.circle.fill")
|
|
632
|
+
.font(.caption2)
|
|
633
|
+
.foregroundStyle(.green)
|
|
634
|
+
}
|
|
635
|
+
Text(isRunning ? "Running…" : "Output")
|
|
636
|
+
.font(.caption)
|
|
637
|
+
.fontWeight(.medium)
|
|
638
|
+
.foregroundStyle(.secondary)
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
Spacer()
|
|
642
|
+
|
|
643
|
+
Button {
|
|
644
|
+
let pb = NSPasteboard.general
|
|
645
|
+
pb.clearContents()
|
|
646
|
+
pb.setString(output, forType: .string)
|
|
647
|
+
} label: {
|
|
648
|
+
Image(systemName: "doc.on.doc")
|
|
649
|
+
.font(.caption)
|
|
650
|
+
}
|
|
651
|
+
.buttonStyle(.plain)
|
|
652
|
+
.foregroundStyle(.secondary)
|
|
653
|
+
.help("Copy output")
|
|
654
|
+
|
|
655
|
+
Button(action: onClear) {
|
|
656
|
+
Image(systemName: "trash")
|
|
657
|
+
.font(.caption)
|
|
658
|
+
}
|
|
659
|
+
.buttonStyle(.plain)
|
|
660
|
+
.foregroundStyle(.secondary)
|
|
661
|
+
.help("Clear output")
|
|
662
|
+
|
|
663
|
+
Button(action: onClose) {
|
|
664
|
+
Image(systemName: "xmark")
|
|
665
|
+
.font(.caption)
|
|
666
|
+
}
|
|
667
|
+
.buttonStyle(.plain)
|
|
668
|
+
.foregroundStyle(.secondary)
|
|
669
|
+
.help("Close output panel")
|
|
670
|
+
}
|
|
671
|
+
.padding(.horizontal, 12)
|
|
672
|
+
.padding(.vertical, 6)
|
|
673
|
+
.background(.quaternary.opacity(0.3))
|
|
674
|
+
|
|
675
|
+
Divider()
|
|
676
|
+
|
|
677
|
+
ScrollViewReader { proxy in
|
|
678
|
+
ScrollView {
|
|
679
|
+
Text(output.isEmpty ? (isRunning ? "Starting…" : "No output.") : output)
|
|
680
|
+
.font(.system(.caption, design: .monospaced))
|
|
681
|
+
.foregroundStyle(output.isEmpty ? .tertiary : .primary)
|
|
682
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
683
|
+
.padding(12)
|
|
684
|
+
.id("bottom")
|
|
685
|
+
}
|
|
686
|
+
.onChange(of: output) { _, _ in
|
|
687
|
+
withAnimation(.easeOut(duration: 0.1)) {
|
|
688
|
+
proxy.scrollTo("bottom", anchor: .bottom)
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
.background(Color(nsColor: .textBackgroundColor))
|
|
576
693
|
}
|
|
577
694
|
}
|
|
578
695
|
}
|
|
@@ -286,7 +286,7 @@
|
|
|
286
286
|
"@executable_path/../Frameworks",
|
|
287
287
|
);
|
|
288
288
|
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
|
289
|
-
MARKETING_VERSION = 0.3.
|
|
289
|
+
MARKETING_VERSION = 0.3.6;
|
|
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.
|
|
325
|
+
MARKETING_VERSION = 0.3.6;
|
|
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.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"description": "Expose your machine to your Poke AI assistant via MCP tunnel",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"poke-gate": "
|
|
7
|
+
"poke-gate": "bin/poke-gate.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node src/app.js",
|
package/src/mcp-server.js
CHANGED
|
@@ -12,6 +12,7 @@ const COMMAND_TIMEOUT = 30_000;
|
|
|
12
12
|
const RUN_COMMAND_LOOP_SUPPRESSION_MS = 60_000;
|
|
13
13
|
const PERMISSION_MODE = normalizePermissionMode(process.env.POKE_GATE_PERMISSION_MODE);
|
|
14
14
|
const SANDBOX_EXEC_PATH = "/usr/bin/sandbox-exec";
|
|
15
|
+
const TUNNEL_MCP_PATH_RE = /^\/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\/mcp$/;
|
|
15
16
|
|
|
16
17
|
let logEnabled = false;
|
|
17
18
|
|
|
@@ -224,6 +225,11 @@ function extractSessionId(req) {
|
|
|
224
225
|
return "default";
|
|
225
226
|
}
|
|
226
227
|
|
|
228
|
+
export function normalizeMcpPathname(pathname) {
|
|
229
|
+
if (pathname === "/mcp" || TUNNEL_MCP_PATH_RE.test(pathname)) return "/mcp";
|
|
230
|
+
return pathname;
|
|
231
|
+
}
|
|
232
|
+
|
|
227
233
|
function buildApprovalResponse(name, cleanArgs, approval) {
|
|
228
234
|
const summary = name === "run_command"
|
|
229
235
|
? `Run command: ${cleanArgs.command}`
|
|
@@ -879,8 +885,9 @@ export function startMcpServer(port = 0) {
|
|
|
879
885
|
}
|
|
880
886
|
|
|
881
887
|
const url = new URL(req.url, "http://localhost");
|
|
888
|
+
const pathname = normalizeMcpPathname(url.pathname);
|
|
882
889
|
|
|
883
|
-
if (
|
|
890
|
+
if (pathname === "/mcp" && req.method === "GET") {
|
|
884
891
|
const accept = req.headers.accept || "";
|
|
885
892
|
if (accept.includes("text/event-stream")) {
|
|
886
893
|
writeMcpEventStream(req, res);
|
|
@@ -891,7 +898,7 @@ export function startMcpServer(port = 0) {
|
|
|
891
898
|
return;
|
|
892
899
|
}
|
|
893
900
|
|
|
894
|
-
if (
|
|
901
|
+
if (pathname === "/mcp" && req.method === "POST") {
|
|
895
902
|
try {
|
|
896
903
|
const body = await readBody(req);
|
|
897
904
|
const parsed = JSON.parse(body);
|
|
@@ -4,6 +4,8 @@ import test from "node:test";
|
|
|
4
4
|
|
|
5
5
|
import { startMcpServer } from "../src/mcp-server.js";
|
|
6
6
|
|
|
7
|
+
const TUNNEL_CONNECTION_ID = "64574786-7a08-4074-ab67-8078a40b7ba2";
|
|
8
|
+
|
|
7
9
|
function request({ port, method = "GET", path = "/", headers = {}, body }) {
|
|
8
10
|
return new Promise((resolve, reject) => {
|
|
9
11
|
const req = http.request({ hostname: "127.0.0.1", port, method, path, headers }, (res) => {
|
|
@@ -68,3 +70,63 @@ test("MCP GET supports event stream transport", async () => {
|
|
|
68
70
|
httpServer.close();
|
|
69
71
|
}
|
|
70
72
|
});
|
|
73
|
+
|
|
74
|
+
test("MCP POST accepts Poke tunnel connection-id prefix", async () => {
|
|
75
|
+
const { httpServer, port } = await startMcpServer();
|
|
76
|
+
try {
|
|
77
|
+
const { res, body } = await request({
|
|
78
|
+
port,
|
|
79
|
+
method: "POST",
|
|
80
|
+
path: `/${TUNNEL_CONNECTION_ID}/mcp`,
|
|
81
|
+
headers: {
|
|
82
|
+
"Content-Type": "application/json",
|
|
83
|
+
"Mcp-Session-Id": "session-prefixed-post",
|
|
84
|
+
},
|
|
85
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list", params: {} }),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
assert.equal(res.statusCode, 200);
|
|
89
|
+
assert.equal(res.headers["mcp-session-id"], "session-prefixed-post");
|
|
90
|
+
assert.equal(JSON.parse(body).result.tools.length > 0, true);
|
|
91
|
+
} finally {
|
|
92
|
+
httpServer.close();
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("MCP GET accepts Poke tunnel connection-id prefix", async () => {
|
|
97
|
+
const { httpServer, port } = await startMcpServer();
|
|
98
|
+
try {
|
|
99
|
+
const { res, body } = await request({
|
|
100
|
+
port,
|
|
101
|
+
path: `/${TUNNEL_CONNECTION_ID}/mcp`,
|
|
102
|
+
headers: {
|
|
103
|
+
Accept: "text/event-stream",
|
|
104
|
+
"Mcp-Session-Id": "session-prefixed-get",
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
assert.equal(res.statusCode, 200);
|
|
109
|
+
assert.equal(res.headers["content-type"], "text/event-stream");
|
|
110
|
+
assert.equal(res.headers["mcp-session-id"], "session-prefixed-get");
|
|
111
|
+
assert.match(body, /: connected/);
|
|
112
|
+
} finally {
|
|
113
|
+
httpServer.close();
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("MCP route rejects non-connection-id prefixes", async () => {
|
|
118
|
+
const { httpServer, port } = await startMcpServer();
|
|
119
|
+
try {
|
|
120
|
+
const { res } = await request({
|
|
121
|
+
port,
|
|
122
|
+
method: "POST",
|
|
123
|
+
path: "/not-a-connection/mcp",
|
|
124
|
+
headers: { "Content-Type": "application/json" },
|
|
125
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list", params: {} }),
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
assert.equal(res.statusCode, 404);
|
|
129
|
+
} finally {
|
|
130
|
+
httpServer.close();
|
|
131
|
+
}
|
|
132
|
+
});
|
|
@@ -1,8 +0,0 @@
|
|
|
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
|