@controlvector/cv-agent 1.9.0 → 1.9.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/dist/bundle.cjs CHANGED
@@ -3847,10 +3847,10 @@ async function runSetup() {
3847
3847
  console.log(` ${source_default.cyan("a")} \u2014 Initialize a new project here: ${cwd}`);
3848
3848
  console.log(` ${source_default.cyan("b")} \u2014 Clone an existing repo from CV-Hub`);
3849
3849
  console.log();
3850
- const choice = await new Promise((resolve) => {
3850
+ const choice = await new Promise((resolve2) => {
3851
3851
  rl.question(" Choose [a/b]: ", (answer) => {
3852
3852
  rl.close();
3853
- resolve(answer.trim().toLowerCase() || "a");
3853
+ resolve2(answer.trim().toLowerCase() || "a");
3854
3854
  });
3855
3855
  });
3856
3856
  if (choice === "b" && token) {
@@ -3878,10 +3878,10 @@ async function runSetup() {
3878
3878
  });
3879
3879
  console.log();
3880
3880
  const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
3881
- const selection = await new Promise((resolve) => {
3881
+ const selection = await new Promise((resolve2) => {
3882
3882
  rl2.question(` Select a repo [1-${displayRepos.length}]: `, (answer) => {
3883
3883
  rl2.close();
3884
- resolve(answer.trim());
3884
+ resolve2(answer.trim());
3885
3885
  });
3886
3886
  });
3887
3887
  const idx = parseInt(selection, 10) - 1;
@@ -3929,6 +3929,41 @@ async function runSetup() {
3929
3929
  } else {
3930
3930
  console.log(source_default.green(" \u2713") + " CLAUDE.md present");
3931
3931
  }
3932
+ const gitignorePath = (0, import_node_path.join)(cwd, ".gitignore");
3933
+ const CREDENTIAL_PATTERNS = [
3934
+ "# Credentials and secrets (auto-added by cv-agent)",
3935
+ ".env",
3936
+ ".env.*",
3937
+ ".claude/",
3938
+ ".claude.json",
3939
+ ".credentials*",
3940
+ "*.pem",
3941
+ "*.key",
3942
+ ".ssh/",
3943
+ ".gnupg/",
3944
+ ".npm/",
3945
+ ".config/",
3946
+ ".zsh_history",
3947
+ ".bash_history",
3948
+ ".zsh_sessions/",
3949
+ "node_modules/",
3950
+ ".DS_Store"
3951
+ ];
3952
+ try {
3953
+ let gitignoreContent = "";
3954
+ if ((0, import_node_fs.existsSync)(gitignorePath)) {
3955
+ gitignoreContent = (0, import_node_fs.readFileSync)(gitignorePath, "utf-8");
3956
+ }
3957
+ const missing = CREDENTIAL_PATTERNS.filter((p) => !gitignoreContent.includes(p));
3958
+ if (missing.length > 0) {
3959
+ const addition = (gitignoreContent && !gitignoreContent.endsWith("\n") ? "\n" : "") + missing.join("\n") + "\n";
3960
+ (0, import_node_fs.writeFileSync)(gitignorePath, gitignoreContent + addition);
3961
+ console.log(source_default.green(" \u2713") + " .gitignore updated with credential protection");
3962
+ } else {
3963
+ console.log(source_default.green(" \u2713") + " .gitignore has credential protection");
3964
+ }
3965
+ } catch {
3966
+ }
3932
3967
  if (!hasCVDir) {
3933
3968
  (0, import_node_fs.mkdirSync)((0, import_node_path.join)(cwd, ".cv"), { recursive: true });
3934
3969
  }
@@ -4015,10 +4050,10 @@ async function runSetup() {
4015
4050
  if (!agentRunning) {
4016
4051
  const readline = await import("node:readline");
4017
4052
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
4018
- const answer = await new Promise((resolve) => {
4053
+ const answer = await new Promise((resolve2) => {
4019
4054
  rl.question(" Start the CV-Agent daemon? (Y/n): ", (a) => {
4020
4055
  rl.close();
4021
- resolve(a.trim().toLowerCase() || "y");
4056
+ resolve2(a.trim().toLowerCase() || "y");
4022
4057
  });
4023
4058
  });
4024
4059
  if (answer === "y" || answer === "yes" || answer === "") {
@@ -4558,11 +4593,78 @@ async function writeConfig(config) {
4558
4593
 
4559
4594
  // src/commands/git-safety.ts
4560
4595
  var import_node_child_process3 = require("node:child_process");
4596
+ var import_node_fs2 = require("node:fs");
4597
+ var import_node_path2 = require("node:path");
4598
+ var import_node_os3 = require("node:os");
4599
+ var DANGEROUS_PATTERNS = [
4600
+ ".claude/",
4601
+ ".claude.json",
4602
+ ".credentials",
4603
+ ".zsh_history",
4604
+ ".bash_history",
4605
+ ".zsh_sessions/",
4606
+ ".ssh/",
4607
+ ".gnupg/",
4608
+ ".npm/",
4609
+ ".config/",
4610
+ ".CFUserTextEncoding",
4611
+ "Library/",
4612
+ "Applications/",
4613
+ ".Trash/",
4614
+ ".DS_Store",
4615
+ "node_modules/",
4616
+ ".env",
4617
+ ".env.local",
4618
+ ".env.production"
4619
+ ];
4561
4620
  function git(cmd, cwd) {
4562
4621
  return (0, import_node_child_process3.execSync)(cmd, { cwd, encoding: "utf8", timeout: 3e4 }).trim();
4563
4622
  }
4623
+ function checkWorkspaceSafety(workspaceRoot) {
4624
+ const resolved = (0, import_node_path2.resolve)(workspaceRoot);
4625
+ const home = (0, import_node_path2.resolve)((0, import_node_os3.homedir)());
4626
+ if (resolved === home) {
4627
+ return `Workspace is user HOME directory (${home}). Refusing to auto-commit to prevent indexing personal files.`;
4628
+ }
4629
+ if (home.startsWith(resolved + "/")) {
4630
+ return `Workspace (${resolved}) is a parent of HOME. Refusing to auto-commit.`;
4631
+ }
4632
+ if (!(0, import_node_fs2.existsSync)((0, import_node_path2.join)(resolved, ".git"))) {
4633
+ return `No .git directory in ${resolved}. Not a git repository.`;
4634
+ }
4635
+ return null;
4636
+ }
4637
+ function filterDangerousFiles(statusLines) {
4638
+ const safe = [];
4639
+ const blocked = [];
4640
+ for (const line of statusLines) {
4641
+ const filePath = line.substring(3).trim();
4642
+ const isDangerous = DANGEROUS_PATTERNS.some(
4643
+ (pattern) => filePath.startsWith(pattern) || filePath.includes("/" + pattern)
4644
+ );
4645
+ if (isDangerous) {
4646
+ blocked.push(filePath);
4647
+ } else {
4648
+ safe.push(line);
4649
+ }
4650
+ }
4651
+ return { safe, blocked };
4652
+ }
4564
4653
  function gitSafetyNet(workspaceRoot, taskTitle, taskId, branch) {
4565
4654
  const targetBranch = branch || "main";
4655
+ const safetyError = checkWorkspaceSafety(workspaceRoot);
4656
+ if (safetyError) {
4657
+ console.log(` [git-safety] BLOCKED: ${safetyError}`);
4658
+ return {
4659
+ hadChanges: false,
4660
+ filesAdded: 0,
4661
+ filesModified: 0,
4662
+ filesDeleted: 0,
4663
+ pushed: false,
4664
+ skipped: true,
4665
+ skipReason: safetyError
4666
+ };
4667
+ }
4566
4668
  try {
4567
4669
  let statusOutput;
4568
4670
  try {
@@ -4570,8 +4672,8 @@ function gitSafetyNet(workspaceRoot, taskTitle, taskId, branch) {
4570
4672
  } catch {
4571
4673
  return { hadChanges: false, filesAdded: 0, filesModified: 0, filesDeleted: 0, pushed: false, error: "git status failed" };
4572
4674
  }
4573
- const lines = statusOutput.split("\n").filter(Boolean);
4574
- if (lines.length === 0) {
4675
+ const allLines = statusOutput.split("\n").filter(Boolean);
4676
+ if (allLines.length === 0) {
4575
4677
  try {
4576
4678
  const unpushed = git(`git log origin/${targetBranch}..HEAD --oneline 2>/dev/null`, workspaceRoot);
4577
4679
  if (unpushed) {
@@ -4583,8 +4685,16 @@ function gitSafetyNet(workspaceRoot, taskTitle, taskId, branch) {
4583
4685
  }
4584
4686
  return { hadChanges: false, filesAdded: 0, filesModified: 0, filesDeleted: 0, pushed: false };
4585
4687
  }
4688
+ const { safe: safeLines, blocked } = filterDangerousFiles(allLines);
4689
+ if (blocked.length > 0) {
4690
+ console.log(` [git-safety] Blocked ${blocked.length} sensitive file(s) from staging: ${blocked.slice(0, 5).join(", ")}${blocked.length > 5 ? "..." : ""}`);
4691
+ }
4692
+ if (safeLines.length === 0) {
4693
+ console.log(` [git-safety] All ${allLines.length} changed files were blocked by safety filter`);
4694
+ return { hadChanges: false, filesAdded: 0, filesModified: 0, filesDeleted: 0, pushed: false };
4695
+ }
4586
4696
  let added = 0, modified = 0, deleted = 0;
4587
- for (const line of lines) {
4697
+ for (const line of safeLines) {
4588
4698
  const code = line.substring(0, 2);
4589
4699
  if (code.includes("?")) added++;
4590
4700
  else if (code.includes("D")) deleted++;
@@ -4592,9 +4702,15 @@ function gitSafetyNet(workspaceRoot, taskTitle, taskId, branch) {
4592
4702
  else added++;
4593
4703
  }
4594
4704
  console.log(
4595
- ` [git-safety] ${lines.length} uncommitted changes (${added} new, ${modified} modified, ${deleted} deleted) \u2014 committing now`
4705
+ ` [git-safety] ${safeLines.length} safe changes (${added} new, ${modified} modified, ${deleted} deleted) \u2014 committing now`
4596
4706
  );
4597
- git("git add -A", workspaceRoot);
4707
+ for (const line of safeLines) {
4708
+ const filePath = line.substring(3).trim();
4709
+ try {
4710
+ git(`git add -- ${JSON.stringify(filePath)}`, workspaceRoot);
4711
+ } catch {
4712
+ }
4713
+ }
4598
4714
  const shortId = taskId.substring(0, 8);
4599
4715
  const commitMsg = `task: ${taskTitle} [${shortId}]
4600
4716
 
@@ -4655,8 +4771,8 @@ Files: ${added} added, ${modified} modified, ${deleted} deleted`;
4655
4771
 
4656
4772
  // src/commands/deploy-manifest.ts
4657
4773
  var import_node_child_process4 = require("node:child_process");
4658
- var import_node_fs2 = require("node:fs");
4659
- var import_node_path2 = require("node:path");
4774
+ var import_node_fs3 = require("node:fs");
4775
+ var import_node_path3 = require("node:path");
4660
4776
  function exec(cmd, cwd, timeoutMs = 3e5, env2) {
4661
4777
  try {
4662
4778
  const stdout = (0, import_node_child_process4.execSync)(cmd, {
@@ -4699,10 +4815,10 @@ function getHeadCommit(cwd) {
4699
4815
  }
4700
4816
  }
4701
4817
  function loadDeployManifest(workspaceRoot) {
4702
- const manifestPath = (0, import_node_path2.join)(workspaceRoot, ".cva", "deploy.json");
4703
- if (!(0, import_node_fs2.existsSync)(manifestPath)) return null;
4818
+ const manifestPath = (0, import_node_path3.join)(workspaceRoot, ".cva", "deploy.json");
4819
+ if (!(0, import_node_fs3.existsSync)(manifestPath)) return null;
4704
4820
  try {
4705
- const raw = (0, import_node_fs2.readFileSync)(manifestPath, "utf8");
4821
+ const raw = (0, import_node_fs3.readFileSync)(manifestPath, "utf8");
4706
4822
  const manifest = JSON.parse(raw);
4707
4823
  if (!manifest.version || manifest.version < 1) {
4708
4824
  console.log(" [deploy] Invalid manifest version");
@@ -4727,7 +4843,7 @@ async function postTaskDeploy(workspaceRoot, taskId, log) {
4727
4843
  console.log(` [deploy] Building: ${buildStep.name}`);
4728
4844
  log("lifecycle", `Building: ${buildStep.name}`);
4729
4845
  const start = Date.now();
4730
- const cwd = (0, import_node_path2.join)(workspaceRoot, buildStep.working_dir || ".");
4846
+ const cwd = (0, import_node_path3.join)(workspaceRoot, buildStep.working_dir || ".");
4731
4847
  const timeoutMs = (buildStep.timeout_seconds || 300) * 1e3;
4732
4848
  const result = exec(buildStep.command, cwd, timeoutMs);
4733
4849
  if (!result.ok) {
@@ -4818,7 +4934,7 @@ async function rollback(workspaceRoot, targetCommit, manifest, log) {
4818
4934
  }
4819
4935
  if (manifest.build?.steps) {
4820
4936
  for (const step of manifest.build.steps) {
4821
- exec(step.command, (0, import_node_path2.join)(workspaceRoot, step.working_dir || "."), (step.timeout_seconds || 300) * 1e3);
4937
+ exec(step.command, (0, import_node_path3.join)(workspaceRoot, step.working_dir || "."), (step.timeout_seconds || 300) * 1e3);
4822
4938
  }
4823
4939
  }
4824
4940
  if (manifest.service?.restart_command) {
@@ -4976,6 +5092,22 @@ Target repository: ${task.owner}/${task.repo}
4976
5092
  `;
4977
5093
  prompt += `
4978
5094
 
5095
+ ## Security \u2014 NEVER Commit Secrets
5096
+ `;
5097
+ prompt += `Do NOT add, commit, or push any of the following:
5098
+ `;
5099
+ prompt += `- API keys, tokens, passwords, or credentials
5100
+ `;
5101
+ prompt += `- .env files, .credentials files, .claude/ directory
5102
+ `;
5103
+ prompt += `- SSH keys (.ssh/), GPG keys (.gnupg/)
5104
+ `;
5105
+ prompt += `- Shell history (.zsh_history, .bash_history)
5106
+ `;
5107
+ prompt += `If you need to reference an API key, use an environment variable placeholder.
5108
+ `;
5109
+ prompt += `
5110
+
4979
5111
  ---
4980
5112
  `;
4981
5113
  prompt += `When complete, provide a brief summary of what you accomplished.
@@ -5054,7 +5186,7 @@ async function launchAutoApproveMode(prompt, options) {
5054
5186
  let fullOutput = "";
5055
5187
  let lastProgressBytes = 0;
5056
5188
  const runOnce = (inputPrompt, isContinue) => {
5057
- return new Promise((resolve, reject) => {
5189
+ return new Promise((resolve2, reject) => {
5058
5190
  const args = isContinue ? ["-p", inputPrompt, "--continue", "--allowedTools", ...ALLOWED_TOOLS] : ["-p", inputPrompt, "--allowedTools", ...ALLOWED_TOOLS];
5059
5191
  if (sessionId && !isContinue) {
5060
5192
  args.push("--session-id", sessionId);
@@ -5153,7 +5285,7 @@ ${source_default.red("!")} Claude Code auth failure (stderr): "${authError}"`);
5153
5285
  });
5154
5286
  child.on("close", (code, signal) => {
5155
5287
  _activeChild = null;
5156
- resolve({
5288
+ resolve2({
5157
5289
  exitCode: signal === "SIGKILL" ? 137 : code ?? 1,
5158
5290
  stderr,
5159
5291
  output: fullOutput,
@@ -5192,7 +5324,7 @@ ${source_default.red("!")} Claude Code auth failure (stderr): "${authError}"`);
5192
5324
  return result;
5193
5325
  }
5194
5326
  async function launchRelayMode(prompt, options) {
5195
- return new Promise((resolve, reject) => {
5327
+ return new Promise((resolve2, reject) => {
5196
5328
  const child = (0, import_node_child_process5.spawn)("claude", [], {
5197
5329
  cwd: options.cwd,
5198
5330
  stdio: ["pipe", "pipe", "pipe"],
@@ -5385,9 +5517,9 @@ ${source_default.red("!")} Claude Code auth failure (stderr): "${authError}"`);
5385
5517
  child.on("close", (code, signal) => {
5386
5518
  _activeChild = null;
5387
5519
  if (signal === "SIGKILL") {
5388
- resolve({ exitCode: 137, stderr, output: fullOutput, authFailure });
5520
+ resolve2({ exitCode: 137, stderr, output: fullOutput, authFailure });
5389
5521
  } else {
5390
- resolve({ exitCode: code ?? 1, stderr, output: fullOutput, authFailure });
5522
+ resolve2({ exitCode: code ?? 1, stderr, output: fullOutput, authFailure });
5391
5523
  }
5392
5524
  });
5393
5525
  child.on("error", (err) => {
@@ -5488,14 +5620,45 @@ async function runAgent(options) {
5488
5620
  console.log();
5489
5621
  process.exit(1);
5490
5622
  }
5623
+ if (process.env.CV_DEBUG) {
5624
+ console.log(source_default.gray(` PATH: ${process.env.PATH}`));
5625
+ console.log(source_default.gray(` HOME: ${process.env.HOME}`));
5626
+ console.log(source_default.gray(` SHELL: ${process.env.SHELL}`));
5627
+ }
5628
+ let claudeBinary = "claude";
5491
5629
  try {
5492
5630
  (0, import_node_child_process5.execSync)("claude --version", { stdio: "pipe", timeout: 5e3 });
5493
5631
  } catch {
5494
- console.log();
5495
- console.log(source_default.red("Claude Code CLI not found.") + " Install it first:");
5496
- console.log(` ${source_default.cyan("npm install -g @anthropic-ai/claude-code")}`);
5497
- console.log();
5498
- process.exit(1);
5632
+ const candidates = [
5633
+ "/usr/local/bin/claude",
5634
+ "/opt/homebrew/bin/claude",
5635
+ `${process.env.HOME}/.npm-global/bin/claude`,
5636
+ `${process.env.HOME}/.nvm/versions/node/*/bin/claude`
5637
+ ];
5638
+ let found = false;
5639
+ for (const candidate of candidates) {
5640
+ try {
5641
+ const resolved = (0, import_node_child_process5.execSync)(`ls ${candidate} 2>/dev/null | head -1`, { encoding: "utf8", timeout: 3e3 }).trim();
5642
+ if (resolved) {
5643
+ (0, import_node_child_process5.execSync)(`${resolved} --version`, { stdio: "pipe", timeout: 5e3 });
5644
+ claudeBinary = resolved;
5645
+ found = true;
5646
+ console.log(source_default.yellow("!") + ` Claude Code found at ${resolved} (not on PATH)`);
5647
+ break;
5648
+ }
5649
+ } catch {
5650
+ }
5651
+ }
5652
+ if (!found) {
5653
+ console.log();
5654
+ console.log(source_default.red("Claude Code CLI not found.") + " Install it first:");
5655
+ console.log(` ${source_default.cyan("npm install -g @anthropic-ai/claude-code")}`);
5656
+ console.log();
5657
+ console.log(source_default.gray(` Current PATH: ${process.env.PATH}`));
5658
+ console.log(source_default.gray(" If running via Launch Agent, ensure PATH includes the directory where claude is installed."));
5659
+ console.log();
5660
+ process.exit(1);
5661
+ }
5499
5662
  }
5500
5663
  const { env: claudeEnv, usingApiKey } = await getClaudeEnv();
5501
5664
  const authCheck = await checkClaudeAuth();
@@ -5562,6 +5725,22 @@ async function runAgent(options) {
5562
5725
  } catch {
5563
5726
  }
5564
5727
  }
5728
+ const gitignorePath = require("path").join(workingDir, ".gitignore");
5729
+ try {
5730
+ const fs3 = require("fs");
5731
+ const credPatterns = ".env\n.env.*\n.claude/\n.claude.json\n.credentials*\n*.pem\n*.key\n.ssh/\n.gnupg/\n.npm/\n.config/\n.zsh_history\n.bash_history\nnode_modules/\n.DS_Store\n";
5732
+ let existing = "";
5733
+ try {
5734
+ existing = fs3.readFileSync(gitignorePath, "utf-8");
5735
+ } catch {
5736
+ }
5737
+ if (!existing.includes(".claude/")) {
5738
+ const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
5739
+ fs3.writeFileSync(gitignorePath, existing + prefix + "# Credentials (auto-added by cv-agent)\n" + credPatterns);
5740
+ console.log(source_default.gray(" Bootstrap: .gitignore credential protection added"));
5741
+ }
5742
+ } catch {
5743
+ }
5565
5744
  const cvDir = require("path").join(workingDir, ".cv");
5566
5745
  if (!require("fs").existsSync(cvDir)) {
5567
5746
  try {
@@ -6049,7 +6228,7 @@ async function promptForToken() {
6049
6228
  input: process.stdin,
6050
6229
  output: process.stdout
6051
6230
  });
6052
- return new Promise((resolve) => {
6231
+ return new Promise((resolve2) => {
6053
6232
  rl.question("Enter your CV-Hub PAT token: ", (answer) => {
6054
6233
  rl.close();
6055
6234
  const token = answer.trim();
@@ -6057,7 +6236,7 @@ async function promptForToken() {
6057
6236
  console.log(source_default.red("No token provided."));
6058
6237
  process.exit(1);
6059
6238
  }
6060
- resolve(token);
6239
+ resolve2(token);
6061
6240
  });
6062
6241
  });
6063
6242
  }
@@ -6336,7 +6515,7 @@ function statusCommand() {
6336
6515
 
6337
6516
  // src/index.ts
6338
6517
  var program2 = new Command();
6339
- program2.name("cva").description('CV-Hub Agent \u2014 bridges Claude Code with CV-Hub task dispatch.\n\nRun "cva setup" to get started.').version(true ? "1.9.0" : "1.6.0");
6518
+ program2.name("cva").description('CV-Hub Agent \u2014 bridges Claude Code with CV-Hub task dispatch.\n\nRun "cva setup" to get started.').version(true ? "1.9.2" : "1.6.0");
6340
6519
  program2.addCommand(setupCommand());
6341
6520
  program2.addCommand(agentCommand());
6342
6521
  program2.addCommand(authCommand());