clawfast 2.1.0 → 2.2.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.
Files changed (2) hide show
  1. package/dist/clawfast.cjs +569 -94
  2. package/package.json +1 -1
package/dist/clawfast.cjs CHANGED
@@ -548,11 +548,11 @@ function fileDefines(filePath, key) {
548
548
  return false;
549
549
  }
550
550
  }
551
- function swapNvidiaKey(key) {
551
+ function swapProviderKey(envVar, key) {
552
552
  const local = import_node_path.default.resolve(process.cwd(), ".env.local");
553
- const target = fileDefines(local, "NVIDIA_API_KEY") ? local : clawfastEnvPath();
554
- setEnvVar(target, "NVIDIA_API_KEY", key);
555
- process.env.NVIDIA_API_KEY = key;
553
+ const target = fileDefines(local, envVar) ? local : clawfastEnvPath();
554
+ setEnvVar(target, envVar, key);
555
+ process.env[envVar] = key;
556
556
  return target;
557
557
  }
558
558
  async function testNvidiaKey(key) {
@@ -586,6 +586,27 @@ async function testNvidiaKey(key) {
586
586
  };
587
587
  }
588
588
  }
589
+ async function testOpenRouterKey(key) {
590
+ try {
591
+ const res = await fetch("https://openrouter.ai/api/v1/key", {
592
+ headers: { Authorization: `Bearer ${key}` }
593
+ });
594
+ if (res.ok) return { ok: true, status: res.status, detail: "OK" };
595
+ let detail = res.statusText || `HTTP ${res.status}`;
596
+ try {
597
+ const body = await res.json();
598
+ detail = body.error?.message || body.message || detail;
599
+ } catch {
600
+ }
601
+ return { ok: false, status: res.status, detail };
602
+ } catch (err) {
603
+ return {
604
+ ok: false,
605
+ status: 0,
606
+ detail: err instanceof Error ? err.message : String(err)
607
+ };
608
+ }
609
+ }
589
610
  function promptLine(question) {
590
611
  const rl = import_node_readline.default.createInterface({
591
612
  input: process.stdin,
@@ -634,7 +655,7 @@ ${C.dim}Para come\xE7ar, preciso de uma chave de modelo. Use a NVIDIA build (mod
634
655
  );
635
656
  return true;
636
657
  }
637
- var import_dotenv, import_node_os, import_node_path, import_node_fs, import_node_readline, clawfastHome, clawfastEnvPath, PROVIDER_KEYS, hasAnyProviderKey, C;
658
+ var import_dotenv, import_node_os, import_node_path, import_node_fs, import_node_readline, clawfastHome, clawfastEnvPath, PROVIDER_KEYS, hasAnyProviderKey, swapNvidiaKey, swapOpenRouterKey, C;
638
659
  var init_config = __esm({
639
660
  "src/config.ts"() {
640
661
  "use strict";
@@ -645,8 +666,14 @@ var init_config = __esm({
645
666
  import_node_readline = __toESM(require("node:readline"));
646
667
  clawfastHome = () => process.env.CLAWFAST_HOME?.trim() || import_node_path.default.join(import_node_os.default.homedir(), ".clawfast");
647
668
  clawfastEnvPath = () => import_node_path.default.join(clawfastHome(), ".env");
648
- PROVIDER_KEYS = ["NVIDIA_API_KEY", "OPENAI_API_KEY"];
669
+ PROVIDER_KEYS = [
670
+ "NVIDIA_API_KEY",
671
+ "OPENAI_API_KEY",
672
+ "OPENROUTER_API_KEY"
673
+ ];
649
674
  hasAnyProviderKey = () => PROVIDER_KEYS.some((name25) => Boolean(process.env[name25]?.trim()));
675
+ swapNvidiaKey = (key) => swapProviderKey("NVIDIA_API_KEY", key);
676
+ swapOpenRouterKey = (key) => swapProviderKey("OPENROUTER_API_KEY", key);
650
677
  C = {
651
678
  reset: "\x1B[0m",
652
679
  dim: "\x1B[90m",
@@ -662,7 +689,7 @@ var clawfastVersion, isDevVersion, isNewerVersion;
662
689
  var init_version = __esm({
663
690
  "src/version.ts"() {
664
691
  "use strict";
665
- clawfastVersion = () => true ? "2.1.0" : "0.0.0-dev";
692
+ clawfastVersion = () => true ? "2.2.0" : devVersionFromPackageJson();
666
693
  isDevVersion = () => clawfastVersion().includes("-dev");
667
694
  isNewerVersion = (a, b) => {
668
695
  const parse3 = (v) => v.split("-")[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
@@ -715,7 +742,7 @@ var init_update = __esm({
715
742
  if (isDevVersion()) return null;
716
743
  const cache = readCache();
717
744
  if (cache && isNewerVersion(cache.latest, current)) {
718
- return `nova versao ${cache.latest} disponivel (voce tem ${current}) \u2014 rode: clawfast update`;
745
+ return `nova versao ${cache.latest} disponivel (voce tem ${current}) \u2014 rode: clawfast update e depois clawfast para usar a nova versao`;
719
746
  }
720
747
  return null;
721
748
  };
@@ -45777,7 +45804,36 @@ function supportsMultimodalToolResults(modelName) {
45777
45804
  const normalized = modelName.toLowerCase();
45778
45805
  return normalized === "ask-model" || normalized.includes("gemini") || normalized.includes("google/") || isAnthropicModel(normalized) || normalized.includes("anthropic/") || normalized.includes("claude") || normalized.includes("openai") || normalized.includes("openai/") || normalized.includes("gpt-") || normalized.includes("o1") || normalized.includes("o3") || normalized.includes("o4") || normalized.includes("x-ai/") || normalized.includes("grok");
45779
45806
  }
45780
- var isRecord, isXaiModelSlug, isGeminiModelSlug, requestCanRouteToXai, requestCanRouteToGemini, hasOwnEncryptedContent, stripEncryptedContent, sanitizeOpenRouterRequestForXai, hasJsonRefKey, wrapToolContentIfGeminiRefSensitive, sanitizeOpenRouterRequestForGeminiFunctionResponses, patchKimiReasoningToolCalls, OPENROUTER_METADATA_HEADER, withOpenRouterMetadataHeader, openrouterPatchFetch, openrouter2, openai2, isNvidiaMistralModel, applyNvidiaMistralConfig, nvidiaPatchFetch, nvidia, deepseek, kimi, buildProviderMap, hasEnvValue, isDeepSeekEnabled, isKimiEnabled, CLI_MODEL_CHAIN, baseProviders, modelCutoffDates, modelDisplayNames, getModelDisplayName, getModelCutoffDate, myProvider;
45807
+ function resolveLanguageModel2(key) {
45808
+ if (isOpenRouterDynamicKey(key)) {
45809
+ return openrouter2(openRouterSlugFromKey(key));
45810
+ }
45811
+ return myProvider.languageModel(key);
45812
+ }
45813
+ async function listOpenRouterModels(signal) {
45814
+ const key = process.env.OPENROUTER_API_KEY?.trim();
45815
+ if (!key) throw new Error("OPENROUTER_API_KEY n\xE3o configurada");
45816
+ const res = await fetch("https://openrouter.ai/api/v1/models", {
45817
+ headers: { Authorization: `Bearer ${key}` },
45818
+ signal
45819
+ });
45820
+ if (!res.ok) {
45821
+ throw new Error(`OpenRouter /models falhou: HTTP ${res.status}`);
45822
+ }
45823
+ const json3 = await res.json();
45824
+ const data = Array.isArray(json3.data) ? json3.data : [];
45825
+ return data.map((raw) => {
45826
+ if (!isRecord(raw) || typeof raw.id !== "string") return null;
45827
+ const pricing = isRecord(raw.pricing) ? raw.pricing : void 0;
45828
+ return {
45829
+ id: raw.id,
45830
+ name: typeof raw.name === "string" && raw.name.trim() ? raw.name : raw.id,
45831
+ contextLength: typeof raw.context_length === "number" ? raw.context_length : void 0,
45832
+ promptPrice: typeof pricing?.prompt === "string" ? pricing.prompt : void 0
45833
+ };
45834
+ }).filter((m) => m !== null).sort((a, b) => a.name.localeCompare(b.name));
45835
+ }
45836
+ var isRecord, isXaiModelSlug, isGeminiModelSlug, requestCanRouteToXai, requestCanRouteToGemini, hasOwnEncryptedContent, stripEncryptedContent, sanitizeOpenRouterRequestForXai, hasJsonRefKey, wrapToolContentIfGeminiRefSensitive, sanitizeOpenRouterRequestForGeminiFunctionResponses, patchKimiReasoningToolCalls, OPENROUTER_METADATA_HEADER, withOpenRouterMetadataHeader, openrouterPatchFetch, openrouter2, openai2, isNvidiaMistralModel, applyNvidiaMistralConfig, nvidiaPatchFetch, nvidia, deepseek, kimi, buildProviderMap, hasEnvValue, isDeepSeekEnabled, isKimiEnabled, CLI_MODEL_CHAIN, baseProviders, modelCutoffDates, modelDisplayNames, getModelDisplayName, getModelCutoffDate, myProvider, OPENROUTER_KEY_PREFIX, hasOpenRouterKey, isOpenRouterDynamicKey, openRouterSlugFromKey, openRouterKeyForSlug;
45781
45837
  var init_providers = __esm({
45782
45838
  "../lib/ai/providers.ts"() {
45783
45839
  "use strict";
@@ -46119,6 +46175,11 @@ var init_providers = __esm({
46119
46175
  myProvider = customProvider({
46120
46176
  languageModels: baseProviders
46121
46177
  });
46178
+ OPENROUTER_KEY_PREFIX = "openrouter:";
46179
+ hasOpenRouterKey = () => Boolean(process.env.OPENROUTER_API_KEY?.trim());
46180
+ isOpenRouterDynamicKey = (key) => key.startsWith(OPENROUTER_KEY_PREFIX);
46181
+ openRouterSlugFromKey = (key) => key.slice(OPENROUTER_KEY_PREFIX.length);
46182
+ openRouterKeyForSlug = (slug) => `${OPENROUTER_KEY_PREFIX}${slug}`;
46122
46183
  }
46123
46184
  });
46124
46185
 
@@ -70372,6 +70433,9 @@ var init_local_sandbox = __esm({
70372
70433
  ["recon_ports.py", true],
70373
70434
  ["recon_content.py", true],
70374
70435
  ["recon_intel.py", true],
70436
+ ["recon_routes.py", true],
70437
+ ["exploit_engine.py", true],
70438
+ ["harden.py", true],
70375
70439
  ["console_recon.js", true],
70376
70440
  ["README.md", true],
70377
70441
  ["scope.txt", false]
@@ -71335,12 +71399,23 @@ function balancedSpan(text2, open, close) {
71335
71399
  function entryToBridgedCall(entry) {
71336
71400
  if (!entry || typeof entry !== "object") return null;
71337
71401
  const e = entry;
71402
+ const brief = typeof e.brief === "string" ? e.brief : "";
71403
+ const explicit = typeof e.tool === "string" ? e.tool : typeof e.tool_name === "string" ? e.tool_name : null;
71404
+ if (explicit && BRIDGEABLE_TOOLS.has(explicit)) {
71405
+ const nested = [e.input, e.args, e.parameters].find(
71406
+ (v) => v && typeof v === "object" && !Array.isArray(v)
71407
+ );
71408
+ const input = { brief, ...nested ?? e };
71409
+ delete input.tool;
71410
+ delete input.tool_name;
71411
+ return { toolName: explicit, input };
71412
+ }
71338
71413
  if (typeof e.command === "string" && e.command.trim()) {
71339
71414
  return {
71340
71415
  toolName: "run_terminal_cmd",
71341
71416
  input: {
71342
71417
  command: e.command,
71343
- brief: typeof e.brief === "string" ? e.brief : "",
71418
+ brief,
71344
71419
  is_background: e.is_background === true,
71345
71420
  interactive: e.interactive === true,
71346
71421
  timeout: typeof e.timeout === "number" ? e.timeout : 60
@@ -71353,13 +71428,25 @@ function entryToBridgedCall(entry) {
71353
71428
  input: {
71354
71429
  action: e.action,
71355
71430
  path: e.path,
71356
- brief: typeof e.brief === "string" ? e.brief : "",
71431
+ brief,
71357
71432
  ...typeof e.text === "string" ? { text: e.text } : {},
71358
71433
  ...Array.isArray(e.range) ? { range: e.range } : {},
71359
71434
  ...Array.isArray(e.edits) ? { edits: e.edits } : {}
71360
71435
  }
71361
71436
  };
71362
71437
  }
71438
+ if (typeof e.action === "string" && FINDINGS_ACTIONS.has(e.action)) {
71439
+ return { toolName: "findings", input: { ...e, brief } };
71440
+ }
71441
+ if (typeof e.url === "string" && e.url.trim()) {
71442
+ return { toolName: "http_request", input: { ...e, brief } };
71443
+ }
71444
+ if (Array.isArray(e.todos)) {
71445
+ return {
71446
+ toolName: "todo_write",
71447
+ input: { merge: e.merge === true, todos: e.todos }
71448
+ };
71449
+ }
71363
71450
  return null;
71364
71451
  }
71365
71452
  function validateBatch(raw) {
@@ -71396,12 +71483,27 @@ function parseProxyToolCalls(text2) {
71396
71483
  }
71397
71484
  function summarizeBridgedCalls(calls) {
71398
71485
  return calls.map((call) => {
71399
- if (call.toolName === "run_terminal_cmd") {
71400
- return `$ ${truncateBridgeSummary(String(call.input.command ?? ""))}`;
71486
+ const i = call.input;
71487
+ switch (call.toolName) {
71488
+ case "run_terminal_cmd":
71489
+ return `$ ${truncateBridgeSummary(String(i.command ?? ""))}`;
71490
+ case "file":
71491
+ return `${String(i.action ?? "file")} ${truncateBridgeSummary(
71492
+ String(i.path ?? ""),
71493
+ 120
71494
+ )}`.trim();
71495
+ case "http_request":
71496
+ return `${String(i.method ?? "GET")} ${truncateBridgeSummary(
71497
+ String(i.url ?? ""),
71498
+ 120
71499
+ )}`.trim();
71500
+ case "findings":
71501
+ return `findings ${String(i.action ?? "")}`.trim();
71502
+ case "todo_write":
71503
+ return `todo_write (${Array.isArray(i.todos) ? i.todos.length : 0} itens)`;
71504
+ default:
71505
+ return call.toolName;
71401
71506
  }
71402
- const action = String(call.input.action ?? "file");
71403
- const target = String(call.input.path ?? "");
71404
- return `${action} ${truncateBridgeSummary(target, 120)}`.trim();
71405
71507
  }).join("; ");
71406
71508
  }
71407
71509
  async function createAgent() {
@@ -71468,6 +71570,7 @@ async function createAgent() {
71468
71570
  system += pythonOnlyPolicy();
71469
71571
  system += deepReconPolicy(sandbox.getWorkdir());
71470
71572
  system += reconPhasesPolicy(sandbox.getWorkdir());
71573
+ system += attackChainPolicy(sandbox.getWorkdir());
71471
71574
  system += httpAndFindingsPolicy(sandbox.getWorkdir());
71472
71575
  system += buildCliNotesSection();
71473
71576
  system += buildSkillsIndexSection();
@@ -71565,6 +71668,33 @@ async function createAgent() {
71565
71668
  selection: getModelSelection()
71566
71669
  };
71567
71670
  }
71671
+ if (isOpenRouterDynamicKey(raw)) {
71672
+ if (!hasOpenRouterKey()) {
71673
+ return {
71674
+ ok: false,
71675
+ message: "OpenRouter indispon\xEDvel: defina OPENROUTER_API_KEY em .env.local (https://openrouter.ai/keys)",
71676
+ selection: getModelSelection()
71677
+ };
71678
+ }
71679
+ const slug = openRouterSlugFromKey(raw).trim();
71680
+ if (!slug) {
71681
+ return {
71682
+ ok: false,
71683
+ message: "informe o modelo OpenRouter (ex.: openrouter:openai/gpt-4o)",
71684
+ selection: getModelSelection()
71685
+ };
71686
+ }
71687
+ const key = openRouterKeyForSlug(slug);
71688
+ selectedModelKey = key;
71689
+ currentModelName = key;
71690
+ context2.modelName = key;
71691
+ await rebuildSystemPrompt(key);
71692
+ return {
71693
+ ok: true,
71694
+ message: `modelo fixado: ${labelFor(key)}`,
71695
+ selection: getModelSelection()
71696
+ };
71697
+ }
71568
71698
  const choices = getModelChoices();
71569
71699
  let match;
71570
71700
  if (/^\d+$/.test(normalized)) {
@@ -71674,6 +71804,28 @@ ${seen}`);
71674
71804
  }
71675
71805
  return sections.join("\n\n");
71676
71806
  }
71807
+ let lastKimiChatId = null;
71808
+ async function reportKimiSession() {
71809
+ const base = process.env.KIMI_BASE_URL?.trim();
71810
+ if (!base) return;
71811
+ try {
71812
+ const url2 = `${base.replace(/\/+$/, "")}/session`;
71813
+ const controller = new AbortController();
71814
+ const timer2 = setTimeout(() => controller.abort(), 1500);
71815
+ const res = await fetch(url2, { signal: controller.signal }).finally(
71816
+ () => clearTimeout(timer2)
71817
+ );
71818
+ if (!res.ok) return;
71819
+ const info = await res.json();
71820
+ if (!info?.active || !info.chatId) return;
71821
+ const isNew = info.chatId !== lastKimiChatId;
71822
+ lastKimiChatId = info.chatId;
71823
+ render.info(
71824
+ `\u25B8 kimi: ${isNew ? "conversa \xFAnica ativa" : "mesma conversa"} (turno ${info.turns ?? "?"}) \u2014 ${info.url ?? info.chatId}`
71825
+ );
71826
+ } catch {
71827
+ }
71828
+ }
71677
71829
  async function send(userInput, signal) {
71678
71830
  history.push({ role: "user", content: userInput });
71679
71831
  if (!auditMode && detectProjectAuditIntent(userInput)) {
@@ -71724,6 +71876,31 @@ ${seen}`);
71724
71876
  }
71725
71877
  let streamError = null;
71726
71878
  let stalled = false;
71879
+ let turnText = "";
71880
+ const turnToolSummaries = [];
71881
+ const preservePartialWork = () => {
71882
+ const segs = [];
71883
+ const text2 = turnText.trim();
71884
+ if (text2) {
71885
+ segs.push(text2.length > 4e3 ? `...${text2.slice(-4e3)}` : text2);
71886
+ }
71887
+ if (turnToolSummaries.length) {
71888
+ segs.push(
71889
+ "A\xE7\xF5es j\xE1 executadas neste turno:\n" + turnToolSummaries.map((s) => `- ${s}`).join("\n")
71890
+ );
71891
+ }
71892
+ if (!segs.length) return;
71893
+ history.push({
71894
+ role: "assistant",
71895
+ content: `[Trabalho parcial de ${labelFor(
71896
+ modelKey
71897
+ )} antes de falhar \u2014 N\xC3O recomece do zero; continue exatamente a partir daqui:]
71898
+
71899
+ ${segs.join(
71900
+ "\n\n"
71901
+ )}`
71902
+ });
71903
+ };
71727
71904
  const stallController = new AbortController();
71728
71905
  const onUserAbort = () => stallController.abort();
71729
71906
  if (signal) {
@@ -71746,7 +71923,7 @@ ${seen}`);
71746
71923
  };
71747
71924
  try {
71748
71925
  const result = streamText({
71749
- model: myProvider.languageModel(modelKey),
71926
+ model: resolveLanguageModel2(modelKey),
71750
71927
  system: turnSystem,
71751
71928
  messages: history,
71752
71929
  tools,
@@ -71758,9 +71935,8 @@ ${seen}`);
71758
71935
  }
71759
71936
  });
71760
71937
  let lastStepText = "";
71761
- let bufferedProxyText = "";
71762
71938
  let turnToolCalls = 0;
71763
- const shouldBufferProxyText = isWebSessionProxyModel(modelKey);
71939
+ const isProxyModel = isWebSessionProxyModel(modelKey);
71764
71940
  let pendingTools = 0;
71765
71941
  const noteActivity = () => {
71766
71942
  if (pendingTools === 0) armStallTimer();
@@ -71772,11 +71948,8 @@ ${seen}`);
71772
71948
  case "text-delta": {
71773
71949
  const delta = part.text ?? part.delta ?? "";
71774
71950
  lastStepText += delta;
71775
- if (shouldBufferProxyText) {
71776
- bufferedProxyText += delta;
71777
- } else {
71778
- render.text(delta);
71779
- }
71951
+ turnText += delta;
71952
+ render.text(delta);
71780
71953
  noteActivity();
71781
71954
  break;
71782
71955
  }
@@ -71784,12 +71957,16 @@ ${seen}`);
71784
71957
  render.reasoning(part.text ?? part.delta ?? "");
71785
71958
  noteActivity();
71786
71959
  break;
71787
- case "tool-call":
71960
+ case "tool-call": {
71788
71961
  turnToolCalls++;
71789
71962
  pendingTools++;
71790
71963
  clearStallTimer();
71791
- render.toolCall(part.toolName, part.input ?? part.args);
71964
+ const input = part.input ?? part.args ?? {};
71965
+ const brief = typeof input.command === "string" ? `$ ${input.command}` : typeof input.path === "string" ? `${String(input.action ?? "file")} ${input.path}` : String(part.toolName);
71966
+ turnToolSummaries.push(truncateBridgeSummary(brief, 160));
71967
+ render.toolCall(part.toolName, input);
71792
71968
  break;
71969
+ }
71793
71970
  case "tool-result":
71794
71971
  if (pendingTools > 0) pendingTools--;
71795
71972
  render.toolResult(part.toolName, part.output ?? part.result);
@@ -71801,7 +71978,6 @@ ${seen}`);
71801
71978
  break;
71802
71979
  case "start-step":
71803
71980
  lastStepText = "";
71804
- bufferedProxyText = "";
71805
71981
  noteActivity();
71806
71982
  break;
71807
71983
  case "error":
@@ -71834,8 +72010,8 @@ ${seen}`);
71834
72010
  const finishReason = String(
71835
72011
  await result.finishReason.catch(() => "unknown")
71836
72012
  );
71837
- const leakedCall = !shouldBufferProxyText && hasLeakedToolCallMarker(lastStepText);
71838
- if (finishReason === "stop" && turnToolCalls === 0 && bridgeSteps < MAX_BRIDGE_STEPS && (shouldBufferProxyText || leakedCall)) {
72013
+ const leakedCall = !isProxyModel && hasLeakedToolCallMarker(lastStepText);
72014
+ if (finishReason === "stop" && turnToolCalls === 0 && bridgeSteps < MAX_BRIDGE_STEPS && (isProxyModel || leakedCall)) {
71839
72015
  const bridged = parseProxyToolCalls(lastStepText);
71840
72016
  if (bridged) {
71841
72017
  bridgeSteps++;
@@ -71877,14 +72053,14 @@ ${resultText}`
71877
72053
  idx--;
71878
72054
  continue;
71879
72055
  }
71880
- if (shouldBufferProxyText && bufferedProxyText) {
71881
- render.text(bufferedProxyText);
71882
- }
71883
72056
  if (finishReason !== "stop") {
71884
72057
  render.info(
71885
72058
  `\u25B8 fim do turno: ${finishReason}` + (autoContinues >= MAX_AUTO_CONTINUES ? " (limite de retomadas autom\xE1ticas atingido)" : "")
71886
72059
  );
71887
72060
  }
72061
+ if (modelKey === PROXY_MODEL_KEYS.kimi) {
72062
+ await reportKimiSession();
72063
+ }
71888
72064
  render.endTurn();
71889
72065
  return;
71890
72066
  } catch (err) {
@@ -71902,6 +72078,7 @@ ${resultText}`
71902
72078
  while (history.length && history[history.length - 1]?.role !== "user") {
71903
72079
  history.pop();
71904
72080
  }
72081
+ preservePartialWork();
71905
72082
  const secs = Math.round(STREAM_STALL_TIMEOUT_MS / 1e3);
71906
72083
  lastError = new Error(
71907
72084
  `stream travou (${secs}s sem dados) em ${labelFor(modelKey)}`
@@ -71945,6 +72122,7 @@ ${resultText}`
71945
72122
  while (history.length && history[history.length - 1]?.role !== "user") {
71946
72123
  history.pop();
71947
72124
  }
72125
+ preservePartialWork();
71948
72126
  if (rateLimited && rateLimitWaits < MAX_RATE_LIMIT_WAITS) {
71949
72127
  const waitMs = Math.min(3e3 * 2 ** rateLimitWaits, 15e3);
71950
72128
  rateLimitWaits++;
@@ -71995,10 +72173,12 @@ ${resultText}`
71995
72173
  getModelChoices,
71996
72174
  getModelSelection,
71997
72175
  setModelSelection,
72176
+ isOpenRouterAvailable: hasOpenRouterKey,
72177
+ listOpenRouterModels: (signal) => listOpenRouterModels(signal),
71998
72178
  close
71999
72179
  };
72000
72180
  }
72001
- var import_promises2, import_node_path10, MAX_STEPS, MAX_OUTPUT_TOKENS, MAX_AUTO_CONTINUES, MAX_RATE_LIMIT_WAITS, STREAM_STALL_TIMEOUT_MS, MAX_STALL_RETRIES, isRateLimitError, sleep4, LEAKED_TOOL_CALL_RE, DANGLING_TAIL_RE, ACTION_ANNOUNCE_RE, BENIGN_CLOSER_RE, TOOL_CALL_NUDGE, MAX_BRIDGE_STEPS, BRIDGE_RESULT_PREAMBLE, LEAKED_CALL_RESULT_PREAMBLE, truncateBridgeSummary, isWebSessionProxyModel, proxyToolProtocolPolicy, SYSTEM_PROMPT_SOURCE, REQUIRED_SYSTEM_PROMPT_MARKERS, MODEL_LABELS, labelFor, loginRequiredHint, CLI_PYTHON_ONLY_POLICY, pythonOnlyPolicy, scriptFilePolicy, deepReconPolicy, httpAndFindingsPolicy, reconPhasesPolicy, skillsScopePolicy, hasEnvValue2, envFlagEnabled, CLI_PERSONALITIES, buildCliUserCustomization, buildCliNotesSection, cliGuardrailsConfig, maybeDumpSystemPrompt, auditSystemPrompt, assertFullSystemPrompt;
72181
+ var import_promises2, import_node_path10, MAX_STEPS, MAX_OUTPUT_TOKENS, MAX_AUTO_CONTINUES, MAX_RATE_LIMIT_WAITS, STREAM_STALL_TIMEOUT_MS, MAX_STALL_RETRIES, isRateLimitError, sleep4, LEAKED_TOOL_CALL_RE, DANGLING_TAIL_RE, ACTION_ANNOUNCE_RE, BENIGN_CLOSER_RE, TOOL_CALL_NUDGE, MAX_BRIDGE_STEPS, BRIDGE_RESULT_PREAMBLE, LEAKED_CALL_RESULT_PREAMBLE, BRIDGEABLE_TOOLS, FINDINGS_ACTIONS, truncateBridgeSummary, isWebSessionProxyModel, proxyToolProtocolPolicy, SYSTEM_PROMPT_SOURCE, REQUIRED_SYSTEM_PROMPT_MARKERS, MODEL_LABELS, labelFor, loginRequiredHint, CLI_PYTHON_ONLY_POLICY, pythonOnlyPolicy, scriptFilePolicy, deepReconPolicy, httpAndFindingsPolicy, reconPhasesPolicy, attackChainPolicy, skillsScopePolicy, hasEnvValue2, envFlagEnabled, CLI_PERSONALITIES, buildCliUserCustomization, buildCliNotesSection, cliGuardrailsConfig, maybeDumpSystemPrompt, auditSystemPrompt, assertFullSystemPrompt;
72002
72182
  var init_agent = __esm({
72003
72183
  "src/agent.ts"() {
72004
72184
  "use strict";
@@ -72060,6 +72240,14 @@ var init_agent = __esm({
72060
72240
  MAX_BRIDGE_STEPS = 50;
72061
72241
  BRIDGE_RESULT_PREAMBLE = "Voc\xEA roda via proxy de sess\xE3o web (sem function-calling nativo), ent\xE3o o runtime do CLI consumiu o bloco JSON anterior como chamada interna e EXECUTOU de verdade. N\xE3o repita o bloco nem o texto anterior. Os resultados REAIS est\xE3o abaixo. Continue a tarefa: se precisar de mais a\xE7\xF5es, emita SOMENTE o PR\xD3XIMO bloco ```json (um array de {brief, command, timeout} para terminal, ou {action, path, text} para arquivo). Um bloco por vez. Quando a tarefa terminar, responda em texto normal SEM bloco json.";
72062
72242
  LEAKED_CALL_RESULT_PREAMBLE = "Sua chamada de ferramenta anterior chegou como TEXTO (n\xE3o como function call nativo), ent\xE3o o runtime do CLI a executou mesmo assim. Os resultados REAIS est\xE3o abaixo. Continue a tarefa chamando as ferramentas NORMALMENTE (run_terminal_cmd, file, todo_write) como function calls de verdade \u2014 N\xC3O cole markup de tool call (`call_tool_function`, `<tool_call>`, etc.) nem JSON de chamada como texto na resposta.";
72243
+ BRIDGEABLE_TOOLS = /* @__PURE__ */ new Set([
72244
+ "run_terminal_cmd",
72245
+ "file",
72246
+ "findings",
72247
+ "http_request",
72248
+ "todo_write"
72249
+ ]);
72250
+ FINDINGS_ACTIONS = /* @__PURE__ */ new Set(["add", "update", "list", "report"]);
72063
72251
  truncateBridgeSummary = (value, max = 180) => value.length > max ? `${value.slice(0, max)}...` : value;
72064
72252
  isWebSessionProxyModel = (modelName) => modelName === "model-deepseek-proxy" || modelName === "model-kimi-proxy";
72065
72253
  proxyToolProtocolPolicy = () => `
@@ -72067,17 +72255,21 @@ var init_agent = __esm({
72067
72255
  <web_session_tool_protocol>
72068
72256
  ATIVO NESTA SESS\xC3O: o modelo atual roda por um proxy de sess\xE3o web e N\xC3O tem function-calling nativo. Para EXECUTAR qualquer a\xE7\xE3o voc\xEA n\xE3o chama ferramentas de fun\xE7\xE3o \u2014 voc\xEA EMITE um \xFAnico bloco de c\xF3digo \`\`\`json com um array de chamadas, e o runtime do CLI executa de verdade e te devolve os resultados como uma mensagem do usu\xE1rio. Repita o ciclo at\xE9 concluir.
72069
72257
 
72070
- Formato EXATO (sempre um array, mesmo para uma \xFAnica a\xE7\xE3o):
72258
+ Formato EXATO (sempre um array, mesmo para uma \xFAnica a\xE7\xE3o). Voc\xEA pode SEMPRE incluir "tool": "<nome>" para deixar expl\xEDcito qual ferramenta chamar (recomendado para findings/http_request/todo_write):
72071
72259
  - Terminal: {"brief": "uma frase", "command": "<comando shell>", "timeout": 60, "is_background": false}
72072
72260
  - Arquivo: {"action": "write"|"edit"|"append"|"read", "path": "<caminho absoluto>", "brief": "uma frase", "text": "<conte\xFAdo completo para write/append>"}
72261
+ - HTTP (Repeater/Intruder): {"tool": "http_request", "brief": "uma frase", "url": "https://alvo/...", "method": "GET", "headers": {"X":"y"}, "body": "...", "fuzz": ["a","b"]}
72262
+ - Findings (mem\xF3ria estruturada de vulns): {"tool": "findings", "action": "add"|"update"|"list"|"report", "brief": "uma frase", "title": "...", "severity": "critical"|"high"|"medium"|"low"|"info", "evidence": "...", "id": "F-001"}
72263
+ - Tarefas (todo): {"tool": "todo_write", "merge": true, "todos": [{"id": "1", "content": "...", "status": "pending"|"in_progress"|"completed"}]}
72073
72264
 
72074
72265
  Regras:
72075
72266
  1. Para agir, a resposta DEVE conter um bloco \`\`\`json com o array. Esse bloco ser\xE1 consumido pelo CLI como chamada interna e n\xE3o deve ser tratado como texto de terminal.
72076
72267
  2. UM bloco por turno; aguarde os resultados antes de emitir o pr\xF3ximo.
72077
- 3. Cada entrada do array deve mapear para uma a\xE7\xE3o (tem "command", ou tem "action"+"path"). N\xE3o misture com dados que n\xE3o s\xE3o a\xE7\xF5es.
72268
+ 3. Cada entrada do array deve mapear para uma a\xE7\xE3o: terminal ("command"), arquivo ("action"+"path"), ou outra ferramenta via "tool" ("http_request" com "url", "findings" com "action" add/update/list/report, "todo_write" com "todos"). N\xE3o misture com dados que n\xE3o s\xE3o a\xE7\xF5es.
72078
72269
  4. Crie/edite scripts e c\xF3digo-fonte SOMENTE via {"action":"write"} / {"action":"edit"} \u2014 nunca por redirecionamento de shell (echo >, cat <<EOF, tee, Out-File...).
72079
72270
  5. Durante a\xE7\xF5es, coloque o conte\xFAdo operacional apenas dentro do JSON. Fora do JSON use no m\xE1ximo uma frase curta.
72080
72271
  6. Quando a tarefa estiver conclu\xEDda, responda em texto normal SEM bloco json.
72272
+ 7. VOC\xCA N\xC3O TEM AMBIENTE PR\xD3PRIO. Voc\xEA n\xE3o possui sandbox, m\xE1quina, terminal ou sistema de arquivos seu. Caminhos como /mnt, /mnt/agents, /mnt/kimi N\xC3O existem. A \xDANICA forma de listar pastas, ler arquivos ou rodar comandos \xE9 emitindo o bloco \`\`\`json acima \u2014 que o runtime executa na m\xE1quina REAL do operador (use o caminho/diret\xF3rio de trabalho informado no system prompt). NUNCA invente sa\xEDda de comando, conte\xFAdo de arquivo ou estrutura de diret\xF3rios; s\xF3 use o que voltar como resultado real. Para "analisar/olhar/listar <caminho>", sua PRIMEIRA resposta deve ser um bloco json, sem pre\xE2mbulo.
72081
72273
  </web_session_tool_protocol>`;
72082
72274
  SYSTEM_PROMPT_SOURCE = "lib/system-prompt.ts";
72083
72275
  REQUIRED_SYSTEM_PROMPT_MARKERS = [
@@ -72105,7 +72297,7 @@ Regras:
72105
72297
  "fallback-openai-chat-latest": "OpenAI - chat-latest",
72106
72298
  "model-nvidia-nemotron": "NVIDIA - nemotron-3-ultra-550b"
72107
72299
  };
72108
- labelFor = (key) => MODEL_LABELS[key] ?? key;
72300
+ labelFor = (key) => isOpenRouterDynamicKey(key) ? `OpenRouter - ${openRouterSlugFromKey(key)}` : MODEL_LABELS[key] ?? key;
72109
72301
  loginRequiredHint = (msg) => {
72110
72302
  if (/account is suspended|violation of user policies|account has been suspended/i.test(
72111
72303
  msg
@@ -72211,6 +72403,26 @@ Beyond recon/recon_deep.py (HTTP/JS/secret/browser recon), the workspace at ${wo
72211
72403
 
72212
72404
  Each module writes a JSON (and where useful Markdown) report under ./reports/. Read it, then record anything actionable with the findings tool. You MAY extend any module via the file tool \u2014 keep it Python and keep the scope gate intact.
72213
72405
  </recon_phases>`;
72406
+ attackChainPolicy = (workdir) => `
72407
+
72408
+ <attack_and_remediation_chain>
72409
+ ABSOLUTE RULE FOR THIS LOCAL CLI SESSION \u2014 applies to EVERY model and EVERY task:
72410
+
72411
+ When the operator asks you to "attack", "explore vulnerabilities", "exploit", "pentest hard", "find a way in", or to FIX/close a vulnerability, follow the seeded offensive\u2192defensive chain at ${workdir}/recon. Do NOT hand-roll a one-off exploit script \u2014 these modules are the canonical engines, scope-gated by recon/scope.txt, AUTHORIZED-USE-ONLY, and strictly NON-DESTRUCTIVE (they prove impact; they never DROP data, mass-exfiltrate, or DoS).
72412
+
72413
+ 1. MAP THE ROUTES (saber as rotas): \`python recon/recon_routes.py https://alvo.com --wayback\`
72414
+ Aggregates crawl + HTML forms + JS endpoints + robots/sitemap + Swagger/OpenAPI (+ Wayback) into ONE route map, annotating each route with its parameters and whether it needs auth. Writes reports/routes_<host>.json. This is the attack surface.
72415
+
72416
+ 2. ATTACK & PROVE (o ataque): \`python recon/exploit_engine.py --routes reports/routes_<host>.json\`
72417
+ (or a single target: \`python recon/exploit_engine.py "https://alvo/item?id=1" --classes sqli,xss,lfi,cmdi,ssrf,redir\`). Runs verified playbooks per OWASP class (SQLi error/boolean/time-based, reflected XSS, LFI/traversal, command injection, SSRF, open redirect, IDOR with --idor-range). It only marks a finding "confirmed" when the LIVE response proves impact, capturing the exact payload + evidence. Use --cookie / --header for authenticated endpoints, --sleep for time-based, --oob for SSRF callbacks. Writes reports/exploit_<host>.json.
72418
+
72419
+ 3. RECORD: take every CONFIRMED finding from the exploit report and store it with the \`findings\` tool (status confirmed, paste the evidence). This is the engagement memory and the executive report.
72420
+
72421
+ 4. CLOSE THE BREACH (fechar a brecha para n\xE3o ter acesso de novo): \`python recon/harden.py --report reports/exploit_<host>.json\`
72422
+ For each confirmed finding it emits the concrete fix (parameterized queries, output encoding, allowlist validation, security headers, WAF rule) AND REPLAYS the PoC to verify: "FIXED" only once the exploit no longer reproduces. After the operator applies a fix, RUN harden.py AGAIN \u2014 a vuln is only closed when its verdict flips to FIXED \u2705. Report what is still STILL OPEN \u274C.
72423
+
72424
+ Dependencies (optional, improve fidelity): \`python -m pip install requests\`. You MAY extend any module via the file tool \u2014 keep it Python, keep the scope gate, keep it non-destructive.
72425
+ </attack_and_remediation_chain>`;
72214
72426
  skillsScopePolicy = () => `
72215
72427
 
72216
72428
  <skills_scope>
@@ -72430,15 +72642,107 @@ var init_interactive_input = __esm({
72430
72642
  };
72431
72643
  });
72432
72644
  }
72645
+ /**
72646
+ * Searchable single-choice list: a live filter bar on top, a scrolling
72647
+ * viewport of matches below. Type to filter, ↑/↓ to move (wraps), Enter to
72648
+ * pick, Esc / Ctrl+C to cancel. Built for big catalogs (e.g. every OpenRouter
72649
+ * model). Returns the chosen item's ORIGINAL index, or null on cancel.
72650
+ */
72651
+ searchSelect(title, items, opts = {}) {
72652
+ const pageSize = Math.max(4, opts.pageSize ?? 10);
72653
+ const placeholder = opts.placeholder ?? "digite para filtrar";
72654
+ let query = "";
72655
+ let sel = 0;
72656
+ let top = 0;
72657
+ this.lastRows = 0;
72658
+ out2("\n");
72659
+ const filtered = () => {
72660
+ const q = query.toLowerCase().trim();
72661
+ const all = items.map((it, i) => ({ it, i }));
72662
+ if (!q) return all;
72663
+ return all.filter(
72664
+ ({ it }) => it.label.toLowerCase().includes(q) || (it.hint ?? "").toLowerCase().includes(q)
72665
+ );
72666
+ };
72667
+ const draw = () => {
72668
+ const list = filtered();
72669
+ if (sel >= list.length) sel = Math.max(0, list.length - 1);
72670
+ if (sel < top) top = sel;
72671
+ if (sel >= top + pageSize) top = sel - pageSize + 1;
72672
+ this.renderSearchList(
72673
+ title,
72674
+ placeholder,
72675
+ query,
72676
+ list,
72677
+ sel,
72678
+ top,
72679
+ pageSize,
72680
+ items.length
72681
+ );
72682
+ };
72683
+ draw();
72684
+ return new Promise((resolve2) => {
72685
+ const finish = (value) => {
72686
+ this.collapseTo(this.lastRows);
72687
+ this.onKey = null;
72688
+ resolve2(value);
72689
+ };
72690
+ this.onKey = (str, key) => {
72691
+ if (key.ctrl && key.name === "c") return finish(null);
72692
+ const list = filtered();
72693
+ switch (key.name) {
72694
+ case "escape":
72695
+ return finish(null);
72696
+ case "return":
72697
+ if (list.length === 0) return;
72698
+ return finish(list[sel].i);
72699
+ case "up":
72700
+ if (list.length) sel = (sel - 1 + list.length) % list.length;
72701
+ draw();
72702
+ return;
72703
+ case "down":
72704
+ case "tab":
72705
+ if (list.length) sel = (sel + 1) % list.length;
72706
+ draw();
72707
+ return;
72708
+ case "backspace":
72709
+ if (query.length) {
72710
+ query = query.slice(0, -1);
72711
+ sel = 0;
72712
+ top = 0;
72713
+ }
72714
+ draw();
72715
+ return;
72716
+ }
72717
+ if (key.ctrl && key.name === "u") {
72718
+ query = "";
72719
+ sel = 0;
72720
+ top = 0;
72721
+ draw();
72722
+ return;
72723
+ }
72724
+ if (str && !key.ctrl && !key.meta && str.charCodeAt(0) >= 32) {
72725
+ query += str;
72726
+ sel = 0;
72727
+ top = 0;
72728
+ draw();
72729
+ }
72730
+ };
72731
+ });
72732
+ }
72433
72733
  /**
72434
72734
  * Lightweight line reader used WHILE a turn is running, so typed lines can be
72435
72735
  * forwarded to an interactive command's stdin. No box/dropdown — just echoes
72436
72736
  * and collects until Enter. Ctrl+C invokes `onSigint`.
72437
72737
  */
72438
- beginTurn(onLine, onSigint) {
72738
+ beginTurn(onLine, onSigint, isActive) {
72439
72739
  let buf = "";
72440
72740
  this.onKey = (str, key) => {
72441
72741
  if (key.ctrl && key.name === "c") return onSigint();
72742
+ if (!isActive()) {
72743
+ buf = "";
72744
+ return;
72745
+ }
72442
72746
  if (key.name === "return") {
72443
72747
  out2("\r\n");
72444
72748
  const line = this.paste.expand(buf);
@@ -72646,12 +72950,12 @@ var init_interactive_input = __esm({
72646
72950
  const prefix = frame2("\u2570\u2500") + gradient("\u276F", { bold: true }) + " ";
72647
72951
  const prefixLen = 4;
72648
72952
  const avail = Math.max(8, width - prefixLen - 1);
72649
- let start = 0;
72650
- if (this.cursor > avail) start = this.cursor - avail;
72651
- const visible = this.buffer.slice(start, start + avail);
72652
- const caretCol = prefixLen + (this.cursor - start);
72653
- const promptLine2 = prefix + visible;
72654
- const lines = [top, idLine, promptLine2];
72953
+ const { textLines, caretLine, caretCol } = this.wrap(
72954
+ prefix,
72955
+ prefixLen,
72956
+ avail
72957
+ );
72958
+ const lines = [top, idLine, ...textLines];
72655
72959
  const items = this.dropdownItems();
72656
72960
  if (items.length > 0) {
72657
72961
  this.ddSel = Math.min(this.ddSel, items.length - 1);
@@ -72676,9 +72980,27 @@ var init_interactive_input = __esm({
72676
72980
  } else {
72677
72981
  this.ddSel = 0;
72678
72982
  }
72679
- this.inputLineIndex = 2;
72983
+ this.inputLineIndex = 2 + caretLine;
72680
72984
  this.paint(lines, caretCol);
72681
72985
  }
72986
+ /**
72987
+ * Split the buffer into terminal-width rows. The first row carries `prefix`
72988
+ * (e.g. "❯ "); continuation rows are indented by `prefixLen` spaces so the
72989
+ * text columns line up. Also returns which wrapped row the caret sits on and
72990
+ * its absolute column, so paint() can place the cursor correctly.
72991
+ */
72992
+ wrap(prefix, prefixLen, avail) {
72993
+ const indent = " ".repeat(prefixLen);
72994
+ const caretLine = Math.floor(this.cursor / avail);
72995
+ const caretColInLine = this.cursor - caretLine * avail;
72996
+ const chunks = [];
72997
+ for (let i = 0; i < this.buffer.length; i += avail) {
72998
+ chunks.push(this.buffer.slice(i, i + avail));
72999
+ }
73000
+ while (chunks.length <= caretLine) chunks.push("");
73001
+ const textLines = chunks.map((c, i) => (i === 0 ? prefix : indent) + c);
73002
+ return { textLines, caretLine, caretCol: prefixLen + caretColInLine };
73003
+ }
72682
73004
  /** Compact single-line labelled prompt ("nome ❯ …") for step-by-step capture. */
72683
73005
  renderCompact() {
72684
73006
  const width = cols();
@@ -72686,12 +73008,13 @@ var init_interactive_input = __esm({
72686
73008
  const prefix = `${gradient(label, { bold: true })} ${gradient("\u276F", { bold: true })} `;
72687
73009
  const prefixLen = vlen(`${label} \u276F `);
72688
73010
  const avail = Math.max(8, width - prefixLen - 1);
72689
- let start = 0;
72690
- if (this.cursor > avail) start = this.cursor - avail;
72691
- const visible = this.buffer.slice(start, start + avail);
72692
- const caretCol = prefixLen + (this.cursor - start);
72693
- this.inputLineIndex = 0;
72694
- this.paint([prefix + visible], caretCol);
73011
+ const { textLines, caretLine, caretCol } = this.wrap(
73012
+ prefix,
73013
+ prefixLen,
73014
+ avail
73015
+ );
73016
+ this.inputLineIndex = caretLine;
73017
+ this.paint(textLines, caretCol);
72695
73018
  }
72696
73019
  /** Repaint the whole block, then place the cursor on the active input line. */
72697
73020
  paint(lines, caretCol) {
@@ -72736,6 +73059,42 @@ var init_interactive_input = __esm({
72736
73059
  );
72737
73060
  this.paintBlock(lines);
72738
73061
  }
73062
+ /** Render the search bar + a scrolling viewport of filtered matches. */
73063
+ renderSearchList(title, placeholder, query, list, sel, top, pageSize, total) {
73064
+ const width = cols();
73065
+ const inner = Math.min(Math.max(40, width - 4), 80);
73066
+ const lines = [];
73067
+ lines.push(`${frame2("\u256D\u2500\u25C6 ")}${gradient(title, { bold: true })}`);
73068
+ const shown = query.length ? query : `${C4.dim}${placeholder}${C4.reset}`;
73069
+ const counter = query.trim() ? `${C4.dim}${list.length}/${total}${C4.reset}` : `${C4.dim}${total}${C4.reset}`;
73070
+ lines.push(
73071
+ `${frame2("\u2502 ")}${gradient("\u2315", { bold: true })} ${shown}${C4.reset} ${counter}`
73072
+ );
73073
+ lines.push(frame2("\u2502"));
73074
+ if (list.length === 0) {
73075
+ lines.push(`${frame2("\u2502 ")}${C4.dim}nenhum modelo corresponde${C4.reset}`);
73076
+ } else {
73077
+ const end = Math.min(top + pageSize, list.length);
73078
+ for (let i = top; i < end; i++) {
73079
+ const active2 = i === sel;
73080
+ const { it } = list[i];
73081
+ const marker25 = active2 ? gradient("\u276F ", { bold: true }) : `${C4.dim} `;
73082
+ const text2 = active2 ? gradient(it.label, { bold: true }) : `${C4.reset}${it.label}`;
73083
+ const labelW = vlen(it.label);
73084
+ const hint = it.hint ? ` ${C4.dim}${truncate4(it.hint, Math.max(6, inner - labelW - 6))}${C4.reset}` : "";
73085
+ lines.push(`${frame2("\u2502 ")}${C4.reset}${marker25}${text2}${C4.reset}${hint}`);
73086
+ }
73087
+ if (list.length > pageSize) {
73088
+ lines.push(
73089
+ `${frame2("\u2502 ")}${C4.dim}${sel + 1}/${list.length}${C4.reset}`
73090
+ );
73091
+ }
73092
+ }
73093
+ lines.push(
73094
+ `${frame2("\u2570\u2500")} ${C4.dim}digite filtra \xB7 \u2191\u2193 navega \xB7 Enter seleciona \xB7 Esc cancela${C4.reset}`
73095
+ );
73096
+ this.paintBlock(lines);
73097
+ }
72739
73098
  /** Repaint a block and leave the cursor just below it (for list/menu modes). */
72740
73099
  paintBlock(lines) {
72741
73100
  let s = "";
@@ -72793,8 +73152,8 @@ async function main() {
72793
73152
  `
72794
73153
  );
72795
73154
  const COMMANDS = [
72796
- { name: "/model", desc: "trocar o modelo (seletor com setas)" },
72797
- { name: "/api", desc: "trocar a chave NVIDIA (testa e ativa na hora)" },
73155
+ { name: "/model", desc: "trocar o modelo (setas; OpenRouter c/ busca)" },
73156
+ { name: "/api", desc: "trocar chave de API (NVIDIA / OpenRouter)" },
72798
73157
  { name: "/skills", desc: "listar as skills instaladas" },
72799
73158
  { name: "/skillcreator", desc: "criar uma nova skill" },
72800
73159
  { name: "/system", desc: "salvar o system prompt (HTML) na \xC1rea de Trabalho" },
@@ -73047,69 +73406,66 @@ ${C5.dim}ja disponivel para todos os modelos nesta sessao.${C5.reset}
73047
73406
  `
73048
73407
  );
73049
73408
  };
73050
- const openModelSelector = async () => {
73051
- const state = agent.getModelSelection();
73052
- const items = [
73053
- {
73054
- label: "Auto \u2014 cadeia de fallback autom\xE1tica",
73055
- hint: "tenta os modelos em ordem"
73056
- },
73057
- ...state.chain.map((c) => ({ label: c.label, hint: c.key }))
73058
- ];
73059
- const activeIdx = state.mode === "auto" ? 0 : Math.max(
73060
- 0,
73061
- 1 + state.chain.findIndex((c) => c.key === state.activeModelKey)
73062
- );
73063
- const choice2 = await inputUI.select("selecionar modelo", items, activeIdx);
73064
- if (choice2 === null) {
73065
- process.stdout.write(`${C5.dim}sele\xE7\xE3o de modelo cancelada${C5.reset}
73066
- `);
73067
- return;
73068
- }
73069
- const arg = choice2 === 0 ? "auto" : state.chain[choice2 - 1].key;
73070
- try {
73071
- printModelResult(await agent.setModelSelection(arg));
73072
- } catch (err) {
73073
- printFatal(err);
73409
+ const KEY_PROVIDERS = [
73410
+ {
73411
+ id: "nvidia",
73412
+ name: "NVIDIA",
73413
+ envVar: "NVIDIA_API_KEY",
73414
+ createUrl: "https://build.nvidia.com/",
73415
+ keyRegex: /nvapi-[A-Za-z0-9_-]+/,
73416
+ keyHint: "nvapi-\u2026",
73417
+ test: testNvidiaKey,
73418
+ swap: swapNvidiaKey
73419
+ },
73420
+ {
73421
+ id: "openrouter",
73422
+ name: "OpenRouter",
73423
+ envVar: "OPENROUTER_API_KEY",
73424
+ createUrl: "https://openrouter.ai/keys",
73425
+ keyRegex: /sk-or-[A-Za-z0-9_-]+/,
73426
+ keyHint: "sk-or-\u2026",
73427
+ test: testOpenRouterKey,
73428
+ swap: swapOpenRouterKey
73074
73429
  }
73075
- };
73076
- const handleApiSwap = async (inlineKey) => {
73430
+ ];
73431
+ const openRouterProvider = KEY_PROVIDERS.find((p) => p.id === "openrouter");
73432
+ const promptAndSwapKey = async (provider, inlineKey = "") => {
73077
73433
  let raw = inlineKey;
73078
73434
  if (!raw) {
73079
73435
  process.stdout.write(
73080
- `${C5.dim}Cole a nova chave NVIDIA (cria em ${C5.reset}${C5.cyan}https://build.nvidia.com/${C5.reset}${C5.dim}). Enter vazio ou Ctrl+C cancela.${C5.reset}
73436
+ `${C5.dim}Cole a nova chave ${provider.name} (cria em ${C5.reset}${C5.cyan}${provider.createUrl}${C5.reset}${C5.dim}). Enter vazio ou Ctrl+C cancela.${C5.reset}
73081
73437
  `
73082
73438
  );
73083
73439
  const res = await inputUI.prompt({
73084
- label: "nova NVIDIA API key",
73440
+ label: `nova ${provider.name} API key`,
73085
73441
  secret: true
73086
73442
  });
73087
73443
  if (res.type !== "line" || !res.value.trim()) {
73088
73444
  process.stdout.write(`${C5.dim}troca de chave cancelada${C5.reset}
73089
73445
  `);
73090
- return;
73446
+ return false;
73091
73447
  }
73092
73448
  raw = res.value;
73093
73449
  }
73094
- const key = raw.replace(/\s+/g, "").match(/nvapi-[A-Za-z0-9_-]+/)?.[0] ?? "";
73450
+ const key = raw.replace(/\s+/g, "").match(provider.keyRegex)?.[0] ?? "";
73095
73451
  if (!key) {
73096
73452
  process.stdout.write(
73097
- `${C5.red}\u2717 n\xE3o encontrei uma chave NVIDIA no que foi colado${C5.reset} ${C5.dim}(esperado um token "nvapi-\u2026"; cole a chave completa).${C5.reset}
73453
+ `${C5.red}\u2717 n\xE3o encontrei uma chave ${provider.name} no que foi colado${C5.reset} ${C5.dim}(esperado um token "${provider.keyHint}"; cole a chave completa).${C5.reset}
73098
73454
  `
73099
73455
  );
73100
- return;
73456
+ return false;
73101
73457
  }
73102
- process.stdout.write(`${C5.dim}testando a chave na NVIDIA\u2026${C5.reset}
73458
+ process.stdout.write(`${C5.dim}testando a chave na ${provider.name}\u2026${C5.reset}
73103
73459
  `);
73104
- const test = await testNvidiaKey(key);
73460
+ const test = await provider.test(key);
73105
73461
  if (!test.ok) {
73106
73462
  if (test.status === 401 || test.status === 403) {
73107
73463
  process.stdout.write(
73108
- `${C5.red}\u2717 a NVIDIA recusou a chave (${test.status}): ${test.detail}${C5.reset}
73109
- ${C5.dim}chave N\xC3O salva \u2014 confira se copiou inteira e se a conta tem acesso de infer\xEAncia.${C5.reset}
73464
+ `${C5.red}\u2717 a ${provider.name} recusou a chave (${test.status}): ${test.detail}${C5.reset}
73465
+ ${C5.dim}chave N\xC3O salva \u2014 confira se copiou inteira e se a conta tem acesso.${C5.reset}
73110
73466
  `
73111
73467
  );
73112
- return;
73468
+ return false;
73113
73469
  }
73114
73470
  process.stdout.write(
73115
73471
  `${C5.yellow}\u26A0 n\xE3o consegui validar (${test.status || "rede"}): ${test.detail}${C5.reset}
@@ -73117,13 +73473,123 @@ ${C5.dim}salvando mesmo assim \u2014 o pr\xF3ximo pedido vai usar a chave nova.$
73117
73473
  `
73118
73474
  );
73119
73475
  }
73120
- const file2 = swapNvidiaKey(key);
73476
+ const file2 = provider.swap(key);
73121
73477
  const masked = `${key.slice(0, 9)}\u2026${key.slice(-4)} (${key.length} chars)`;
73122
73478
  process.stdout.write(
73123
- `${C5.green}\u2713 chave NVIDIA trocada e ativa${C5.reset} ${C5.dim}${masked}${C5.reset}
73479
+ `${C5.green}\u2713 chave ${provider.name} trocada e ativa${C5.reset} ${C5.dim}${masked}${C5.reset}
73124
73480
  ${C5.dim}salva em ${C5.reset}${C5.cyan}${file2}${C5.reset}
73125
73481
  `
73126
73482
  );
73483
+ return true;
73484
+ };
73485
+ const openOpenRouterPicker = async () => {
73486
+ if (!agent.isOpenRouterAvailable()) {
73487
+ process.stdout.write(
73488
+ `${C5.dim}OpenRouter ainda sem chave. Vamos adicionar uma.${C5.reset}
73489
+ `
73490
+ );
73491
+ const ok = await promptAndSwapKey(openRouterProvider);
73492
+ if (!ok) return;
73493
+ }
73494
+ process.stdout.write(`${C5.dim}buscando cat\xE1logo OpenRouter\u2026${C5.reset}
73495
+ `);
73496
+ let models;
73497
+ try {
73498
+ models = await agent.listOpenRouterModels();
73499
+ } catch (err) {
73500
+ process.stdout.write(
73501
+ `${C5.red}\u2717 falha ao listar modelos OpenRouter: ${err instanceof Error ? err.message : String(err)}${C5.reset}
73502
+ `
73503
+ );
73504
+ return;
73505
+ }
73506
+ if (models.length === 0) {
73507
+ process.stdout.write(
73508
+ `${C5.yellow}\u26A0 a OpenRouter n\xE3o retornou nenhum modelo${C5.reset}
73509
+ `
73510
+ );
73511
+ return;
73512
+ }
73513
+ const items = models.map((m) => ({
73514
+ label: m.name,
73515
+ hint: `${m.id}${m.promptPrice === "0" ? " \xB7 free" : ""}`
73516
+ }));
73517
+ const choice2 = await inputUI.searchSelect(
73518
+ "OpenRouter \u2014 buscar modelo",
73519
+ items,
73520
+ { placeholder: "digite para filtrar (nome ou slug)" }
73521
+ );
73522
+ if (choice2 === null) {
73523
+ process.stdout.write(`${C5.dim}sele\xE7\xE3o de modelo cancelada${C5.reset}
73524
+ `);
73525
+ return;
73526
+ }
73527
+ try {
73528
+ printModelResult(
73529
+ await agent.setModelSelection(`openrouter:${models[choice2].id}`)
73530
+ );
73531
+ } catch (err) {
73532
+ printFatal(err);
73533
+ }
73534
+ };
73535
+ const openModelSelector = async () => {
73536
+ const state = agent.getModelSelection();
73537
+ const orAvailable = agent.isOpenRouterAvailable();
73538
+ const items = [
73539
+ {
73540
+ label: "Auto \u2014 cadeia de fallback autom\xE1tica",
73541
+ hint: "tenta os modelos em ordem"
73542
+ },
73543
+ ...state.chain.map((c) => ({ label: c.label, hint: c.key }))
73544
+ ];
73545
+ const openRouterIdx = items.length;
73546
+ items.push({
73547
+ label: "OpenRouter \u2014 buscar no cat\xE1logo completo",
73548
+ hint: orAvailable ? "lista todos os modelos da sua conta" : "pede a API key e lista o cat\xE1logo"
73549
+ });
73550
+ const activeIsOpenRouter = state.activeModelKey.startsWith("openrouter:");
73551
+ const activeIdx = state.mode === "auto" ? 0 : activeIsOpenRouter ? openRouterIdx : Math.max(
73552
+ 0,
73553
+ 1 + state.chain.findIndex((c) => c.key === state.activeModelKey)
73554
+ );
73555
+ const choice2 = await inputUI.select("selecionar modelo", items, activeIdx);
73556
+ if (choice2 === null) {
73557
+ process.stdout.write(`${C5.dim}sele\xE7\xE3o de modelo cancelada${C5.reset}
73558
+ `);
73559
+ return;
73560
+ }
73561
+ if (choice2 === openRouterIdx) {
73562
+ await openOpenRouterPicker();
73563
+ return;
73564
+ }
73565
+ const arg = choice2 === 0 ? "auto" : state.chain[choice2 - 1].key;
73566
+ try {
73567
+ printModelResult(await agent.setModelSelection(arg));
73568
+ } catch (err) {
73569
+ printFatal(err);
73570
+ }
73571
+ };
73572
+ const handleApiSwap = async (inlineKey) => {
73573
+ if (inlineKey) {
73574
+ const stripped = inlineKey.replace(/\s+/g, "");
73575
+ const provider = KEY_PROVIDERS.find((p) => p.keyRegex.test(stripped)) ?? KEY_PROVIDERS[0];
73576
+ await promptAndSwapKey(provider, inlineKey);
73577
+ return;
73578
+ }
73579
+ const idx = await inputUI.select(
73580
+ "trocar chave de API",
73581
+ KEY_PROVIDERS.map((p) => ({
73582
+ label: p.name,
73583
+ hint: process.env[p.envVar]?.trim() ? `${p.envVar} (configurada)` : p.envVar
73584
+ })),
73585
+ 0
73586
+ );
73587
+ if (idx === null) {
73588
+ process.stdout.write(`${C5.dim}troca de chave cancelada${C5.reset}
73589
+ `);
73590
+ return;
73591
+ }
73592
+ await promptAndSwapKey(KEY_PROVIDERS[idx]);
73127
73593
  };
73128
73594
  const handleLine = async (line) => {
73129
73595
  if (closing) return;
@@ -73150,6 +73616,10 @@ ${C5.dim}salva em ${C5.reset}${C5.cyan}${file2}${C5.reset}
73150
73616
  }
73151
73617
  if (input.startsWith("/model ") || input.startsWith("/models ")) {
73152
73618
  const arg = input.replace(/^\/models?\s*/, "");
73619
+ if (arg.toLowerCase() === "openrouter" || arg.toLowerCase() === "or") {
73620
+ await openOpenRouterPicker();
73621
+ return;
73622
+ }
73153
73623
  try {
73154
73624
  printModelResult(await agent.setModelSelection(arg));
73155
73625
  } catch (err) {
@@ -73218,7 +73688,10 @@ ${C5.dim}interrompido \u2014 Ctrl+C de novo para fechar${C5.reset}
73218
73688
  );
73219
73689
  }
73220
73690
  armExit();
73221
- }
73691
+ },
73692
+ // Only echo/forward typed input while a foreground command is waiting on
73693
+ // stdin; otherwise type-ahead during the model's reply is dropped silently.
73694
+ () => agent.isRunningCommand()
73222
73695
  );
73223
73696
  try {
73224
73697
  await agent.send(input, activeAbort.signal);
@@ -73300,11 +73773,12 @@ var init_index = __esm({
73300
73773
  configuredModelProviders = [
73301
73774
  deepseekEnabled ? "DeepSeek (proxy/web session)" : null,
73302
73775
  process.env.NVIDIA_API_KEY?.trim() ? "NVIDIA build" : null,
73303
- process.env.OPENAI_API_KEY?.trim() ? "OpenAI" : null
73776
+ process.env.OPENAI_API_KEY?.trim() ? "OpenAI" : null,
73777
+ process.env.OPENROUTER_API_KEY?.trim() ? "OpenRouter" : null
73304
73778
  ].filter((name25) => Boolean(name25));
73305
73779
  if (configuredModelProviders.length === 0) {
73306
73780
  console.error(
73307
- `Nenhuma chave de modelo configurada. Defina NVIDIA_API_KEY (ou OPENAI_API_KEY) em ${clawfastEnvPath()} ou como vari\xE1vel de ambiente.`
73781
+ `Nenhuma chave de modelo configurada. Defina NVIDIA_API_KEY (ou OPENAI_API_KEY / OPENROUTER_API_KEY) em ${clawfastEnvPath()} ou como vari\xE1vel de ambiente.`
73308
73782
  );
73309
73783
  process.exit(1);
73310
73784
  }
@@ -73337,6 +73811,7 @@ async function handleSubcommand() {
73337
73811
  process.stdout.write(
73338
73812
  `
73339
73813
  \x1B[92m\x1B[1mSeja bem vindo ao submundo.\x1B[0m
73814
+ \x1B[2mRode \x1B[0m\x1B[36mclawfast\x1B[0m\x1B[2m para usar a nova versao.\x1B[0m
73340
73815
  `
73341
73816
  );
73342
73817
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawfast",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "CLAWFAST — agente de pentest no terminal. Com o clawfast você faz tudo.",
5
5
  "bin": {
6
6
  "clawfast": "dist/clawfast.cjs"