@lovenyberg/ove 0.6.0 → 0.7.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 CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src="logo.png" width="180" alt="Ove" />
2
+ <img src="https://raw.githubusercontent.com/jacksoncage/ove/main/logo.png" width="180" alt="Ove" />
3
3
  </p>
4
4
 
5
5
  <h1 align="center">Ove</h1>
@@ -16,16 +16,16 @@ Talk to Ove from Slack, WhatsApp, Telegram, Discord, GitHub issues, a Web UI, or
16
16
  **Just chat.** You don't need to memorize commands. Talk to Ove like you'd talk to a colleague — ask questions, describe what you need, paste error messages, think out loud. He understands natural language. The commands below are shortcuts, not requirements.
17
17
 
18
18
  <p align="center">
19
- <img src="screenshot-telegram.png" width="320" alt="Ove on Telegram" />
19
+ <img src="https://raw.githubusercontent.com/jacksoncage/ove/main/docs/images/screenshot-telegram.png" width="320" alt="Ove on Telegram" />
20
20
  </p>
21
21
 
22
22
  ### Web UI
23
23
 
24
- ![Ove Web UI](screenshot-chat.png)
24
+ ![Ove Web UI](https://raw.githubusercontent.com/jacksoncage/ove/main/docs/images/screenshot-chat.png)
25
25
 
26
26
  ### Trace Viewer
27
27
 
28
- ![Ove Trace Viewer](screenshot-trace.png)
28
+ ![Ove Trace Viewer](https://raw.githubusercontent.com/jacksoncage/ove/main/docs/images/screenshot-trace.png)
29
29
 
30
30
  ## Quick Start
31
31
 
@@ -0,0 +1,16 @@
1
+ [Unit]
2
+ Description=Ove - Personal AI coding assistant
3
+ After=network.target
4
+
5
+ [Service]
6
+ Type=simple
7
+ User=YOUR_USER
8
+ WorkingDirectory=/path/to/ove
9
+ ExecStart=/path/to/bun run src/index.ts
10
+ Restart=always
11
+ RestartSec=5
12
+ EnvironmentFile=/path/to/ove/.env
13
+ Environment=PATH=/home/YOUR_USER/.local/bin:/home/YOUR_USER/.bun/bin:/usr/local/bin:/usr/bin:/bin
14
+
15
+ [Install]
16
+ WantedBy=multi-user.target
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovenyberg/ove",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Your grumpy but meticulous dev companion. AI coding agent for Slack, WhatsApp, Telegram, Discord, GitHub, HTTP API, and CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,5 +1,6 @@
1
1
  import { readFileSync, existsSync } from "node:fs";
2
- import { join, extname } from "node:path";
2
+ import { join, extname, resolve } from "node:path";
3
+ import { timingSafeEqual } from "node:crypto";
3
4
  import type { EventAdapter, IncomingEvent, IncomingMessage, ChatAdapter, AdapterStatus } from "./types";
4
5
  import type { TraceStore } from "../trace";
5
6
  import type { TaskQueue } from "../queue";
@@ -13,6 +14,13 @@ interface PendingChat {
13
14
  sseControllers: ReadableStreamDefaultController[];
14
15
  }
15
16
 
17
+ function safeEqual(a: string, b: string): boolean {
18
+ const bufA = Buffer.from(a);
19
+ const bufB = Buffer.from(b);
20
+ if (bufA.length !== bufB.length) return false;
21
+ return timingSafeEqual(bufA, bufB);
22
+ }
23
+
16
24
  export class HttpApiAdapter implements EventAdapter {
17
25
  private port: number;
18
26
  private apiKey: string;
@@ -40,7 +48,7 @@ export class HttpApiAdapter implements EventAdapter {
40
48
  this.trace = trace;
41
49
  this.queue = queue || null;
42
50
  this.sessions = sessions || null;
43
- const publicDir = join(import.meta.dir, "../../public");
51
+ const publicDir = resolve(import.meta.dir, "../../public");
44
52
  this.publicDir = publicDir;
45
53
  try {
46
54
  this.webUiHtml = readFileSync(join(publicDir, "index.html"), "utf-8");
@@ -114,7 +122,7 @@ export class HttpApiAdapter implements EventAdapter {
114
122
  // Auth check for API routes
115
123
  if (path.startsWith("/api/")) {
116
124
  const key = req.headers.get("X-API-Key") || url.searchParams.get("key");
117
- if (key !== self.apiKey) {
125
+ if (!key || !safeEqual(key, self.apiKey)) {
118
126
  return Response.json({ error: "Unauthorized" }, { status: 401 });
119
127
  }
120
128
  }
@@ -159,9 +167,20 @@ export class HttpApiAdapter implements EventAdapter {
159
167
 
160
168
  // POST /api/message — submit a chat message (full chat pipeline)
161
169
  if (path === "/api/message" && req.method === "POST") {
162
- const body = await req.json() as { text: string; userId?: string };
170
+ let body: { text: string };
171
+ try {
172
+ body = await req.json() as { text: string };
173
+ } catch {
174
+ return Response.json({ error: "Invalid JSON body" }, { status: 400 });
175
+ }
176
+ if (!body || typeof body.text !== "string" || body.text.trim().length === 0) {
177
+ return Response.json({ error: "Missing or invalid 'text' field" }, { status: 400 });
178
+ }
179
+ if (body.text.length > 50000) {
180
+ return Response.json({ error: "Message too long (max 50000 chars)" }, { status: 400 });
181
+ }
163
182
  const chatId = crypto.randomUUID();
164
- const userId = body.userId || "http:web";
183
+ const userId = "http:web";
165
184
 
166
185
  const chat: PendingChat = { status: "pending", replies: [], sseControllers: [] };
167
186
  self.chats.set(chatId, chat);
@@ -290,7 +309,10 @@ export class HttpApiAdapter implements EventAdapter {
290
309
  const MIME: Record<string, string> = { ".png": "image/png", ".ico": "image/x-icon", ".svg": "image/svg+xml", ".jpg": "image/jpeg", ".css": "text/css", ".js": "application/javascript" };
291
310
  const ext = extname(path);
292
311
  if (ext && MIME[ext]) {
293
- const filePath = join(self.publicDir, path);
312
+ const filePath = resolve(self.publicDir, "." + path);
313
+ if (!filePath.startsWith(self.publicDir + "/")) {
314
+ return Response.json({ error: "Not found" }, { status: 404 });
315
+ }
294
316
  if (existsSync(filePath)) {
295
317
  const data = readFileSync(filePath);
296
318
  return new Response(data, { headers: { "Content-Type": MIME[ext], "Cache-Control": "public, max-age=3600" } });
package/src/handlers.ts CHANGED
@@ -254,6 +254,10 @@ async function handleCancelTask(msg: IncomingMessage, args: Record<string, any>,
254
254
  const active = deps.queue.listActive();
255
255
  const pendingMatch = active.find((t) => t.id.toLowerCase().startsWith(prefix) && t.status === "pending");
256
256
  if (pendingMatch) {
257
+ if (pendingMatch.userId !== msg.userId) {
258
+ await msg.reply("That's not your task.");
259
+ return;
260
+ }
257
261
  deps.queue.cancel(pendingMatch.id);
258
262
  await msg.reply(`Cancelled pending task ${pendingMatch.id.slice(0, 7)} on ${pendingMatch.repo}.`);
259
263
  return;
@@ -261,6 +265,10 @@ async function handleCancelTask(msg: IncomingMessage, args: Record<string, any>,
261
265
  await msg.reply(`No task found matching "${prefix}". Use /tasks to see what's running.`);
262
266
  return;
263
267
  }
268
+ if (match.task.userId !== msg.userId) {
269
+ await msg.reply("That's not your task.");
270
+ return;
271
+ }
264
272
  match.abort.abort();
265
273
  deps.queue.cancel(match.task.id);
266
274
  await msg.reply(`Killed task ${match.task.id.slice(0, 7)} on ${match.task.repo}. Gone.`);
package/.dockerignore DELETED
@@ -1,7 +0,0 @@
1
- node_modules/
2
- repos/
3
- .env
4
- ove.db
5
- .git/
6
- docs/
7
- *.md
@@ -1,16 +0,0 @@
1
- name: CI
2
-
3
- on:
4
- push:
5
- branches: [main]
6
- pull_request:
7
- branches: [main]
8
-
9
- jobs:
10
- test:
11
- runs-on: ubuntu-latest
12
- steps:
13
- - uses: actions/checkout@v4
14
- - uses: oven-sh/setup-bun@v2
15
- - run: bun install
16
- - run: bun test
@@ -1,33 +0,0 @@
1
- name: Deploy Pages
2
-
3
- on:
4
- push:
5
- branches: [main]
6
- paths: [docs/**]
7
-
8
- permissions:
9
- contents: read
10
- pages: write
11
- id-token: write
12
-
13
- concurrency:
14
- group: pages
15
- cancel-in-progress: true
16
-
17
- jobs:
18
- deploy:
19
- environment:
20
- name: github-pages
21
- url: ${{ steps.deployment.outputs.page_url }}
22
- runs-on: ubuntu-latest
23
- steps:
24
- - uses: actions/checkout@v4
25
-
26
- - uses: actions/configure-pages@v5
27
-
28
- - uses: actions/upload-pages-artifact@v3
29
- with:
30
- path: docs
31
-
32
- - id: deployment
33
- uses: actions/deploy-pages@v4
@@ -1,45 +0,0 @@
1
- name: Publish Package
2
-
3
- on:
4
- release:
5
- types: [published]
6
-
7
- permissions:
8
- contents: read
9
- packages: write
10
-
11
- jobs:
12
- test:
13
- runs-on: ubuntu-latest
14
- steps:
15
- - uses: actions/checkout@v4
16
- - uses: oven-sh/setup-bun@v2
17
- - run: bun install
18
- - run: bun test
19
-
20
- publish:
21
- needs: test
22
- runs-on: ubuntu-latest
23
- steps:
24
- - uses: actions/checkout@v4
25
-
26
- - uses: actions/setup-node@v4
27
- with:
28
- node-version: 20
29
- registry-url: https://registry.npmjs.org
30
-
31
- - uses: oven-sh/setup-bun@v2
32
-
33
- - run: bun install
34
-
35
- # Update version from release tag (safe: GITHUB_REF_NAME is not user-controlled)
36
- - name: Set version from tag
37
- env:
38
- REF_NAME: ${{ github.ref_name }}
39
- run: |
40
- VERSION="${REF_NAME#v}"
41
- npm version "$VERSION" --no-git-tag-version --allow-same-version
42
-
43
- - run: npm publish
44
- env:
45
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/Dockerfile DELETED
@@ -1,37 +0,0 @@
1
- FROM oven/bun:1 AS base
2
-
3
- # System deps: git for repo management, ssh for private repos
4
- RUN apt-get update && \
5
- apt-get install -y --no-install-recommends git openssh-client && \
6
- rm -rf /var/lib/apt/lists/*
7
-
8
- # Claude CLI (installed via npm since bun global install has quirks)
9
- RUN bunx --bun npm i -g @anthropic-ai/claude-code
10
-
11
- WORKDIR /app
12
-
13
- # Install dependencies
14
- COPY package.json bun.lock ./
15
- RUN bun install --frozen-lockfile --production
16
-
17
- # Copy source
18
- COPY bin/ bin/
19
- COPY src/ src/
20
- COPY tsconfig.json ./
21
- COPY config.example.json .env.example ./
22
-
23
- # Non-root user (Claude CLI refuses --dangerously-skip-permissions as root)
24
- # Default UID/GID 1000 matches most host users; override with --build-arg
25
- ARG UID=1000
26
- ARG GID=1000
27
- RUN groupadd -g $GID ove 2>/dev/null || true && \
28
- useradd -m -s /bin/bash -u $UID -g $GID ove 2>/dev/null || true && \
29
- mkdir -p repos && \
30
- chown -R $UID:$GID /app
31
- USER $UID
32
-
33
- # Git safe.directory for mounted volumes
34
- RUN git config --global --add safe.directory '*'
35
-
36
- ENTRYPOINT ["bun", "run", "bin/ove.ts"]
37
- CMD ["start"]