@goodtek/vibeops 0.7.22 → 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.
Files changed (144) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +51 -380
  3. package/dist/bootstrap/installer.js +42 -20
  4. package/dist/bootstrap/manifest.js +23 -3
  5. package/dist/cli.js +69 -154
  6. package/dist/commands/init.js +279 -114
  7. package/dist/commands/llm.js +177 -0
  8. package/dist/commands/status.js +181 -8
  9. package/dist/commands/task-add.js +89 -162
  10. package/dist/commands/task-done.js +112 -73
  11. package/dist/lib/config.js +77 -113
  12. package/dist/lib/env.js +32 -0
  13. package/dist/lib/git-config.js +21 -0
  14. package/dist/lib/git-host.js +12 -0
  15. package/dist/lib/git-policy.js +67 -0
  16. package/dist/lib/git-remote.js +108 -0
  17. package/dist/lib/git.js +44 -1
  18. package/dist/lib/init-clients.js +55 -0
  19. package/dist/lib/llm-complete.js +18 -19
  20. package/dist/lib/llm-connect.js +150 -0
  21. package/dist/lib/llm-preference.js +102 -0
  22. package/dist/lib/llm-status.js +19 -0
  23. package/dist/lib/paths.js +7 -3
  24. package/dist/lib/plan-llm-openai.js +0 -63
  25. package/dist/lib/pr-create.js +66 -0
  26. package/dist/lib/resolve-task.js +34 -0
  27. package/dist/lib/task-add-llm.js +53 -233
  28. package/dist/lib/task-context.js +68 -0
  29. package/dist/lib/task-done-llm.js +155 -0
  30. package/dist/lib/task-pr-llm.js +50 -0
  31. package/dist/lib/task-push-pr.js +131 -0
  32. package/dist/lib/task-scaffold.js +39 -43
  33. package/dist/lib/task-start.js +100 -0
  34. package/dist/lib/task-summary.js +12 -239
  35. package/dist/lib/task.js +30 -24
  36. package/dist/types/config.js +0 -16
  37. package/package.json +14 -14
  38. package/templates/.cursor/rules/02-git-safety.mdc +12 -0
  39. package/templates/AGENTS.md +27 -87
  40. package/templates/clients/claude/CLAUDE.md +28 -0
  41. package/templates/clients/cursor/.cursor/rules/01-task-source-of-truth.mdc +12 -0
  42. package/templates/clients/cursor/.cursor/rules/02-git-safety.mdc +11 -0
  43. package/templates/clients/cursor/.cursor/rules/03-docs-on-done.mdc +13 -0
  44. package/templates/core/AGENTS.md +44 -0
  45. package/templates/core/docs/logs/README.md +7 -0
  46. package/templates/core/docs/project/03-architecture.md +15 -0
  47. package/templates/core/docs/project/05-current-state.md +15 -0
  48. package/templates/core/docs/project/06-decisions.md +9 -0
  49. package/templates/core/docs/tasks/TASK-000-template.md +37 -0
  50. package/templates/skills-shared/implement-task/SKILL.md +22 -0
  51. package/templates/skills-shared/plan-task/SKILL.md +19 -0
  52. package/dist/agent/loader.js +0 -71
  53. package/dist/agent/prompt.js +0 -66
  54. package/dist/commands/agent-list.js +0 -32
  55. package/dist/commands/agent-prompt.js +0 -59
  56. package/dist/commands/agent-show.js +0 -26
  57. package/dist/commands/done.js +0 -160
  58. package/dist/commands/github-init.js +0 -554
  59. package/dist/commands/github-status.js +0 -164
  60. package/dist/commands/next.js +0 -283
  61. package/dist/commands/notion-init.js +0 -764
  62. package/dist/commands/notion-sync.js +0 -405
  63. package/dist/commands/notion-test.js +0 -595
  64. package/dist/commands/plan.js +0 -146
  65. package/dist/commands/rollback.js +0 -126
  66. package/dist/commands/start.js +0 -129
  67. package/dist/commands/task-check.js +0 -155
  68. package/dist/commands/task-generate.js +0 -206
  69. package/dist/commands/task-pull.js +0 -277
  70. package/dist/commands/task-rollback.js +0 -174
  71. package/dist/commands/task-start.js +0 -102
  72. package/dist/commands/task-status.js +0 -122
  73. package/dist/lib/brief.js +0 -356
  74. package/dist/lib/cursor-task-prompt.js +0 -60
  75. package/dist/lib/done-follow-up.js +0 -308
  76. package/dist/lib/github-cli.js +0 -247
  77. package/dist/lib/guide-state.js +0 -69
  78. package/dist/lib/mvp-artifacts.js +0 -204
  79. package/dist/lib/mvp-constants.js +0 -15
  80. package/dist/lib/mvp-diff-summary.js +0 -43
  81. package/dist/lib/mvp-done-summary.js +0 -335
  82. package/dist/lib/notion-client.js +0 -459
  83. package/dist/lib/notion-discovery.js +0 -671
  84. package/dist/lib/notion-env.js +0 -140
  85. package/dist/lib/notion-mappers.js +0 -148
  86. package/dist/lib/notion-schema.js +0 -272
  87. package/dist/lib/notion-sync.js +0 -337
  88. package/dist/lib/notion-target.js +0 -247
  89. package/dist/lib/package-json.js +0 -133
  90. package/dist/lib/plan-apply-planner.js +0 -131
  91. package/dist/lib/plan-codex-pkce.js +0 -234
  92. package/dist/lib/plan-llm-models.js +0 -237
  93. package/dist/lib/plan-llm-normalize.js +0 -146
  94. package/dist/lib/plan-llm-session.js +0 -682
  95. package/dist/lib/plan-post-apply-setup.js +0 -116
  96. package/dist/lib/project-docs.js +0 -95
  97. package/dist/lib/prompt-builder.js +0 -135
  98. package/dist/lib/resolve-command-task.js +0 -40
  99. package/dist/lib/task-add-build-prompt.js +0 -39
  100. package/dist/lib/task-add-cursor.js +0 -81
  101. package/dist/lib/task-generator.js +0 -183
  102. package/dist/lib/task-merge.js +0 -220
  103. package/dist/lib/task-prompt.js +0 -23
  104. package/dist/lib/task-pull.js +0 -354
  105. package/dist/lib/task-status-brief.js +0 -132
  106. package/dist/lib/workflow-guide.js +0 -485
  107. package/dist/status/collector.js +0 -103
  108. package/dist/status/format.js +0 -181
  109. package/dist/types/brief.js +0 -126
  110. package/templates/.cursor/rules/00-project-governance.mdc +0 -28
  111. package/templates/.cursor/rules/01-agent-orchestration.mdc +0 -48
  112. package/templates/.cursor/rules/02-task-workflow.mdc +0 -38
  113. package/templates/.cursor/rules/03-git-safety.mdc +0 -30
  114. package/templates/.cursor/rules/04-docs-update.mdc +0 -22
  115. package/templates/.vibeops/agents/architect.md +0 -47
  116. package/templates/.vibeops/agents/builder.md +0 -38
  117. package/templates/.vibeops/agents/docs.md +0 -54
  118. package/templates/.vibeops/agents/orchestrator.md +0 -40
  119. package/templates/.vibeops/agents/planner.md +0 -60
  120. package/templates/.vibeops/agents/recovery.md +0 -49
  121. package/templates/.vibeops/agents/reviewer.md +0 -47
  122. package/templates/.vibeops/agents/tester.md +0 -43
  123. package/templates/.vibeops/prompts/create-plan.md +0 -33
  124. package/templates/.vibeops/prompts/generate-tasks.md +0 -41
  125. package/templates/.vibeops/prompts/implement-task.md +0 -39
  126. package/templates/.vibeops/prompts/review-task.md +0 -34
  127. package/templates/.vibeops/prompts/rollback.md +0 -32
  128. package/templates/.vibeops/prompts/start-project.md +0 -39
  129. package/templates/.vibeops/workflows/notion-sync.md +0 -53
  130. package/templates/.vibeops/workflows/project-start.md +0 -73
  131. package/templates/.vibeops/workflows/rollback.md +0 -45
  132. package/templates/.vibeops/workflows/task-lifecycle.md +0 -71
  133. package/templates/docs/logs/README.md +0 -38
  134. package/templates/docs/project/00-overview.md +0 -27
  135. package/templates/docs/project/01-requirements.md +0 -30
  136. package/templates/docs/project/02-mvp-scope.md +0 -36
  137. package/templates/docs/project/03-architecture.md +0 -34
  138. package/templates/docs/project/04-tech-stack.md +0 -29
  139. package/templates/docs/project/05-current-state.md +0 -35
  140. package/templates/docs/project/06-decisions.md +0 -20
  141. package/templates/docs/project/07-backlog.md +0 -23
  142. package/templates/docs/project/08-env.md +0 -29
  143. package/templates/docs/project/09-deployment.md +0 -28
  144. package/templates/docs/tasks/TASK-000-template.md +0 -72
@@ -0,0 +1,108 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { askInput } from "./inquirer-helpers.js";
4
+ import { detectGitHost } from "./git-host.js";
5
+ import { gitRemoteUrl, runGit } from "./git.js";
6
+ import { dim, log } from "./logger.js";
7
+ const execFileAsync = promisify(execFile);
8
+ async function probeGh() {
9
+ try {
10
+ await execFileAsync("gh", ["auth", "status"], { maxBuffer: 1024 * 1024 });
11
+ return true;
12
+ }
13
+ catch {
14
+ return false;
15
+ }
16
+ }
17
+ async function probeGlab() {
18
+ try {
19
+ await execFileAsync("glab", ["auth", "status"], { maxBuffer: 1024 * 1024 });
20
+ return true;
21
+ }
22
+ catch {
23
+ return false;
24
+ }
25
+ }
26
+ async function createGithubRepo(cwd, name) {
27
+ const { stdout } = await execFileAsync("gh",
28
+ // gh requires an explicit visibility flag when non-interactive.
29
+ ["repo", "create", name, "--source=.", "--remote=origin", "--push", "--private"], { cwd, maxBuffer: 8 * 1024 * 1024 });
30
+ const match = stdout.match(/https:\/\/github\.com\/[^\s]+/);
31
+ if (match)
32
+ return match[0].replace(/\.git$/, "") + ".git";
33
+ const url = await gitRemoteUrl(cwd, "origin");
34
+ if (url)
35
+ return url;
36
+ throw new Error("gh repo create finished but origin URL could not be read.");
37
+ }
38
+ async function createGitlabRepo(cwd, name, defaultBranch) {
39
+ await execFileAsync("glab", ["repo", "create", name, "--remote=origin", "--defaultBranch", defaultBranch], { cwd, maxBuffer: 8 * 1024 * 1024 });
40
+ const url = await gitRemoteUrl(cwd, "origin");
41
+ if (url)
42
+ return url;
43
+ throw new Error("glab repo create finished but origin URL could not be read.");
44
+ }
45
+ /**
46
+ * Ensures `origin` (or named remote) exists. Interactive path can create via gh/glab or manual URL.
47
+ */
48
+ export async function ensureOriginRemote(opts) {
49
+ const remote = opts.remoteName ?? "origin";
50
+ const existing = await gitRemoteUrl(opts.cwd, remote);
51
+ if (existing) {
52
+ const host = detectGitHost(existing);
53
+ if (!host) {
54
+ log.warn(`Remote ${remote} URL is not GitHub or GitLab: ${existing}`);
55
+ if (opts.allowMissing)
56
+ return null;
57
+ throw new Error("Only GitHub and GitLab remotes are supported.");
58
+ }
59
+ return { remote, url: existing, host };
60
+ }
61
+ if (opts.dryRun) {
62
+ log.info(dim(`would configure git remote ${remote}`));
63
+ return { remote, url: "(dry-run)", host: "github" };
64
+ }
65
+ if (opts.allowMissing) {
66
+ return null;
67
+ }
68
+ if (opts.nonInteractive) {
69
+ throw new Error(`Git remote "${remote}" is not configured. Add it with: git remote add ${remote} <url>`);
70
+ }
71
+ log.blank();
72
+ log.info("Remote origin is required for push and merge requests.");
73
+ const ghOk = await probeGh();
74
+ const glabOk = await probeGlab();
75
+ if (ghOk) {
76
+ const name = await askInput({
77
+ message: "GitHub repository name (owner/repo or name for gh to create)",
78
+ nonInteractive: false,
79
+ required: true,
80
+ });
81
+ log.info(dim("Creating GitHub repository via gh…"));
82
+ const url = await createGithubRepo(opts.cwd, name);
83
+ return { remote, url, host: "github" };
84
+ }
85
+ if (glabOk) {
86
+ const name = await askInput({
87
+ message: "GitLab project path (group/project)",
88
+ nonInteractive: false,
89
+ required: true,
90
+ });
91
+ log.info(dim("Creating GitLab project via glab…"));
92
+ const defaultBranch = typeof opts.defaultBranch === "string" && opts.defaultBranch.length > 0
93
+ ? opts.defaultBranch
94
+ : "main";
95
+ const url = await createGitlabRepo(opts.cwd, name, defaultBranch);
96
+ return { remote, url, host: "gitlab" };
97
+ }
98
+ const url = await askInput({
99
+ message: `Remote URL for ${remote} (https://github.com/… or https://gitlab.com/…)`,
100
+ nonInteractive: false,
101
+ required: true,
102
+ });
103
+ await runGit(opts.cwd, ["remote", "add", remote, url.trim()]);
104
+ const host = detectGitHost(url);
105
+ if (!host)
106
+ throw new Error("Only GitHub and GitLab HTTPS/SSH URLs are supported.");
107
+ return { remote, url: url.trim(), host };
108
+ }
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)
@@ -370,7 +400,7 @@ export async function gitAddPaths(cwd, paths) {
370
400
  /**
371
401
  * When the index/worktree is dirty, classify whether every changed path is
372
402
  * limited to governance docs and VibeOps metadata (`.vibeops/**`, docs/tasks, etc.).
373
- * Used by `vibeops task start` so `task done --finalize` on main does not
403
+ * Used when switching task branches so governance paths are not left off-disk after switch.
374
404
  * block the next TASK unless application code is also dirty.
375
405
  */
376
406
  export async function gitGovernanceOnlyDirty(cwd) {
@@ -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
+ }
@@ -1,30 +1,20 @@
1
- import { probeCodexOAuthFile } from "./plan-codex-auth.js";
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 provider = opts.provider ?? (await resolveAvailableLlmProvider(opts.cwd));
11
+ const pref = opts.preference ?? "auto";
12
+ const provider = opts.provider ?? (await resolveLlmProviderForUse(opts.cwd, pref));
26
13
  if (provider === null) {
27
- throw new Error("No LLM provider available. Use Codex OAuth (~/.codex/auth.json), Cursor Agent CLI (`agent login`), or OPENAI_API_KEY.");
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 provider = opts.provider ?? (await resolveAvailableLlmProvider(opts.cwd));
35
+ const pref = opts.preference ?? "auto";
36
+ const provider = opts.provider ?? (await resolveLlmProviderForUse(opts.cwd, pref));
46
37
  if (provider === null) {
47
- throw new Error("No LLM provider available. Use Codex OAuth (~/.codex/auth.json), Cursor Agent CLI (`agent login`), or OPENAI_API_KEY.");
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
+ }
package/dist/lib/paths.js CHANGED
@@ -14,13 +14,17 @@ export function projectPaths(root) {
14
14
  envExample: join(abs, VIBEOPS_ENV_EXAMPLE_FILE),
15
15
  agentsMd: join(abs, "AGENTS.md"),
16
16
  cursorRules: join(abs, ".cursor", "rules"),
17
+ cursorSkills: join(abs, ".cursor", "skills"),
17
18
  docsProject: join(abs, "docs", "project"),
18
19
  docsTasks: join(abs, "docs", "tasks"),
19
20
  docsLogs: join(abs, "docs", "logs"),
20
21
  vibeopsDir: join(abs, ".vibeops"),
21
- vibeopsAgents: join(abs, ".vibeops", "agents"),
22
- vibeopsPrompts: join(abs, ".vibeops", "prompts"),
23
- vibeopsWorkflows: join(abs, ".vibeops", "workflows"),
24
22
  gitignore: join(abs, ".gitignore"),
25
23
  };
26
24
  }
25
+ /** Project memory files updated by `vibeops task done`. */
26
+ export const PROJECT_MEMORY_FILES = {
27
+ currentState: "docs/project/05-current-state.md",
28
+ architecture: "docs/project/03-architecture.md",
29
+ decisions: "docs/project/06-decisions.md",
30
+ };
@@ -29,7 +29,6 @@ export async function openAiChatCompletionJson(params) {
29
29
  }
30
30
  return content.trim();
31
31
  }
32
- /** Plain assistant text (no `response_format`) — used for planner markdown apply. */
33
32
  export async function openAiChatCompletionText(params) {
34
33
  const key = process.env.OPENAI_API_KEY?.trim();
35
34
  if (!key) {
@@ -70,65 +69,3 @@ export function extractJsonObject(text) {
70
69
  return trimmed.slice(start, end + 1);
71
70
  return trimmed;
72
71
  }
73
- export function parsePlanLlmTurn(jsonText) {
74
- const raw = JSON.parse(jsonText);
75
- const turn = raw.turn;
76
- if (turn === "done") {
77
- const pb = raw.projectBrief;
78
- if (!pb || typeof pb !== "object" || Array.isArray(pb)) {
79
- throw new Error('Invalid JSON: "done" turn requires object "projectBrief".');
80
- }
81
- const plannerAssumptions = raw.plannerAssumptions;
82
- const pa = Array.isArray(plannerAssumptions) && plannerAssumptions.every((x) => typeof x === "string")
83
- ? plannerAssumptions
84
- : undefined;
85
- return {
86
- turn: "done",
87
- projectBrief: pb,
88
- plannerAssumptions: pa,
89
- };
90
- }
91
- if (turn === "confirm") {
92
- const readableSummary = typeof raw.readableSummary === "string" ? raw.readableSummary.trim() : "";
93
- if (readableSummary.length === 0) {
94
- throw new Error('Invalid JSON: "confirm" turn needs non-empty "readableSummary" (markdown).');
95
- }
96
- const plannerNote = typeof raw.plannerNote === "string" ? raw.plannerNote.trim() : undefined;
97
- return {
98
- turn: "confirm",
99
- readableSummary,
100
- plannerNote: plannerNote && plannerNote.length > 0 ? plannerNote : undefined,
101
- };
102
- }
103
- if (turn === "question") {
104
- const message = typeof raw.message === "string" ? raw.message : "";
105
- if (message.length === 0) {
106
- throw new Error('Invalid JSON: "question" turn needs non-empty "message".');
107
- }
108
- const options = Array.isArray(raw.options)
109
- ? raw.options.filter((x) => typeof x === "string" && x.trim().length > 0)
110
- : [];
111
- const rawQt = raw.questionType;
112
- let questionType;
113
- if (rawQt === "single" || rawQt === "multi" || rawQt === "text") {
114
- questionType = rawQt;
115
- }
116
- else if (options.length >= 2) {
117
- /** Model often omits or mistypes questionType — multi is the safer default when lists are offered. */
118
- questionType = "multi";
119
- }
120
- else if (options.length === 1) {
121
- questionType = "single";
122
- }
123
- else {
124
- questionType = "text";
125
- }
126
- return {
127
- turn: "question",
128
- message,
129
- questionType,
130
- options: options.length > 0 ? options : undefined,
131
- };
132
- }
133
- throw new Error('Invalid JSON: "turn" must be "question", "confirm", or "done".');
134
- }