@memoring/cli 0.9.0 → 0.11.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.
@@ -28,6 +28,110 @@ export async function createDeck(name, parentId = null) {
28
28
  throw e;
29
29
  }
30
30
  }
31
+ const COVER_TONES = ['gray', 'orange', 'green', 'blue', 'purple'];
32
+ const GOAL_MAX_LENGTH = 160;
33
+ const GOAL_DISPLAY_MAX = 30;
34
+ /** "4,5" (vírgula decimal pt-BR) ou "—" quando sem relevância. */
35
+ function formatRelevance(relevance) {
36
+ return relevance === null ? '—' : relevance.toFixed(1).replace('.', ',');
37
+ }
38
+ /** META da listagem/update: texto do goal, célula de ~30 chars. */
39
+ function formatGoal(deck) {
40
+ let goal = (deck.goal ?? '').trim();
41
+ if (goal.length > GOAL_DISPLAY_MAX) {
42
+ goal = `${goal.slice(0, GOAL_DISPLAY_MAX - 1).trimEnd()}…`;
43
+ }
44
+ return goal === '' ? '—' : goal;
45
+ }
46
+ /** 45s | 12min | 1h05 — segundos inteiros; médias chegam já arredondadas. */
47
+ function formatRunSeconds(seconds) {
48
+ if (seconds < 60) {
49
+ return `${seconds}s`;
50
+ }
51
+ const minutes = Math.round(seconds / 60);
52
+ if (minutes < 60) {
53
+ return `${minutes}min`;
54
+ }
55
+ return `${Math.floor(minutes / 60)}h${String(minutes % 60).padStart(2, '0')}`;
56
+ }
57
+ /**
58
+ * Recorde exibido: para o pai é a MÉDIA ARITMÉTICA dos filhos com recorde (o
59
+ * fastestRunSeconds do DeckView é só do próprio baralho — quem tem a árvore
60
+ * toda calcula a média); sem filho com recorde, cai no próprio do baralho.
61
+ */
62
+ function recordOf(deck, childrenByParent) {
63
+ const records = (childrenByParent.get(deck.id) ?? [])
64
+ .map((child) => child.fastestRunSeconds)
65
+ .filter((seconds) => seconds !== null);
66
+ if (records.length > 0) {
67
+ return Math.round(records.reduce((sum, seconds) => sum + seconds, 0) / records.length);
68
+ }
69
+ return deck.fastestRunSeconds;
70
+ }
71
+ function parseRelevanceFlag(raw) {
72
+ const normalized = raw.replace(',', '.');
73
+ if (!/^\d+(\.\d)?$/.test(normalized)) {
74
+ fail(`--relevance inválido: use 0 a 5 com no máximo 1 casa decimal (ex.: 4 ou 4,5 — veio ${JSON.stringify(raw)}).`);
75
+ }
76
+ const value = Number(normalized);
77
+ if (value > 5) {
78
+ fail(`--relevance deve estar entre 0 e 5 (veio ${raw}).`);
79
+ }
80
+ return value;
81
+ }
82
+ function parseGoalFlag(raw) {
83
+ const goal = raw.trim();
84
+ if (goal === '') {
85
+ fail('--goal vazio — para limpar a meta use --clear-goal.');
86
+ }
87
+ if (goal.length > GOAL_MAX_LENGTH) {
88
+ fail(`--goal excede ${GOAL_MAX_LENGTH} caracteres (veio ${goal.length}).`);
89
+ }
90
+ return goal;
91
+ }
92
+ function parseToneFlag(raw) {
93
+ const tone = raw.trim().toLowerCase();
94
+ if (!COVER_TONES.includes(tone)) {
95
+ fail(`--tone inválido: use ${COVER_TONES.join(' | ')} (veio ${JSON.stringify(raw)}).`);
96
+ }
97
+ return tone;
98
+ }
99
+ const METADATA_SPECS = [
100
+ { valueFlag: 'relevance', clearFlag: 'clear-relevance', field: 'relevance', parse: parseRelevanceFlag },
101
+ { valueFlag: 'goal', clearFlag: 'clear-goal', field: 'goal', parse: parseGoalFlag },
102
+ { valueFlag: 'tone', clearFlag: 'clear-tone', field: 'coverTone', parse: parseToneFlag },
103
+ ];
104
+ /**
105
+ * Monta o body do PUT /metadata com SÓ os campos informados: flag de valor
106
+ * envia o valor validado, --clear-* envia null explícito (limpa), os dois
107
+ * juntos é erro. Ausente dos dois = campo intacto (o PUT não toca).
108
+ */
109
+ function metadataBody(flags) {
110
+ const body = {};
111
+ for (const spec of METADATA_SPECS) {
112
+ const raw = stringFlag(flags, spec.valueFlag);
113
+ const clear = flags[spec.clearFlag] === true;
114
+ if (raw !== undefined && clear) {
115
+ fail(`--${spec.valueFlag} e --${spec.clearFlag} não podem ir juntos — use um ou outro.`);
116
+ }
117
+ if (raw !== undefined) {
118
+ body[spec.field] = spec.parse(raw);
119
+ }
120
+ else if (clear) {
121
+ body[spec.field] = null;
122
+ }
123
+ }
124
+ if (Object.keys(body).length === 0) {
125
+ fail('nada a atualizar — informe ao menos um campo: --relevance, --goal, --tone ou um --clear-*.');
126
+ }
127
+ return body;
128
+ }
129
+ function printDeckSummary(deck) {
130
+ process.stdout.write(green(`Baralho "${deck.name}" (#${deck.id}) atualizado.\n`));
131
+ process.stdout.write(` relevância: ${formatRelevance(deck.relevance)}\n`);
132
+ process.stdout.write(` meta: ${formatGoal(deck)}\n`);
133
+ process.stdout.write(` tom de capa: ${deck.coverTone ?? '—'}\n`);
134
+ }
31
135
  export async function deckCommand(parsed) {
32
136
  const [sub, ...rest] = parsed._;
33
137
  switch (sub) {
@@ -42,16 +146,33 @@ export async function deckCommand(parsed) {
42
146
  return;
43
147
  }
44
148
  const nameById = new Map(decks.map((d) => [d.id, d.name]));
149
+ const childrenByParent = new Map();
150
+ for (const d of decks) {
151
+ if (d.parentId !== null) {
152
+ const siblings = childrenByParent.get(d.parentId) ?? [];
153
+ siblings.push(d);
154
+ childrenByParent.set(d.parentId, siblings);
155
+ }
156
+ }
45
157
  printTable(decks.map((d) => ({
46
158
  id: `#${d.id}`,
47
159
  name: d.name,
48
160
  parent: d.parentId !== null ? (nameById.get(d.parentId) ?? `#${d.parentId}`) : '—',
49
161
  due: d.dueCount,
162
+ relevance: formatRelevance(d.relevance),
163
+ goal: formatGoal(d),
164
+ record: (() => {
165
+ const record = recordOf(d, childrenByParent);
166
+ return record === null ? '—' : formatRunSeconds(record);
167
+ })(),
50
168
  })), [
51
169
  { key: 'id', label: 'ID' },
52
170
  { key: 'name', label: 'NOME' },
53
171
  { key: 'parent', label: 'BARALHO-PAI' },
54
172
  { key: 'due', label: 'A REVISAR' },
173
+ { key: 'relevance', label: 'RELEVÂNCIA' },
174
+ { key: 'goal', label: 'META' },
175
+ { key: 'record', label: 'RECORDE' },
55
176
  ]);
56
177
  process.stdout.write(`\n${decks.length} baralho(s).\n`);
57
178
  return;
@@ -87,11 +208,42 @@ export async function deckCommand(parsed) {
87
208
  process.stdout.write(green(`Baralho "${deck.name}" criado (#${deck.id}).\n`));
88
209
  return;
89
210
  }
211
+ case 'update': {
212
+ const ref = rest.join(' ').trim();
213
+ if (ref === '') {
214
+ fail('informe o baralho: memoring deck update <id|nome> [--relevance 0-5] [--goal "txt"] ...');
215
+ }
216
+ const deck = resolveDeck(await listDecks(), ref);
217
+ if (!deck) {
218
+ fail(`baralho "${ref}" não encontrado — veja os ids/nomes com \`memoring deck list\`.`);
219
+ }
220
+ const body = metadataBody(parsed.flags);
221
+ let updated;
222
+ try {
223
+ updated = (await apiFetch(`/notes/decks/${deck.id}/metadata`, { method: 'PUT', body }));
224
+ }
225
+ catch (e) {
226
+ if (e instanceof ApiError) {
227
+ fail(`o servidor rejeitou a atualização do baralho "${deck.name}" (#${deck.id}) (${e.status}): ${e.body}`);
228
+ }
229
+ throw e;
230
+ }
231
+ if (parsed.flags.json) {
232
+ printJson(updated);
233
+ return;
234
+ }
235
+ printDeckSummary(updated);
236
+ return;
237
+ }
90
238
  default:
91
- process.stdout.write('Uso: memoring deck <list|create> [nome] [--parent <id>] [--json]\n' +
92
- ' list lista seus baralhos (id, nome, pai, cartões a revisar)\n' +
93
- ' create <nome> cria um baralho (idempotente: nome existente só retorna o atual)\n' +
94
- ' --parent <id|nome> cria como sub-baralho\n');
239
+ process.stdout.write('Uso: memoring deck <list|create|update> [id|nome] [flags]\n' +
240
+ ' list lista seus baralhos (id, nome, pai, a revisar, relevância, meta, recorde)\n' +
241
+ ' create <nome> cria um baralho (idempotente: nome existente só retorna o atual)\n' +
242
+ ' --parent <id|nome> cria como sub-baralho\n' +
243
+ ' update <id|nome> define metadados do baralho\n' +
244
+ ' --relevance 0-5 (1 casa) | --goal "txt" (<=160)\n' +
245
+ ' --tone gray|orange|green|blue|purple\n' +
246
+ ' --clear-relevance --clear-goal --clear-tone (limpa o campo) | --json\n');
95
247
  if (sub) {
96
248
  process.exitCode = 1;
97
249
  }
@@ -189,7 +189,11 @@ const SCHEMA_TEXT = `Contrato dos itens da caixa de revisão (POST /api/notes/re
189
189
  question objeto — snapshot da questão (obrigatório quando type=QUESTION)
190
190
  deckName string — baralho de destino (OBRIGATÓRIO — cartão não fica sem baralho:
191
191
  veja \`memoring deck list\`, reutilize ou crie
192
- com \`memoring deck create\`; --deck = padrão do lote)
192
+ com \`memoring deck create\`; --deck = padrão do lote.
193
+ Espelhe a hierarquia do material: matéria = baralho
194
+ raiz; tema da aula/capítulo = sub-baralho criado
195
+ ANTES com \`deck create "<tema>" --parent "<matéria>"\`
196
+ — o create-por-nome só cria baralho raiz)
193
197
  subjectName? string — matéria (ex.: "Direito Constitucional")
194
198
  subtopicName? string — subtópico
195
199
  tags? string[]
package/dist/main.js CHANGED
@@ -9,7 +9,7 @@ import { updateCommand } from './commands/update.js';
9
9
  import { skillCommand } from './commands/skill.js';
10
10
  import { parseArgs as parse } from './commands/shared.js';
11
11
  import { newerVersionAvailable, scheduleUpdateCheck } from './update.js';
12
- const VERSION = '0.9.0';
12
+ const VERSION = '0.11.0';
13
13
  const HELP = `Memoring CLI — flashcards e notas por IA, direto do terminal.
14
14
 
15
15
  Uso: memoring <comando> [subcomando] [flags]
@@ -28,8 +28,10 @@ Comandos:
28
28
  inbox discard-all descarta todos os itens pendentes
29
29
  inbox update <id> edita um item pendente (--file parcial e/ou flags,
30
30
  inclui --deck <id|nome>)
31
- deck list seus baralhos (id, nome, pai)
32
- deck create <nome> cria um baralho (--parent <id> p/ sub-baralho)
31
+ deck list seus baralhos (id, nome, pai, a revisar, relevância, meta, recorde)
32
+ deck create <nome> cria um baralho (--parent <id|nome> p/ sub-baralho)
33
+ deck update <id|nome> metadados do baralho (--relevance/--goal/--tone;
34
+ --clear-* limpa um campo)
33
35
  update atualiza a CLI para a versão mais recente (via npm)
34
36
  skill guia completo da CLI para agentes de IA (imprime o SKILL.md)
35
37
  version versão da CLI
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memoring/cli",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "CLI do Memoring — envie flashcards/notas gerados por IA para a sua caixa de revisão, direto do terminal.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -21,8 +21,9 @@ O Memoring é um app de estudos com revisão espaçada (FSRS). A CLI `memoring`
21
21
  | `memoring stage schema [--json]` | O CONTRATO dos itens + limites por tipo + regras de qualidade — leia SEMPRE, logado, antes de gerar |
22
22
  | `memoring stage caps [--max-* N\|livre]` | limites por tipo do usuário (preferência DELE — nunca escreva sem ele pedir) |
23
23
  | `memoring stage create --file f [--replace] [--max-* N] [--deck nome]` | envia o lote (1 a 50 itens) para a caixa de revisão — **baralho OBRIGATÓRIO** (deckName por item ou `--deck` no lote; nomes novos viram baralhos); `--json` devolve `created` + `ids` |
24
- | `memoring deck list` | seus baralhos (id, nome, pai) — consulte ANTES de gerar |
24
+ | `memoring deck list` | seus baralhos (id, nome, pai, a revisar, relevância, meta, recorde) — consulte ANTES de gerar |
25
25
  | `memoring deck create <nome> [--parent id]` | cria baralho (idempotente) |
26
+ | `memoring deck update <id\|nome> [--relevance <0-5>] [--goal "<txt>"] [--tone <gray\|orange\|green\|blue\|purple>] [--clear-*]` | define metadados do baralho (relevância, meta, tom de capa; `--clear-*` limpa o campo) |
26
27
  | `memoring inbox list` | itens pendentes de revisão |
27
28
  | `memoring inbox accept <id>\|accept-all` | aceita itens (entram nas listagens e na fila FSRS) |
28
29
  | `memoring inbox discard <id>\|discard-all` | descarta item(ns) pendente(s) |
@@ -37,7 +38,7 @@ Flags globais: `--json` (saída legível por máquina), `--no-color`, `--api-bas
37
38
  1. **Entenda o pedido** — qual pasta/arquivo/material vira flashcards/notas e se o usuário pediu quantidades.
38
39
  2. **Confira a sessão**: `memoring auth status --json`. Sem `authenticated: true`, PARE e peça o login (ver Pré-requisitos).
39
40
  3. **Leia o contrato, os limites e as regras**: `memoring stage schema --json` (logado). Além do contrato, a saída traz: `userCaps` — os limites por tipo DO USUÁRIO (`-1` = à vontade, `0` = não gerar o tipo, `N` = teto), respeite-os SEMPRE; `qualityRules` — as regras de qualidade INTEGRAIS (as mesmas que o app envia à LLM), aplique-as em TODOS os itens; `customInstructions` — instruções adicionais do usuário (quando não-nulas), também sempre aplicadas. Tudo isso vale mesmo sem pedido explícito no chat.
40
- 4. **Defina o baralho** — cartão NÃO fica sem baralho. Rode `memoring deck list`, escolha o baralho adequado ao material e reutilize o nome; se não existir, crie com `memoring deck create "<nome>"` (ou deixe o próprio `stage create` criar pelo nome). Todo item do lote deve levar `deckName` — ou use `--deck "<nome>"` no create como padrão do lote. Lote sem baralho é REJEITADO pela validação local.
41
+ 4. **Defina o baralho** — cartão NÃO fica sem baralho. Rode `memoring deck list`, escolha o baralho adequado ao material e reutilize o nome; se não existir, crie com `memoring deck create "<nome>"` (ou deixe o próprio `stage create` criar pelo nome). **Espelhe a hierarquia do material:** a matéria vira baralho raiz; o tema da aula/capítulo vira sub-baralho criado ANTES com `memoring deck create "<tema>" --parent "<matéria>"` — o create-por-nome do `stage create` só cria baralho raiz. Todo item do lote deve levar `deckName` — ou use `--deck "<nome>"` no create como padrão do lote. Lote sem baralho é REJEITADO pela validação local.
41
42
  5. **Leia os materiais** e gere os itens no contrato, num arquivo JSON (array ou `{ "items": [...] }`).
42
43
  6. **Envie**: `memoring stage create --file <arquivo> --json`. Se o lote for rejeitado por limite de tipo, ajuste o LOTE — não relaxe o limite do usuário.
43
44
  7. **Guarde os `ids` retornados** pelo create: enquanto os itens estiverem PENDING_REVIEW você pode corrigi-los com `memoring inbox update <id>` — sem precisar descartar e reenviar.
@@ -47,7 +48,7 @@ Flags globais: `--json` (saída legível por máquina), `--no-color`, `--api-bas
47
48
 
48
49
  - `type`: `NOTE` (nota, exige `content` markdown; `NORMAL` legado é aceito e mapeado para `NOTE`), `FLASHCARD` ou `ONE_SHOT` (exigem `front` + `back`) ou `QUESTION` (exige `question` — ver abaixo).
49
50
  - `title` sempre obrigatório; lote de 1 a 50 itens, all-or-nothing — qualquer item inválido rejeita o lote inteiro.
50
- - `deckName` **OBRIGATÓRIO** (ou `--deck` no create como padrão do lote): cartão não fica sem baralho. Use o nome de um baralho de `memoring deck list`; nome inexistente é criado no envio.
51
+ - `deckName` **OBRIGATÓRIO** (ou `--deck` no create como padrão do lote): cartão não fica sem baralho. Use o nome de um baralho de `memoring deck list`; nome inexistente é criado no envio. Espelhe a hierarquia do material: matéria = baralho raiz; tema da aula/capítulo = sub-baralho criado ANTES com `deck create "<tema>" --parent "<matéria>"` (o `stage create` só cria raiz pelo nome).
51
52
  - `ONE_SHOT` = pergunta de resposta única (UMA palavra ou termo curto, texto puro). Ative o tipo sempre que o cobrável for recall de termo puro — nome de princípio/instituto, termo em latim, sigla, número de lei/artigo, data, prazo. Resposta cabe num termo → `ONE_SHOT`; exige frase ou explicação → `FLASHCARD`.
52
53
  - `QUESTION` = questão objetiva para verificação: `question.kind` (`MULTIPLA_ESCOLHA` | `CERTO_ERRADO`), `question.statementHtml` (enunciado), `question.correctAlternative` (gabarito, int 1-based; em CERTO_ERRADO, 1 = Certo, 2 = Errado), `question.alternatives` (2 a 5 textos — só em MULTIPLA_ESCOLHA), `question.commentHtml` (explicação do gabarito). **Se o material contiver questões prontas (provas, simulados, listas C/E), APROVEITE-AS como QUESTION** preservando enunciado e gabarito — não as achate em flashcards.
53
54
  - Preencha `source` com a origem real do material (nome do arquivo/pasta) — não use o default quando souber a origem.