poke-gate 0.1.1 → 0.1.4

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,56 @@
1
+ name: Deploy Docs
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ paths:
7
+ - 'docs/**'
8
+
9
+ permissions:
10
+ contents: read
11
+ pages: write
12
+ id-token: write
13
+
14
+ concurrency:
15
+ group: pages
16
+ cancel-in-progress: false
17
+
18
+ jobs:
19
+ build:
20
+ runs-on: ubuntu-latest
21
+ steps:
22
+ - name: Checkout
23
+ uses: actions/checkout@v4
24
+ with:
25
+ fetch-depth: 0
26
+
27
+ - name: Setup Node
28
+ uses: actions/setup-node@v4
29
+ with:
30
+ node-version: 22
31
+ cache: npm
32
+ cache-dependency-path: docs/package-lock.json
33
+
34
+ - name: Install dependencies
35
+ run: npm ci
36
+ working-directory: docs
37
+
38
+ - name: Build
39
+ run: npm run build
40
+ working-directory: docs
41
+
42
+ - name: Upload artifact
43
+ uses: actions/upload-pages-artifact@v3
44
+ with:
45
+ path: docs/.vitepress/dist
46
+
47
+ deploy:
48
+ environment:
49
+ name: github-pages
50
+ url: ${{ steps.deployment.outputs.page_url }}
51
+ needs: build
52
+ runs-on: ubuntu-latest
53
+ steps:
54
+ - name: Deploy to GitHub Pages
55
+ id: deployment
56
+ uses: actions/deploy-pages@v4
package/README.md CHANGED
@@ -137,6 +137,10 @@ Config is stored at `~/.config/poke-gate/config.json`.
137
137
 
138
138
  ## Agents
139
139
 
140
+ <p align="center">
141
+ <img src="assets/screenshots/agents-editor.png" width="600" alt="Agents Editor">
142
+ </p>
143
+
140
144
  Agents are scheduled scripts that run automatically in the background. They live in `~/.config/poke-gate/agents/` and follow a simple naming convention:
141
145
 
142
146
  ```
@@ -238,16 +242,16 @@ Only run Poke Gate on machines and networks you trust.
238
242
  clients/
239
243
  Poke macOS Gate/ macOS menu bar app (SwiftUI)
240
244
  bin/
241
- poke-gate.js CLI entry point, run-agent subcommand
245
+ poke-gate.js CLI entry point, run-agent + agent get subcommands
242
246
  src/
243
247
  app.js Startup: MCP server + tunnel + agent scheduler
244
- agents.js Agent discovery, scheduling, and runner
248
+ agents.js Agent discovery, scheduling, env loading, download
245
249
  mcp-server.js JSON-RPC MCP handler with OS tools
246
250
  tunnel.js PokeTunnel wrapper
247
251
  examples/
248
252
  agents/
249
- beeper.1h.js Example: Beeper message digest
250
- .env.beeper Example env file
253
+ beeper.1h.js Example: Beeper message digest agent
254
+ .env.beeper Example env file for beeper agent
251
255
  ```
252
256
 
253
257
  ## Credits
@@ -0,0 +1,485 @@
1
+ import SwiftUI
2
+ import Combine
3
+
4
+ struct AgentFile: Identifiable, Hashable {
5
+ let id: String
6
+ var fileName: String
7
+ var name: String
8
+ var agentId: String
9
+ var description: String
10
+ var interval: String
11
+ var path: URL
12
+ var envPath: URL
13
+
14
+ var hasEnv: Bool {
15
+ FileManager.default.fileExists(atPath: envPath.path)
16
+ }
17
+
18
+ func hash(into hasher: inout Hasher) { hasher.combine(id) }
19
+ static func == (lhs: AgentFile, rhs: AgentFile) -> Bool { lhs.id == rhs.id }
20
+ }
21
+
22
+ @MainActor
23
+ class AgentsViewModel: ObservableObject {
24
+ @Published var agents: [AgentFile] = []
25
+ @Published var selectedAgent: AgentFile?
26
+ @Published var editorContent: String = ""
27
+ @Published var showingEnv: Bool = false
28
+
29
+ private var fileWatcher: DispatchSourceFileSystemObject?
30
+ private var dirFD: Int32 = -1
31
+
32
+ private var agentsDir: URL {
33
+ let configDir: URL
34
+ if let xdg = ProcessInfo.processInfo.environment["XDG_CONFIG_HOME"] {
35
+ configDir = URL(fileURLWithPath: xdg)
36
+ } else {
37
+ configDir = FileManager.default.homeDirectoryForCurrentUser
38
+ .appendingPathComponent(".config")
39
+ }
40
+ return configDir
41
+ .appendingPathComponent("poke-gate")
42
+ .appendingPathComponent("agents")
43
+ }
44
+
45
+ func startWatching() {
46
+ stopWatching()
47
+ let dir = agentsDir
48
+ try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
49
+
50
+ dirFD = open(dir.path, O_EVTONLY)
51
+ guard dirFD >= 0 else { return }
52
+
53
+ let source = DispatchSource.makeFileSystemObjectSource(
54
+ fileDescriptor: dirFD,
55
+ eventMask: [.write, .rename, .delete, .attrib],
56
+ queue: .main
57
+ )
58
+
59
+ source.setEventHandler { [weak self] in
60
+ self?.load()
61
+ }
62
+
63
+ source.setCancelHandler { [weak self] in
64
+ if let fd = self?.dirFD, fd >= 0 { close(fd) }
65
+ self?.dirFD = -1
66
+ }
67
+
68
+ source.resume()
69
+ fileWatcher = source
70
+ }
71
+
72
+ func stopWatching() {
73
+ fileWatcher?.cancel()
74
+ fileWatcher = nil
75
+ }
76
+
77
+ func load() {
78
+ let dir = agentsDir
79
+ try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
80
+
81
+ guard let files = try? FileManager.default.contentsOfDirectory(at: dir, includingPropertiesForKeys: nil)
82
+ .filter({ $0.pathExtension == "js" }) else { return }
83
+
84
+ agents = files.compactMap { url in
85
+ let fileName = url.lastPathComponent
86
+ let base = fileName.replacingOccurrences(of: ".js", with: "")
87
+ let parts = base.split(separator: ".")
88
+ guard parts.count >= 2 else { return nil }
89
+
90
+ let interval = String(parts.last!)
91
+ let agentId = parts.dropLast().joined(separator: ".")
92
+ let envPath = dir.appendingPathComponent(".env.\(agentId)")
93
+
94
+ let content = (try? String(contentsOf: url, encoding: .utf8)) ?? ""
95
+ let meta = parseFrontmatter(content)
96
+
97
+ return AgentFile(
98
+ id: fileName,
99
+ fileName: fileName,
100
+ name: meta["name"] ?? agentId,
101
+ agentId: agentId,
102
+ description: meta["description"] ?? "",
103
+ interval: interval,
104
+ path: url,
105
+ envPath: envPath
106
+ )
107
+ }.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending }
108
+ }
109
+
110
+ func select(_ agent: AgentFile) {
111
+ selectedAgent = agent
112
+ showingEnv = false
113
+ loadContent()
114
+ }
115
+
116
+ func loadContent() {
117
+ guard let agent = selectedAgent else { return }
118
+ let url = showingEnv ? agent.envPath : agent.path
119
+ editorContent = (try? String(contentsOf: url, encoding: .utf8)) ?? (showingEnv ? "# No .env file yet\n" : "")
120
+ }
121
+
122
+ func save() {
123
+ guard let agent = selectedAgent else { return }
124
+ let url = showingEnv ? agent.envPath : agent.path
125
+ try? editorContent.write(to: url, atomically: true, encoding: .utf8)
126
+ }
127
+
128
+ func changeInterval(_ agent: AgentFile, to newInterval: String) {
129
+ let newFileName = "\(agent.agentId).\(newInterval).js"
130
+ let newPath = agentsDir.appendingPathComponent(newFileName)
131
+ guard newPath != agent.path else { return }
132
+
133
+ try? FileManager.default.moveItem(at: agent.path, to: newPath)
134
+ load()
135
+
136
+ if let updated = agents.first(where: { $0.agentId == agent.agentId }) {
137
+ select(updated)
138
+ }
139
+ }
140
+
141
+ func addAgent() {
142
+ let template = """
143
+ /**
144
+ * @agent my-agent
145
+ * @name My Agent
146
+ * @description Describe what this agent does.
147
+ * @interval 1h
148
+ */
149
+
150
+ import { Poke, getToken } from "poke";
151
+
152
+ const poke = new Poke({ apiKey: getToken() });
153
+ await poke.sendMessage("Hello from my agent!");
154
+ """
155
+
156
+ var name = "my-agent"
157
+ var counter = 1
158
+ while agents.contains(where: { $0.agentId == name }) {
159
+ name = "my-agent-\(counter)"
160
+ counter += 1
161
+ }
162
+
163
+ let filePath = agentsDir.appendingPathComponent("\(name).1h.js")
164
+ try? template.write(to: filePath, atomically: true, encoding: .utf8)
165
+ load()
166
+
167
+ if let newAgent = agents.first(where: { $0.agentId == name }) {
168
+ select(newAgent)
169
+ }
170
+ }
171
+
172
+ func deleteAgent(_ agent: AgentFile) {
173
+ try? FileManager.default.removeItem(at: agent.path)
174
+ if agent.hasEnv {
175
+ try? FileManager.default.removeItem(at: agent.envPath)
176
+ }
177
+ if selectedAgent?.id == agent.id {
178
+ selectedAgent = nil
179
+ editorContent = ""
180
+ }
181
+ load()
182
+ }
183
+
184
+ private func parseFrontmatter(_ content: String) -> [String: String] {
185
+ guard let match = content.range(of: #"/\*\*[\s\S]*?\*/"#, options: .regularExpression) else { return [:] }
186
+ let block = String(content[match])
187
+ var meta: [String: String] = [:]
188
+ for line in block.split(separator: "\n") {
189
+ let s = String(line)
190
+ if let tagMatch = s.range(of: #"@(\w+)\s+(.*)"#, options: .regularExpression) {
191
+ let tagContent = String(s[tagMatch])
192
+ let parts = tagContent.dropFirst(1).split(separator: " ", maxSplits: 1)
193
+ if parts.count == 2 {
194
+ meta[String(parts[0])] = parts[1].trimmingCharacters(in: .whitespaces).replacingOccurrences(of: "*/", with: "").trimmingCharacters(in: .whitespaces)
195
+ }
196
+ }
197
+ }
198
+ return meta
199
+ }
200
+ }
201
+
202
+ struct AgentsView: View {
203
+ @StateObject private var viewModel = AgentsViewModel()
204
+
205
+ var body: some View {
206
+ NavigationSplitView {
207
+ List(viewModel.agents, selection: Binding(
208
+ get: { viewModel.selectedAgent },
209
+ set: { if let a = $0 { viewModel.select(a) } }
210
+ )) { agent in
211
+ VStack(alignment: .leading, spacing: 2) {
212
+ HStack {
213
+ Text(agent.name)
214
+ .font(.subheadline)
215
+ .fontWeight(.medium)
216
+
217
+ Spacer()
218
+
219
+ Text(agent.interval)
220
+ .font(.caption2)
221
+ .fontWeight(.medium)
222
+ .padding(.horizontal, 6)
223
+ .padding(.vertical, 2)
224
+ .background(.quaternary)
225
+ .cornerRadius(4)
226
+ }
227
+
228
+ if !agent.description.isEmpty {
229
+ Text(agent.description)
230
+ .font(.caption)
231
+ .foregroundStyle(.secondary)
232
+ .lineLimit(2)
233
+ }
234
+ }
235
+ .padding(.vertical, 2)
236
+ .tag(agent)
237
+ .contextMenu {
238
+ Button("Delete", role: .destructive) {
239
+ viewModel.deleteAgent(agent)
240
+ }
241
+ }
242
+ }
243
+ .listStyle(.sidebar)
244
+ .navigationSplitViewColumnWidth(min: 180, ideal: 220)
245
+ .safeAreaInset(edge: .bottom) {
246
+ Button {
247
+ viewModel.addAgent()
248
+ } label: {
249
+ Label("New Agent", systemImage: "plus")
250
+ .font(.caption)
251
+ }
252
+ .buttonStyle(.plain)
253
+ .padding(8)
254
+ .frame(maxWidth: .infinity, alignment: .leading)
255
+ }
256
+ } detail: {
257
+ if let agent = viewModel.selectedAgent {
258
+ AgentDetailView(viewModel: viewModel, agent: agent)
259
+ } else {
260
+ Text("Select an agent")
261
+ .foregroundStyle(.secondary)
262
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
263
+ }
264
+ }
265
+ .onAppear {
266
+ viewModel.load()
267
+ viewModel.startWatching()
268
+ }
269
+ .onDisappear {
270
+ viewModel.stopWatching()
271
+ }
272
+ }
273
+ }
274
+
275
+ struct AgentDetailView: View {
276
+ @ObservedObject var viewModel: AgentsViewModel
277
+ let agent: AgentFile
278
+ @State private var intervalInput: String = ""
279
+
280
+ var body: some View {
281
+ VStack(spacing: 0) {
282
+ HStack {
283
+ Text(agent.fileName)
284
+ .font(.system(.caption, design: .monospaced))
285
+ .foregroundStyle(.secondary)
286
+
287
+ Spacer()
288
+
289
+ HStack(spacing: 4) {
290
+ Text("every")
291
+ .font(.caption)
292
+ .foregroundStyle(.secondary)
293
+
294
+ TextField("1h", text: $intervalInput)
295
+ .font(.system(.caption, design: .monospaced))
296
+ .textFieldStyle(.roundedBorder)
297
+ .frame(width: 50)
298
+ .onSubmit {
299
+ if !intervalInput.isEmpty && intervalInput != agent.interval {
300
+ viewModel.changeInterval(agent, to: intervalInput)
301
+ }
302
+ }
303
+ }
304
+ }
305
+ .padding(.horizontal, 12)
306
+ .padding(.vertical, 8)
307
+
308
+ Divider()
309
+
310
+ HStack(spacing: 0) {
311
+ Button {
312
+ viewModel.showingEnv = false
313
+ viewModel.loadContent()
314
+ } label: {
315
+ Text(agent.fileName)
316
+ .font(.caption)
317
+ .padding(.horizontal, 12)
318
+ .padding(.vertical, 6)
319
+ .background(viewModel.showingEnv ? Color.clear : Color.accentColor.opacity(0.15))
320
+ }
321
+ .buttonStyle(.plain)
322
+
323
+ Button {
324
+ viewModel.showingEnv = true
325
+ viewModel.loadContent()
326
+ } label: {
327
+ Text(".env.\(agent.agentId)")
328
+ .font(.caption)
329
+ .padding(.horizontal, 12)
330
+ .padding(.vertical, 6)
331
+ .background(viewModel.showingEnv ? Color.accentColor.opacity(0.15) : Color.clear)
332
+ }
333
+ .buttonStyle(.plain)
334
+
335
+ Spacer()
336
+
337
+ Button {
338
+ viewModel.save()
339
+ } label: {
340
+ Label("Save", systemImage: "square.and.arrow.down")
341
+ .font(.caption)
342
+ }
343
+ .buttonStyle(.plain)
344
+ .padding(.horizontal, 12)
345
+ .keyboardShortcut("s")
346
+ }
347
+ .padding(.vertical, 2)
348
+ .background(.quaternary.opacity(0.3))
349
+
350
+ Divider()
351
+
352
+ HighlightedCodeEditor(
353
+ text: $viewModel.editorContent,
354
+ language: viewModel.showingEnv ? "env" : "javascript"
355
+ )
356
+ }
357
+ .onAppear {
358
+ intervalInput = agent.interval
359
+ }
360
+ .onChange(of: agent.id) { _, _ in
361
+ intervalInput = agent.interval
362
+ }
363
+ }
364
+ }
365
+
366
+ // MARK: - Syntax Highlighting Editor
367
+
368
+ struct HighlightedCodeEditor: NSViewRepresentable {
369
+ @Binding var text: String
370
+ var language: String
371
+
372
+ func makeCoordinator() -> Coordinator { Coordinator(self) }
373
+
374
+ func makeNSView(context: Context) -> NSScrollView {
375
+ let scrollView = NSTextView.scrollableTextView()
376
+ let textView = scrollView.documentView as! NSTextView
377
+
378
+ textView.isEditable = true
379
+ textView.isSelectable = true
380
+ textView.allowsUndo = true
381
+ textView.isRichText = false
382
+ textView.font = NSFont.monospacedSystemFont(ofSize: 12, weight: .regular)
383
+ textView.backgroundColor = .textBackgroundColor
384
+ textView.isAutomaticQuoteSubstitutionEnabled = false
385
+ textView.isAutomaticDashSubstitutionEnabled = false
386
+ textView.isAutomaticTextReplacementEnabled = false
387
+ textView.isAutomaticSpellingCorrectionEnabled = false
388
+ textView.delegate = context.coordinator
389
+ textView.textContainerInset = NSSize(width: 8, height: 8)
390
+
391
+ return scrollView
392
+ }
393
+
394
+ func updateNSView(_ nsView: NSScrollView, context: Context) {
395
+ let textView = nsView.documentView as! NSTextView
396
+ if textView.string != text {
397
+ let selectedRanges = textView.selectedRanges
398
+ textView.string = text
399
+ SyntaxHighlighter.highlight(textView: textView, language: language)
400
+ textView.selectedRanges = selectedRanges
401
+ }
402
+ }
403
+
404
+ class Coordinator: NSObject, NSTextViewDelegate {
405
+ var parent: HighlightedCodeEditor
406
+ init(_ parent: HighlightedCodeEditor) { self.parent = parent }
407
+
408
+ func textDidChange(_ notification: Notification) {
409
+ guard let textView = notification.object as? NSTextView else { return }
410
+ parent.text = textView.string
411
+ SyntaxHighlighter.highlight(textView: textView, language: parent.language)
412
+ }
413
+ }
414
+ }
415
+
416
+ enum SyntaxHighlighter {
417
+ static func highlight(textView: NSTextView, language: String) {
418
+ let storage = textView.textStorage!
419
+ let source = storage.string
420
+ let fullRange = NSRange(location: 0, length: (source as NSString).length)
421
+ let font = NSFont.monospacedSystemFont(ofSize: 12, weight: .regular)
422
+
423
+ storage.beginEditing()
424
+ storage.addAttribute(.font, value: font, range: fullRange)
425
+ storage.addAttribute(.foregroundColor, value: NSColor.labelColor, range: fullRange)
426
+
427
+ if language == "env" {
428
+ highlightEnv(storage: storage, source: source)
429
+ } else {
430
+ highlightJS(storage: storage, source: source)
431
+ }
432
+
433
+ storage.endEditing()
434
+ }
435
+
436
+ private static func apply(_ storage: NSTextStorage, pattern: String, color: NSColor, source: String, options: NSRegularExpression.Options = []) {
437
+ guard let regex = try? NSRegularExpression(pattern: pattern, options: options) else { return }
438
+ let fullRange = NSRange(location: 0, length: (source as NSString).length)
439
+ for match in regex.matches(in: source, range: fullRange) {
440
+ storage.addAttribute(.foregroundColor, value: color, range: match.range)
441
+ }
442
+ }
443
+
444
+ private static func highlightJS(storage: NSTextStorage, source: String) {
445
+ let keyword = NSColor.systemPink
446
+ let string = NSColor.systemGreen
447
+ let comment = NSColor.systemGray
448
+ let number = NSColor.systemOrange
449
+ let tag = NSColor.systemCyan
450
+ let builtIn = NSColor.systemPurple
451
+
452
+ // Comments (block and line)
453
+ apply(storage, pattern: #"/\*[\s\S]*?\*/"#, color: comment, source: source, options: .dotMatchesLineSeparators)
454
+ apply(storage, pattern: #"//.*$"#, color: comment, source: source, options: .anchorsMatchLines)
455
+
456
+ // Strings
457
+ apply(storage, pattern: #"\"(?:[^\"\\]|\\.)*\""#, color: string, source: source)
458
+ apply(storage, pattern: #"'(?:[^'\\]|\\.)*'"#, color: string, source: source)
459
+ apply(storage, pattern: #"`(?:[^`\\]|\\.)*`"#, color: string, source: source)
460
+
461
+ // Numbers
462
+ apply(storage, pattern: #"\b\d+\.?\d*\b"#, color: number, source: source)
463
+
464
+ // Keywords
465
+ apply(storage, pattern: #"\b(import|export|from|const|let|var|function|async|await|return|if|else|for|while|do|switch|case|break|continue|new|class|try|catch|throw|finally|default|typeof|instanceof|in|of|void|null|undefined|true|false|this|super)\b"#, color: keyword, source: source)
466
+
467
+ // Built-ins
468
+ apply(storage, pattern: #"\b(console|process|require|module|exports|Promise|Array|Object|String|Number|JSON|Math|Date|Error|Map|Set|Buffer|URL|fetch|setTimeout|setInterval)\b"#, color: builtIn, source: source)
469
+
470
+ // JSDoc tags
471
+ apply(storage, pattern: #"@\w+"#, color: tag, source: source)
472
+ }
473
+
474
+ private static func highlightEnv(storage: NSTextStorage, source: String) {
475
+ // Comments
476
+ apply(storage, pattern: #"^\s*#.*$"#, color: .systemGray, source: source, options: .anchorsMatchLines)
477
+
478
+ // Keys
479
+ apply(storage, pattern: #"^[A-Z_][A-Z0-9_]*(?==)"#, color: .systemCyan, source: source, options: .anchorsMatchLines)
480
+
481
+ // Values (after =)
482
+ apply(storage, pattern: #"(?<==).+$"#, color: .systemGreen, source: source, options: .anchorsMatchLines)
483
+ }
484
+ }
485
+
@@ -23,6 +23,11 @@ struct Poke_macOS_GateApp: App {
23
23
  }
24
24
  .windowResizability(.contentSize)
25
25
 
26
+ Window("Agents", id: "agents") {
27
+ AgentsView()
28
+ }
29
+ .defaultSize(width: 700, height: 480)
30
+
26
31
  Window("About", id: "about") {
27
32
  AboutView()
28
33
  }
@@ -110,6 +115,11 @@ struct PopoverContent: View {
110
115
  openWindow(id: "logs")
111
116
  }
112
117
 
118
+ ActionButton(icon: "bolt.fill", label: "Agents") {
119
+ NSApp.activate(ignoringOtherApps: true)
120
+ openWindow(id: "agents")
121
+ }
122
+
113
123
  ActionButton(icon: "gearshape", label: "Settings") {
114
124
  NSApp.activate(ignoringOtherApps: true)
115
125
  openWindow(id: "settings")
@@ -264,7 +264,7 @@
264
264
  "$(inherited)",
265
265
  "@executable_path/../Frameworks",
266
266
  );
267
- MARKETING_VERSION = 0.1.0;
267
+ MARKETING_VERSION = 0.1.3;
268
268
  PRODUCT_BUNDLE_IDENTIFIER = "dev.fka.Poke-macOS-Gate";
269
269
  PRODUCT_NAME = "$(TARGET_NAME)";
270
270
  REGISTER_APP_GROUPS = YES;
@@ -296,7 +296,7 @@
296
296
  "$(inherited)",
297
297
  "@executable_path/../Frameworks",
298
298
  );
299
- MARKETING_VERSION = 0.1.0;
299
+ MARKETING_VERSION = 0.1.3;
300
300
  PRODUCT_BUNDLE_IDENTIFIER = "dev.fka.Poke-macOS-Gate";
301
301
  PRODUCT_NAME = "$(TARGET_NAME)";
302
302
  REGISTER_APP_GROUPS = YES;
@@ -0,0 +1,75 @@
1
+ import { defineConfig } from 'vitepress'
2
+ import { withMermaid } from 'vitepress-plugin-mermaid'
3
+
4
+ export default withMermaid(
5
+ defineConfig({
6
+ title: 'Poke Gate',
7
+ description: 'Let your Poke AI assistant access your machine',
8
+ head: [
9
+ ['link', { rel: 'icon', href: '/logo.png' }],
10
+ ],
11
+ themeConfig: {
12
+ logo: '/logo.png',
13
+ nav: [
14
+ { text: 'Guide', link: '/getting-started' },
15
+ { text: 'Agents', link: '/agents/' },
16
+ { text: 'CLI', link: '/cli' },
17
+ {
18
+ text: 'Download',
19
+ items: [
20
+ { text: 'macOS App', link: 'https://github.com/f/poke-gate/releases/latest' },
21
+ { text: 'Homebrew', link: '/getting-started#homebrew' },
22
+ { text: 'npm', link: 'https://www.npmjs.com/package/poke-gate' },
23
+ ]
24
+ }
25
+ ],
26
+ sidebar: [
27
+ {
28
+ text: 'Guide',
29
+ items: [
30
+ { text: 'Getting Started', link: '/getting-started' },
31
+ { text: 'How It Works', link: '/how-it-works' },
32
+ { text: 'Tools', link: '/tools' },
33
+ ]
34
+ },
35
+ {
36
+ text: 'Agents',
37
+ items: [
38
+ { text: 'Overview', link: '/agents/' },
39
+ { text: 'Creating Agents', link: '/agents/creating' },
40
+ { text: 'Installing Agents', link: '/agents/installing' },
41
+ { text: 'Community Agents', link: '/agents/community' },
42
+ { text: 'Beeper Example', link: '/agents/beeper' },
43
+ { text: 'Sharing Agents', link: '/agents/sharing' },
44
+ ]
45
+ },
46
+ {
47
+ text: 'Reference',
48
+ items: [
49
+ { text: 'macOS App', link: '/macos-app' },
50
+ { text: 'CLI Reference', link: '/cli' },
51
+ { text: 'Security', link: '/security' },
52
+ ]
53
+ }
54
+ ],
55
+ socialLinks: [
56
+ { icon: 'github', link: 'https://github.com/f/poke-gate' },
57
+ { icon: 'npm', link: 'https://www.npmjs.com/package/poke-gate' },
58
+ ],
59
+ footer: {
60
+ message: 'Community project — not affiliated with Poke or The Interaction Company.',
61
+ copyright: 'Released under the MIT License.',
62
+ },
63
+ editLink: {
64
+ pattern: 'https://github.com/f/poke-gate/edit/main/docs/:path',
65
+ text: 'Edit this page on GitHub',
66
+ },
67
+ },
68
+ mermaid: {
69
+ theme: 'neutral',
70
+ themeVariables: {
71
+ fontSize: '13px',
72
+ },
73
+ },
74
+ })
75
+ )