@lovenyberg/ove 0.4.0 → 0.5.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovenyberg/ove",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
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": {
@@ -31,9 +31,12 @@ export class HttpApiAdapter implements EventAdapter {
31
31
  private eventAdapters: EventAdapter[] = [];
32
32
  private startedAt?: string;
33
33
 
34
- constructor(port: number, apiKey: string, trace: TraceStore, queue?: TaskQueue, sessions?: SessionStore) {
34
+ private hostname: string;
35
+
36
+ constructor(port: number, apiKey: string, trace: TraceStore, queue?: TaskQueue, sessions?: SessionStore, hostname?: string) {
35
37
  this.port = port;
36
38
  this.apiKey = apiKey;
39
+ this.hostname = hostname || "0.0.0.0";
37
40
  this.trace = trace;
38
41
  this.queue = queue || null;
39
42
  this.sessions = sessions || null;
@@ -83,6 +86,7 @@ export class HttpApiAdapter implements EventAdapter {
83
86
 
84
87
  this.server = Bun.serve({
85
88
  port: this.port,
89
+ hostname: this.hostname,
86
90
  idleTimeout: 255, // SSE connections need to stay open for long-running tasks
87
91
  async fetch(req) {
88
92
  const url = new URL(req.url);
package/src/index.ts CHANGED
@@ -140,7 +140,8 @@ if (process.env.HTTP_API_PORT) {
140
140
  process.env.HTTP_API_KEY || crypto.randomUUID(),
141
141
  trace,
142
142
  queue,
143
- sessions
143
+ sessions,
144
+ process.env.HTTP_API_HOST
144
145
  );
145
146
  eventAdapters.push(httpAdapter);
146
147
  }
package/src/setup.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { execFileSync } from "node:child_process";
3
+ import { resolve } from "node:path";
4
+ import { userInfo } from "node:os";
2
5
  import { createInterface } from "node:readline/promises";
3
6
  import type { Config } from "./config";
4
7
 
@@ -206,6 +209,8 @@ export async function runSetup(opts?: { fixOnly?: string[] }): Promise<void> {
206
209
  process.stdout.write("\n");
207
210
  const port = (await ask(rl, "HTTP API port [3000]")) || "3000";
208
211
  envValues.HTTP_API_PORT = port;
212
+ const host = await ask(rl, "Bind address [127.0.0.1] (0.0.0.0 for all interfaces)");
213
+ envValues.HTTP_API_HOST = host || "127.0.0.1";
209
214
  const key = await ask(rl, "API key (leave empty to generate)");
210
215
  envValues.HTTP_API_KEY = key || crypto.randomUUID();
211
216
  }
@@ -222,6 +227,15 @@ export async function runSetup(opts?: { fixOnly?: string[] }): Promise<void> {
222
227
  envValues.CLI_MODE = "true";
223
228
  }
224
229
 
230
+ // Tracing
231
+ if (!fixing) {
232
+ process.stdout.write("\n");
233
+ const enableTrace = await ask(rl, "Enable task tracing? (y/n)");
234
+ if (enableTrace.toLowerCase() === "y") {
235
+ envValues.OVE_TRACE = "true";
236
+ }
237
+ }
238
+
225
239
  // Collect repos
226
240
  const repos: Record<string, { url: string; defaultBranch: string }> = existingConfig.repos
227
241
  ? { ...existingConfig.repos }
@@ -322,6 +336,7 @@ export async function runSetup(opts?: { fixOnly?: string[] }): Promise<void> {
322
336
  if (has("HTTP API")) {
323
337
  envLines.push("# HTTP API + Web UI");
324
338
  envLines.push(`HTTP_API_PORT=${envValues.HTTP_API_PORT || "3000"}`);
339
+ envLines.push(`HTTP_API_HOST=${envValues.HTTP_API_HOST || "127.0.0.1"}`);
325
340
  envLines.push(`HTTP_API_KEY=${envValues.HTTP_API_KEY || ""}`);
326
341
  envLines.push("");
327
342
  }
@@ -341,6 +356,13 @@ export async function runSetup(opts?: { fixOnly?: string[] }): Promise<void> {
341
356
  envLines.push("");
342
357
  }
343
358
 
359
+ // Tracing
360
+ if (envValues.OVE_TRACE) {
361
+ envLines.push("# Tracing");
362
+ envLines.push(`OVE_TRACE=${envValues.OVE_TRACE}`);
363
+ envLines.push("");
364
+ }
365
+
344
366
  // Always include repos dir
345
367
  envLines.push("# Repos directory");
346
368
  envLines.push(`REPOS_DIR=${envValues.REPOS_DIR || "./repos"}`);
@@ -363,8 +385,39 @@ export async function runSetup(opts?: { fixOnly?: string[] }): Promise<void> {
363
385
  process.stdout.write(" Wrote config.json\n");
364
386
  }
365
387
 
388
+ // Systemd service setup
389
+ let installedSystemd = false;
366
390
  if (!fixing) {
367
- process.stdout.write("\n Nåväl. Run 'ove start' when you're ready.\n\n");
391
+ const installService = await ask(rl, "Install as systemd service? (y/n)");
392
+ if (installService.toLowerCase() === "y") {
393
+ installedSystemd = await installSystemdService(rl);
394
+ }
395
+ }
396
+
397
+ if (!fixing) {
398
+ process.stdout.write("\n Nåväl.\n");
399
+ if (installedSystemd) {
400
+ process.stdout.write("\n Useful commands:\n");
401
+ process.stdout.write(" sudo systemctl status ove # check status\n");
402
+ process.stdout.write(" sudo journalctl -u ove -f # follow logs\n");
403
+ process.stdout.write(" sudo systemctl restart ove # restart\n");
404
+ process.stdout.write(" sudo systemctl stop ove # stop\n");
405
+ if (has("HTTP API")) {
406
+ const port = envValues.HTTP_API_PORT || "3000";
407
+ const host = envValues.HTTP_API_HOST || "127.0.0.1";
408
+ const displayHost = host === "0.0.0.0" ? "<your-ip>" : host;
409
+ process.stdout.write(`\n Web UI: http://${displayHost}:${port}\n`);
410
+ if (envValues.OVE_TRACE === "true") {
411
+ process.stdout.write(` Traces: http://${displayHost}:${port}/trace\n`);
412
+ }
413
+ if (host === "0.0.0.0") {
414
+ process.stdout.write(" (bound to all interfaces)\n");
415
+ }
416
+ }
417
+ process.stdout.write("\n");
418
+ } else {
419
+ process.stdout.write(" Run 'ove start' when you're ready.\n\n");
420
+ }
368
421
  } else {
369
422
  process.stdout.write("\n");
370
423
  }
@@ -372,3 +425,65 @@ export async function runSetup(opts?: { fixOnly?: string[] }): Promise<void> {
372
425
  rl.close();
373
426
  }
374
427
  }
428
+
429
+ async function installSystemdService(rl: ReturnType<typeof createInterface>): Promise<boolean> {
430
+ const detectedUser = userInfo().username;
431
+ const detectedDir = resolve(".");
432
+ let detectedBun = "";
433
+ try {
434
+ detectedBun = execFileSync("which", ["bun"]).toString().trim();
435
+ } catch {
436
+ // bun not in PATH
437
+ }
438
+
439
+ const user = (await ask(rl, `User [${detectedUser}]`)) || detectedUser;
440
+ const workDir = (await ask(rl, `Working directory [${detectedDir}]`)) || detectedDir;
441
+ const bunPath = (await ask(rl, `Bun path [${detectedBun}]`)) || detectedBun;
442
+
443
+ if (!bunPath) {
444
+ process.stdout.write(" Could not find bun. Skipping service install.\n");
445
+ return false;
446
+ }
447
+
448
+ const envPath = resolve(workDir, ".env");
449
+ const service = `[Unit]
450
+ Description=Ove - Personal AI coding assistant
451
+ After=network.target
452
+
453
+ [Service]
454
+ Type=simple
455
+ User=${user}
456
+ WorkingDirectory=${workDir}
457
+ ExecStart=${bunPath} run src/index.ts
458
+ Restart=always
459
+ RestartSec=5
460
+ EnvironmentFile=${envPath}
461
+
462
+ [Install]
463
+ WantedBy=multi-user.target
464
+ `;
465
+
466
+ const servicePath = resolve(workDir, "ove.service");
467
+ writeFileSync(servicePath, service);
468
+ process.stdout.write(` Wrote ${servicePath}\n`);
469
+
470
+ const install = await ask(rl, "Install and enable now? Requires sudo (y/n)");
471
+ if (install.toLowerCase() === "y") {
472
+ try {
473
+ execFileSync("sudo", ["cp", servicePath, "/etc/systemd/system/ove.service"]);
474
+ execFileSync("sudo", ["systemctl", "daemon-reload"]);
475
+ execFileSync("sudo", ["systemctl", "enable", "ove"]);
476
+ process.stdout.write(" Service installed and enabled.\n");
477
+
478
+ const startNow = await ask(rl, "Start service now? (y/n)");
479
+ if (startNow.toLowerCase() === "y") {
480
+ execFileSync("sudo", ["systemctl", "start", "ove"]);
481
+ process.stdout.write(" Service started.\n");
482
+ }
483
+ return true;
484
+ } catch (err) {
485
+ process.stdout.write(` Failed to install service: ${err}\n`);
486
+ }
487
+ }
488
+ return false;
489
+ }
@@ -1,15 +0,0 @@
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
-
14
- [Install]
15
- WantedBy=multi-user.target