poke-gate 0.0.8 → 0.1.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.
@@ -0,0 +1,41 @@
1
+ name: Build and push Docker image
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ build-and-push:
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ contents: read
13
+ packages: write
14
+
15
+ steps:
16
+ - name: Checkout repo
17
+ uses: actions/checkout@v4
18
+
19
+ - name: Log in to GHCR
20
+ uses: docker/login-action@v3
21
+ with:
22
+ registry: ghcr.io
23
+ username: ${{ github.actor }}
24
+ password: ${{ secrets.GITHUB_TOKEN }}
25
+
26
+ - name: Get tag
27
+ id: vars
28
+ run: echo "tag=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
29
+
30
+ - name: Build and push
31
+ uses: docker/build-push-action@v5
32
+ with:
33
+ context: .
34
+ push: true
35
+ tags: |
36
+ ghcr.io/f/poke-gate:latest
37
+ ghcr.io/f/poke-gate:${{ steps.vars.outputs.tag }}
38
+ labels: |
39
+ org.opencontainers.image.source=https://github.com/f/poke-gate
40
+ org.opencontainers.image.description=Expose your machine to your Poke AI assistant via MCP tunnel
41
+ org.opencontainers.image.licenses=MIT
package/.prettierrc ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "printWidth": 100,
3
+ "singleQuote": true,
4
+ "trailingComma": "all",
5
+ "endOfLine": "auto",
6
+ "bracketSpacing": true
7
+ }
package/.remarkignore ADDED
@@ -0,0 +1,3 @@
1
+ node_modules/
2
+ dist/
3
+ coverage/
package/.remarkrc.mjs ADDED
@@ -0,0 +1,13 @@
1
+ import remarkParse from "remark-parse";
2
+ import remarkStringify from "remark-stringify";
3
+ import presetConsistent from "remark-preset-lint-consistent";
4
+ import presetRecommended from "remark-preset-lint-recommended";
5
+ import listItemIndent from "remark-lint-list-item-indent";
6
+
7
+ export default {
8
+ settings: {
9
+ bullet: "*",
10
+ listItemIndent: "one"
11
+ },
12
+ plugins: [remarkParse, remarkStringify, presetConsistent, presetRecommended, [listItemIndent, "one"]]
13
+ };
@@ -0,0 +1,38 @@
1
+ # Code of Conduct
2
+
3
+ ## Our Commitment
4
+
5
+ We are committed to a respectful, inclusive, and harassment-free community.
6
+
7
+ ## Expected Behavior
8
+
9
+ * Be respectful and constructive.
10
+ * Critique ideas, not people.
11
+ * Give and accept feedback professionally.
12
+ * Prioritize the health of the project and community.
13
+
14
+ ## Unacceptable Behavior
15
+
16
+ * Harassment, discrimination, threats, or personal attacks
17
+ * Insults, intimidation, or hostile behavior
18
+ * Publishing private information without consent
19
+ * Any conduct that creates an unsafe environment
20
+
21
+ ## Enforcement
22
+
23
+ Project maintainers are responsible for interpreting and enforcing this Code of Conduct. They may remove or reject comments, commits, issues, pull requests, or other contributions that violate these rules.
24
+
25
+ Possible actions include:
26
+
27
+ 1. Warning
28
+ 2. Temporary restriction
29
+ 3. Permanent ban
30
+
31
+ ## Reporting
32
+
33
+ Report unacceptable behavior to maintainers through:
34
+
35
+ * https://github.com/f/poke-gate/issues
36
+ * https://github.com/f/poke-gate/security/advisories/new
37
+
38
+ Reports will be reviewed in good faith and handled as privately as possible.
@@ -0,0 +1,32 @@
1
+ # Contributing
2
+
3
+ Thanks for contributing to `poke-gate`.
4
+
5
+ ## How To Contribute
6
+
7
+ 1. Fork the repository.
8
+ 2. Create a branch from `main`.
9
+ 3. Keep changes small and focused.
10
+ 4. Use clear commit messages.
11
+ 5. Open a pull request with a short technical summary.
12
+
13
+ ## Local Validation
14
+
15
+ Before opening a PR, run:
16
+
17
+ 1. `npm install`
18
+ 2. `npm run lint`
19
+ 3. `npm run lint:md`
20
+ 4. `npm run start`
21
+
22
+ ## Pull Request Notes
23
+
24
+ Include these in the PR description:
25
+
26
+ 1. What changed
27
+ 2. Why it changed
28
+ 3. How you validated it
29
+
30
+ ## Community Rules
31
+
32
+ Please read [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md) before contributing.
package/Dockerfile ADDED
@@ -0,0 +1,13 @@
1
+ FROM node:20-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY . .
6
+
7
+ RUN npm install -g .
8
+
9
+ LABEL org.opencontainers.image.source="https://github.com/f/poke-gate"
10
+ LABEL org.opencontainers.image.description="Expose your machine to your Poke AI assistant via MCP tunnel"
11
+ LABEL org.opencontainers.image.licenses="MIT"
12
+
13
+ ENTRYPOINT ["poke-gate"]
package/README.md CHANGED
@@ -135,6 +135,93 @@ npx poke-gate --verbose
135
135
 
136
136
  Config is stored at `~/.config/poke-gate/config.json`.
137
137
 
138
+ ## Agents
139
+
140
+ Agents are scheduled scripts that run automatically in the background. They live in `~/.config/poke-gate/agents/` and follow a simple naming convention:
141
+
142
+ ```
143
+ <name>.<interval>.js
144
+ ```
145
+
146
+ | File | Runs |
147
+ |------|------|
148
+ | `beeper.1h.js` | Every hour |
149
+ | `backup.2h.js` | Every 2 hours |
150
+ | `health.10m.js` | Every 10 minutes |
151
+ | `cleanup.30m.js` | Every 30 minutes |
152
+
153
+ Intervals: `Nm` (minutes) or `Nh` (hours). Minimum is 10 minutes.
154
+
155
+ ### Install an agent
156
+
157
+ Download a community agent from the repository:
158
+
159
+ ```bash
160
+ npx poke-gate agent get beeper
161
+ ```
162
+
163
+ This downloads `beeper.1h.js` and `.env.beeper` to `~/.config/poke-gate/agents/`. Edit the env file with your credentials and test it:
164
+
165
+ ```bash
166
+ nano ~/.config/poke-gate/agents/.env.beeper
167
+ npx poke-gate run-agent beeper
168
+ ```
169
+
170
+ ### Per-agent env files
171
+
172
+ Each agent can have a `.env.<name>` file for secrets:
173
+
174
+ ```
175
+ ~/.config/poke-gate/agents/.env.beeper
176
+ ```
177
+
178
+ ```env
179
+ BEEPER_TOKEN=your_token_here
180
+ ```
181
+
182
+ Variables are injected into the agent process automatically.
183
+
184
+ ### Agent frontmatter
185
+
186
+ Each agent file starts with a JSDoc-style frontmatter block:
187
+
188
+ ```javascript
189
+ /**
190
+ * @agent beeper
191
+ * @name Beeper Message Digest
192
+ * @description Fetches messages from the last hour and sends a summary to Poke.
193
+ * @interval 1h
194
+ * @env BEEPER_TOKEN - Beeper Desktop local API token
195
+ * @author f
196
+ */
197
+ ```
198
+
199
+ ### Creating your own agent
200
+
201
+ An agent is just a JS file that runs with Node.js. It has access to:
202
+
203
+ - `process.env` — variables from `.env.<name>`
204
+ - `poke` package — `import { Poke, getToken } from "poke"`
205
+ - Any npm package installed globally or via npx
206
+
207
+ ```javascript
208
+ /**
209
+ * @agent my-agent
210
+ * @name My Custom Agent
211
+ * @description Does something useful every 30 minutes.
212
+ * @interval 30m
213
+ */
214
+
215
+ import { Poke, getToken } from "poke";
216
+
217
+ const poke = new Poke({ apiKey: getToken() });
218
+ await poke.sendMessage("Hello from my agent!");
219
+ ```
220
+
221
+ Save as `~/.config/poke-gate/agents/my-agent.30m.js` and it runs automatically when poke-gate is connected.
222
+
223
+ Agents start running when poke-gate connects and run once immediately on startup.
224
+
138
225
  ## Security
139
226
 
140
227
  **Poke Gate grants full shell access to your Poke agent.** This means:
@@ -151,11 +238,16 @@ Only run Poke Gate on machines and networks you trust.
151
238
  clients/
152
239
  Poke macOS Gate/ macOS menu bar app (SwiftUI)
153
240
  bin/
154
- poke-gate.js CLI entry point + onboarding
241
+ poke-gate.js CLI entry point, run-agent + agent get subcommands
155
242
  src/
156
- app.js Startup: MCP server + tunnel
243
+ app.js Startup: MCP server + tunnel + agent scheduler
244
+ agents.js Agent discovery, scheduling, env loading, download
157
245
  mcp-server.js JSON-RPC MCP handler with OS tools
158
246
  tunnel.js PokeTunnel wrapper
247
+ examples/
248
+ agents/
249
+ beeper.1h.js Example: Beeper message digest agent
250
+ .env.beeper Example env file for beeper agent
159
251
  ```
160
252
 
161
253
  ## Credits
package/SECURITY.md ADDED
@@ -0,0 +1,18 @@
1
+ # Security Policy
2
+
3
+ ## Reporting a Vulnerability
4
+
5
+ If you discover a security vulnerability in this project, please report it privately through GitHub Security Advisories:
6
+
7
+ https://github.com/f/poke-gate/security/advisories/new
8
+
9
+ Please do not disclose vulnerabilities publicly until maintainers have had a chance to investigate and patch.
10
+
11
+ ## What to Include
12
+
13
+ 1. A clear description of the issue.
14
+ 2. Reproduction steps or a proof of concept.
15
+ 3. The expected impact.
16
+ 4. Any suggested mitigation (optional).
17
+
18
+ We will aim to acknowledge reports within 72 hours and provide updates as the investigation progresses.
package/bin/poke-gate.js CHANGED
@@ -1,101 +1,29 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
4
- import { join } from "node:path";
5
- import { homedir } from "node:os";
6
- import { createInterface } from "node:readline";
7
-
8
- const CONFIG_DIR = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
9
- const CONFIG_PATH = join(CONFIG_DIR, "poke-gate", "config.json");
10
-
11
- function loadConfig() {
12
- try {
13
- return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
14
- } catch {
15
- return {};
16
- }
17
- }
18
-
19
- function saveConfig(config) {
20
- mkdirSync(join(CONFIG_DIR, "poke-gate"), { recursive: true });
21
- writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
22
- }
23
-
24
- function loadPokeCredentials() {
25
- try {
26
- const creds = JSON.parse(readFileSync(join(CONFIG_DIR, "poke", "credentials.json"), "utf-8"));
27
- if (creds.token) return creds.token;
28
- } catch {}
29
- return null;
30
- }
31
-
32
- function resolveToken() {
33
- if (process.env.POKE_API_KEY) return process.env.POKE_API_KEY;
34
- const config = loadConfig();
35
- if (config.apiKey) return config.apiKey;
36
- const pokeCreds = loadPokeCredentials();
37
- if (pokeCreds) return pokeCreds;
38
- return null;
39
- }
40
-
41
- function ask(question) {
42
- const rl = createInterface({ input: process.stdin, output: process.stdout });
43
- return new Promise((resolve) => {
44
- rl.question(question, (answer) => {
45
- rl.close();
46
- resolve(answer.trim());
47
- });
48
- });
49
- }
50
-
51
- async function onboarding() {
52
- console.log();
53
- console.log(" poke-gate — expose your machine to Poke");
54
- console.log();
55
- console.log(" Your Poke agent will be able to run commands,");
56
- console.log(" read/write files, and access system info on this machine.");
57
- console.log();
58
- console.log(" ⚠ This grants full shell access. Only run on trusted networks.");
59
- console.log();
60
- console.log(" To get started, either:");
61
- console.log();
62
- console.log(" Option 1: Run 'npx poke login' (recommended)");
63
- console.log(" Option 2: Paste an API key from https://poke.com/kitchen/api-keys");
64
- console.log();
65
-
66
- const key = await ask(" API key (or press Enter if you ran poke login): ");
67
-
68
- if (!key) {
69
- const pokeCreds = loadPokeCredentials();
70
- if (pokeCreds) {
71
- console.log();
72
- console.log(" Found poke login credentials! Starting...");
73
- console.log();
74
- return pokeCreds;
75
- }
76
- console.log();
77
- console.log(" No credentials found. Run: npx poke login");
78
- console.log();
79
- process.exit(1);
80
- }
81
-
82
- saveConfig({ apiKey: key });
83
- console.log();
84
- console.log(" Saved! Starting poke-gate...");
85
- console.log();
86
-
87
- return key;
88
- }
3
+ const args = process.argv.slice(2);
89
4
 
90
5
  async function main() {
91
- let token = resolveToken();
92
-
93
- if (!token) {
94
- token = await onboarding();
6
+ if (args[0] === "run-agent") {
7
+ const name = args[1];
8
+ if (!name) {
9
+ console.error("Usage: poke-gate run-agent <name>");
10
+ console.error("Example: poke-gate run-agent beeper");
11
+ process.exit(1);
12
+ }
13
+ const { runAgent } = await import("../src/agents.js");
14
+ await runAgent(name);
15
+ } else if (args[0] === "agent" && args[1] === "get") {
16
+ const name = args[2];
17
+ if (!name) {
18
+ console.error("Usage: poke-gate agent get <name>");
19
+ console.error("Example: poke-gate agent get beeper");
20
+ process.exit(1);
21
+ }
22
+ const { downloadAgent } = await import("../src/agents.js");
23
+ await downloadAgent(name);
24
+ } else {
25
+ await import("../src/app.js");
95
26
  }
96
-
97
- process.env.POKE_API_KEY = token;
98
- await import("../src/app.js");
99
27
  }
100
28
 
101
29
  main();
@@ -0,0 +1,54 @@
1
+ import SwiftUI
2
+
3
+ struct AboutView: View {
4
+ @Environment(\.dismiss) private var dismiss
5
+
6
+ var body: some View {
7
+ VStack(spacing: 16) {
8
+ Image(nsImage: NSApp.applicationIconImage)
9
+ .resizable()
10
+ .frame(width: 80, height: 80)
11
+
12
+ Text("Poke Gate")
13
+ .font(.title2)
14
+ .fontWeight(.semibold)
15
+
16
+ Text("Version 0.0.8")
17
+ .font(.caption)
18
+ .foregroundStyle(.secondary)
19
+
20
+ Text("Let your Poke AI assistant access your machine.\nRun commands, read files, take screenshots — from anywhere.")
21
+ .font(.caption)
22
+ .foregroundStyle(.secondary)
23
+ .multilineTextAlignment(.center)
24
+ .fixedSize(horizontal: false, vertical: true)
25
+
26
+ Divider()
27
+
28
+ VStack(spacing: 4) {
29
+ Text("A community project — not affiliated with Poke")
30
+ .font(.caption2)
31
+ .foregroundStyle(.tertiary)
32
+
33
+ Text("or The Interaction Company of California.")
34
+ .font(.caption2)
35
+ .foregroundStyle(.tertiary)
36
+ }
37
+
38
+ HStack(spacing: 16) {
39
+ Link("GitHub", destination: URL(string: "https://github.com/f/poke-gate")!)
40
+ .font(.caption)
41
+
42
+ Link("poke.com", destination: URL(string: "https://poke.com")!)
43
+ .font(.caption)
44
+ }
45
+
46
+ Button("Close") {
47
+ dismiss()
48
+ }
49
+ .keyboardShortcut(.cancelAction)
50
+ }
51
+ .padding(24)
52
+ .frame(width: 300)
53
+ }
54
+ }
@@ -30,6 +30,16 @@ class GateService: ObservableObject {
30
30
  resolveToken() != nil
31
31
  }
32
32
 
33
+ func runPokeLogin() {
34
+ let fullPath = shellPath()
35
+ let proc = Process()
36
+ proc.executableURL = URL(fileURLWithPath: "/bin/zsh")
37
+ proc.arguments = ["-c", "npx -y poke@latest login"]
38
+ proc.environment = ["HOME": NSHomeDirectory(), "PATH": fullPath]
39
+ try? proc.run()
40
+ appendLog("Launched poke login — check your browser.")
41
+ }
42
+
33
43
  func autoStartIfNeeded() {
34
44
  guard !hasAutoStarted else { return }
35
45
  hasAutoStarted = true
@@ -6,42 +6,76 @@ struct LogsView: View {
6
6
  var body: some View {
7
7
  VStack(spacing: 0) {
8
8
  HStack {
9
- Text("Logs")
10
- .font(.headline)
9
+ Text("\(service.logs.count) entries")
10
+ .font(.caption)
11
+ .foregroundStyle(.tertiary)
12
+
11
13
  Spacer()
12
- Button("Clear") {
14
+
15
+ Button {
16
+ let text = service.logs.joined(separator: "\n")
17
+ NSPasteboard.general.clearContents()
18
+ NSPasteboard.general.setString(text, forType: .string)
19
+ } label: {
20
+ Image(systemName: "doc.on.doc")
21
+ .font(.caption)
22
+ }
23
+ .buttonStyle(.plain)
24
+ .foregroundStyle(.secondary)
25
+ .help("Copy all logs")
26
+
27
+ Button {
13
28
  service.logs.removeAll()
29
+ } label: {
30
+ Image(systemName: "trash")
31
+ .font(.caption)
14
32
  }
15
33
  .buttonStyle(.plain)
16
34
  .foregroundStyle(.secondary)
17
- .font(.caption)
35
+ .help("Clear logs")
18
36
  }
19
- .padding(.horizontal, 12)
37
+ .padding(.horizontal, 16)
20
38
  .padding(.vertical, 8)
21
39
 
22
40
  Divider()
23
41
 
24
42
  ScrollViewReader { proxy in
25
43
  ScrollView {
26
- LazyVStack(alignment: .leading, spacing: 2) {
44
+ LazyVStack(alignment: .leading, spacing: 1) {
27
45
  ForEach(Array(service.logs.enumerated()), id: \.offset) { index, line in
28
46
  Text(line)
29
47
  .font(.system(.caption, design: .monospaced))
30
- .foregroundStyle(.primary)
48
+ .foregroundStyle(lineColor(line))
31
49
  .textSelection(.enabled)
50
+ .fixedSize(horizontal: false, vertical: true)
51
+ .padding(.horizontal, 16)
52
+ .padding(.vertical, 2)
53
+ .frame(maxWidth: .infinity, alignment: .leading)
54
+ .background(index % 2 == 0 ? Color.clear : Color.primary.opacity(0.02))
32
55
  .id(index)
33
56
  }
34
57
  }
35
- .padding(.horizontal, 12)
36
- .padding(.vertical, 8)
58
+ .padding(.vertical, 4)
37
59
  }
38
60
  .onChange(of: service.logs.count) { _, _ in
39
61
  if let last = service.logs.indices.last {
40
- proxy.scrollTo(last, anchor: .bottom)
62
+ withAnimation(.easeOut(duration: 0.15)) {
63
+ proxy.scrollTo(last, anchor: .bottom)
64
+ }
41
65
  }
42
66
  }
43
67
  }
44
68
  }
45
- .frame(width: 480, height: 320)
69
+ .frame(minWidth: 480, minHeight: 300)
70
+ }
71
+
72
+ private func lineColor(_ line: String) -> Color {
73
+ if line.contains("error") || line.contains("Error") || line.contains("failed") {
74
+ return .red
75
+ }
76
+ if line.contains("tool:") || line.contains("$") {
77
+ return .primary
78
+ }
79
+ return .secondary
46
80
  }
47
81
  }