@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 +4 -4
- package/deploy/ove.service +16 -0
- package/package.json +1 -1
- package/src/adapters/http.ts +28 -6
- package/src/handlers.ts +8 -0
- package/.dockerignore +0 -7
- package/.github/workflows/ci.yml +0 -16
- package/.github/workflows/pages.yml +0 -33
- package/.github/workflows/publish.yml +0 -45
- package/Dockerfile +0 -37
- package/bun.lock +0 -503
- package/bunfig.toml +0 -2
- package/docker-compose.yml +0 -15
- package/docs/examples.md +0 -247
- package/docs/favicon.ico +0 -0
- package/docs/index.html +0 -1012
- package/docs/logo.png +0 -0
- package/docs/plans/2026-02-21-codex-runner-design.md +0 -51
- package/docs/plans/2026-02-21-codex-runner-plan.md +0 -475
- package/docs/plans/2026-02-22-repo-autodiscovery-design.md +0 -98
- package/docs/plans/2026-02-22-repo-autodiscovery-plan.md +0 -826
- package/docs/plans/2026-02-23-conversation-repo-memory.md +0 -272
- package/docs/plans/2026-02-25-landing-page-harmonization-design.md +0 -89
- package/docs/plans/2026-02-25-landing-page-harmonization-plan.md +0 -604
- package/docs/screenshot-chat.png +0 -0
- package/docs/screenshot-telegram.png +0 -0
- package/docs/screenshot-trace.png +0 -0
- package/logo.png +0 -0
- package/public/logo.png +0 -0
- package/screenshot-chat.png +0 -0
- package/screenshot-telegram.png +0 -0
- package/screenshot-trace.png +0 -0
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
|
-

|
|
24
|
+

|
|
25
25
|
|
|
26
26
|
### Trace Viewer
|
|
27
27
|
|
|
28
|
-

|
|
28
|
+

|
|
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
package/src/adapters/http.ts
CHANGED
|
@@ -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 =
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
package/.github/workflows/ci.yml
DELETED
|
@@ -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"]
|