@nalvietnam/avatar-cli 1.5.0 → 1.6.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/dist/index.js CHANGED
@@ -473,27 +473,126 @@ async function promptAiProviderChoice(globalInfo = detectGlobalClaudeSettings())
473
473
  message: "Ch\u1ECDn provider cho AI features:",
474
474
  choices: [
475
475
  {
476
- name: "1. Claude Code Subscription (d\xF9ng quota c\xE1 nh\xE2n Anthropic)",
476
+ name: "1. Claude Code Subscription (d\xF9ng quota c\xE1 nh\xE2n Anthropic, OAuth login)",
477
477
  value: "subscription"
478
478
  },
479
479
  {
480
- name: "2. LLMLite API key (llm.nal.vn \u2014 NAL c\u1EA5p)",
480
+ name: "2. LLMLite API key (llm.nal.vn \u2014 NAL gateway, key sk-...)",
481
481
  value: "llmlite"
482
+ },
483
+ {
484
+ name: "3. Anthropic API key tr\u1EF1c ti\u1EBFp (console.anthropic.com, key sk-ant-...)",
485
+ value: "anthropic"
482
486
  }
483
487
  ]
484
488
  });
485
489
  }
486
490
 
491
+ // src/lib/setup-anthropic-api-key-and-model.ts
492
+ import { password, select as select2 } from "@inquirer/prompts";
493
+ var ANTHROPIC_BASE_URL = "https://api.anthropic.com";
494
+ var ANTHROPIC_API_VERSION = "2023-06-01";
495
+ var FETCH_TIMEOUT_MS = 1e4;
496
+ function maskAnthropicKey(key) {
497
+ if (key.length <= 12) return "sk-ant-***";
498
+ return `${key.slice(0, 7)}...${key.slice(-4)}`;
499
+ }
500
+ function validateAnthropicKeyFormat(key) {
501
+ const trimmed = key.trim();
502
+ if (trimmed.length === 0) return "API key b\u1EAFt bu\u1ED9c";
503
+ if (!trimmed.startsWith("sk-ant-")) {
504
+ return "Anthropic API key th\u01B0\u1EDDng b\u1EAFt \u0111\u1EA7u b\u1EB1ng 'sk-ant-' (l\u1EA5y t\u1EEB console.anthropic.com).";
505
+ }
506
+ return true;
507
+ }
508
+ async function promptAnthropicKeyHidden() {
509
+ return await password({
510
+ message: "Anthropic API key (sk-ant-..., \u1EA9n input):",
511
+ mask: "*",
512
+ validate: validateAnthropicKeyFormat
513
+ });
514
+ }
515
+ async function fetchAnthropicModels(apiKey) {
516
+ const controller = new AbortController();
517
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
518
+ try {
519
+ const res = await fetch(`${ANTHROPIC_BASE_URL}/v1/models`, {
520
+ method: "GET",
521
+ headers: {
522
+ "x-api-key": apiKey,
523
+ "anthropic-version": ANTHROPIC_API_VERSION,
524
+ Accept: "application/json"
525
+ },
526
+ signal: controller.signal
527
+ });
528
+ if (res.status === 401) {
529
+ throw new Error("API key invalid (HTTP 401). Check key tr\xEAn console.anthropic.com.");
530
+ }
531
+ if (res.status === 403) {
532
+ throw new Error(
533
+ "API key b\u1ECB reject (HTTP 403). Key c\xF3 th\u1EC3 \u0111\xE3 b\u1ECB revoke ho\u1EB7c thi\u1EBFu permission."
534
+ );
535
+ }
536
+ if (res.status === 429) {
537
+ throw new Error("Rate limit (HTTP 429). Ch\u1EDD v\xE0i gi\xE2y r\u1ED3i th\u1EED l\u1EA1i.");
538
+ }
539
+ if (!res.ok) {
540
+ throw new Error(`Fetch models th\u1EA5t b\u1EA1i (HTTP ${res.status}).`);
541
+ }
542
+ const json = await res.json();
543
+ const models = (json.data || []).map((m) => typeof m.id === "string" ? m.id : null).filter((id) => id !== null);
544
+ if (models.length === 0) {
545
+ throw new Error("Anthropic tr\u1EA3 v\u1EC1 list r\u1ED7ng. Li\xEAn h\u1EC7 support ho\u1EB7c check account.");
546
+ }
547
+ return models;
548
+ } catch (err) {
549
+ if (err.name === "AbortError") {
550
+ throw new Error(`Connect ${ANTHROPIC_BASE_URL} timeout sau ${FETCH_TIMEOUT_MS / 1e3}s.`);
551
+ }
552
+ throw err;
553
+ } finally {
554
+ clearTimeout(timer);
555
+ }
556
+ }
557
+ async function promptAnthropicModelChoice(models) {
558
+ if (models.length === 1) {
559
+ log.info(`Auto-pick model: ${models[0]} (ch\u1EC9 1 model available)`);
560
+ return models[0];
561
+ }
562
+ const sorted = [...models].sort((a, b) => {
563
+ const score = (m) => {
564
+ const lower = m.toLowerCase();
565
+ if (lower.includes("sonnet")) return 0;
566
+ if (lower.includes("opus")) return 1;
567
+ if (lower.includes("haiku")) return 2;
568
+ return 3;
569
+ };
570
+ return score(a) - score(b);
571
+ });
572
+ return await select2({
573
+ message: "Ch\u1ECDn model m\u1EB7c \u0111\u1ECBnh cho project:",
574
+ choices: sorted.map((m) => ({ name: m, value: m }))
575
+ });
576
+ }
577
+ async function setupAnthropicApiKeyAndModel() {
578
+ const apiKey = await promptAnthropicKeyHidden();
579
+ log.info(`Verify key (${maskAnthropicKey(apiKey)}) qua ${ANTHROPIC_BASE_URL}/v1/models...`);
580
+ const models = await fetchAnthropicModels(apiKey);
581
+ log.success(`Endpoint OK \u2014 ${models.length} models available`);
582
+ const model = await promptAnthropicModelChoice(models);
583
+ return { apiKey, baseUrl: ANTHROPIC_BASE_URL, model };
584
+ }
585
+
487
586
  // src/lib/setup-llmlite-api-key-and-model.ts
488
- import { input, password, select as select2 } from "@inquirer/prompts";
587
+ import { input, password as password2, select as select3 } from "@inquirer/prompts";
489
588
  var DEFAULT_BASE_URL = "https://llm.nal.vn";
490
- var FETCH_TIMEOUT_MS = 1e4;
589
+ var FETCH_TIMEOUT_MS2 = 1e4;
491
590
  function maskApiKey(key) {
492
591
  if (key.length <= 8) return "sk-***";
493
592
  return `${key.slice(0, 3)}...${key.slice(-4)}`;
494
593
  }
495
594
  async function promptApiKeyHidden() {
496
- return await password({
595
+ return await password2({
497
596
  message: "LLMLite API key (\u1EA9n input):",
498
597
  mask: "*",
499
598
  validate: (v) => v.trim().length > 0 ? true : "API key b\u1EAFt bu\u1ED9c"
@@ -509,7 +608,7 @@ async function promptBaseUrl(defaultUrl = DEFAULT_BASE_URL) {
509
608
  }
510
609
  async function fetchAvailableModels(baseUrl, apiKey) {
511
610
  const controller = new AbortController();
512
- const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
611
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS2);
513
612
  try {
514
613
  const res = await fetch(`${baseUrl}/v1/models`, {
515
614
  method: "GET",
@@ -536,7 +635,7 @@ async function fetchAvailableModels(baseUrl, apiKey) {
536
635
  return models;
537
636
  } catch (err) {
538
637
  if (err.name === "AbortError") {
539
- throw new Error(`Connect ${baseUrl} timeout sau ${FETCH_TIMEOUT_MS / 1e3}s.`);
638
+ throw new Error(`Connect ${baseUrl} timeout sau ${FETCH_TIMEOUT_MS2 / 1e3}s.`);
540
639
  }
541
640
  throw err;
542
641
  } finally {
@@ -550,7 +649,7 @@ async function promptModelChoice(models) {
550
649
  return claudeAliases[0];
551
650
  }
552
651
  const choiceList = claudeAliases.length > 0 ? claudeAliases : models;
553
- return await select2({
652
+ return await select3({
554
653
  message: "Ch\u1ECDn model m\u1EB7c \u0111\u1ECBnh cho project:",
555
654
  choices: choiceList.map((m) => ({ name: m, value: m }))
556
655
  });
@@ -586,7 +685,12 @@ function applySubscription(existing, model) {
586
685
  const { env: existingEnv, ...rest } = existing;
587
686
  const merged = { ...rest, model };
588
687
  if (existingEnv) {
589
- const { ANTHROPIC_BASE_URL: _b, ANTHROPIC_AUTH_TOKEN: _t, ...envRest } = existingEnv;
688
+ const {
689
+ ANTHROPIC_BASE_URL: _b,
690
+ ANTHROPIC_AUTH_TOKEN: _t,
691
+ ANTHROPIC_API_KEY: _k,
692
+ ...envRest
693
+ } = existingEnv;
590
694
  if (Object.keys(envRest).length > 0) {
591
695
  merged.env = envRest;
592
696
  }
@@ -594,16 +698,29 @@ function applySubscription(existing, model) {
594
698
  return merged;
595
699
  }
596
700
  function applyLLMLite(existing, apiKey, baseUrl, model) {
701
+ const { ANTHROPIC_API_KEY: _k, ...envRest } = existing.env || {};
597
702
  return {
598
703
  ...existing,
599
704
  env: {
600
- ...existing.env || {},
705
+ ...envRest,
601
706
  ANTHROPIC_BASE_URL: baseUrl,
602
707
  ANTHROPIC_AUTH_TOKEN: apiKey
603
708
  },
604
709
  model
605
710
  };
606
711
  }
712
+ function applyAnthropic(existing, apiKey, baseUrl, model) {
713
+ const { ANTHROPIC_AUTH_TOKEN: _t, ...envRest } = existing.env || {};
714
+ return {
715
+ ...existing,
716
+ env: {
717
+ ...envRest,
718
+ ANTHROPIC_BASE_URL: baseUrl,
719
+ ANTHROPIC_API_KEY: apiKey
720
+ },
721
+ model
722
+ };
723
+ }
607
724
  function applyUseGlobal(existing, source) {
608
725
  const sourceEnv = source.env || {};
609
726
  const sourceModel = typeof source.model === "string" ? source.model : void 0;
@@ -627,6 +744,9 @@ async function writeClaudeSettings(workspacePath, input6) {
627
744
  case "llmlite":
628
745
  merged = applyLLMLite(existing, input6.apiKey, input6.baseUrl, input6.model);
629
746
  break;
747
+ case "anthropic":
748
+ merged = applyAnthropic(existing, input6.apiKey, input6.baseUrl, input6.model);
749
+ break;
630
750
  case "use-global":
631
751
  merged = applyUseGlobal(existing, input6.sourceSettings);
632
752
  break;
@@ -701,6 +821,23 @@ async function runAiSetupPhase(args) {
701
821
  log.success(`AI ready \xB7 LLMLite \xB7 model=${llmConfig.model} \xB7 ${llmConfig.baseUrl}`);
702
822
  return { ok: true, provider: "llmlite", model: llmConfig.model };
703
823
  }
824
+ case "anthropic": {
825
+ const anthropicConfig = await setupAnthropicApiKeyAndModel();
826
+ await writeClaudeSettings(args.workspacePath, {
827
+ provider: "anthropic",
828
+ apiKey: anthropicConfig.apiKey,
829
+ baseUrl: anthropicConfig.baseUrl,
830
+ model: anthropicConfig.model
831
+ });
832
+ await appendAuditEntry(
833
+ "ai_setup",
834
+ `provider=anthropic,result=ok,model=${anthropicConfig.model}`
835
+ );
836
+ log.success(
837
+ `AI ready \xB7 Anthropic Direct \xB7 model=${anthropicConfig.model} \xB7 ${anthropicConfig.baseUrl}`
838
+ );
839
+ return { ok: true, provider: "anthropic", model: anthropicConfig.model };
840
+ }
704
841
  case "use-global": {
705
842
  if (!globalInfo.rawSettings) {
706
843
  throw new Error("use-global ch\u1ECDn nh\u01B0ng kh\xF4ng \u0111\u1ECDc \u0111\u01B0\u1EE3c global settings.");
@@ -725,14 +862,15 @@ async function runAiSetupPhase(args) {
725
862
 
726
863
  // src/lib/test-ai-provider-by-detected-mode.ts
727
864
  import { spawnSync as spawnSync4 } from "child_process";
728
- var FETCH_TIMEOUT_MS2 = 1e4;
865
+ var FETCH_TIMEOUT_MS3 = 1e4;
729
866
  var CLAUDE_PRINT_TIMEOUT_MS = 3e4;
730
867
  var TEST_CHAT_MAX_TOKENS = 5;
731
868
  var TEST_CHAT_PROMPT = "say ok";
869
+ var ANTHROPIC_API_VERSION2 = "2023-06-01";
732
870
  async function testLLMLiteProvider(baseUrl, token, model) {
733
871
  log.info(`Testing LLMLite provider: ${baseUrl} (key: ${maskApiKey(token)})`);
734
872
  const controller = new AbortController();
735
- const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS2);
873
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS3);
736
874
  try {
737
875
  const modelsRes = await fetch(`${baseUrl}/v1/models`, {
738
876
  headers: { Authorization: `Bearer ${token}` },
@@ -777,7 +915,7 @@ async function testLLMLiteProvider(baseUrl, token, model) {
777
915
  log.dim(` Tokens used: ${tokens}`);
778
916
  } catch (err) {
779
917
  if (err.name === "AbortError") {
780
- throw new Error(`Timeout ${FETCH_TIMEOUT_MS2 / 1e3}s. Check m\u1EA1ng / endpoint ${baseUrl}.`);
918
+ throw new Error(`Timeout ${FETCH_TIMEOUT_MS3 / 1e3}s. Check m\u1EA1ng / endpoint ${baseUrl}.`);
781
919
  }
782
920
  throw err;
783
921
  } finally {
@@ -806,11 +944,63 @@ function testSubscriptionProvider() {
806
944
  }
807
945
  log.success(`Response: "${(result.stdout || "").trim().slice(0, 100)}"`);
808
946
  }
947
+ async function testAnthropicProvider(baseUrl, apiKey, model) {
948
+ log.info(`Testing Anthropic Direct provider: ${baseUrl} (key: ${maskApiKey(apiKey)})`);
949
+ const controller = new AbortController();
950
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS3);
951
+ try {
952
+ const modelsRes = await fetch(`${baseUrl}/v1/models`, {
953
+ headers: {
954
+ "x-api-key": apiKey,
955
+ "anthropic-version": ANTHROPIC_API_VERSION2
956
+ },
957
+ signal: controller.signal
958
+ });
959
+ if (modelsRes.status === 401 || modelsRes.status === 403) {
960
+ throw new Error(`API key invalid (HTTP ${modelsRes.status}). Re-run: avatar ai setup`);
961
+ }
962
+ if (!modelsRes.ok) {
963
+ throw new Error(`Endpoint /v1/models l\u1ED7i (HTTP ${modelsRes.status}).`);
964
+ }
965
+ const modelsJson = await modelsRes.json();
966
+ const models = (modelsJson.data || []).map((m) => typeof m.id === "string" ? m.id : null).filter((id) => id !== null);
967
+ log.success(`Connectivity OK \xB7 ${models.length} models available`);
968
+ log.info(`Testing message v\u1EDBi model "${model}"...`);
969
+ const msgRes = await fetch(`${baseUrl}/v1/messages`, {
970
+ method: "POST",
971
+ headers: {
972
+ "x-api-key": apiKey,
973
+ "anthropic-version": ANTHROPIC_API_VERSION2,
974
+ "Content-Type": "application/json"
975
+ },
976
+ body: JSON.stringify({
977
+ model,
978
+ max_tokens: TEST_CHAT_MAX_TOKENS,
979
+ messages: [{ role: "user", content: TEST_CHAT_PROMPT }]
980
+ }),
981
+ signal: controller.signal
982
+ });
983
+ if (!msgRes.ok) {
984
+ const errText = (await msgRes.text()).slice(0, 200);
985
+ throw new Error(`Message endpoint fail (HTTP ${msgRes.status}): ${errText}`);
986
+ }
987
+ const msgJson = await msgRes.json();
988
+ const text = (msgJson.content || []).map((c) => typeof c.text === "string" ? c.text : "").join("").trim().slice(0, 100);
989
+ log.success(`Response: "${text}"`);
990
+ } finally {
991
+ clearTimeout(timer);
992
+ }
993
+ }
809
994
  async function testAiProviderByDetectedMode(settings) {
810
995
  const env = settings.env || {};
811
996
  const baseUrl = typeof env.ANTHROPIC_BASE_URL === "string" ? env.ANTHROPIC_BASE_URL : void 0;
812
997
  const token = typeof env.ANTHROPIC_AUTH_TOKEN === "string" ? env.ANTHROPIC_AUTH_TOKEN : void 0;
998
+ const apiKey = typeof env.ANTHROPIC_API_KEY === "string" ? env.ANTHROPIC_API_KEY : void 0;
813
999
  const model = typeof settings.model === "string" ? settings.model : "default";
1000
+ if (apiKey && baseUrl) {
1001
+ await testAnthropicProvider(baseUrl, apiKey, model);
1002
+ return { ok: true, provider: "anthropic", message: "Anthropic Direct provider working" };
1003
+ }
814
1004
  if (baseUrl && token) {
815
1005
  await testLLMLiteProvider(baseUrl, token, model);
816
1006
  return { ok: true, provider: "llmlite", message: "LLMLite provider working" };
@@ -856,12 +1046,27 @@ async function runAiStatus() {
856
1046
  const env = settings.env || {};
857
1047
  const baseUrl = typeof env.ANTHROPIC_BASE_URL === "string" ? env.ANTHROPIC_BASE_URL : void 0;
858
1048
  const token = typeof env.ANTHROPIC_AUTH_TOKEN === "string" ? env.ANTHROPIC_AUTH_TOKEN : void 0;
1049
+ const apiKey = typeof env.ANTHROPIC_API_KEY === "string" ? env.ANTHROPIC_API_KEY : void 0;
859
1050
  const model = typeof settings.model === "string" ? settings.model : void 0;
860
- const provider = baseUrl ? "LLMLite" : token ? "Custom" : "Subscription (default)";
1051
+ let provider;
1052
+ let credentialDisplay;
1053
+ if (apiKey) {
1054
+ provider = baseUrl?.includes("api.anthropic.com") || apiKey.startsWith("sk-ant-") ? "Anthropic Direct" : "Custom (API key)";
1055
+ credentialDisplay = maskApiKey(apiKey);
1056
+ } else if (baseUrl && token) {
1057
+ provider = "LLMLite";
1058
+ credentialDisplay = maskApiKey(token);
1059
+ } else if (token) {
1060
+ provider = "Custom";
1061
+ credentialDisplay = maskApiKey(token);
1062
+ } else {
1063
+ provider = "Subscription (default)";
1064
+ credentialDisplay = "(kh\xF4ng set \u2014 d\xF9ng subscription auth)";
1065
+ }
861
1066
  log.info(`Project: ${workspacePath}`);
862
1067
  log.info(`Provider: ${provider}${baseUrl ? ` (${baseUrl})` : ""}`);
863
1068
  log.info(`Model: ${model ?? "(default \u2014 Claude Code ch\u1ECDn)"}`);
864
- log.info(`Token: ${token ? maskApiKey(token) : "(kh\xF4ng set \u2014 d\xF9ng subscription auth)"}`);
1069
+ log.info(`Token: ${credentialDisplay}`);
865
1070
  }
866
1071
  async function runAiTest() {
867
1072
  const workspacePath = await ensureWorkspaceCwd();
@@ -891,7 +1096,12 @@ async function runAiReset(opts) {
891
1096
  const { env: existingEnv, ...rest } = settings;
892
1097
  const reset = { ...rest };
893
1098
  if (existingEnv) {
894
- const { ANTHROPIC_BASE_URL: _b, ANTHROPIC_AUTH_TOKEN: _t, ...envRest } = existingEnv;
1099
+ const {
1100
+ ANTHROPIC_BASE_URL: _b,
1101
+ ANTHROPIC_AUTH_TOKEN: _t,
1102
+ ANTHROPIC_API_KEY: _k,
1103
+ ...envRest
1104
+ } = existingEnv;
895
1105
  if (Object.keys(envRest).length > 0) {
896
1106
  reset.env = envRest;
897
1107
  }
@@ -1549,7 +1759,7 @@ function installGitnexusViaNpm() {
1549
1759
  }
1550
1760
 
1551
1761
  // src/lib/prompt-recovery-action-on-failure.ts
1552
- import { input as input3, select as select3 } from "@inquirer/prompts";
1762
+ import { input as input3, select as select4 } from "@inquirer/prompts";
1553
1763
  var UserAbortedRecoveryError = class extends Error {
1554
1764
  constructor(message) {
1555
1765
  super(message);
@@ -1566,7 +1776,7 @@ async function promptRetryOrSkip(args) {
1566
1776
  choices.push({ name: "B\u1ECF qua b\u01B0\u1EDBc n\xE0y v\xE0 ti\u1EBFp t\u1EE5c (Skip)", value: "skip" });
1567
1777
  }
1568
1778
  choices.push({ name: "T\u1EA1m ng\u01B0ng init \u2014 ch\u1EA1y l\u1EA1i sau (Abort)", value: "abort" });
1569
- return await select3({
1779
+ return await select4({
1570
1780
  message: "C\xE1ch x\u1EED l\xFD?",
1571
1781
  choices
1572
1782
  });
@@ -1642,17 +1852,41 @@ import { join as join14 } from "path";
1642
1852
  import { confirm as confirm2 } from "@inquirer/prompts";
1643
1853
  var WIKI_TIMEOUT_MS = 15 * 60 * 1e3;
1644
1854
  var DEFAULT_LLMLITE_MODEL = "nal-claude";
1855
+ var DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-4-5";
1856
+ function normalizeAnthropicBaseUrl(rawBaseUrl) {
1857
+ const cleaned = rawBaseUrl.replace(/\/+$/, "");
1858
+ if (cleaned.endsWith("/v1")) return `${cleaned}/`;
1859
+ if (cleaned.endsWith("/v1/")) return cleaned;
1860
+ return `${cleaned}/v1/`;
1861
+ }
1645
1862
  async function readSettingsForWikiCredentials(workspacePath) {
1646
1863
  const settingsPath = join14(workspacePath, ".claude", "settings.json");
1647
1864
  if (!await pathExists(settingsPath)) return null;
1648
1865
  try {
1649
1866
  const settings = await readJson(settingsPath);
1650
1867
  const env = settings.env || {};
1651
- const apiKey = typeof env.ANTHROPIC_AUTH_TOKEN === "string" ? env.ANTHROPIC_AUTH_TOKEN : null;
1652
1868
  const baseUrl = typeof env.ANTHROPIC_BASE_URL === "string" ? env.ANTHROPIC_BASE_URL : null;
1653
- if (!apiKey || !baseUrl) return null;
1654
- const model = typeof env.ANTHROPIC_MODEL === "string" && env.ANTHROPIC_MODEL.length > 0 ? env.ANTHROPIC_MODEL : DEFAULT_LLMLITE_MODEL;
1655
- return { apiKey, baseUrl, model };
1869
+ if (!baseUrl) return null;
1870
+ const userModel = typeof env.ANTHROPIC_MODEL === "string" ? env.ANTHROPIC_MODEL : "";
1871
+ const anthropicKey = typeof env.ANTHROPIC_API_KEY === "string" ? env.ANTHROPIC_API_KEY : null;
1872
+ if (anthropicKey) {
1873
+ return {
1874
+ provider: "anthropic",
1875
+ apiKey: anthropicKey,
1876
+ baseUrl: normalizeAnthropicBaseUrl(baseUrl),
1877
+ model: userModel.length > 0 ? userModel : DEFAULT_ANTHROPIC_MODEL
1878
+ };
1879
+ }
1880
+ const llmliteToken = typeof env.ANTHROPIC_AUTH_TOKEN === "string" ? env.ANTHROPIC_AUTH_TOKEN : null;
1881
+ if (llmliteToken) {
1882
+ return {
1883
+ provider: "llmlite",
1884
+ apiKey: llmliteToken,
1885
+ baseUrl,
1886
+ model: userModel.length > 0 ? userModel : DEFAULT_LLMLITE_MODEL
1887
+ };
1888
+ }
1889
+ return null;
1656
1890
  } catch {
1657
1891
  return null;
1658
1892
  }
@@ -1670,8 +1904,10 @@ function tailLines2(text, n) {
1670
1904
  async function runGitnexusWikiConditional(workspacePath) {
1671
1905
  const creds = await readSettingsForWikiCredentials(workspacePath);
1672
1906
  if (!creds) {
1673
- log.warn("Subscription mode (ho\u1EB7c settings.json kh\xF4ng c\xF3 LLMLite key) \u2192 skip wiki gen.");
1674
- log.dim("\u0110\u1EC3 gen wiki sau, ch\u1EA1y manual: gitnexus wiki . --api-key <openai-key>");
1907
+ log.warn("Subscription mode (OAuth, kh\xF4ng c\xF3 API key trong settings.json) \u2192 skip wiki gen.");
1908
+ log.dim(
1909
+ "\u0110\u1EC3 gen wiki sau, ch\u1EA1y manual:\n gitnexus wiki . --api-key <key> --base-url <url> --model <model>"
1910
+ );
1675
1911
  return { ran: false, skipped: true, reason: "subscription-mode" };
1676
1912
  }
1677
1913
  const proceed = await confirmWikiGeneration(creds.baseUrl, creds.model);
@@ -1681,7 +1917,9 @@ async function runGitnexusWikiConditional(workspacePath) {
1681
1917
  );
1682
1918
  return { ran: false, skipped: true, reason: "user-declined" };
1683
1919
  }
1684
- const sp = spinnerWithElapsed(`Generating wiki via ${creds.baseUrl} model=${creds.model}`);
1920
+ const sp = spinnerWithElapsed(
1921
+ `Generating wiki via ${creds.baseUrl} (${creds.provider}) model=${creds.model}`
1922
+ );
1685
1923
  const result = spawnSync10(
1686
1924
  "gitnexus",
1687
1925
  ["wiki", ".", "--api-key", creds.apiKey, "--base-url", creds.baseUrl, "--model", creds.model],
@@ -1960,19 +2198,19 @@ function registerGitnexusCommand(program2) {
1960
2198
 
1961
2199
  // src/commands/init.ts
1962
2200
  import { basename, join as join22, relative as relative2, resolve } from "path";
1963
- import { confirm as confirm5, input as input5, select as select8 } from "@inquirer/prompts";
2201
+ import { confirm as confirm5, input as input5, select as select9 } from "@inquirer/prompts";
1964
2202
  import boxen5 from "boxen";
1965
2203
 
1966
2204
  // src/lib/add-team-pack-submodule-with-retry-on-network-fail.ts
1967
2205
  import { spawnSync as spawnSync13 } from "child_process";
1968
- import { select as select5 } from "@inquirer/prompts";
2206
+ import { select as select6 } from "@inquirer/prompts";
1969
2207
 
1970
2208
  // src/lib/team-pack-submodule-manager.ts
1971
2209
  import { join as join16 } from "path";
1972
2210
 
1973
2211
  // src/lib/check-team-pack-access-with-retry-loop.ts
1974
2212
  import { spawnSync as spawnSync12 } from "child_process";
1975
- import { confirm as confirm4, select as select4 } from "@inquirer/prompts";
2213
+ import { confirm as confirm4, select as select5 } from "@inquirer/prompts";
1976
2214
  import boxen3 from "boxen";
1977
2215
  function parseRepoSlugFromGitUrl(url) {
1978
2216
  const httpsMatch = url.match(/github\.com[/:]([^/]+\/[^/]+?)(?:\.git)?$/);
@@ -2051,7 +2289,7 @@ async function ensureTeamPackAccessWithRetry(args) {
2051
2289
  while (true) {
2052
2290
  const ghUser = getCurrentGhUser();
2053
2291
  const ghUserDisplay = ghUser ?? "(ch\u01B0a gh auth)";
2054
- const action = await select4({
2292
+ const action = await select5({
2055
2293
  message: "C\xE1ch x\u1EED l\xFD?",
2056
2294
  choices: [
2057
2295
  {
@@ -2168,7 +2406,7 @@ function openGithubSshKeysPage() {
2168
2406
  }
2169
2407
  }
2170
2408
  async function handleSshPermissionError() {
2171
- return await select5({
2409
+ return await select6({
2172
2410
  message: "SSH permission denied. C\xE1ch x\u1EED l\xFD?",
2173
2411
  choices: [
2174
2412
  {
@@ -2431,7 +2669,7 @@ function detectPackageManager() {
2431
2669
 
2432
2670
  // src/lib/handle-remote-access-failure-with-account-switch.ts
2433
2671
  import { spawnSync as spawnSync19 } from "child_process";
2434
- import { input as input4, select as select6 } from "@inquirer/prompts";
2672
+ import { input as input4, select as select7 } from "@inquirer/prompts";
2435
2673
 
2436
2674
  // src/lib/verify-git-remote-accessible.ts
2437
2675
  import { spawnSync as spawnSync18 } from "child_process";
@@ -2515,7 +2753,7 @@ async function handleRemoteAccessFailureWithAccountSwitch(args) {
2515
2753
  log.dim(` L\xFD do: ${reason}${detail ? ` \u2014 ${detail.slice(0, 150)}` : ""}`);
2516
2754
  log.info(getReasonHint(reason, currentUrl, ghUser));
2517
2755
  if (ghUser) log.dim(` gh CLI hi\u1EC7n \u0111ang login: ${ghUser}`);
2518
- const action = await select6({
2756
+ const action = await select7({
2519
2757
  message: "C\xE1ch x\u1EED l\xFD?",
2520
2758
  choices: [
2521
2759
  {
@@ -2824,7 +3062,7 @@ function linkExistingRemoteToWorkspace(args) {
2824
3062
 
2825
3063
  // src/lib/safe-bootstrap-for-dirty-folder.ts
2826
3064
  import { readdirSync } from "fs";
2827
- import { select as select7 } from "@inquirer/prompts";
3065
+ import { select as select8 } from "@inquirer/prompts";
2828
3066
  import { simpleGit as simpleGit3 } from "simple-git";
2829
3067
 
2830
3068
  // src/lib/check-folder-has-git.ts
@@ -2961,7 +3199,7 @@ async function promptBootstrapStrategy(state, opts) {
2961
3199
  if (opts.presetStrategy) return opts.presetStrategy;
2962
3200
  if (opts.autoYes) return "stash";
2963
3201
  if (state === "empty" || state === "clean") return "commit-all";
2964
- return await select7({
3202
+ return await select8({
2965
3203
  message: state === "dirty" ? "Folder c\xF3 changes ch\u01B0a commit. C\xE1ch x\u1EED l\xFD:" : "Folder c\xF3 file ch\u01B0a version. C\xE1ch x\u1EED l\xFD:",
2966
3204
  choices: [
2967
3205
  {
@@ -3446,7 +3684,7 @@ async function runInit(opts) {
3446
3684
  }
3447
3685
  }
3448
3686
  async function promptProjectStatus() {
3449
- return await select8({
3687
+ return await select9({
3450
3688
  message: "T\xECnh tr\u1EA1ng d\u1EF1 \xE1n c\u1EE7a b\u1EA1n?",
3451
3689
  choices: [
3452
3690
  { name: "1. \u0110\xE3 c\xF3 repo git remote (URL c\xF3 s\u1EB5n)", value: "existing-remote" },
@@ -3529,7 +3767,7 @@ async function runInitFromScratch(opts, ownerEmail) {
3529
3767
  message: "T\xEAn d\u1EF1 \xE1n:",
3530
3768
  validate: (v) => v.length > 0 ? true : "T\xEAn b\u1EAFt bu\u1ED9c"
3531
3769
  });
3532
- const visibility = opts.repoVisibility ?? await select8({
3770
+ const visibility = opts.repoVisibility ?? await select9({
3533
3771
  message: "Visibility?",
3534
3772
  choices: [
3535
3773
  { name: "private (m\u1EB7c \u0111\u1ECBnh)", value: "private" },
@@ -3604,7 +3842,7 @@ async function getOrCreateOriginRemote(folderPath, opts) {
3604
3842
  return void 0;
3605
3843
  }
3606
3844
  await ensureGitHubReady();
3607
- const visibility = opts.repoVisibility ?? await select8({
3845
+ const visibility = opts.repoVisibility ?? await select9({
3608
3846
  message: "Visibility?",
3609
3847
  choices: [
3610
3848
  { name: "private (m\u1EB7c \u0111\u1ECBnh)", value: "private" },
@@ -3730,7 +3968,7 @@ async function maybeCreateWorkspaceRemote(args) {
3730
3968
  });
3731
3969
  }
3732
3970
  if (!shouldCreate) return;
3733
- const visibility = args.repoVisibility ?? (args.autoYes ? "private" : await select8({
3971
+ const visibility = args.repoVisibility ?? (args.autoYes ? "private" : await select9({
3734
3972
  message: "Workspace visibility?",
3735
3973
  choices: [
3736
3974
  { name: "private (m\u1EB7c \u0111\u1ECBnh, an to\xE0n)", value: "private" },
@@ -3749,7 +3987,7 @@ async function maybeCreateWorkspaceRemote(args) {
3749
3987
  } catch (err) {
3750
3988
  if (err instanceof CreateWorkspaceRemoteError && err.reason === "repo-exists") {
3751
3989
  const fullName = err.fullName;
3752
- const reuseAction = await select8({
3990
+ const reuseAction = await select9({
3753
3991
  message: `Repo '${fullName}' \u0111\xE3 t\u1ED3n t\u1EA1i tr\xEAn GitHub. C\xE1ch x\u1EED l\xFD?`,
3754
3992
  choices: [
3755
3993
  {
@@ -3818,7 +4056,7 @@ async function resolveWorkspacePath(parent, desiredName, force) {
3818
4056
  }
3819
4057
  choices.push({ name: "Nh\u1EADp t\xEAn workspace kh\xE1c (manual)", value: "manual" });
3820
4058
  choices.push({ name: "T\u1EA1m ng\u01B0ng init", value: "abort" });
3821
- const action = await select8({
4059
+ const action = await select9({
3822
4060
  message: "C\xE1ch x\u1EED l\xFD workspace path conflict?",
3823
4061
  choices
3824
4062
  });
@@ -4536,7 +4774,7 @@ async function removeSubmoduleEntry(gitmodulesPath, submodulePath) {
4536
4774
  }
4537
4775
 
4538
4776
  // src/commands/uninstall.ts
4539
- var CLI_VERSION = "1.5.0";
4777
+ var CLI_VERSION = "1.6.1";
4540
4778
  function registerUninstallCommand(program2) {
4541
4779
  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) => {
4542
4780
  try {
@@ -4618,7 +4856,7 @@ function printUninstallSuccessBox(backupPath) {
4618
4856
  }
4619
4857
 
4620
4858
  // src/index.ts
4621
- var CLI_VERSION2 = "1.5.0";
4859
+ var CLI_VERSION2 = "1.6.1";
4622
4860
  var program = new Command();
4623
4861
  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(
4624
4862
  "beforeAll",