poke-gate 0.3.5 → 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/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 +1 -1
- package/src/mcp-server.js +9 -2
- package/test/mcp-server-transport.test.js +62 -0
- package/.github/copilot-desktop.yml +0 -8
|
@@ -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
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
|