@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
|
-
|
|
1501
|
-
gitignoreOk =
|
|
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
|
-
|
|
3129
|
-
|
|
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.
|
|
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.
|
|
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",
|