@lovenyberg/ove 0.5.1 → 0.5.2
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 +10 -3
- package/bun.lock +1 -1
- package/docs/index.html +194 -196
- package/docs/plans/2026-02-25-landing-page-harmonization-design.md +89 -0
- package/docs/plans/2026-02-25-landing-page-harmonization-plan.md +604 -0
- package/package.json +1 -1
- package/src/handlers.ts +10 -1
- package/src/router.ts +40 -0
- package/src/setup.ts +51 -16
package/src/handlers.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
1
2
|
import { parseMessage, buildContextualPrompt } from "./router";
|
|
2
3
|
import type { ParsedMessage } from "./router";
|
|
3
4
|
import { isAuthorized, getUserRepos, addRepo, addUser } from "./config";
|
|
@@ -332,6 +333,7 @@ async function handleDiscuss(msg: IncomingMessage, parsed: ParsedMessage, histor
|
|
|
332
333
|
await msg.updateStatus("Thinking...");
|
|
333
334
|
|
|
334
335
|
try {
|
|
336
|
+
mkdirSync(deps.config.reposDir, { recursive: true });
|
|
335
337
|
const discussRunner = deps.getRunner(deps.config.runner?.name);
|
|
336
338
|
const result = await discussRunner.run(
|
|
337
339
|
prompt,
|
|
@@ -452,7 +454,7 @@ async function handleTaskMessage(msg: IncomingMessage, parsed: ParsedMessage, de
|
|
|
452
454
|
}
|
|
453
455
|
}
|
|
454
456
|
} else {
|
|
455
|
-
const reply =
|
|
457
|
+
const reply = `No repos configured yet. Add one with:\n\`init repo <name> <git-url> [branch]\`\nExample: \`init repo my-app git@github.com:user/my-app.git\``;
|
|
456
458
|
await msg.reply(reply);
|
|
457
459
|
return;
|
|
458
460
|
}
|
|
@@ -521,6 +523,13 @@ export function createMessageHandler(deps: HandlerDeps): (msg: IncomingMessage)
|
|
|
521
523
|
return;
|
|
522
524
|
}
|
|
523
525
|
|
|
526
|
+
// If user has no repos, fall back to discuss mode so they can still chat
|
|
527
|
+
const userRepos = getUserRepos(deps.config, msg.userId);
|
|
528
|
+
if (userRepos.length === 0) {
|
|
529
|
+
const history = deps.sessions.getHistory(msg.userId, 6);
|
|
530
|
+
return handleDiscuss(msg, { ...parsed, type: "discuss", args: { topic: parsed.rawText } }, history, deps);
|
|
531
|
+
}
|
|
532
|
+
|
|
524
533
|
// For all other types (free-form, review-pr, fix-issue, simplify, validate) — task dispatch
|
|
525
534
|
await handleTaskMessage(msg, parsed, deps);
|
|
526
535
|
};
|
package/src/router.ts
CHANGED
|
@@ -116,6 +116,46 @@ export function parseMessage(text: string): ParsedMessage {
|
|
|
116
116
|
};
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
// Natural language repo setup: "clone org/repo", "setup org/repo", "add org/repo"
|
|
120
|
+
const naturalRepoMatch = trimmed.match(/^(?:clone|setup|add|init|use)\s+(?:(?:the|repo)\s+)?([a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+)(?:\s+(?:repo(?:sitory)?))?\s*$/i);
|
|
121
|
+
if (naturalRepoMatch) {
|
|
122
|
+
const slug = naturalRepoMatch[1];
|
|
123
|
+
const name = slug.split("/").pop()!;
|
|
124
|
+
const url = `git@github.com:${slug}.git`;
|
|
125
|
+
return {
|
|
126
|
+
type: "init-repo",
|
|
127
|
+
args: { name, url, branch: "main" },
|
|
128
|
+
rawText: trimmed,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Detect org/repo or GitHub URLs anywhere in a message that looks like a setup request
|
|
133
|
+
const setupIntent = /(?:clone|setup|add|init|use|start\s+(?:with|on)|work\s+(?:on|with))/i.test(lower);
|
|
134
|
+
if (setupIntent) {
|
|
135
|
+
const ghUrl = trimmed.match(/((?:git@github\.com:|https:\/\/github\.com\/)([a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+?)(?:\.git)?)\b/);
|
|
136
|
+
if (ghUrl) {
|
|
137
|
+
const slug = ghUrl[2];
|
|
138
|
+
const name = slug.split("/").pop()!;
|
|
139
|
+
const url = ghUrl[1].endsWith(".git") ? ghUrl[1] : ghUrl[1] + ".git";
|
|
140
|
+
return {
|
|
141
|
+
type: "init-repo",
|
|
142
|
+
args: { name, url: url.startsWith("git@") ? url : `git@github.com:${slug}.git`, branch: "main" },
|
|
143
|
+
rawText: trimmed,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
const slugMatch = trimmed.match(/\b([a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+)\b/);
|
|
147
|
+
if (slugMatch && slugMatch[1].indexOf("/") > 0) {
|
|
148
|
+
const slug = slugMatch[1];
|
|
149
|
+
const name = slug.split("/").pop()!;
|
|
150
|
+
const url = `git@github.com:${slug}.git`;
|
|
151
|
+
return {
|
|
152
|
+
type: "init-repo",
|
|
153
|
+
args: { name, url, branch: "main" },
|
|
154
|
+
rawText: trimmed,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
119
159
|
const repoHint = trimmed.match(/(?:in|on)\s+(\S+?)[?.!,]*\s*$/i);
|
|
120
160
|
return { type: "free-form", repo: repoHint?.[1], args: {}, rawText: trimmed };
|
|
121
161
|
}
|
package/src/setup.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
|
-
import { resolve } from "node:path";
|
|
3
|
+
import { resolve, dirname } from "node:path";
|
|
4
4
|
import { userInfo } from "node:os";
|
|
5
5
|
import { createInterface } from "node:readline/promises";
|
|
6
6
|
import type { Config } from "./config";
|
|
@@ -59,7 +59,9 @@ export function validateConfig(opts?: { configPath?: string; envPath?: string })
|
|
|
59
59
|
if (existsSync(configPath)) {
|
|
60
60
|
try {
|
|
61
61
|
const config = JSON.parse(readFileSync(configPath, "utf-8")) as Partial<Config>;
|
|
62
|
-
|
|
62
|
+
const hasGithubSync = config.github?.orgs && config.github.orgs.length > 0;
|
|
63
|
+
const hasWildcardUser = Object.values(config.users || {}).some(u => u.repos?.includes("*"));
|
|
64
|
+
if (!hasGithubSync && !hasWildcardUser && (!config.repos || Object.keys(config.repos).length === 0)) {
|
|
63
65
|
issues.push("No repos configured");
|
|
64
66
|
}
|
|
65
67
|
if (!config.users || Object.keys(config.users).length === 0) {
|
|
@@ -236,23 +238,42 @@ export async function runSetup(opts?: { fixOnly?: string[] }): Promise<void> {
|
|
|
236
238
|
}
|
|
237
239
|
}
|
|
238
240
|
|
|
239
|
-
// Collect repos
|
|
241
|
+
// Collect repos — GitHub sync or manual
|
|
240
242
|
const repos: Record<string, { url: string; defaultBranch: string }> = existingConfig.repos
|
|
241
243
|
? { ...existingConfig.repos }
|
|
242
244
|
: {};
|
|
245
|
+
let githubOrgs: string[] = existingConfig.github?.orgs || [];
|
|
246
|
+
let useWildcard = false;
|
|
243
247
|
|
|
244
248
|
if (needsRepos || needsConfigFile) {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
249
|
+
const repoMode = await choose(rl, "How do you want to manage repos?", [
|
|
250
|
+
"Auto-discover from GitHub (uses gh CLI)",
|
|
251
|
+
"Add repos manually",
|
|
252
|
+
"Skip — add repos later via chat",
|
|
253
|
+
]);
|
|
254
|
+
|
|
255
|
+
if (repoMode === 0) {
|
|
256
|
+
// GitHub auto-discovery
|
|
257
|
+
const orgs = await ask(rl, "GitHub orgs/users to sync (comma-separated, or empty for your repos)");
|
|
258
|
+
githubOrgs = orgs ? orgs.split(",").map(s => s.trim()).filter(Boolean) : [];
|
|
259
|
+
useWildcard = true;
|
|
260
|
+
process.stdout.write(" Repos will sync from GitHub on startup.\n");
|
|
261
|
+
} else if (repoMode === 1) {
|
|
262
|
+
let addMore = true;
|
|
263
|
+
while (addMore) {
|
|
264
|
+
process.stdout.write("\n Add a repo:\n");
|
|
265
|
+
const name = await ask(rl, "Name");
|
|
266
|
+
if (!name) break;
|
|
267
|
+
const url = await ask(rl, "Git URL");
|
|
268
|
+
const branch = (await ask(rl, "Default branch [main]")) || "main";
|
|
269
|
+
repos[name] = { url, defaultBranch: branch };
|
|
270
|
+
|
|
271
|
+
const again = await ask(rl, "Add another repo? (y/n)");
|
|
272
|
+
addMore = again.toLowerCase() === "y";
|
|
273
|
+
}
|
|
274
|
+
} else {
|
|
275
|
+
useWildcard = true;
|
|
276
|
+
process.stdout.write(" You can add repos later: 'clone org/repo' in chat.\n");
|
|
256
277
|
}
|
|
257
278
|
}
|
|
258
279
|
|
|
@@ -260,7 +281,7 @@ export async function runSetup(opts?: { fixOnly?: string[] }): Promise<void> {
|
|
|
260
281
|
const users: Record<string, { name: string; repos: string[] }> = existingConfig.users
|
|
261
282
|
? { ...existingConfig.users }
|
|
262
283
|
: {};
|
|
263
|
-
const repoNames = Object.keys(repos);
|
|
284
|
+
const repoNames = useWildcard ? ["*"] : Object.keys(repos);
|
|
264
285
|
|
|
265
286
|
if (needsUsers || needsConfigFile) {
|
|
266
287
|
// Ask for user name once
|
|
@@ -276,7 +297,7 @@ export async function runSetup(opts?: { fixOnly?: string[] }): Promise<void> {
|
|
|
276
297
|
if (userId) users[`slack:${userId}`] = { name: userName || "user", repos: repoNames };
|
|
277
298
|
}
|
|
278
299
|
if (has("Telegram")) {
|
|
279
|
-
const userId = await ask(rl, "Your Telegram user ID");
|
|
300
|
+
const userId = await ask(rl, "Your Telegram user ID (send /start to @userinfobot to find it)");
|
|
280
301
|
if (userId) users[`telegram:${userId}`] = { name: userName || "user", repos: repoNames };
|
|
281
302
|
}
|
|
282
303
|
if (has("Discord")) {
|
|
@@ -378,6 +399,7 @@ export async function runSetup(opts?: { fixOnly?: string[] }): Promise<void> {
|
|
|
378
399
|
repos,
|
|
379
400
|
users,
|
|
380
401
|
claude: existingConfig.claude || { maxTurns: 25 },
|
|
402
|
+
...(githubOrgs.length > 0 ? { github: { orgs: githubOrgs } } : existingConfig.github ? { github: existingConfig.github } : {}),
|
|
381
403
|
...(existingConfig.mcpServers ? { mcpServers: existingConfig.mcpServers } : {}),
|
|
382
404
|
...(existingConfig.cron ? { cron: existingConfig.cron } : {}),
|
|
383
405
|
};
|
|
@@ -446,6 +468,18 @@ async function installSystemdService(rl: ReturnType<typeof createInterface>): Pr
|
|
|
446
468
|
}
|
|
447
469
|
|
|
448
470
|
const envPath = resolve(workDir, ".env");
|
|
471
|
+
|
|
472
|
+
// Build PATH that includes dirs for bun, claude, and gh
|
|
473
|
+
const pathDirs = new Set<string>();
|
|
474
|
+
for (const bin of [bunPath, "claude", "gh"]) {
|
|
475
|
+
let resolved = "";
|
|
476
|
+
try { resolved = execFileSync("which", [bin]).toString().trim(); } catch {}
|
|
477
|
+
if (resolved) pathDirs.add(dirname(resolved));
|
|
478
|
+
}
|
|
479
|
+
// Always include standard dirs
|
|
480
|
+
for (const d of ["/usr/local/bin", "/usr/bin", "/bin"]) pathDirs.add(d);
|
|
481
|
+
const pathLine = Array.from(pathDirs).join(":");
|
|
482
|
+
|
|
449
483
|
const service = `[Unit]
|
|
450
484
|
Description=Ove - Personal AI coding assistant
|
|
451
485
|
After=network.target
|
|
@@ -458,6 +492,7 @@ ExecStart=${bunPath} run src/index.ts
|
|
|
458
492
|
Restart=always
|
|
459
493
|
RestartSec=5
|
|
460
494
|
EnvironmentFile=${envPath}
|
|
495
|
+
Environment=PATH=${pathLine}
|
|
461
496
|
|
|
462
497
|
[Install]
|
|
463
498
|
WantedBy=multi-user.target
|