poke-gate 0.2.2 → 0.3.0
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/README.md +31 -26
- package/bin/poke-gate.js +3 -0
- package/clients/Poke macOS Gate/Poke macOS Gate/GateService.swift +113 -1
- package/clients/Poke macOS Gate/Poke macOS Gate/Poke_macOS_GateApp.swift +71 -1
- package/clients/Poke macOS Gate/Poke macOS Gate.xcodeproj/project.pbxproj +2 -2
- package/docs/cli.md +16 -0
- package/package.json +1 -1
- package/src/app.js +2 -2
- package/src/download-macos.js +133 -0
- package/src/mcp-server.js +31 -6
package/README.md
CHANGED
|
@@ -28,6 +28,25 @@ Run Poke Gate on your Mac, then message Poke from iMessage, Telegram, or SMS to
|
|
|
28
28
|
brew install f/tap/poke-gate
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
**Install via npx**
|
|
32
|
+
|
|
33
|
+
If you have Node.js installed, you can download and install the macOS app with a single command:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx poke-gate download-macos
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
This downloads the latest DMG from GitHub Releases, installs the app to `/Applications`, and clears the quarantine flag automatically.
|
|
40
|
+
|
|
41
|
+
**Don't have Node.js?** Install it first:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Option 1: Homebrew
|
|
45
|
+
brew install node
|
|
46
|
+
|
|
47
|
+
# Option 2: Download from https://nodejs.org
|
|
48
|
+
```
|
|
49
|
+
|
|
31
50
|
**Manual download**
|
|
32
51
|
|
|
33
52
|
Download the latest **Poke.macOS.Gate.dmg** from [Releases](https://github.com/f/poke-gate/releases), open it, and drag to Applications. Since the app is not notarized, you may need to run:
|
|
@@ -36,7 +55,9 @@ Download the latest **Poke.macOS.Gate.dmg** from [Releases](https://github.com/f
|
|
|
36
55
|
xattr -cr /Applications/Poke\ macOS\ Gate.app
|
|
37
56
|
```
|
|
38
57
|
|
|
39
|
-
**CLI** (no macOS app needed)
|
|
58
|
+
**CLI only** (no macOS app needed)
|
|
59
|
+
|
|
60
|
+
If you just want to run poke-gate from the terminal without the menu bar app:
|
|
40
61
|
|
|
41
62
|
```bash
|
|
42
63
|
npx poke-gate
|
|
@@ -123,7 +144,9 @@ Hit **Run** in Xcode, or build from the command line:
|
|
|
123
144
|
|
|
124
145
|
## CLI usage
|
|
125
146
|
|
|
126
|
-
If you
|
|
147
|
+
The CLI requires [Node.js](https://nodejs.org) 18 or later. If you don't have it, install via `brew install node` or download from [nodejs.org](https://nodejs.org).
|
|
148
|
+
|
|
149
|
+
Start the gate:
|
|
127
150
|
|
|
128
151
|
```bash
|
|
129
152
|
npx poke-gate
|
|
@@ -142,6 +165,12 @@ npx poke-gate --mode limited
|
|
|
142
165
|
npx poke-gate --mode sandbox
|
|
143
166
|
```
|
|
144
167
|
|
|
168
|
+
Install or update the macOS app:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
npx poke-gate download-macos
|
|
172
|
+
```
|
|
173
|
+
|
|
145
174
|
Config is stored at `~/.config/poke-gate/config.json`.
|
|
146
175
|
|
|
147
176
|
## Agents
|
|
@@ -264,30 +293,6 @@ POKE_GATE_PERMISSION_MODE=limited npx poke-gate
|
|
|
264
293
|
|
|
265
294
|
Only run Poke Gate on machines and networks you trust. Use `limited` or `sandbox` mode if you want tighter restrictions.
|
|
266
295
|
|
|
267
|
-
## Project structure
|
|
268
|
-
|
|
269
|
-
```
|
|
270
|
-
clients/
|
|
271
|
-
Poke macOS Gate/ macOS menu bar app (SwiftUI)
|
|
272
|
-
bin/
|
|
273
|
-
poke-gate.js CLI entry point with --mode flag
|
|
274
|
-
src/
|
|
275
|
-
app.js Startup: MCP server + tunnel + agent scheduler
|
|
276
|
-
agents.js Agent discovery, scheduling, env loading, download
|
|
277
|
-
mcp-server.js JSON-RPC MCP handler, tools, access policy, sandbox
|
|
278
|
-
permission-service.js HMAC approval tokens, session whitelisting
|
|
279
|
-
tunnel.js PokeTunnel wrapper
|
|
280
|
-
test/
|
|
281
|
-
mcp-server-access-policy.test.js
|
|
282
|
-
mcp-server-loop-guard.test.js
|
|
283
|
-
mcp-server-sandbox-command.test.js
|
|
284
|
-
permission-service.test.js
|
|
285
|
-
examples/
|
|
286
|
-
agents/
|
|
287
|
-
beeper.1h.js Example: Beeper message digest agent
|
|
288
|
-
.env.beeper Example env file for beeper agent
|
|
289
|
-
```
|
|
290
|
-
|
|
291
296
|
## Credits
|
|
292
297
|
|
|
293
298
|
- [Poke](https://poke.com) by [The Interaction Company of California](https://interaction.co)
|
package/bin/poke-gate.js
CHANGED
|
@@ -39,6 +39,9 @@ async function main() {
|
|
|
39
39
|
const prompt = promptIdx !== -1 ? args.slice(promptIdx + 1).join(" ") : args.slice(2).join(" ") || null;
|
|
40
40
|
const { createAgent } = await import("../src/agent-create.js");
|
|
41
41
|
await createAgent(prompt);
|
|
42
|
+
} else if (args[0] === "download-macos") {
|
|
43
|
+
const { downloadMacOSApp } = await import("../src/download-macos.js");
|
|
44
|
+
await downloadMacOSApp();
|
|
42
45
|
} else {
|
|
43
46
|
const mode = parseMode();
|
|
44
47
|
if (mode) {
|
|
@@ -97,6 +97,10 @@ class GateService: ObservableObject {
|
|
|
97
97
|
@Published var permissionMode: PermissionMode
|
|
98
98
|
@Published var hasCompletedSetup: Bool
|
|
99
99
|
@Published var systemPermissionStatuses: [SystemPermissionStatus] = []
|
|
100
|
+
@Published var availableUpdate: String? = nil
|
|
101
|
+
@Published var isUpdating = false
|
|
102
|
+
@Published var isCheckingForUpdate = false
|
|
103
|
+
@Published var updateCheckResult: String? = nil
|
|
100
104
|
|
|
101
105
|
private var hasAutoStarted = false
|
|
102
106
|
private var process: Process?
|
|
@@ -121,7 +125,9 @@ class GateService: ObservableObject {
|
|
|
121
125
|
object: nil,
|
|
122
126
|
queue: .main
|
|
123
127
|
) { [weak self] _ in
|
|
124
|
-
|
|
128
|
+
Task { @MainActor in
|
|
129
|
+
self?.refreshSystemPermissions()
|
|
130
|
+
}
|
|
125
131
|
}
|
|
126
132
|
}
|
|
127
133
|
|
|
@@ -302,6 +308,112 @@ class GateService: ObservableObject {
|
|
|
302
308
|
if hasAPIKey {
|
|
303
309
|
start()
|
|
304
310
|
}
|
|
311
|
+
checkForUpdate(silent: true)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
func checkForUpdate(silent: Bool = false) {
|
|
315
|
+
let currentVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "0.0.0"
|
|
316
|
+
isCheckingForUpdate = true
|
|
317
|
+
updateCheckResult = nil
|
|
318
|
+
|
|
319
|
+
Task.detached {
|
|
320
|
+
let start = ContinuousClock.now
|
|
321
|
+
var found = false
|
|
322
|
+
|
|
323
|
+
defer {
|
|
324
|
+
Task { @MainActor in
|
|
325
|
+
self.isCheckingForUpdate = false
|
|
326
|
+
if !found && !silent {
|
|
327
|
+
self.updateCheckResult = "You're on the latest version."
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
guard let url = URL(string: "https://registry.npmjs.org/poke-gate/latest") else { return }
|
|
333
|
+
do {
|
|
334
|
+
let (data, _) = try await URLSession.shared.data(from: url)
|
|
335
|
+
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
336
|
+
let latestVersion = json["version"] as? String else { return }
|
|
337
|
+
|
|
338
|
+
let elapsed = ContinuousClock.now - start
|
|
339
|
+
if elapsed < .seconds(1) {
|
|
340
|
+
try await Task.sleep(for: .seconds(1) - elapsed)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if Self.isNewer(latestVersion, than: currentVersion) {
|
|
344
|
+
found = true
|
|
345
|
+
await MainActor.run {
|
|
346
|
+
self.availableUpdate = latestVersion
|
|
347
|
+
self.appendLog("Update available: v\(latestVersion) (current: v\(currentVersion))")
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
} catch {}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
private nonisolated static func isNewer(_ remote: String, than local: String) -> Bool {
|
|
355
|
+
let r = remote.split(separator: ".").compactMap { Int($0) }
|
|
356
|
+
let l = local.split(separator: ".").compactMap { Int($0) }
|
|
357
|
+
for i in 0..<max(r.count, l.count) {
|
|
358
|
+
let rv = i < r.count ? r[i] : 0
|
|
359
|
+
let lv = i < l.count ? l[i] : 0
|
|
360
|
+
if rv > lv { return true }
|
|
361
|
+
if rv < lv { return false }
|
|
362
|
+
}
|
|
363
|
+
return false
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
func performUpdate() {
|
|
367
|
+
guard let version = availableUpdate else { return }
|
|
368
|
+
isUpdating = true
|
|
369
|
+
appendLog("Updating to v\(version)...")
|
|
370
|
+
|
|
371
|
+
stop()
|
|
372
|
+
|
|
373
|
+
let fullPath = shellPath()
|
|
374
|
+
let npxBin = findNpx()
|
|
375
|
+
|
|
376
|
+
let proc = Process()
|
|
377
|
+
let pipe = Pipe()
|
|
378
|
+
proc.executableURL = URL(fileURLWithPath: "/bin/zsh")
|
|
379
|
+
proc.arguments = ["-c", "\(npxBin) -y poke-gate@\(version) download-macos"]
|
|
380
|
+
proc.environment = ProcessInfo.processInfo.environment.merging(
|
|
381
|
+
["PATH": fullPath],
|
|
382
|
+
uniquingKeysWith: { _, new in new }
|
|
383
|
+
)
|
|
384
|
+
proc.standardOutput = pipe
|
|
385
|
+
proc.standardError = pipe
|
|
386
|
+
proc.currentDirectoryURL = FileManager.default.homeDirectoryForCurrentUser
|
|
387
|
+
|
|
388
|
+
let handle = pipe.fileHandleForReading
|
|
389
|
+
handle.readabilityHandler = { [weak self] fh in
|
|
390
|
+
let data = fh.availableData
|
|
391
|
+
guard !data.isEmpty, let line = String(data: data, encoding: .utf8) else { return }
|
|
392
|
+
DispatchQueue.main.async {
|
|
393
|
+
for l in line.components(separatedBy: .newlines) where !l.isEmpty {
|
|
394
|
+
self?.appendLog(l)
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
proc.terminationHandler = { [weak self] proc in
|
|
400
|
+
DispatchQueue.main.async {
|
|
401
|
+
self?.isUpdating = false
|
|
402
|
+
if proc.terminationStatus == 0 {
|
|
403
|
+
self?.appendLog("Update complete. The app will relaunch.")
|
|
404
|
+
self?.availableUpdate = nil
|
|
405
|
+
} else {
|
|
406
|
+
self?.appendLog("Update failed (exit \(proc.terminationStatus)).")
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
do {
|
|
412
|
+
try proc.run()
|
|
413
|
+
} catch {
|
|
414
|
+
isUpdating = false
|
|
415
|
+
appendLog("Failed to start update: \(error.localizedDescription)")
|
|
416
|
+
}
|
|
305
417
|
}
|
|
306
418
|
|
|
307
419
|
func start() {
|
|
@@ -111,6 +111,9 @@ struct PopoverContent: View {
|
|
|
111
111
|
SetupView(service: service)
|
|
112
112
|
} else {
|
|
113
113
|
VStack(spacing: 10) {
|
|
114
|
+
if service.availableUpdate != nil {
|
|
115
|
+
updateBanner
|
|
116
|
+
}
|
|
114
117
|
statusSection
|
|
115
118
|
recentActivitySection
|
|
116
119
|
accessModeSection
|
|
@@ -128,6 +131,37 @@ struct PopoverContent: View {
|
|
|
128
131
|
}
|
|
129
132
|
}
|
|
130
133
|
|
|
134
|
+
private var updateBanner: some View {
|
|
135
|
+
HStack(spacing: 8) {
|
|
136
|
+
Image(systemName: "arrow.down.circle.fill")
|
|
137
|
+
.foregroundStyle(.blue)
|
|
138
|
+
|
|
139
|
+
VStack(alignment: .leading, spacing: 1) {
|
|
140
|
+
Text("Update available: v\(service.availableUpdate ?? "")")
|
|
141
|
+
.font(.caption)
|
|
142
|
+
.fontWeight(.semibold)
|
|
143
|
+
Text("A new version of Poke Gate is ready.")
|
|
144
|
+
.font(.caption2)
|
|
145
|
+
.foregroundStyle(.secondary)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
Spacer()
|
|
149
|
+
|
|
150
|
+
if service.isUpdating {
|
|
151
|
+
ProgressView()
|
|
152
|
+
.controlSize(.small)
|
|
153
|
+
} else {
|
|
154
|
+
Button("Update") {
|
|
155
|
+
service.performUpdate()
|
|
156
|
+
}
|
|
157
|
+
.controlSize(.small)
|
|
158
|
+
.buttonStyle(.borderedProminent)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
.padding(10)
|
|
162
|
+
.macPanelStyle(.neutral, cornerRadius: 10)
|
|
163
|
+
}
|
|
164
|
+
|
|
131
165
|
private var statusSection: some View {
|
|
132
166
|
VStack(alignment: .leading, spacing: 6) {
|
|
133
167
|
HStack(spacing: 8) {
|
|
@@ -303,8 +337,10 @@ struct PopoverContent: View {
|
|
|
303
337
|
.macPanelStyle(.neutral, cornerRadius: 12)
|
|
304
338
|
}
|
|
305
339
|
|
|
340
|
+
@State private var refreshRotation: Double = 0
|
|
341
|
+
|
|
306
342
|
private var footerSection: some View {
|
|
307
|
-
HStack {
|
|
343
|
+
HStack(spacing: 6) {
|
|
308
344
|
Button {
|
|
309
345
|
NSApp.activate(ignoringOtherApps: true)
|
|
310
346
|
openWindow(id: "about")
|
|
@@ -315,6 +351,28 @@ struct PopoverContent: View {
|
|
|
315
351
|
}
|
|
316
352
|
.buttonStyle(.plain)
|
|
317
353
|
|
|
354
|
+
Button {
|
|
355
|
+
service.checkForUpdate()
|
|
356
|
+
} label: {
|
|
357
|
+
Image(systemName: "arrow.triangle.2.circlepath")
|
|
358
|
+
.font(.system(size: 9))
|
|
359
|
+
.foregroundStyle(MacVisualStyle.sectionTitleColor)
|
|
360
|
+
.rotationEffect(.degrees(refreshRotation))
|
|
361
|
+
}
|
|
362
|
+
.buttonStyle(.plain)
|
|
363
|
+
.disabled(service.isCheckingForUpdate)
|
|
364
|
+
.onChange(of: service.isCheckingForUpdate) { _, checking in
|
|
365
|
+
if checking {
|
|
366
|
+
withAnimation(.linear(duration: 1).repeatForever(autoreverses: false)) {
|
|
367
|
+
refreshRotation = 360
|
|
368
|
+
}
|
|
369
|
+
} else {
|
|
370
|
+
withAnimation(.default) {
|
|
371
|
+
refreshRotation = 0
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
318
376
|
Spacer()
|
|
319
377
|
|
|
320
378
|
Text("Not affiliated with Poke")
|
|
@@ -322,6 +380,18 @@ struct PopoverContent: View {
|
|
|
322
380
|
.foregroundStyle(MacVisualStyle.sectionTitleColor.opacity(0.7))
|
|
323
381
|
}
|
|
324
382
|
.padding(.horizontal, 6)
|
|
383
|
+
.onChange(of: service.updateCheckResult) { _, result in
|
|
384
|
+
guard let message = result else { return }
|
|
385
|
+
service.updateCheckResult = nil
|
|
386
|
+
DispatchQueue.main.async {
|
|
387
|
+
let alert = NSAlert()
|
|
388
|
+
alert.messageText = "Version Check"
|
|
389
|
+
alert.informativeText = message
|
|
390
|
+
alert.alertStyle = .informational
|
|
391
|
+
alert.addButton(withTitle: "OK")
|
|
392
|
+
alert.runModal()
|
|
393
|
+
}
|
|
394
|
+
}
|
|
325
395
|
}
|
|
326
396
|
|
|
327
397
|
private func sectionTitle(_ text: String) -> some View {
|
|
@@ -286,7 +286,7 @@
|
|
|
286
286
|
"@executable_path/../Frameworks",
|
|
287
287
|
);
|
|
288
288
|
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
|
289
|
-
MARKETING_VERSION = 0.2.
|
|
289
|
+
MARKETING_VERSION = 0.2.2;
|
|
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.2.
|
|
325
|
+
MARKETING_VERSION = 0.2.2;
|
|
326
326
|
PRODUCT_BUNDLE_IDENTIFIER = "dev.fka.Poke-macOS-Gate";
|
|
327
327
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
328
328
|
REGISTER_APP_GROUPS = YES;
|
package/docs/cli.md
CHANGED
|
@@ -82,6 +82,22 @@ npx poke-gate agent create --prompt "send me a daily git commit summary across a
|
|
|
82
82
|
npx poke-gate agent create --prompt "track Spotify listening and log my music taste"
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
+
## Download the macOS app
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
npx poke-gate download-macos
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Downloads and installs the Poke macOS Gate app from GitHub Releases. Matches the version of the npm package being run.
|
|
92
|
+
|
|
93
|
+
This command:
|
|
94
|
+
1. Downloads the DMG from the matching GitHub release
|
|
95
|
+
2. Mounts the DMG, copies the app to `/Applications`
|
|
96
|
+
3. Clears the quarantine flag (`xattr -cr`)
|
|
97
|
+
4. Launches the app
|
|
98
|
+
|
|
99
|
+
The macOS app also checks for updates automatically on startup and shows a banner when a new version is available.
|
|
100
|
+
|
|
85
101
|
## Install an agent
|
|
86
102
|
|
|
87
103
|
```bash
|
package/package.json
CHANGED
package/src/app.js
CHANGED
|
@@ -146,8 +146,8 @@ function buildAccessModeMessage(mode) {
|
|
|
146
146
|
default:
|
|
147
147
|
return (
|
|
148
148
|
"Access mode: Full. " +
|
|
149
|
-
"You can run any shell command, read
|
|
150
|
-
"
|
|
149
|
+
"You can run any shell command, read files, list directories, take screenshots, and check system info — no approval needed. " +
|
|
150
|
+
"Only destructive actions (deleting files, rm, write_file) require a one-time approval; after that, everything is auto-approved for the session."
|
|
151
151
|
);
|
|
152
152
|
}
|
|
153
153
|
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import https from "node:https";
|
|
2
|
+
import { createWriteStream, unlinkSync, readFileSync } from "node:fs";
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join, dirname } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
const APP_NAME = "Poke macOS Gate";
|
|
9
|
+
const DMG_ASSET = "Poke.macOS.Gate.dmg";
|
|
10
|
+
const INSTALL_PATH = `/Applications/${APP_NAME}.app`;
|
|
11
|
+
const REPO = "f/poke-gate";
|
|
12
|
+
|
|
13
|
+
function getPackageVersion() {
|
|
14
|
+
const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
15
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
16
|
+
return pkg.version;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function httpGet(url) {
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
https.get(url, { headers: { "User-Agent": "poke-gate" } }, (res) => {
|
|
22
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
23
|
+
return httpGet(res.headers.location).then(resolve, reject);
|
|
24
|
+
}
|
|
25
|
+
resolve(res);
|
|
26
|
+
}).on("error", reject);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function downloadFile(url, dest, version) {
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
const follow = (followUrl) => {
|
|
33
|
+
https.get(followUrl, { headers: { "User-Agent": "poke-gate" } }, (res) => {
|
|
34
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
35
|
+
return follow(res.headers.location);
|
|
36
|
+
}
|
|
37
|
+
if (res.statusCode !== 200) {
|
|
38
|
+
reject(new Error(`Download failed: HTTP ${res.statusCode}`));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const total = parseInt(res.headers["content-length"], 10) || 0;
|
|
43
|
+
let downloaded = 0;
|
|
44
|
+
const file = createWriteStream(dest);
|
|
45
|
+
|
|
46
|
+
res.on("data", (chunk) => {
|
|
47
|
+
downloaded += chunk.length;
|
|
48
|
+
if (total > 0) {
|
|
49
|
+
const pct = Math.round((downloaded / total) * 100);
|
|
50
|
+
const dlMB = (downloaded / 1024 / 1024).toFixed(1);
|
|
51
|
+
const totalMB = (total / 1024 / 1024).toFixed(1);
|
|
52
|
+
process.stdout.write(`\rDownloading ${APP_NAME} v${version}... ${pct}% (${dlMB}/${totalMB} MB)`);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
res.pipe(file);
|
|
57
|
+
|
|
58
|
+
file.on("finish", () => {
|
|
59
|
+
file.close(() => {
|
|
60
|
+
console.log("");
|
|
61
|
+
resolve();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
file.on("error", (err) => {
|
|
66
|
+
file.close();
|
|
67
|
+
reject(err);
|
|
68
|
+
});
|
|
69
|
+
}).on("error", reject);
|
|
70
|
+
};
|
|
71
|
+
follow(url);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function run(cmd) {
|
|
76
|
+
return execSync(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function downloadMacOSApp() {
|
|
80
|
+
const version = getPackageVersion();
|
|
81
|
+
const tag = `v${version}`;
|
|
82
|
+
const dmgUrl = `https://github.com/${REPO}/releases/download/${tag}/${DMG_ASSET}`;
|
|
83
|
+
|
|
84
|
+
console.log(`Poke Gate macOS installer (${tag})`);
|
|
85
|
+
console.log("");
|
|
86
|
+
|
|
87
|
+
const res = await httpGet(`https://api.github.com/repos/${REPO}/releases/tags/${tag}`);
|
|
88
|
+
const chunks = [];
|
|
89
|
+
for await (const chunk of res) chunks.push(chunk);
|
|
90
|
+
const release = JSON.parse(Buffer.concat(chunks).toString());
|
|
91
|
+
|
|
92
|
+
if (!release.assets || !release.assets.find((a) => a.name === DMG_ASSET)) {
|
|
93
|
+
console.error(`No DMG found for ${tag}. The release may not have finished building yet.`);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const dmgPath = join(tmpdir(), `poke-gate-${version}.dmg`);
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
await downloadFile(dmgUrl, dmgPath, version);
|
|
101
|
+
|
|
102
|
+
console.log("Mounting DMG...");
|
|
103
|
+
const mountOutput = run(`hdiutil attach "${dmgPath}" -nobrowse`);
|
|
104
|
+
const mountMatch = mountOutput.match(/\/Volumes\/.+/);
|
|
105
|
+
if (!mountMatch) {
|
|
106
|
+
throw new Error("Failed to detect mount point from hdiutil output.");
|
|
107
|
+
}
|
|
108
|
+
const mountPoint = mountMatch[0].trim();
|
|
109
|
+
const appSource = `${mountPoint}/${APP_NAME}.app`;
|
|
110
|
+
|
|
111
|
+
console.log("Stopping running instances...");
|
|
112
|
+
try { run(`pkill -f "${APP_NAME}"`); } catch {}
|
|
113
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
114
|
+
|
|
115
|
+
console.log(`Installing to ${INSTALL_PATH}...`);
|
|
116
|
+
try { run(`rm -rf "${INSTALL_PATH}"`); } catch {}
|
|
117
|
+
run(`cp -R "${appSource}" "${INSTALL_PATH}"`);
|
|
118
|
+
|
|
119
|
+
console.log("Clearing quarantine...");
|
|
120
|
+
run(`xattr -cr "${INSTALL_PATH}"`);
|
|
121
|
+
|
|
122
|
+
console.log("Unmounting DMG...");
|
|
123
|
+
try { run(`hdiutil detach "${mountPoint}" -quiet`); } catch {}
|
|
124
|
+
|
|
125
|
+
console.log("");
|
|
126
|
+
console.log(`Poke macOS Gate v${version} installed successfully.`);
|
|
127
|
+
console.log("");
|
|
128
|
+
console.log("Launching...");
|
|
129
|
+
try { run(`open "${INSTALL_PATH}"`); } catch {}
|
|
130
|
+
} finally {
|
|
131
|
+
try { unlinkSync(dmgPath); } catch {}
|
|
132
|
+
}
|
|
133
|
+
}
|
package/src/mcp-server.js
CHANGED
|
@@ -22,6 +22,15 @@ const runCommandLoopState = new Map();
|
|
|
22
22
|
|
|
23
23
|
const SAFE_TOOL_NAMES = new Set(["read_file", "read_image", "list_directory", "system_info", "network_speed"]);
|
|
24
24
|
|
|
25
|
+
const DESTRUCTIVE_COMMAND_PATTERNS = [
|
|
26
|
+
/\brm\b/i,
|
|
27
|
+
/\brmdir\b/i,
|
|
28
|
+
/\bunlink\b/i,
|
|
29
|
+
/\bmkfs\b/i,
|
|
30
|
+
/\bdiskutil\s+erase/i,
|
|
31
|
+
/>\s*\//,
|
|
32
|
+
];
|
|
33
|
+
|
|
25
34
|
const LIMITED_RUN_COMMANDS = new Set([
|
|
26
35
|
"curl", "yt-dlp", "youtube-dl",
|
|
27
36
|
"ls", "pwd", "cat", "grep", "find", "head", "tail", "wc", "sed", "awk",
|
|
@@ -270,6 +279,15 @@ function hasDangerousPattern(commandText) {
|
|
|
270
279
|
return DANGEROUS_COMMAND_PATTERNS.some((pattern) => pattern.test(commandText));
|
|
271
280
|
}
|
|
272
281
|
|
|
282
|
+
function isDestructiveInFullMode(name, cleanArgs) {
|
|
283
|
+
if (name === "write_file") return true;
|
|
284
|
+
if (name === "run_command") {
|
|
285
|
+
const cmd = typeof cleanArgs.command === "string" ? cleanArgs.command : "";
|
|
286
|
+
return DESTRUCTIVE_COMMAND_PATTERNS.some((p) => p.test(cmd));
|
|
287
|
+
}
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
|
|
273
291
|
function validateRunCommandAgainstAllowlist(commandText, allowlist) {
|
|
274
292
|
if (typeof commandText !== "string" || commandText.trim().length === 0) {
|
|
275
293
|
return "Command is empty.";
|
|
@@ -565,7 +583,11 @@ function handleToolCall(name, args, context = {}) {
|
|
|
565
583
|
return blocked;
|
|
566
584
|
}
|
|
567
585
|
|
|
568
|
-
|
|
586
|
+
const needsApproval = PERMISSION_MODE === "full"
|
|
587
|
+
? isDestructiveInFullMode(name, cleanArgs)
|
|
588
|
+
: permissionService.isRisky(name);
|
|
589
|
+
|
|
590
|
+
if (needsApproval) {
|
|
569
591
|
const commandText = typeof cleanArgs.command === "string" ? cleanArgs.command : "";
|
|
570
592
|
const alreadyAllowed = sessionAutoApproveAllRisky.has(sessionId) ||
|
|
571
593
|
(commandText && permissionService.isAllowedBySessionPattern(sessionId, commandText));
|
|
@@ -581,12 +603,15 @@ function handleToolCall(name, args, context = {}) {
|
|
|
581
603
|
return buildApprovalResponse(name, cleanArgs, approval);
|
|
582
604
|
}
|
|
583
605
|
|
|
584
|
-
if (
|
|
585
|
-
permissionService.allowPatternForSession(sessionId, commandText);
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
if (args.remember_all_risky === true) {
|
|
606
|
+
if (PERMISSION_MODE === "full") {
|
|
589
607
|
sessionAutoApproveAllRisky.add(sessionId);
|
|
608
|
+
} else {
|
|
609
|
+
if (name === "run_command" && args.remember_in_session === true && commandText) {
|
|
610
|
+
permissionService.allowPatternForSession(sessionId, commandText);
|
|
611
|
+
}
|
|
612
|
+
if (args.remember_all_risky === true) {
|
|
613
|
+
sessionAutoApproveAllRisky.add(sessionId);
|
|
614
|
+
}
|
|
590
615
|
}
|
|
591
616
|
}
|
|
592
617
|
}
|