@goodtek/vibeops 1.0.0 → 1.1.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/CHANGELOG.md +31 -0
- package/README.md +51 -60
- package/dist/bootstrap/installer.js +38 -4
- package/dist/bootstrap/manifest.js +23 -3
- package/dist/cli.js +64 -15
- package/dist/commands/init.js +279 -114
- package/dist/commands/llm.js +177 -0
- package/dist/commands/status.js +37 -1
- package/dist/commands/task-add.js +33 -6
- package/dist/commands/task-done.js +27 -31
- package/dist/lib/config.js +84 -4
- package/dist/lib/env.js +32 -0
- package/dist/lib/git-config.js +21 -0
- package/dist/lib/git-host.js +12 -0
- package/dist/lib/git-policy.js +67 -0
- package/dist/lib/git-remote.js +108 -0
- package/dist/lib/git.js +43 -0
- package/dist/lib/init-clients.js +55 -0
- package/dist/lib/llm-complete.js +18 -19
- package/dist/lib/llm-connect.js +150 -0
- package/dist/lib/llm-preference.js +102 -0
- package/dist/lib/llm-status.js +19 -0
- package/dist/lib/pr-create.js +66 -0
- package/dist/lib/task-add-llm.js +8 -1
- package/dist/lib/task-done-llm.js +8 -1
- package/dist/lib/task-pr-llm.js +50 -0
- package/dist/lib/task-push-pr.js +131 -0
- package/dist/lib/task-start.js +46 -18
- package/dist/lib/task.js +15 -0
- package/package.json +13 -11
- package/templates/.cursor/rules/02-git-safety.mdc +5 -4
- package/templates/AGENTS.md +2 -1
- package/templates/clients/claude/CLAUDE.md +28 -0
- package/templates/clients/cursor/.cursor/rules/02-git-safety.mdc +11 -0
- package/templates/core/AGENTS.md +44 -0
- package/templates/{.cursor/skills → skills-shared}/implement-task/SKILL.md +1 -1
- package/templates/{.cursor/skills → skills-shared}/plan-task/SKILL.md +3 -3
- package/dist/lib/task-merge.js +0 -115
- /package/templates/{.cursor → clients/cursor/.cursor}/rules/01-task-source-of-truth.mdc +0 -0
- /package/templates/{.cursor → clients/cursor/.cursor}/rules/03-docs-on-done.mdc +0 -0
- /package/templates/{docs → core/docs}/logs/README.md +0 -0
- /package/templates/{docs → core/docs}/project/03-architecture.md +0 -0
- /package/templates/{docs → core/docs}/project/05-current-state.md +0 -0
- /package/templates/{docs → core/docs}/project/06-decisions.md +0 -0
- /package/templates/{docs → core/docs}/tasks/TASK-000-template.md +0 -0
package/dist/lib/git.js
CHANGED
|
@@ -83,6 +83,36 @@ export async function gitBranchExists(cwd, name) {
|
|
|
83
83
|
const res = await tryGit(cwd, ["show-ref", "--verify", "--quiet", `refs/heads/${name}`]);
|
|
84
84
|
return res !== null;
|
|
85
85
|
}
|
|
86
|
+
export async function gitRemoteBranchExists(cwd, remote, branch) {
|
|
87
|
+
const res = await tryGit(cwd, [
|
|
88
|
+
"show-ref",
|
|
89
|
+
"--verify",
|
|
90
|
+
"--quiet",
|
|
91
|
+
`refs/remotes/${remote}/${branch}`,
|
|
92
|
+
]);
|
|
93
|
+
return res !== null;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Checkout local `branch`, creating it from `remote/branch` when only the remote ref exists.
|
|
97
|
+
*/
|
|
98
|
+
export async function gitSwitchToBranch(cwd, branch, remote = "origin") {
|
|
99
|
+
if (await gitBranchExists(cwd, branch)) {
|
|
100
|
+
await gitCheckout(cwd, branch);
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
if ((await gitRemoteUrl(cwd, remote)) === null)
|
|
104
|
+
return false;
|
|
105
|
+
try {
|
|
106
|
+
await gitFetch(cwd, remote, branch);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// branch may not exist on remote yet
|
|
110
|
+
}
|
|
111
|
+
if (!(await gitRemoteBranchExists(cwd, remote, branch)))
|
|
112
|
+
return false;
|
|
113
|
+
await gitCheckoutNewBranch(cwd, branch, `${remote}/${branch}`);
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
86
116
|
export async function gitCreateBranch(cwd, name, startPoint) {
|
|
87
117
|
const args = ["branch", name];
|
|
88
118
|
if (typeof startPoint === "string" && startPoint.length > 0)
|
|
@@ -459,3 +489,16 @@ export async function gitRemoteUrl(cwd, name = "origin") {
|
|
|
459
489
|
const url = res.stdout.trim();
|
|
460
490
|
return url.length > 0 ? url : null;
|
|
461
491
|
}
|
|
492
|
+
export async function gitFetch(cwd, remote, ref) {
|
|
493
|
+
const args = ["fetch", remote];
|
|
494
|
+
if (typeof ref === "string" && ref.length > 0)
|
|
495
|
+
args.push(ref);
|
|
496
|
+
await runGit(cwd, args);
|
|
497
|
+
}
|
|
498
|
+
export async function gitPullFastForwardOnly(cwd, remote, branch) {
|
|
499
|
+
await runGit(cwd, ["pull", "--ff-only", remote, branch]);
|
|
500
|
+
}
|
|
501
|
+
export async function gitPush(cwd, remote, branch, setUpstream = true) {
|
|
502
|
+
const args = setUpstream ? ["push", "-u", remote, branch] : ["push", remote, branch];
|
|
503
|
+
await runGit(cwd, args);
|
|
504
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { checkbox } from "@inquirer/prompts";
|
|
2
|
+
export const VIBEOPS_CLIENT_IDS = ["cursor", "claude", "codex"];
|
|
3
|
+
export const CLIENT_LABELS = {
|
|
4
|
+
cursor: "Cursor (.cursor/rules, .cursor/skills)",
|
|
5
|
+
claude: "Claude Code (CLAUDE.md, .claude/skills)",
|
|
6
|
+
codex: "Codex CLI (.agents/skills)",
|
|
7
|
+
};
|
|
8
|
+
/** Relative skill root per client (under project root). */
|
|
9
|
+
export const CLIENT_SKILL_ROOT = {
|
|
10
|
+
cursor: ".cursor/skills",
|
|
11
|
+
claude: ".claude/skills",
|
|
12
|
+
codex: ".agents/skills",
|
|
13
|
+
};
|
|
14
|
+
export function parseClientsArg(raw) {
|
|
15
|
+
if (raw === undefined || raw.trim().length === 0)
|
|
16
|
+
return null;
|
|
17
|
+
const parts = raw.split(",").map((p) => p.trim().toLowerCase());
|
|
18
|
+
const out = [];
|
|
19
|
+
for (const p of parts) {
|
|
20
|
+
const id = parseOneClient(p);
|
|
21
|
+
if (id === null)
|
|
22
|
+
return null;
|
|
23
|
+
if (!out.includes(id))
|
|
24
|
+
out.push(id);
|
|
25
|
+
}
|
|
26
|
+
return out.length > 0 ? out : null;
|
|
27
|
+
}
|
|
28
|
+
function parseOneClient(token) {
|
|
29
|
+
if (token === "cursor")
|
|
30
|
+
return "cursor";
|
|
31
|
+
if (token === "claude" || token === "claude-code" || token === "claudecode")
|
|
32
|
+
return "claude";
|
|
33
|
+
if (token === "codex" || token === "codex-cli")
|
|
34
|
+
return "codex";
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
export function isVibeopsClientId(value) {
|
|
38
|
+
return VIBEOPS_CLIENT_IDS.includes(value);
|
|
39
|
+
}
|
|
40
|
+
export async function askInitClients() {
|
|
41
|
+
const selected = await checkbox({
|
|
42
|
+
message: "Which coding agents will you use? (Space toggle · Enter confirm)",
|
|
43
|
+
choices: VIBEOPS_CLIENT_IDS.map((id) => ({
|
|
44
|
+
name: CLIENT_LABELS[id],
|
|
45
|
+
value: id,
|
|
46
|
+
checked: id === "cursor",
|
|
47
|
+
})),
|
|
48
|
+
loop: false,
|
|
49
|
+
validate: (answer) => answer.length > 0 ? true : "Select at least one agent (Cursor, Claude Code, and/or Codex).",
|
|
50
|
+
});
|
|
51
|
+
return selected;
|
|
52
|
+
}
|
|
53
|
+
export function formatClientsList(clients) {
|
|
54
|
+
return clients.map((c) => CLIENT_LABELS[c]).join(", ");
|
|
55
|
+
}
|
package/dist/lib/llm-complete.js
CHANGED
|
@@ -1,30 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveAvailableLlmProvider, resolveLlmProviderForUse } from "./llm-preference.js";
|
|
2
2
|
import { codexOAuthTextCompletion } from "./plan-codex-responses.js";
|
|
3
3
|
import { cursorAgentPrint, extractAgentAssistantText } from "./plan-llm-cursor-agent.js";
|
|
4
|
-
import { probeCursorAgentCli, probeOpenAiApiKey } from "./plan-llm-detect.js";
|
|
5
4
|
import { extractJsonObject, openAiChatCompletionJson, openAiChatCompletionText, } from "./plan-llm-openai.js";
|
|
6
|
-
/** Non-interactive: Codex OAuth → Cursor Agent → OpenAI API key (same sources as `vibeops plan`). */
|
|
7
|
-
export async function resolveAvailableLlmProvider(cwd) {
|
|
8
|
-
const codex = await probeCodexOAuthFile();
|
|
9
|
-
if (codex.ok)
|
|
10
|
-
return "codex-oauth";
|
|
11
|
-
const cursor = await probeCursorAgentCli(cwd);
|
|
12
|
-
if (cursor.ok)
|
|
13
|
-
return "cursor-agent";
|
|
14
|
-
const openai = await probeOpenAiApiKey();
|
|
15
|
-
if (openai.ok)
|
|
16
|
-
return "openai";
|
|
17
|
-
return null;
|
|
18
|
-
}
|
|
19
5
|
function formatMessagesForCursor(messages) {
|
|
20
6
|
return messages
|
|
21
7
|
.map((m) => `### ${m.role}\n\n${m.content}`)
|
|
22
8
|
.join("\n\n---\n\n");
|
|
23
9
|
}
|
|
24
10
|
export async function llmCompleteText(messages, opts) {
|
|
25
|
-
const
|
|
11
|
+
const pref = opts.preference ?? "auto";
|
|
12
|
+
const provider = opts.provider ?? (await resolveLlmProviderForUse(opts.cwd, pref));
|
|
26
13
|
if (provider === null) {
|
|
27
|
-
|
|
14
|
+
if (pref !== "auto") {
|
|
15
|
+
throw new Error(`Preferred LLM "${pref}" is not available (not installed or not authenticated). Run \`vibeops llm connect\` or \`vibeops llm use auto\`.`);
|
|
16
|
+
}
|
|
17
|
+
throw new Error("No LLM provider available. Run `vibeops llm connect` or set Codex OAuth, Cursor Agent CLI (`agent login`), or OPENAI_API_KEY.");
|
|
28
18
|
}
|
|
29
19
|
if (provider === "openai") {
|
|
30
20
|
const text = await openAiChatCompletionText({ messages, model: opts.model });
|
|
@@ -42,9 +32,13 @@ export async function llmCompleteText(messages, opts) {
|
|
|
42
32
|
return { text: extractAgentAssistantText(raw), provider: "cursor-agent" };
|
|
43
33
|
}
|
|
44
34
|
export async function llmCompleteJson(messages, opts) {
|
|
45
|
-
const
|
|
35
|
+
const pref = opts.preference ?? "auto";
|
|
36
|
+
const provider = opts.provider ?? (await resolveLlmProviderForUse(opts.cwd, pref));
|
|
46
37
|
if (provider === null) {
|
|
47
|
-
|
|
38
|
+
if (pref !== "auto") {
|
|
39
|
+
throw new Error(`Preferred LLM "${pref}" is not available (not installed or not authenticated). Run \`vibeops llm connect\` or \`vibeops llm use auto\`.`);
|
|
40
|
+
}
|
|
41
|
+
throw new Error("No LLM provider available. Run `vibeops llm connect` or set Codex OAuth, Cursor Agent CLI (`agent login`), or OPENAI_API_KEY.");
|
|
48
42
|
}
|
|
49
43
|
if (provider === "openai") {
|
|
50
44
|
const text = await openAiChatCompletionJson({ messages, model: opts.model });
|
|
@@ -60,3 +54,8 @@ export async function llmCompleteJson(messages, opts) {
|
|
|
60
54
|
const { text: raw } = await llmCompleteText(jsonMessages, { ...opts, provider });
|
|
61
55
|
return { text: extractJsonObject(raw), provider };
|
|
62
56
|
}
|
|
57
|
+
export { resolveAvailableLlmProvider } from "./llm-preference.js";
|
|
58
|
+
/** True if any provider can run completions (ignores project preference). */
|
|
59
|
+
export async function anyLlmProviderAvailable(cwd) {
|
|
60
|
+
return (await resolveAvailableLlmProvider(cwd)) !== null;
|
|
61
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { access, appendFile, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { dim, log } from "./logger.js";
|
|
5
|
+
import { codexAuthJsonPath, probeCodexOAuthFile } from "./plan-codex-auth.js";
|
|
6
|
+
import { probeCursorAgentCli, probeOpenAiApiKey } from "./plan-llm-detect.js";
|
|
7
|
+
import { VIBEOPS_ENV_FILE } from "./paths.js";
|
|
8
|
+
function agentBin() {
|
|
9
|
+
return process.env.VIBEOPS_CURSOR_AGENT_BIN?.trim() || "agent";
|
|
10
|
+
}
|
|
11
|
+
async function fileExists(path) {
|
|
12
|
+
try {
|
|
13
|
+
await access(path);
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export async function runCursorAgentLogin(cwd) {
|
|
21
|
+
const command = agentBin();
|
|
22
|
+
const before = await probeCursorAgentCli(cwd);
|
|
23
|
+
if (before.reason?.includes("not found on PATH")) {
|
|
24
|
+
return {
|
|
25
|
+
ok: false,
|
|
26
|
+
message: [
|
|
27
|
+
`Command "${command}" is not on PATH.`,
|
|
28
|
+
"Install Cursor Agent CLI: https://cursor.com/docs/cli",
|
|
29
|
+
"Then rerun: vibeops llm connect",
|
|
30
|
+
].join("\n"),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
log.info(`Running ${command} login — complete auth in the browser if prompted.`);
|
|
34
|
+
log.blank();
|
|
35
|
+
return await new Promise((resolve) => {
|
|
36
|
+
const child = spawn(command, ["login"], {
|
|
37
|
+
cwd,
|
|
38
|
+
stdio: "inherit",
|
|
39
|
+
env: process.env,
|
|
40
|
+
shell: process.platform === "win32",
|
|
41
|
+
});
|
|
42
|
+
child.on("error", (err) => {
|
|
43
|
+
if (err.code === "ENOENT") {
|
|
44
|
+
resolve({
|
|
45
|
+
ok: false,
|
|
46
|
+
message: `Command "${command}" not found. Install from https://cursor.com/docs/cli`,
|
|
47
|
+
});
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
resolve({ ok: false, message: err.message });
|
|
51
|
+
});
|
|
52
|
+
child.on("close", async (code) => {
|
|
53
|
+
const after = await probeCursorAgentCli(cwd);
|
|
54
|
+
if (after.ok) {
|
|
55
|
+
resolve({ ok: true, message: "Cursor Agent CLI is authenticated." });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (code === 0) {
|
|
59
|
+
resolve({
|
|
60
|
+
ok: false,
|
|
61
|
+
message: after.reason ?? "Login finished but `agent status` still reports not authenticated.",
|
|
62
|
+
});
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
resolve({
|
|
66
|
+
ok: false,
|
|
67
|
+
message: after.reason ?? `${command} login exited with code ${code ?? "unknown"}.`,
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
export async function guideCodexOAuth() {
|
|
73
|
+
const path = codexAuthJsonPath();
|
|
74
|
+
const probe = await probeCodexOAuthFile();
|
|
75
|
+
if (probe.ok) {
|
|
76
|
+
return { ok: true, message: `Codex OAuth already configured at ${path}` };
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
message: [
|
|
81
|
+
"Codex OAuth is not configured yet.",
|
|
82
|
+
"",
|
|
83
|
+
"Option A — official Codex CLI:",
|
|
84
|
+
" codex login",
|
|
85
|
+
"",
|
|
86
|
+
"Option B — copy auth from another machine:",
|
|
87
|
+
` Place auth.json at ${path}`,
|
|
88
|
+
"",
|
|
89
|
+
"Then rerun: vibeops llm connect",
|
|
90
|
+
].join("\n"),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
export async function configureOpenAiKey(projectRoot, apiKey) {
|
|
94
|
+
const trimmed = apiKey.trim();
|
|
95
|
+
const envPath = join(projectRoot, VIBEOPS_ENV_FILE);
|
|
96
|
+
if (trimmed.length === 0) {
|
|
97
|
+
return { ok: false, message: "API key cannot be empty.", envPath };
|
|
98
|
+
}
|
|
99
|
+
const line = `OPENAI_API_KEY=${trimmed}`;
|
|
100
|
+
if (await fileExists(envPath)) {
|
|
101
|
+
const existing = await readFile(envPath, "utf-8");
|
|
102
|
+
if (/^OPENAI_API_KEY=/m.test(existing)) {
|
|
103
|
+
const next = existing.replace(/^OPENAI_API_KEY=.*$/m, line);
|
|
104
|
+
await writeFile(envPath, next.endsWith("\n") ? next : `${next}\n`, "utf-8");
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
await appendFile(envPath, existing.endsWith("\n") ? `${line}\n` : `\n${line}\n`, "utf-8");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
await writeFile(envPath, ["# VibeOps · optional environment (gitignored)", "# Never commit secrets.", "", line, ""].join("\n"), "utf-8");
|
|
112
|
+
}
|
|
113
|
+
process.env.OPENAI_API_KEY = trimmed;
|
|
114
|
+
const verify = await probeOpenAiApiKey();
|
|
115
|
+
if (!verify.ok) {
|
|
116
|
+
return {
|
|
117
|
+
ok: false,
|
|
118
|
+
message: verify.reason ?? "Key saved but OpenAI verification failed.",
|
|
119
|
+
envPath,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
ok: true,
|
|
124
|
+
message: `OpenAI API key saved to ${VIBEOPS_ENV_FILE} and verified.`,
|
|
125
|
+
envPath,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
export function codexAuthHint() {
|
|
129
|
+
return codexAuthJsonPath();
|
|
130
|
+
}
|
|
131
|
+
export function cursorAgentHint() {
|
|
132
|
+
return agentBin();
|
|
133
|
+
}
|
|
134
|
+
export function openAiEnvHint(projectRoot) {
|
|
135
|
+
return join(projectRoot, VIBEOPS_ENV_FILE);
|
|
136
|
+
}
|
|
137
|
+
export function printConnectSetupHint(provider) {
|
|
138
|
+
switch (provider) {
|
|
139
|
+
case "cursor-agent":
|
|
140
|
+
log.info(dim(" https://cursor.com/docs/cli"));
|
|
141
|
+
log.info(dim(" Then: agent login (or vibeops llm connect → Set up Cursor Agent CLI)"));
|
|
142
|
+
break;
|
|
143
|
+
case "codex-oauth":
|
|
144
|
+
log.info(dim(` codex login → ${codexAuthHint()}`));
|
|
145
|
+
break;
|
|
146
|
+
case "openai":
|
|
147
|
+
log.info(dim(` vibeops llm connect → Set up OpenAI API key (writes ${VIBEOPS_ENV_FILE})`));
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { relative } from "node:path";
|
|
2
|
+
import { probeCodexOAuthFile } from "./plan-codex-auth.js";
|
|
3
|
+
import { probeCursorAgentCli, probeOpenAiApiKey } from "./plan-llm-detect.js";
|
|
4
|
+
export const LLM_PROVIDER_ORDER = [
|
|
5
|
+
"codex-oauth",
|
|
6
|
+
"cursor-agent",
|
|
7
|
+
"openai",
|
|
8
|
+
];
|
|
9
|
+
function shortPath(cwd, abs) {
|
|
10
|
+
const r = relative(cwd, abs);
|
|
11
|
+
return r.startsWith("..") || r.length === 0 ? abs : r;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* First available provider in Codex → Cursor Agent → OpenAI order.
|
|
15
|
+
*/
|
|
16
|
+
export async function resolveAvailableLlmProvider(cwd) {
|
|
17
|
+
const codex = await probeCodexOAuthFile();
|
|
18
|
+
if (codex.ok)
|
|
19
|
+
return "codex-oauth";
|
|
20
|
+
const cursor = await probeCursorAgentCli(cwd);
|
|
21
|
+
if (cursor.ok)
|
|
22
|
+
return "cursor-agent";
|
|
23
|
+
const openai = await probeOpenAiApiKey();
|
|
24
|
+
if (openai.ok)
|
|
25
|
+
return "openai";
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
export async function probeAllLlmProviders(cwd) {
|
|
29
|
+
const codex = await probeCodexOAuthFile();
|
|
30
|
+
const cursor = await probeCursorAgentCli(cwd);
|
|
31
|
+
const openai = await probeOpenAiApiKey();
|
|
32
|
+
return [
|
|
33
|
+
{
|
|
34
|
+
id: "codex-oauth",
|
|
35
|
+
ok: codex.ok,
|
|
36
|
+
summary: codex.ok ? `ok · ${shortPath(cwd, codex.path)}` : (codex.reason ?? "not configured"),
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "cursor-agent",
|
|
40
|
+
ok: cursor.ok,
|
|
41
|
+
summary: cursor.ok ? `ok · command: ${cursor.command}` : (cursor.reason ?? "not configured"),
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: "openai",
|
|
45
|
+
ok: openai.ok,
|
|
46
|
+
summary: openai.ok ? "ok · OPENAI_API_KEY" : (openai.reason ?? "not configured"),
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
export function parseLlmProviderPreference(raw) {
|
|
51
|
+
const t = raw.trim().toLowerCase();
|
|
52
|
+
if (t === "auto")
|
|
53
|
+
return "auto";
|
|
54
|
+
if (t === "codex-oauth" || t === "codex")
|
|
55
|
+
return "codex-oauth";
|
|
56
|
+
if (t === "cursor-agent" || t === "cursor")
|
|
57
|
+
return "cursor-agent";
|
|
58
|
+
if (t === "openai")
|
|
59
|
+
return "openai";
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
export function labelLlmProvider(id) {
|
|
63
|
+
switch (id) {
|
|
64
|
+
case "auto":
|
|
65
|
+
return "Auto (Codex → Cursor → OpenAI)";
|
|
66
|
+
case "codex-oauth":
|
|
67
|
+
return "Codex OAuth (~/.codex/auth.json)";
|
|
68
|
+
case "cursor-agent":
|
|
69
|
+
return "Cursor Agent CLI (`agent`)";
|
|
70
|
+
case "openai":
|
|
71
|
+
return "OpenAI API (OPENAI_API_KEY)";
|
|
72
|
+
default:
|
|
73
|
+
return id;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Resolves which concrete provider to use given project preference.
|
|
78
|
+
* - `auto` / undefined: same as `resolveAvailableLlmProvider`.
|
|
79
|
+
* - explicit: that provider only if its probe passes.
|
|
80
|
+
*/
|
|
81
|
+
export async function resolveLlmProviderForUse(cwd, preference) {
|
|
82
|
+
const pref = preference ?? "auto";
|
|
83
|
+
if (pref === "auto") {
|
|
84
|
+
return resolveAvailableLlmProvider(cwd);
|
|
85
|
+
}
|
|
86
|
+
if (pref === "codex-oauth") {
|
|
87
|
+
const c = await probeCodexOAuthFile();
|
|
88
|
+
return c.ok ? "codex-oauth" : null;
|
|
89
|
+
}
|
|
90
|
+
if (pref === "cursor-agent") {
|
|
91
|
+
const c = await probeCursorAgentCli(cwd);
|
|
92
|
+
return c.ok ? "cursor-agent" : null;
|
|
93
|
+
}
|
|
94
|
+
const o = await probeOpenAiApiKey();
|
|
95
|
+
return o.ok ? "openai" : null;
|
|
96
|
+
}
|
|
97
|
+
export function getLlmPreferenceFromConfig(config) {
|
|
98
|
+
const p = config?.llm?.provider;
|
|
99
|
+
if (p === "auto" || p === "codex-oauth" || p === "cursor-agent" || p === "openai")
|
|
100
|
+
return p;
|
|
101
|
+
return "auto";
|
|
102
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { readConfig } from "./config.js";
|
|
2
|
+
import { getLlmPreferenceFromConfig, labelLlmProvider, probeAllLlmProviders, resolveLlmProviderForUse, } from "./llm-preference.js";
|
|
3
|
+
export async function buildLlmStatusReport(cwd) {
|
|
4
|
+
const config = await readConfig(cwd);
|
|
5
|
+
const preference = getLlmPreferenceFromConfig(config);
|
|
6
|
+
const providers = await probeAllLlmProviders(cwd);
|
|
7
|
+
const active = await resolveLlmProviderForUse(cwd, preference);
|
|
8
|
+
const anyAvailable = providers.some((p) => p.ok);
|
|
9
|
+
return { preference, active, providers, anyAvailable };
|
|
10
|
+
}
|
|
11
|
+
export function formatLlmStatusLine(report) {
|
|
12
|
+
const preferenceLabel = labelLlmProvider(report.preference);
|
|
13
|
+
const activeLabel = report.active !== null
|
|
14
|
+
? `${labelLlmProvider(report.active)} (ok)`
|
|
15
|
+
: report.anyAvailable
|
|
16
|
+
? "none (preference blocked or misconfigured)"
|
|
17
|
+
: "none — run vibeops llm connect";
|
|
18
|
+
return { preferenceLabel, activeLabel };
|
|
19
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { mergeRequestLabel } from "./git-host.js";
|
|
4
|
+
import { log } from "./logger.js";
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
async function ghPrCreate(opts) {
|
|
7
|
+
const { stdout } = await execFileAsync("gh", [
|
|
8
|
+
"pr",
|
|
9
|
+
"create",
|
|
10
|
+
"--base",
|
|
11
|
+
opts.baseBranch,
|
|
12
|
+
"--head",
|
|
13
|
+
opts.headBranch,
|
|
14
|
+
"--title",
|
|
15
|
+
opts.title,
|
|
16
|
+
"--body",
|
|
17
|
+
opts.body,
|
|
18
|
+
], { cwd: opts.cwd, maxBuffer: 4 * 1024 * 1024 });
|
|
19
|
+
const line = stdout.trim().split("\n").pop()?.trim();
|
|
20
|
+
if (line && line.startsWith("http"))
|
|
21
|
+
return line;
|
|
22
|
+
throw new Error("gh pr create did not return a URL.");
|
|
23
|
+
}
|
|
24
|
+
async function glabMrCreate(opts) {
|
|
25
|
+
const { stdout } = await execFileAsync("glab", [
|
|
26
|
+
"mr",
|
|
27
|
+
"create",
|
|
28
|
+
"--target-branch",
|
|
29
|
+
opts.baseBranch,
|
|
30
|
+
"--source-branch",
|
|
31
|
+
opts.headBranch,
|
|
32
|
+
"--title",
|
|
33
|
+
opts.title,
|
|
34
|
+
"--description",
|
|
35
|
+
opts.body,
|
|
36
|
+
], { cwd: opts.cwd, maxBuffer: 4 * 1024 * 1024 });
|
|
37
|
+
const line = stdout.trim().split("\n").pop()?.trim();
|
|
38
|
+
if (line && (line.startsWith("http") || line.includes("merge_requests")))
|
|
39
|
+
return line;
|
|
40
|
+
const match = stdout.match(/https?:\/\/[^\s]+/);
|
|
41
|
+
if (match)
|
|
42
|
+
return match[0];
|
|
43
|
+
throw new Error("glab mr create did not return a URL.");
|
|
44
|
+
}
|
|
45
|
+
export async function createMergeRequest(opts) {
|
|
46
|
+
const label = mergeRequestLabel(opts.host);
|
|
47
|
+
if (opts.dryRun) {
|
|
48
|
+
log.info(`would create ${label}: ${opts.headBranch} → ${opts.baseBranch}`);
|
|
49
|
+
return { url: `(dry-run ${label})` };
|
|
50
|
+
}
|
|
51
|
+
const url = opts.host === "gitlab" ? await glabMrCreate(opts) : await ghPrCreate(opts);
|
|
52
|
+
return { url };
|
|
53
|
+
}
|
|
54
|
+
export async function probeMergeRequestCli(host) {
|
|
55
|
+
try {
|
|
56
|
+
if (host === "gitlab") {
|
|
57
|
+
await execFileAsync("glab", ["--version"], { maxBuffer: 1024 });
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
await execFileAsync("gh", ["--version"], { maxBuffer: 1024 });
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
package/dist/lib/task-add-llm.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { join, relative } from "node:path";
|
|
2
2
|
import { readTextOrNull } from "./filesystem.js";
|
|
3
|
+
import { readConfig } from "./config.js";
|
|
3
4
|
import { llmCompleteJson } from "./llm-complete.js";
|
|
5
|
+
import { getLlmPreferenceFromConfig } from "./llm-preference.js";
|
|
4
6
|
import { projectPaths } from "./paths.js";
|
|
5
7
|
import { slugify } from "./slug.js";
|
|
6
8
|
import { buildMinimalTaskMarkdown, ensureV3Sections, normalizeTaskHeader, titleFromIdea, } from "./task-scaffold.js";
|
|
@@ -53,7 +55,12 @@ export async function llmScaffoldTask(params) {
|
|
|
53
55
|
content: [`TASK id: ${params.taskId}`, `Idea: ${params.idea}`, "", context].join("\n"),
|
|
54
56
|
},
|
|
55
57
|
];
|
|
56
|
-
const
|
|
58
|
+
const config = await readConfig(params.cwd);
|
|
59
|
+
const preference = getLlmPreferenceFromConfig(config);
|
|
60
|
+
const { text: raw, provider } = await llmCompleteJson(messages, {
|
|
61
|
+
cwd: params.cwd,
|
|
62
|
+
preference,
|
|
63
|
+
});
|
|
57
64
|
const parsed = JSON.parse(extractJsonObject(raw));
|
|
58
65
|
const title = typeof parsed.title === "string"
|
|
59
66
|
? parsed.title.replace(/^TASK-\d+\s*[:\-]\s*/i, "").trim()
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { readText, readTextOrNull, writeText } from "./filesystem.js";
|
|
3
|
+
import { readConfig } from "./config.js";
|
|
3
4
|
import { llmCompleteJson } from "./llm-complete.js";
|
|
5
|
+
import { getLlmPreferenceFromConfig } from "./llm-preference.js";
|
|
4
6
|
import { gitDiffNameOnly, readGitInfo } from "./git.js";
|
|
5
7
|
import { projectPaths, PROJECT_MEMORY_FILES } from "./paths.js";
|
|
6
8
|
import { extractJsonObject } from "./plan-llm-openai.js";
|
|
@@ -64,7 +66,12 @@ Be factual. Do not invent files not in the diff.`,
|
|
|
64
66
|
},
|
|
65
67
|
];
|
|
66
68
|
try {
|
|
67
|
-
const
|
|
69
|
+
const config = await readConfig(params.cwd);
|
|
70
|
+
const preference = getLlmPreferenceFromConfig(config);
|
|
71
|
+
const { text: raw, provider } = await llmCompleteJson(messages, {
|
|
72
|
+
cwd: params.cwd,
|
|
73
|
+
preference,
|
|
74
|
+
});
|
|
68
75
|
const parsed = JSON.parse(extractJsonObject(raw));
|
|
69
76
|
const result = typeof parsed.result === "string" ? parsed.result.trim() : "";
|
|
70
77
|
const testResult = typeof parsed.testResult === "string" ? parsed.testResult.trim() : "";
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { llmCompleteJson } from "./llm-complete.js";
|
|
2
|
+
const SYSTEM = `You write pull/merge request titles and bodies for a software task.
|
|
3
|
+
Output valid JSON only: { "title": string, "body": string }.
|
|
4
|
+
Title: under 72 chars, include task id.
|
|
5
|
+
Body: markdown with Summary, Changes, Test plan sections. Facts only from the input.`;
|
|
6
|
+
export async function generateTaskPrWithLlm(input, cwd, preference) {
|
|
7
|
+
const user = [
|
|
8
|
+
`Task: ${input.taskId}`,
|
|
9
|
+
`Title: ${input.title}`,
|
|
10
|
+
`Base branch: ${input.baseBranch}`,
|
|
11
|
+
`Head branch: ${input.headBranch}`,
|
|
12
|
+
"",
|
|
13
|
+
"## TASK",
|
|
14
|
+
input.taskBody.slice(0, 12000),
|
|
15
|
+
"",
|
|
16
|
+
"## Diff summary",
|
|
17
|
+
input.diffSummary.slice(0, 8000) || "(no diff captured)",
|
|
18
|
+
].join("\n");
|
|
19
|
+
const messages = [
|
|
20
|
+
{ role: "system", content: SYSTEM },
|
|
21
|
+
{ role: "user", content: user },
|
|
22
|
+
];
|
|
23
|
+
try {
|
|
24
|
+
const { text: raw } = await llmCompleteJson(messages, { cwd, preference });
|
|
25
|
+
const parsed = JSON.parse(raw);
|
|
26
|
+
const prTitle = typeof parsed.title === "string" ? parsed.title.trim() : "";
|
|
27
|
+
const prBody = typeof parsed.body === "string" ? parsed.body.trim() : "";
|
|
28
|
+
if (!prTitle || !prBody)
|
|
29
|
+
return null;
|
|
30
|
+
return { prTitle, prBody };
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function fallbackTaskPr(input) {
|
|
37
|
+
const prTitle = `${input.taskId}: ${input.title}`.slice(0, 72);
|
|
38
|
+
const prBody = [
|
|
39
|
+
"## Summary",
|
|
40
|
+
input.title,
|
|
41
|
+
"",
|
|
42
|
+
"## Changes",
|
|
43
|
+
input.diffSummary || "(see commits on branch)",
|
|
44
|
+
"",
|
|
45
|
+
"## Test plan",
|
|
46
|
+
"- [ ] Run project test/lint commands",
|
|
47
|
+
"- [ ] Review TASK acceptance criteria",
|
|
48
|
+
].join("\n");
|
|
49
|
+
return { prTitle, prBody };
|
|
50
|
+
}
|