@soltaoverbo/cli 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -3,11 +3,8 @@
3
3
  // src/bin.ts
4
4
  import { execSync } from "child_process";
5
5
 
6
- // src/commands/install.ts
7
- import { existsSync, readFileSync, writeFileSync } from "fs";
8
- import { homedir } from "os";
9
- import { join } from "path";
10
- import { buildTermIndex, loadTerms, SpacedRepetition } from "@soltaoverbo/core";
6
+ // src/commands/config.ts
7
+ import { getApiKey, setApiKey } from "@soltaoverbo/core";
11
8
 
12
9
  // src/ui.ts
13
10
  var R = "\x1B[0m";
@@ -22,7 +19,101 @@ function progressBar(pct, width) {
22
19
  return `\x1B[32m${"█".repeat(filled)}\x1B[2m${"░".repeat(empty)}${R}`;
23
20
  }
24
21
 
22
+ // src/commands/config.ts
23
+ function runConfig(args) {
24
+ const [subcmd, ...rest2] = args;
25
+ switch (subcmd) {
26
+ case "set-key": {
27
+ const key = rest2[0];
28
+ if (!key) {
29
+ console.error(`${bold("Uso:")} verbo config set-key <chave>`);
30
+ process.exit(1);
31
+ }
32
+ setApiKey(key);
33
+ console.log(`${bold("[verbo]")} API key salva em ${dim("~/.verbo/config.json")}`);
34
+ break;
35
+ }
36
+ case "show-key": {
37
+ const key = getApiKey();
38
+ if (!key) {
39
+ console.log(dim("Nenhuma API key configurada."));
40
+ } else {
41
+ console.log(`API key: ${cyan(`${key.slice(0, 8)}...${key.slice(-4)}`)}`);
42
+ }
43
+ break;
44
+ }
45
+ default:
46
+ console.log(
47
+ `${bold("verbo config")} — configurações do verbo
48
+
49
+ ${bold("set-key")} ${dim("<chave>")} Salva a API key da Anthropic em ~/.verbo/config.json
50
+ ${bold("show-key")} Exibe a API key atual (parcial)
51
+ `
52
+ );
53
+ }
54
+ }
55
+
56
+ // src/commands/explain.ts
57
+ import { createInterface } from "readline";
58
+ import { readFileSync } from "fs";
59
+ import { basename, extname } from "path";
60
+ import { explainCode, getApiKey as getApiKey2, hasConsented, injectExplanations, setConsented } from "@soltaoverbo/core";
61
+ async function runExplain(args) {
62
+ const filePath = args[0];
63
+ if (!filePath) {
64
+ console.error(`${bold("Uso:")} verbo explain <arquivo>`);
65
+ process.exit(1);
66
+ }
67
+ const apiKey = getApiKey2();
68
+ if (!apiKey) {
69
+ console.error(
70
+ `${bold("verbo explain")} requer uma API key da Anthropic.
71
+ Configure com: ${cyan("verbo config set-key <sua-chave>")}
72
+ Obtenha sua chave em: https://console.anthropic.com/`
73
+ );
74
+ process.exit(1);
75
+ }
76
+ if (!hasConsented()) {
77
+ const confirmed = await askConsent(basename(filePath));
78
+ if (!confirmed) {
79
+ console.error("Operação cancelada.");
80
+ process.exit(0);
81
+ }
82
+ setConsented();
83
+ }
84
+ let code;
85
+ try {
86
+ code = readFileSync(filePath, "utf-8");
87
+ } catch {
88
+ console.error(`Não foi possível ler o arquivo: ${filePath}`);
89
+ process.exit(1);
90
+ }
91
+ const lang = extname(filePath).replace(".", "") || "código";
92
+ process.stderr.write(dim(`[verbo] Explicando ${basename(filePath)}...
93
+ `));
94
+ const explanations = await explainCode(code, lang, apiKey);
95
+ const annotated = injectExplanations(code, explanations);
96
+ process.stdout.write(annotated + "\n");
97
+ }
98
+ function askConsent(fileName) {
99
+ return new Promise((resolve) => {
100
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
101
+ rl.question(
102
+ `${bold("[verbo]")} O conteúdo de ${cyan(fileName)} será enviado para a API da Anthropic.
103
+ Esta mensagem só aparece uma vez. Continuar? (s/N): `,
104
+ (answer) => {
105
+ rl.close();
106
+ resolve(answer.trim().toLowerCase() === "s");
107
+ }
108
+ );
109
+ });
110
+ }
111
+
25
112
  // src/commands/install.ts
113
+ import { existsSync, readFileSync as readFileSync2, writeFileSync } from "fs";
114
+ import { homedir } from "os";
115
+ import { join } from "path";
116
+ import { buildTermIndex, loadTerms, SpacedRepetition } from "@soltaoverbo/core";
26
117
  var CLAUDE_SETTINGS = join(homedir(), ".claude", "settings.json");
27
118
  var VERBO_PREFIX = "verbo · ";
28
119
  function runInstall() {
@@ -30,7 +121,7 @@ function runInstall() {
30
121
  console.error(`Claude Code não encontrado em ${CLAUDE_SETTINGS}`);
31
122
  process.exit(1);
32
123
  }
33
- const settings = JSON.parse(readFileSync(CLAUDE_SETTINGS, "utf-8"));
124
+ const settings = JSON.parse(readFileSync2(CLAUDE_SETTINGS, "utf-8"));
34
125
  const sr = new SpacedRepetition();
35
126
  const allTerms = buildTermIndex(loadTerms());
36
127
  const absorbed = sr.absorbedIds();
@@ -110,10 +201,10 @@ function runList(args) {
110
201
  }
111
202
 
112
203
  // src/commands/reset.ts
113
- import { createInterface } from "readline";
204
+ import { createInterface as createInterface2 } from "readline";
114
205
  import { SpacedRepetition as SpacedRepetition3 } from "@soltaoverbo/core";
115
206
  async function runReset() {
116
- const rl = createInterface({ input: process.stdin, output: process.stdout });
207
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
117
208
  await new Promise((resolve) => {
118
209
  rl.question(
119
210
  `${yellow("⚠")} Isso apagará todo o histórico de aprendizado. Confirmar? ${dim("[s/N]")} `,
@@ -132,9 +223,9 @@ async function runReset() {
132
223
  }
133
224
 
134
225
  // src/commands/start.ts
135
- import { createInterface as createInterface2 } from "readline";
136
- import { readFileSync as readFileSync2 } from "fs";
137
- import { extname } from "path";
226
+ import { createInterface as createInterface3 } from "readline";
227
+ import { readFileSync as readFileSync3 } from "fs";
228
+ import { extname as extname2 } from "path";
138
229
  import {
139
230
  buildTermIndex as buildTermIndex2,
140
231
  injectComments,
@@ -187,7 +278,7 @@ async function runStart(args) {
187
278
  }
188
279
  }
189
280
  async function pipeMode(sr, terms) {
190
- const rl = createInterface2({ input: process.stdin, terminal: false });
281
+ const rl = createInterface3({ input: process.stdin, terminal: false });
191
282
  const lines = [];
192
283
  for await (const line of rl) lines.push(line);
193
284
  const seenTerms = sr.absorbedIds();
@@ -211,10 +302,10 @@ async function watchMode(dir, extensions, sr, terms) {
211
302
  awaitWriteFinish: { stabilityThreshold: 150, pollInterval: 50 }
212
303
  });
213
304
  const handle = (filePath) => {
214
- if (!extensions.has(extname(filePath))) return;
305
+ if (!extensions.has(extname2(filePath))) return;
215
306
  let source;
216
307
  try {
217
- source = readFileSync2(filePath, "utf-8");
308
+ source = readFileSync3(filePath, "utf-8");
218
309
  } catch {
219
310
  return;
220
311
  }
@@ -301,6 +392,8 @@ function help() {
301
392
  ${bold("COMANDOS")}
302
393
  ${bold("start")} Modo pipe: lê stdin e injeta comentários em pt-BR
303
394
  ${bold("start --watch")} ${dim("<dir>")} Observa diretório e loga termos novos no terminal
395
+ ${bold("explain")} ${dim("<arquivo>")} Explica o código linha por linha via IA (pt-BR)
396
+ ${bold("config set-key")} ${dim("<chave>")} Salva a API key da Anthropic
304
397
  ${bold("stats")} Mostra seu progresso de aprendizado
305
398
  ${bold("stats --short")} Saída compacta para status bars
306
399
  ${bold("install")} Injeta termos no Claude Code (barra de pensamento)
@@ -312,6 +405,8 @@ function help() {
312
405
  ${bold("EXEMPLOS")}
313
406
  cat handler.ts | verbo start
314
407
  verbo start --watch ./src
408
+ verbo explain handler.ts
409
+ verbo config set-key sk-ant-...
315
410
  verbo stats
316
411
  verbo install
317
412
  verbo list --category backend
@@ -325,6 +420,12 @@ switch (sub) {
325
420
  case "start":
326
421
  await runStart(rest);
327
422
  break;
423
+ case "explain":
424
+ await runExplain(rest);
425
+ break;
426
+ case "config":
427
+ runConfig(rest);
428
+ break;
328
429
  case "stats":
329
430
  runStats(rest);
330
431
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soltaoverbo/cli",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "CLI para verbo.dev — aprenda inglês técnico passivamente",
5
5
  "bin": {
6
6
  "verbo": "./dist/bin.js"
@@ -8,7 +8,7 @@
8
8
  "type": "module",
9
9
  "dependencies": {
10
10
  "chokidar": "^3.6.0",
11
- "@soltaoverbo/core": "0.3.0"
11
+ "@soltaoverbo/core": "0.4.0"
12
12
  },
13
13
  "devDependencies": {
14
14
  "tsup": "^8.3.5",
package/src/bin.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import { execSync } from "node:child_process"
2
+ import { runConfig } from "./commands/config.ts"
3
+ import { runExplain } from "./commands/explain.ts"
2
4
  import { runInstall } from "./commands/install.ts"
3
5
  import { runList } from "./commands/list.ts"
4
6
  import { runReset } from "./commands/reset.ts"
@@ -34,6 +36,8 @@ function help(): void {
34
36
  ${bold("COMANDOS")}
35
37
  ${bold("start")} Modo pipe: lê stdin e injeta comentários em pt-BR
36
38
  ${bold("start --watch")} ${dim("<dir>")} Observa diretório e loga termos novos no terminal
39
+ ${bold("explain")} ${dim("<arquivo>")} Explica o código linha por linha via IA (pt-BR)
40
+ ${bold("config set-key")} ${dim("<chave>")} Salva a API key da Anthropic
37
41
  ${bold("stats")} Mostra seu progresso de aprendizado
38
42
  ${bold("stats --short")} Saída compacta para status bars
39
43
  ${bold("install")} Injeta termos no Claude Code (barra de pensamento)
@@ -45,6 +49,8 @@ function help(): void {
45
49
  ${bold("EXEMPLOS")}
46
50
  cat handler.ts | verbo start
47
51
  verbo start --watch ./src
52
+ verbo explain handler.ts
53
+ verbo config set-key sk-ant-...
48
54
  verbo stats
49
55
  verbo install
50
56
  verbo list --category backend
@@ -59,6 +65,12 @@ switch (sub) {
59
65
  case "start":
60
66
  await runStart(rest)
61
67
  break
68
+ case "explain":
69
+ await runExplain(rest)
70
+ break
71
+ case "config":
72
+ runConfig(rest)
73
+ break
62
74
  case "stats":
63
75
  runStats(rest)
64
76
  break
@@ -0,0 +1,35 @@
1
+ import { getApiKey, setApiKey } from "@soltaoverbo/core"
2
+ import { bold, cyan, dim } from "../ui.ts"
3
+
4
+ export function runConfig(args: string[]): void {
5
+ const [subcmd, ...rest] = args
6
+
7
+ switch (subcmd) {
8
+ case "set-key": {
9
+ const key = rest[0]
10
+ if (!key) {
11
+ console.error(`${bold("Uso:")} verbo config set-key <chave>`)
12
+ process.exit(1)
13
+ }
14
+ setApiKey(key)
15
+ console.log(`${bold("[verbo]")} API key salva em ${dim("~/.verbo/config.json")}`)
16
+ break
17
+ }
18
+ case "show-key": {
19
+ const key = getApiKey()
20
+ if (!key) {
21
+ console.log(dim("Nenhuma API key configurada."))
22
+ } else {
23
+ // Show only first 8 and last 4 chars — never log the full key
24
+ console.log(`API key: ${cyan(`${key.slice(0, 8)}...${key.slice(-4)}`)}`)
25
+ }
26
+ break
27
+ }
28
+ default:
29
+ console.log(
30
+ `${bold("verbo config")} — configurações do verbo\n\n` +
31
+ ` ${bold("set-key")} ${dim("<chave>")} Salva a API key da Anthropic em ~/.verbo/config.json\n` +
32
+ ` ${bold("show-key")} Exibe a API key atual (parcial)\n`,
33
+ )
34
+ }
35
+ }
@@ -0,0 +1,62 @@
1
+ import { createInterface } from "node:readline"
2
+ import { readFileSync } from "node:fs"
3
+ import { basename, extname } from "node:path"
4
+ import { explainCode, getApiKey, hasConsented, injectExplanations, setConsented } from "@soltaoverbo/core"
5
+ import { bold, cyan, dim } from "../ui.ts"
6
+
7
+ export async function runExplain(args: string[]): Promise<void> {
8
+ const filePath = args[0]
9
+ if (!filePath) {
10
+ console.error(`${bold("Uso:")} verbo explain <arquivo>`)
11
+ process.exit(1)
12
+ }
13
+
14
+ const apiKey = getApiKey()
15
+ if (!apiKey) {
16
+ console.error(
17
+ `${bold("verbo explain")} requer uma API key da Anthropic.\n` +
18
+ `Configure com: ${cyan("verbo config set-key <sua-chave>")}\n` +
19
+ `Obtenha sua chave em: https://console.anthropic.com/`,
20
+ )
21
+ process.exit(1)
22
+ }
23
+
24
+ if (!hasConsented()) {
25
+ const confirmed = await askConsent(basename(filePath))
26
+ if (!confirmed) {
27
+ console.error("Operação cancelada.")
28
+ process.exit(0)
29
+ }
30
+ setConsented()
31
+ }
32
+
33
+ let code: string
34
+ try {
35
+ code = readFileSync(filePath, "utf-8")
36
+ } catch {
37
+ console.error(`Não foi possível ler o arquivo: ${filePath}`)
38
+ process.exit(1)
39
+ }
40
+
41
+ const lang = extname(filePath).replace(".", "") || "código"
42
+
43
+ process.stderr.write(dim(`[verbo] Explicando ${basename(filePath)}...\n`))
44
+
45
+ const explanations = await explainCode(code, lang, apiKey)
46
+ const annotated = injectExplanations(code, explanations)
47
+ process.stdout.write(annotated + "\n")
48
+ }
49
+
50
+ function askConsent(fileName: string): Promise<boolean> {
51
+ return new Promise((resolve) => {
52
+ const rl = createInterface({ input: process.stdin, output: process.stderr })
53
+ rl.question(
54
+ `${bold("[verbo]")} O conteúdo de ${cyan(fileName)} será enviado para a API da Anthropic.\n` +
55
+ ` Esta mensagem só aparece uma vez. Continuar? (s/N): `,
56
+ (answer) => {
57
+ rl.close()
58
+ resolve(answer.trim().toLowerCase() === "s")
59
+ },
60
+ )
61
+ })
62
+ }