@nalvietnam/avatar-cli 1.6.3 → 1.7.0

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/index.js CHANGED
@@ -761,6 +761,14 @@ async function writeClaudeSettings(workspacePath, input6) {
761
761
 
762
762
  // src/lib/run-ai-setup-phase.ts
763
763
  var SUBSCRIPTION_DEFAULT_MODEL = "sonnet";
764
+ function warnAboutPlaintextSecret(providerLabel) {
765
+ log.warn(
766
+ `\u{1F512} ${providerLabel} key \u0111\xE3 l\u01B0u PLAINTEXT v\xE0o .claude/settings.json.
767
+ File n\xE0y \u0111\u01B0\u1EE3c gitignore t\u1EEB Avatar v1.7.0, nh\u01B0ng workspace c\u0169 c\xF3 th\u1EC3 CH\u01AFA.
768
+ Ki\u1EC3m tra: grep '.claude/settings.json' .gitignore
769
+ N\u1EBFu ch\u01B0a c\xF3 \u2192 TH\xCAM NGAY, tr\xE1nh leak key khi commit/push.`
770
+ );
771
+ }
764
772
  async function runAiSetupPhase(args) {
765
773
  try {
766
774
  log.info("Setup AI provider cho workspace...");
@@ -819,6 +827,7 @@ async function runAiSetupPhase(args) {
819
827
  `provider=llmlite,result=ok,model=${llmConfig.model},base=${llmConfig.baseUrl}`
820
828
  );
821
829
  log.success(`AI ready \xB7 LLMLite \xB7 model=${llmConfig.model} \xB7 ${llmConfig.baseUrl}`);
830
+ warnAboutPlaintextSecret("LLMLite");
822
831
  return { ok: true, provider: "llmlite", model: llmConfig.model };
823
832
  }
824
833
  case "anthropic": {
@@ -836,6 +845,7 @@ async function runAiSetupPhase(args) {
836
845
  log.success(
837
846
  `AI ready \xB7 Anthropic Direct \xB7 model=${anthropicConfig.model} \xB7 ${anthropicConfig.baseUrl}`
838
847
  );
848
+ warnAboutPlaintextSecret("Anthropic Direct API");
839
849
  return { ok: true, provider: "anthropic", model: anthropicConfig.model };
840
850
  }
841
851
  case "use-global": {
@@ -1494,11 +1504,12 @@ async function runChecks(cwd) {
1494
1504
  });
1495
1505
  }
1496
1506
  const gitignorePath = join11(cwd, ".gitignore");
1507
+ let gitignoreContent = "";
1497
1508
  if (gitRepo) {
1498
1509
  let gitignoreOk = false;
1499
1510
  if (await pathExists(gitignorePath)) {
1500
- const content = await fs6.readFile(gitignorePath, "utf8");
1501
- gitignoreOk = content.includes(".claude/_pending/");
1511
+ gitignoreContent = await fs6.readFile(gitignorePath, "utf8");
1512
+ gitignoreOk = gitignoreContent.includes(".claude/_pending/");
1502
1513
  }
1503
1514
  checks.push({
1504
1515
  name: ".gitignore Avatar entries",
@@ -1506,6 +1517,66 @@ async function runChecks(cwd) {
1506
1517
  detail: gitignoreOk ? "c\xF3 .claude/_pending/, .claude/_backup/" : "thi\u1EBFu entries",
1507
1518
  fixable: false
1508
1519
  });
1520
+ const settingsGitignored = gitignoreContent.includes(".claude/settings.json") || gitignoreContent.includes("/.claude/settings.json") || gitignoreContent.includes(".claude/*.json");
1521
+ checks.push({
1522
+ name: "\u{1F512} settings.json gitignored",
1523
+ status: settingsGitignored ? "ok" : "fail",
1524
+ detail: settingsGitignored ? "an to\xE0n \u2014 settings.json kh\xF4ng commit nh\u1EA7m" : "CRITICAL: settings.json ch\u1EE9a API key \u2014 ch\u1EA1y 'avatar doctor --fix' \u0111\u1EC3 add gitignore",
1525
+ fixable: !settingsGitignored,
1526
+ fix: settingsGitignored ? void 0 : async () => {
1527
+ const addition = "\n# Avatar v1.7.0 \u2014 Security: settings.json ch\u1EE9a raw API key, KH\xD4NG commit.\n.claude/settings.json\n.claude/settings.json.backup-*\n";
1528
+ await fs6.appendFile(gitignorePath, addition);
1529
+ }
1530
+ });
1531
+ }
1532
+ const pythonCheck = spawnSync6("which", ["python"]);
1533
+ const python3Check = spawnSync6("which", ["python3"]);
1534
+ const hasPython = pythonCheck.status === 0;
1535
+ const hasPython3 = python3Check.status === 0;
1536
+ if (hasPython3 && !hasPython) {
1537
+ checks.push({
1538
+ name: "Python binary alias",
1539
+ status: "warn",
1540
+ detail: `Ch\u1EC9 c\xF3 python3 (modern macOS). Pack scripts th\u01B0\u1EDDng ref 'python' \u2192 suggest: ln -s ${python3Check.stdout.toString().trim()} ~/.local/bin/python`,
1541
+ fixable: false
1542
+ });
1543
+ } else if (hasPython) {
1544
+ checks.push({
1545
+ name: "Python binary",
1546
+ status: "ok",
1547
+ detail: `python: ${pythonCheck.stdout.toString().trim()}`,
1548
+ fixable: false
1549
+ });
1550
+ } else if (hasPython3) {
1551
+ checks.push({
1552
+ name: "Python binary",
1553
+ status: "ok",
1554
+ detail: `python3: ${python3Check.stdout.toString().trim()}`,
1555
+ fixable: false
1556
+ });
1557
+ }
1558
+ const settingsPath = join11(cwd, ".claude", "settings.json");
1559
+ if (await pathExists(settingsPath)) {
1560
+ try {
1561
+ const settingsRaw = await fs6.readFile(settingsPath, "utf8");
1562
+ const settings = JSON.parse(settingsRaw);
1563
+ if (settings.statusLine?.command) {
1564
+ const cmd = settings.statusLine.command.trim();
1565
+ const match = cmd.match(/^(node|python|python3|bash|sh)\s+([^\s]+)/);
1566
+ if (match) {
1567
+ const refFile = match[2];
1568
+ const fullPath = refFile.startsWith("/") ? refFile : join11(cwd, refFile);
1569
+ const fileExists = await pathExists(fullPath);
1570
+ checks.push({
1571
+ name: "statusLine command",
1572
+ status: fileExists ? "ok" : "fail",
1573
+ detail: fileExists ? `ref OK: ${refFile}` : `BROKEN: settings.json ref '${refFile}' nh\u01B0ng file kh\xF4ng t\u1ED3n t\u1EA1i. Strip field statusLine ho\u1EB7c fix path.`,
1574
+ fixable: false
1575
+ });
1576
+ }
1577
+ }
1578
+ } catch {
1579
+ }
1509
1580
  }
1510
1581
  const which = spawnSync6("which", ["claude"]);
1511
1582
  const hasClaudeCli = which.status === 0;
@@ -1850,6 +1921,28 @@ import { spawnSync as spawnSync10 } from "child_process";
1850
1921
  import { existsSync as existsSync5 } from "fs";
1851
1922
  import { join as join14 } from "path";
1852
1923
  import { confirm as confirm2 } from "@inquirer/prompts";
1924
+
1925
+ // src/lib/detect-reasoning-model-from-name.ts
1926
+ var REASONING_PATTERNS = [
1927
+ // Anthropic Claude 4+ với extended thinking.
1928
+ // Match: claude-opus-4-7, claude-opus-4-8, claude-sonnet-4-5, claude-opus-4-1, ...
1929
+ /^claude-(opus|sonnet)-4/i,
1930
+ // Anthropic Claude 5+ (future-proof — assume reasoning theo trend).
1931
+ /^claude-(opus|sonnet|haiku)-[5-9]/i,
1932
+ // OpenAI o-series (o1, o3, o4).
1933
+ /^o1(-|$)/i,
1934
+ /^o3(-|$)/i,
1935
+ /^o4(-|$)/i,
1936
+ // LLMLite NAL alias mapping — phổ biến nal-claude-opus-* trỏ tới opus-4+.
1937
+ // (Conservative: chỉ match nếu name có chứa opus/sonnet + version số.)
1938
+ /nal-claude-(opus|sonnet)-?4/i
1939
+ ];
1940
+ function isReasoningModel(modelName) {
1941
+ if (!modelName) return false;
1942
+ return REASONING_PATTERNS.some((pattern) => pattern.test(modelName));
1943
+ }
1944
+
1945
+ // src/lib/run-gitnexus-wiki-conditional.ts
1853
1946
  var WIKI_TIMEOUT_MS = 15 * 60 * 1e3;
1854
1947
  var FALLBACK_LLMLITE_MODEL = "nal-claude";
1855
1948
  var FALLBACK_ANTHROPIC_MODEL = "claude-sonnet-4-5";
@@ -1919,19 +2012,29 @@ async function runGitnexusWikiConditional(workspacePath) {
1919
2012
  );
1920
2013
  return { ran: false, skipped: true, reason: "user-declined" };
1921
2014
  }
2015
+ const reasoningMode = isReasoningModel(creds.model);
2016
+ const args = [
2017
+ "wiki",
2018
+ ".",
2019
+ "--api-key",
2020
+ creds.apiKey,
2021
+ "--base-url",
2022
+ creds.baseUrl,
2023
+ "--model",
2024
+ creds.model
2025
+ ];
2026
+ if (reasoningMode) {
2027
+ args.push("--reasoning-model");
2028
+ }
1922
2029
  const sp = spinnerWithElapsed(
1923
- `Generating wiki via ${creds.baseUrl} (${creds.provider}) model=${creds.model}`
1924
- );
1925
- const result = spawnSync10(
1926
- "gitnexus",
1927
- ["wiki", ".", "--api-key", creds.apiKey, "--base-url", creds.baseUrl, "--model", creds.model],
1928
- {
1929
- cwd: workspacePath,
1930
- stdio: ["ignore", "pipe", "pipe"],
1931
- timeout: WIKI_TIMEOUT_MS,
1932
- encoding: "utf8"
1933
- }
2030
+ `Generating wiki via ${creds.baseUrl} (${creds.provider}) model=${creds.model}${reasoningMode ? " [reasoning]" : ""}`
1934
2031
  );
2032
+ const result = spawnSync10("gitnexus", args, {
2033
+ cwd: workspacePath,
2034
+ stdio: ["ignore", "pipe", "pipe"],
2035
+ timeout: WIKI_TIMEOUT_MS,
2036
+ encoding: "utf8"
2037
+ });
1935
2038
  if (result.status !== 0 || result.signal === "SIGTERM") {
1936
2039
  const reason = result.signal === "SIGTERM" ? "timeout" : "non-zero-exit";
1937
2040
  sp.fail(`Wiki gen ${reason} (exit ${result.status ?? "null"})`);
@@ -3065,6 +3168,16 @@ function linkExistingRemoteToWorkspace(args) {
3065
3168
  // src/lib/merge-pack-settings-into-project-settings.ts
3066
3169
  import { promises as fs9 } from "fs";
3067
3170
  import { join as join17 } from "path";
3171
+ async function isStatusLineCommandResolvable(workspacePath, command) {
3172
+ const trimmed = command.trim();
3173
+ const match = trimmed.match(/^(node|python|python3|bash|sh)\s+([^\s]+)/);
3174
+ if (!match) {
3175
+ return true;
3176
+ }
3177
+ const filePath = match[2];
3178
+ const fullPath = filePath.startsWith("/") ? filePath : join17(workspacePath, filePath);
3179
+ return await pathExists(fullPath);
3180
+ }
3068
3181
  function backupFilename(originalPath) {
3069
3182
  const d = /* @__PURE__ */ new Date();
3070
3183
  const stamp = `${d.getFullYear().toString().slice(-2) + String(d.getMonth() + 1).padStart(2, "0") + String(d.getDate()).padStart(2, "0")}-${String(d.getHours()).padStart(2, "0")}${String(d.getMinutes()).padStart(2, "0")}`;
@@ -3125,8 +3238,18 @@ async function mergePackSettingsIntoProjectSettings(workspacePath) {
3125
3238
  const changes = [];
3126
3239
  const merged = { ...userSettings };
3127
3240
  if (packTemplate.statusLine && !userSettings.statusLine) {
3128
- merged.statusLine = packTemplate.statusLine;
3129
- changes.push("statusLine added");
3241
+ const statusLineOk = await isStatusLineCommandResolvable(
3242
+ workspacePath,
3243
+ packTemplate.statusLine.command
3244
+ );
3245
+ if (statusLineOk) {
3246
+ merged.statusLine = packTemplate.statusLine;
3247
+ changes.push("statusLine added");
3248
+ } else {
3249
+ changes.push(
3250
+ `statusLine SKIPPED (file ref '${packTemplate.statusLine.command}' kh\xF4ng t\u1ED3n t\u1EA1i)`
3251
+ );
3252
+ }
3130
3253
  }
3131
3254
  if (typeof packTemplate.includeCoAuthoredBy === "boolean" && typeof userSettings.includeCoAuthoredBy !== "boolean") {
3132
3255
  merged.includeCoAuthoredBy = packTemplate.includeCoAuthoredBy;
@@ -4808,7 +4931,7 @@ async function removeSubmoduleEntry(gitmodulesPath, submodulePath) {
4808
4931
  }
4809
4932
 
4810
4933
  // src/commands/uninstall.ts
4811
- var CLI_VERSION = "1.6.3";
4934
+ var CLI_VERSION = "1.7.0";
4812
4935
  function registerUninstallCommand(program2) {
4813
4936
  program2.command("uninstall").description("G\u1EE1 Avatar kh\u1ECFi project \u2014 backup t\u1EF1 \u0111\u1ED9ng (M11)").option("--yes", "Skip confirm prompt").option("--no-backup", "Kh\xF4ng t\u1EA1o backup tr\u01B0\u1EDBc khi x\xF3a (nguy hi\u1EC3m)").option("--keep-submodule", "Gi\u1EEF submodule .claude/pack/").option("--keep-hooks", "Gi\u1EEF git hooks post-merge, pre-push").option("--dry-run", "Hi\u1EC3n th\u1ECB danh s\xE1ch s\u1EBD x\xF3a, kh\xF4ng th\u1EF1c thi").action(async (opts) => {
4814
4937
  try {
@@ -4890,7 +5013,7 @@ function printUninstallSuccessBox(backupPath) {
4890
5013
  }
4891
5014
 
4892
5015
  // src/index.ts
4893
- var CLI_VERSION2 = "1.6.3";
5016
+ var CLI_VERSION2 = "1.7.0";
4894
5017
  var program = new Command();
4895
5018
  program.name("avatar").description("AI harness CLI for NAL Vietnam engineering").version(CLI_VERSION2, "-v, --version", "Hi\u1EC3n th\u1ECB phi\xEAn b\u1EA3n Avatar CLI").addHelpText(
4896
5019
  "beforeAll",