kafox-cli 0.9.7 → 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 +40 -12
package/package.json CHANGED
@@ -1,7 +1,23 @@
1
1
  {
2
2
  "name": "kafox-cli",
3
- "version": "0.9.7",
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
@@ -489,6 +489,9 @@ function initialState() {
489
489
  messages: [],
490
490
  context: [],
491
491
  streaming: null,
492
+ // Texto ja comitado em finishStreaming() neste turno: usado para nao
493
+ // duplicar a resposta quando runAgent devolve o texto no final.
494
+ streamedText: "",
492
495
  approval: null,
493
496
  autoApprove: false,
494
497
  busy: false,
@@ -902,8 +905,36 @@ function formatTokens(count) {
902
905
  return `${(count / 1_000_000).toFixed(1)}M`;
903
906
  }
904
907
 
905
- function estimateTokens(state) {
906
- 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;
907
938
  for (const message of state.context) chars += String(message.content || "").length;
908
939
  if (state.streaming) chars += String(state.streaming.text || "").length;
909
940
  return Math.round(chars / 4);
@@ -1950,13 +1981,10 @@ async function runTUI({
1950
1981
  if (result.messages) state.context = result.messages;
1951
1982
  } else if (result.ok) {
1952
1983
  const text = String(result.text || "").trim();
1953
- if (text) {
1954
- state.messages.push({
1955
- role: "assistant",
1956
- text,
1957
- ms: result.ms,
1958
- });
1959
- }
1984
+ // O stream ja foi commitado em finishStreaming(): so empurra aqui se for
1985
+ // texto novo (ou se nao houve streaming, ex.: resposta sem onText).
1986
+ if (text) commitFinalText(state, text, result.ms);
1987
+ state.streamedText = "";
1960
1988
  if (result.messages) state.context = result.messages;
1961
1989
  } else {
1962
1990
  state.messages.push({
@@ -1983,9 +2011,7 @@ async function runTUI({
1983
2011
  ms: streaming.thinkingStartedAt ? Date.now() - streaming.thinkingStartedAt : 0,
1984
2012
  });
1985
2013
  }
1986
- if (streaming.text.trim()) {
1987
- state.messages.push({ role: "assistant", text: streaming.text.trim() });
1988
- }
2014
+ if (streaming.text.trim()) commitStreamedText(state, streaming.text.trim());
1989
2015
  state.streaming = { text: "", reasoning: "", thinkingStartedAt: 0 };
1990
2016
  }
1991
2017
 
@@ -2383,4 +2409,6 @@ module.exports = {
2383
2409
  planRepaint,
2384
2410
  APPROVAL_OPTIONS,
2385
2411
  layoutFor,
2412
+ commitStreamedText,
2413
+ commitFinalText,
2386
2414
  };