kafox-cli 0.9.8 → 0.9.9

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 (3) hide show
  1. package/package.json +18 -1
  2. package/src/tools.js +13 -2
  3. package/src/tui.js +34 -19
package/package.json CHANGED
@@ -1,7 +1,23 @@
1
1
  {
2
2
  "name": "kafox-cli",
3
- "version": "0.9.8",
3
+ "version": "0.9.9",
4
4
  "description": "Kafox CLI: TUI com streaming, thinking, edit com diff, agent tools (files/shell), MCP e temas",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/kafox/kafox-cli.git"
8
+ },
9
+ "homepage": "https://kafox.com",
10
+ "bugs": {
11
+ "url": "https://github.com/kafox/kafox-cli/issues"
12
+ },
13
+ "keywords": [
14
+ "cli",
15
+ "tui",
16
+ "ai",
17
+ "agent",
18
+ "llm",
19
+ "mcp"
20
+ ],
5
21
  "bin": {
6
22
  "kafox": "bin/kafox.js"
7
23
  },
@@ -14,6 +30,7 @@
14
30
  "node": ">=22.5"
15
31
  },
16
32
  "license": "MIT",
33
+ "author": "Kafox <dev@kafox.com> (https://kafox.com)",
17
34
  "dependencies": {
18
35
  "chalk": "4.1.2",
19
36
  "commander": "12.1.0",
package/src/tools.js CHANGED
@@ -122,9 +122,20 @@ function toolMeta(name, hub) {
122
122
 
123
123
  function resolveInside(cwd, target) {
124
124
  const root = path.resolve(cwd);
125
- const full = path.resolve(root, target || ".");
125
+ const raw = String(target ?? ".");
126
+ // Caminho absoluto de outro sistema (ex.: "C:\Windows\..." no Linux) e
127
+ // NUL nunca valem: sem isso o `path.resolve` trata como nome de arquivo.
128
+ if (raw.includes("\0")) throw new Error(`caminho invalido: ${raw}`);
129
+ if (path.win32.isAbsolute(raw) || path.posix.isAbsolute(raw)) {
130
+ const full = path.isAbsolute(raw) ? path.resolve(raw) : null;
131
+ if (!full || (full !== root && !full.startsWith(root + path.sep))) {
132
+ throw new Error(`caminho fora do diretorio de trabalho: ${raw}`);
133
+ }
134
+ return full;
135
+ }
136
+ const full = path.resolve(root, raw);
126
137
  if (full !== root && !full.startsWith(root + path.sep)) {
127
- throw new Error(`caminho fora do diretorio de trabalho: ${target}`);
138
+ throw new Error(`caminho fora do diretorio de trabalho: ${raw}`);
128
139
  }
129
140
  return full;
130
141
  }
package/src/tui.js CHANGED
@@ -905,8 +905,36 @@ function formatTokens(count) {
905
905
  return `${(count / 1_000_000).toFixed(1)}M`;
906
906
  }
907
907
 
908
- function estimateTokens(state) {
909
- let chars = 0;
908
+ /**
909
+ * Commita o texto que chegou pelo stream como mensagem `assistant`.
910
+ * Nao duplica quando o mesmo trecho ja foi commitado neste turno (retry do
911
+ * gateway repetindo tokens, `onToolStart` chamado duas vezes, etc.).
912
+ */
913
+ function commitStreamedText(state, text) {
914
+ const value = String(text || "").trim();
915
+ if (!value) return false;
916
+ if (state.streamedText.includes(value)) return false;
917
+ const previous = [...state.messages].reverse().find((message) => message.role === "assistant");
918
+ if (previous && previous.text === value) return false;
919
+ state.messages.push({ role: "assistant", text: value });
920
+ state.streamedText = `${state.streamedText}${value}`;
921
+ return true;
922
+ }
923
+
924
+ /**
925
+ * Ultimo texto devolvido por `runAgent`. Quando o stream ja mostrou esse texto
926
+ * (commit em `finishStreaming`), nao empurra de novo: era isso que fazia a
927
+ * resposta aparecer duas vezes na tela.
928
+ */
929
+ function commitFinalText(state, text, ms) {
930
+ const value = String(text || "").trim();
931
+ if (!value) return false;
932
+ if (state.streamedText.includes(value)) return false;
933
+ state.messages.push({ role: "assistant", text: value, ms });
934
+ return true;
935
+ }
936
+
937
+ function estimateTokens(state) { let chars = 0;
910
938
  for (const message of state.context) chars += String(message.content || "").length;
911
939
  if (state.streaming) chars += String(state.streaming.text || "").length;
912
940
  return Math.round(chars / 4);
@@ -1955,13 +1983,7 @@ async function runTUI({
1955
1983
  const text = String(result.text || "").trim();
1956
1984
  // O stream ja foi commitado em finishStreaming(): so empurra aqui se for
1957
1985
  // texto novo (ou se nao houve streaming, ex.: resposta sem onText).
1958
- if (text && !state.streamedText.includes(text)) {
1959
- state.messages.push({
1960
- role: "assistant",
1961
- text,
1962
- ms: result.ms,
1963
- });
1964
- }
1986
+ if (text) commitFinalText(state, text, result.ms);
1965
1987
  state.streamedText = "";
1966
1988
  if (result.messages) state.context = result.messages;
1967
1989
  } else {
@@ -1989,16 +2011,7 @@ async function runTUI({
1989
2011
  ms: streaming.thinkingStartedAt ? Date.now() - streaming.thinkingStartedAt : 0,
1990
2012
  });
1991
2013
  }
1992
- if (streaming.text.trim()) {
1993
- const text = streaming.text.trim();
1994
- // Evita repetir o texto quando o mesmo trecho ja foi commitado (retry
1995
- // que repetiu tokens ou onToolStart chamado duas vezes).
1996
- const previous = [...state.messages].reverse().find((message) => message.role === "assistant");
1997
- if (!previous || previous.text !== text) {
1998
- state.messages.push({ role: "assistant", text });
1999
- state.streamedText += text;
2000
- }
2001
- }
2014
+ if (streaming.text.trim()) commitStreamedText(state, streaming.text.trim());
2002
2015
  state.streaming = { text: "", reasoning: "", thinkingStartedAt: 0 };
2003
2016
  }
2004
2017
 
@@ -2396,4 +2409,6 @@ module.exports = {
2396
2409
  planRepaint,
2397
2410
  APPROVAL_OPTIONS,
2398
2411
  layoutFor,
2412
+ commitStreamedText,
2413
+ commitFinalText,
2399
2414
  };