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.
@@ -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
- HighlightedCodeEditor(
567
- text: $viewModel.editorContent,
568
- language: viewModel.showingEnv ? "env" : "javascript"
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.4;
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.4;
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,6 +1,6 @@
1
1
  {
2
2
  "name": "poke-gate",
3
- "version": "0.3.5",
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": {
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 (url.pathname === "/mcp" && req.method === "GET") {
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 (url.pathname === "/mcp" && req.method === "POST") {
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