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.
- package/.github/workflows/docker.yml +41 -0
- package/.prettierrc +7 -0
- package/.remarkignore +3 -0
- package/.remarkrc.mjs +13 -0
- package/CODE_OF_CONDUCT.md +38 -0
- package/CONTRIBUTING.md +32 -0
- package/Dockerfile +13 -0
- package/README.md +94 -2
- package/SECURITY.md +18 -0
- package/bin/poke-gate.js +21 -93
- package/clients/Poke macOS Gate/Poke macOS Gate/AboutView.swift +54 -0
- package/clients/Poke macOS Gate/Poke macOS Gate/GateService.swift +10 -0
- package/clients/Poke macOS Gate/Poke macOS Gate/LogsView.swift +45 -11
- package/clients/Poke macOS Gate/Poke macOS Gate/Poke_macOS_GateApp.swift +143 -75
- package/clients/Poke macOS Gate/Poke macOS Gate/SettingsView.swift +76 -71
- package/clients/Poke macOS Gate/Poke macOS Gate.xcodeproj/project.pbxproj +2 -2
- package/clients/Poke macOS Gate/Poke macOS Gate.xcodeproj/project.xcworkspace/xcuserdata/fka.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- package/eslint.config.mjs +23 -0
- package/examples/agents/.env.beeper +6 -0
- package/examples/agents/beeper.1h.js +109 -0
- package/package.json +19 -2
- package/src/agents.js +257 -0
- package/src/app.js +24 -34
- package/src/mcp-server.js +3 -3
- package/src/tunnel.js +9 -5
|
@@ -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
package/.remarkignore
ADDED
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.
|
package/CONTRIBUTING.md
ADDED
|
@@ -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 +
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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("
|
|
10
|
-
.font(.
|
|
9
|
+
Text("\(service.logs.count) entries")
|
|
10
|
+
.font(.caption)
|
|
11
|
+
.foregroundStyle(.tertiary)
|
|
12
|
+
|
|
11
13
|
Spacer()
|
|
12
|
-
|
|
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
|
-
.
|
|
35
|
+
.help("Clear logs")
|
|
18
36
|
}
|
|
19
|
-
.padding(.horizontal,
|
|
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:
|
|
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(
|
|
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(.
|
|
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
|
-
|
|
62
|
+
withAnimation(.easeOut(duration: 0.15)) {
|
|
63
|
+
proxy.scrollTo(last, anchor: .bottom)
|
|
64
|
+
}
|
|
41
65
|
}
|
|
42
66
|
}
|
|
43
67
|
}
|
|
44
68
|
}
|
|
45
|
-
.frame(
|
|
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
|
}
|