@memoring/cli 0.6.0 → 0.9.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/README.md CHANGED
@@ -55,7 +55,8 @@ Os itens enviados nascem **PENDING_REVIEW** — nada entra nas suas listagens ne
55
55
  | --- | --- |
56
56
  | `memoring auth` | login / logout / status |
57
57
  | `memoring stage` | schema (o contrato + seus limites por tipo + as regras de qualidade e suas instruções custom — tudo que o agente deve seguir) / caps (mostra/grava os limites — valem para o app e para os agentes) / create (envia o lote, `--file` ou stdin, `--replace` esvazia a caixa antes; `--max-*` vence a preferência por lote) |
58
- | `memoring inbox` | list / accept `<id>` / accept-all / discard `<id>` |
58
+ | `memoring inbox` | list / accept `<id>` / accept-all / discard `<id>` / discard-all / update `<id>` (edita item PENDENTE — merge do `--file` parcial e/ou flags `--title --front --back --tags a,b --deck nome ...`) |
59
+ | `memoring deck` | list (seus baralhos) / create `<nome>` (idempotente; `--parent` p/ sub-baralho) |
59
60
  | `memoring update` | atualiza a CLI para a última versão (via npm) |
60
61
  | `memoring skill` | imprime o guia completo da CLI para agentes de IA (o SKILL.md empacotado) |
61
62
  | `memoring version` | versão da CLI |
@@ -91,9 +92,11 @@ Cada item do lote (1 a 50, all-or-nothing):
91
92
  }
92
93
  ```
93
94
 
94
- - `type`: `NOTE` (nota, exige `content` em markdown; `NORMAL` legado é aceito e mapeado para `NOTE`), `FLASHCARD` ou `ONE_SHOT` (exigem `front` + `back`).
95
+ - `type`: `NOTE` (nota, exige `content` em markdown; `NORMAL` legado é aceito e mapeado para `NOTE`), `FLASHCARD` ou `ONE_SHOT` (exigem `front` + `back`), `QUESTION` (exige `question`: `kind` `MULTIPLA_ESCOLHA`/`CERTO_ERRADO`, `statementHtml`, `correctAlternative` 1-based — 1=Certo/2=Errado —, `alternatives` 2–5 só em múltipla escolha, `commentHtml`).
96
+ - `deckName`: **obrigatório** (ou `--deck` no create) — cartão não fica sem baralho; o nome de um baralho existente (`memoring deck list`) é reutilizado, nome novo vira baralho criado no envio.
95
97
  - `source`: origem do material — default `"Importação assistida (CLI)"`.
96
98
  - Limites por tipo: `memoring stage caps` mostra/grava sua preferência (`livre`/`-1` = à vontade — o padrão; `0` = não gerar o tipo) — o `stage schema` a expõe ao agente (`userCaps`) e o `create` a impõe; flags `--max-*` no `create` vencem por lote.
99
+ - Sem sessão, o `stage schema --json` sai com `authenticated: false`, `userCaps`/`qualityRules`/`customInstructions` nulos e um `warning` — agentes devem logar (`auth login`) e reler o schema ANTES de gerar.
97
100
 
98
101
  ## Para agentes de IA
99
102
 
@@ -0,0 +1,99 @@
1
+ import { apiFetch, ApiError } from '../api.js';
2
+ import { fail, green, printJson, printTable, stringFlag } from './shared.js';
3
+ export async function listDecks() {
4
+ return (await apiFetch('/notes/decks'));
5
+ }
6
+ /**
7
+ * Resolve uma referência de baralho ("123" por id ou nome exato, case-insensitive)
8
+ * contra a lista do usuário. Null quando não existe — quem chama decide se cria
9
+ * (stage create) ou falha (inbox update).
10
+ */
11
+ export function resolveDeck(decks, ref) {
12
+ const asNumber = Number(ref);
13
+ if (Number.isInteger(asNumber) && asNumber > 0) {
14
+ return decks.find((d) => d.id === asNumber) ?? null;
15
+ }
16
+ const norm = ref.trim().toLowerCase();
17
+ return decks.find((d) => d.name.trim().toLowerCase() === norm) ?? null;
18
+ }
19
+ /** Cria o baralho (parentId null = baralho raiz/matéria). */
20
+ export async function createDeck(name, parentId = null) {
21
+ try {
22
+ return (await apiFetch('/notes/decks', { method: 'POST', body: { name, parentId } }));
23
+ }
24
+ catch (e) {
25
+ if (e instanceof ApiError) {
26
+ fail(`o servidor rejeitou a criação do baralho "${name}" (${e.status}): ${e.body}`);
27
+ }
28
+ throw e;
29
+ }
30
+ }
31
+ export async function deckCommand(parsed) {
32
+ const [sub, ...rest] = parsed._;
33
+ switch (sub) {
34
+ case 'list': {
35
+ const decks = await listDecks();
36
+ if (parsed.flags.json) {
37
+ printJson(decks);
38
+ return;
39
+ }
40
+ if (decks.length === 0) {
41
+ process.stdout.write('Nenhum baralho — crie com `memoring deck create "<nome>"`.\n');
42
+ return;
43
+ }
44
+ const nameById = new Map(decks.map((d) => [d.id, d.name]));
45
+ printTable(decks.map((d) => ({
46
+ id: `#${d.id}`,
47
+ name: d.name,
48
+ parent: d.parentId !== null ? (nameById.get(d.parentId) ?? `#${d.parentId}`) : '—',
49
+ due: d.dueCount,
50
+ })), [
51
+ { key: 'id', label: 'ID' },
52
+ { key: 'name', label: 'NOME' },
53
+ { key: 'parent', label: 'BARALHO-PAI' },
54
+ { key: 'due', label: 'A REVISAR' },
55
+ ]);
56
+ process.stdout.write(`\n${decks.length} baralho(s).\n`);
57
+ return;
58
+ }
59
+ case 'create': {
60
+ const name = rest.join(' ').trim();
61
+ if (name === '') {
62
+ fail('informe o nome: memoring deck create "Direito Tributário" [--parent <id>].');
63
+ }
64
+ let parentId = null;
65
+ const parentRef = stringFlag(parsed.flags, 'parent');
66
+ if (parentRef !== undefined) {
67
+ const parent = resolveDeck(await listDecks(), parentRef);
68
+ if (!parent) {
69
+ fail(`baralho-pai "${parentRef}" não encontrado — veja os ids/nomes com \`memoring deck list\`.`);
70
+ }
71
+ parentId = parent.id;
72
+ }
73
+ const existing = resolveDeck(await listDecks(), name);
74
+ if (existing) {
75
+ if (parsed.flags.json) {
76
+ printJson({ deck: existing, alreadyExisted: true });
77
+ return;
78
+ }
79
+ process.stdout.write(`Baralho "${existing.name}" já existe (#${existing.id}).\n`);
80
+ return;
81
+ }
82
+ const deck = await createDeck(name, parentId);
83
+ if (parsed.flags.json) {
84
+ printJson({ deck, alreadyExisted: false });
85
+ return;
86
+ }
87
+ process.stdout.write(green(`Baralho "${deck.name}" criado (#${deck.id}).\n`));
88
+ return;
89
+ }
90
+ 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');
95
+ if (sub) {
96
+ process.exitCode = 1;
97
+ }
98
+ }
99
+ }
@@ -1,8 +1,84 @@
1
+ import { readFileSync } from 'node:fs';
1
2
  import { apiFetch, ApiError } from '../api.js';
2
- import { fail, green, printJson, printTable } from './shared.js';
3
+ import { listDecks, resolveDeck } from './deck.js';
4
+ import { fail, green, printJson, printTable, stringFlag } from './shared.js';
3
5
  async function listInbox() {
4
6
  return (await apiFetch('/notes/review-inbox'));
5
7
  }
8
+ function parseId(idArg, sub) {
9
+ const id = Number(idArg);
10
+ if (!idArg || !Number.isInteger(id) || id <= 0) {
11
+ fail(`informe o id numérico do item: memoring inbox ${sub} <id> (veja os ids com \`memoring inbox list\`).`);
12
+ }
13
+ return id;
14
+ }
15
+ function parseTagsFlag(raw) {
16
+ return raw
17
+ .split(',')
18
+ .map((t) => t.trim())
19
+ .filter((t) => t !== '');
20
+ }
21
+ /**
22
+ * Edição de item PENDENTE (merge + PUT /api/notes/{id}): o PUT exige o objeto completo,
23
+ * então o estado atual vem do `inbox list` — que só retorna PENDING_REVIEW, o que já
24
+ * garante que o update não toca item aceito. Overrides vêm de --file (JSON parcial)
25
+ * e/ou flags; flags vencem o arquivo.
26
+ */
27
+ async function updateInboxItem(parsed, id) {
28
+ const current = (await listInbox()).find((n) => n.id === id);
29
+ if (!current) {
30
+ fail(`item #${id} não está pendente de revisão — update só vale para PENDING_REVIEW (veja \`memoring inbox list\`).`);
31
+ }
32
+ let overrides = {};
33
+ const file = stringFlag(parsed.flags, 'file');
34
+ if (file) {
35
+ try {
36
+ overrides = JSON.parse(readFileSync(file, 'utf8'));
37
+ }
38
+ catch (e) {
39
+ fail(`JSON inválido em --file: ${e.message}`);
40
+ }
41
+ }
42
+ const flags = parsed.flags;
43
+ const tagsFlag = stringFlag(flags, 'tags');
44
+ const deckRef = stringFlag(flags, 'deck');
45
+ let deckId;
46
+ if (deckRef !== undefined) {
47
+ const deck = resolveDeck(await listDecks(), deckRef);
48
+ if (!deck) {
49
+ fail(`baralho "${deckRef}" não encontrado — veja \`memoring deck list\` ou crie com \`memoring deck create "${deckRef}"\` e tente de novo.`);
50
+ }
51
+ deckId = deck.id;
52
+ }
53
+ const merged = {
54
+ type: overrides.type ?? stringFlag(flags, 'type') ?? current.type,
55
+ title: overrides.title ?? stringFlag(flags, 'title') ?? current.title,
56
+ content: overrides.content ?? stringFlag(flags, 'content') ?? current.content ?? null,
57
+ front: overrides.front ?? stringFlag(flags, 'front') ?? current.front ?? null,
58
+ back: overrides.back ?? stringFlag(flags, 'back') ?? current.back ?? null,
59
+ subjectName: overrides.subjectName ?? stringFlag(flags, 'subject') ?? current.subjectName ?? null,
60
+ subtopicName: overrides.subtopicName ?? stringFlag(flags, 'subtopic') ?? current.subtopicName ?? null,
61
+ tags: overrides.tags ?? (tagsFlag !== undefined ? parseTagsFlag(tagsFlag) : current.tags ?? []),
62
+ source: overrides.source ?? stringFlag(flags, 'source') ?? current.source ?? null,
63
+ deckId: deckId ?? overrides.deckId ?? current.deckId ?? null,
64
+ question: overrides.question ?? current.question ?? null,
65
+ };
66
+ let updated;
67
+ try {
68
+ updated = (await apiFetch(`/notes/${id}`, { method: 'PUT', body: merged }));
69
+ }
70
+ catch (e) {
71
+ if (e instanceof ApiError) {
72
+ fail(`o servidor rejeitou a edição (${e.status}): ${e.body}`);
73
+ }
74
+ throw e;
75
+ }
76
+ if (parsed.flags.json) {
77
+ printJson(updated);
78
+ return;
79
+ }
80
+ process.stdout.write(green(`Item #${updated.id} [${updated.type}] "${updated.title}" atualizado — continua pendente de revisão.\n`));
81
+ }
6
82
  export async function inboxCommand(parsed) {
7
83
  const [sub, idArg] = parsed._;
8
84
  switch (sub) {
@@ -26,10 +102,7 @@ export async function inboxCommand(parsed) {
26
102
  }
27
103
  case 'accept':
28
104
  case 'discard': {
29
- const id = Number(idArg);
30
- if (!idArg || !Number.isInteger(id) || id <= 0) {
31
- fail(`informe o id numérico do item: memoring inbox ${sub} <id> (veja os ids com \`memoring inbox list\`).`);
32
- }
105
+ const id = parseId(idArg, sub);
33
106
  try {
34
107
  if (sub === 'accept') {
35
108
  const note = (await apiFetch(`/notes/review-inbox/${id}/accept`, { method: 'POST' }));
@@ -68,12 +141,32 @@ export async function inboxCommand(parsed) {
68
141
  process.stdout.write(green(`${res.accepted} item(ns) aceito(s).\n`));
69
142
  return;
70
143
  }
144
+ case 'discard-all': {
145
+ const pending = await listInbox();
146
+ for (const note of pending) {
147
+ await apiFetch(`/notes/review-inbox/${note.id}`, { method: 'DELETE' });
148
+ }
149
+ if (parsed.flags.json) {
150
+ printJson({ discarded: pending.length });
151
+ return;
152
+ }
153
+ process.stdout.write(green(`${pending.length} item(ns) pendente(s) descartado(s).\n`));
154
+ return;
155
+ }
156
+ case 'update': {
157
+ await updateInboxItem(parsed, parseId(idArg, sub));
158
+ return;
159
+ }
71
160
  default:
72
- process.stdout.write('Uso: memoring inbox <list|accept|accept-all|discard> [id] [--json]\n' +
73
- ' list lista os itens pendentes de revisão\n' +
74
- ' accept <id> aceita um item (entra nas listagens e na fila FSRS)\n' +
75
- ' accept-all aceita todos os itens pendentes\n' +
76
- ' discard <id> descarta um item pendente\n');
161
+ process.stdout.write('Uso: memoring inbox <list|accept|accept-all|discard|discard-all|update> [id] [--json]\n' +
162
+ ' list lista os itens pendentes de revisão\n' +
163
+ ' accept <id> aceita um item (entra nas listagens e na fila FSRS)\n' +
164
+ ' accept-all aceita todos os itens pendentes\n' +
165
+ ' discard <id> descarta um item pendente\n' +
166
+ ' discard-all descarta todos os itens pendentes\n' +
167
+ ' update <id> edita um item PENDENTE (merge: --file parcial.json e/ou flags\n' +
168
+ ' --title --content --front --back --subject --subtopic\n' +
169
+ ' --tags a,b,c --source --type)\n');
77
170
  if (sub) {
78
171
  process.exitCode = 1;
79
172
  }
@@ -2,12 +2,14 @@ import { createHash } from 'node:crypto';
2
2
  import { readFileSync } from 'node:fs';
3
3
  import { apiFetch, ApiDedupedError, ApiError } from '../api.js';
4
4
  import { hasSession } from '../config.js';
5
+ import { createDeck, listDecks, resolveDeck } from './deck.js';
5
6
  import { dim, fail, green, printJson, stringFlag, yellow } from './shared.js';
6
- const TYPES = ['NOTE', 'FLASHCARD', 'ONE_SHOT'];
7
+ const TYPES = ['NOTE', 'FLASHCARD', 'ONE_SHOT', 'QUESTION'];
7
8
  // Retrocompat: itens.json antigos com "type": "NORMAL" são normalizados para "NOTE".
8
9
  const LEGACY_TYPE_ALIASES = { NORMAL: 'NOTE' };
9
10
  const MAX_ITEMS = 50;
10
11
  const DEFAULT_SOURCE = 'Importação assistida (CLI)';
12
+ const QUESTION_KINDS = ['MULTIPLA_ESCOLHA', 'CERTO_ERRADO'];
11
13
  const UNLIMITED = -1;
12
14
  /**
13
15
  * GET /notes/import/agent-guidance, fail-open como o fetchStoredCaps: sem sessão/rede
@@ -66,11 +68,13 @@ const MAX_FLAG_BY_TYPE = {
66
68
  NOTE: 'max-notes',
67
69
  FLASHCARD: 'max-flashcards',
68
70
  ONE_SHOT: 'max-one-shots',
71
+ QUESTION: 'max-questions',
69
72
  };
70
73
  const TYPE_LABEL = {
71
74
  NOTE: 'notas',
72
75
  FLASHCARD: 'flashcards',
73
76
  ONE_SHOT: '1-shots',
77
+ QUESTION: 'questões',
74
78
  };
75
79
  /** Campo do ImportCaps ↔ flag (o `stage caps` também aceita --max-questions, usado pelo app). */
76
80
  const CAPS_FLAG_BY_FIELD = {
@@ -86,7 +90,12 @@ const CAPS_FLAG_BY_FIELD = {
86
90
  */
87
91
  function enforceTypeCaps(items, flags, stored) {
88
92
  const storedByType = stored
89
- ? { NOTE: stored.maxNotes, FLASHCARD: stored.maxFlashcards, ONE_SHOT: stored.maxOneShots }
93
+ ? {
94
+ NOTE: stored.maxNotes,
95
+ FLASHCARD: stored.maxFlashcards,
96
+ ONE_SHOT: stored.maxOneShots,
97
+ QUESTION: stored.maxQuestions,
98
+ }
90
99
  : {};
91
100
  for (const [type, flag] of Object.entries(MAX_FLAG_BY_TYPE)) {
92
101
  const fromFlag = parseCapFlag(stringFlag(flags, flag), flag);
@@ -113,10 +122,24 @@ function enforceTypeCaps(items, flags, stored) {
113
122
  }
114
123
  }
115
124
  }
125
+ /**
126
+ * Contrato do snapshot de questão estagiável pela CLI: apenas questões MANUAIS
127
+ * (sem identidade TEC — tecQuestionId e cia. nascem no servidor, nunca num payload
128
+ * de client). Espelha a validação de QuestionSnapshot.manual do backend.
129
+ */
130
+ const QUESTION_CONTRACT = {
131
+ kind: 'MULTIPLA_ESCOLHA | CERTO_ERRADO (obrigatório)',
132
+ statementHtml: 'string — enunciado da questão (obrigatório; HTML leve)',
133
+ alternatives: 'string[] — 2 a 5 alternativas, não vazias (obrigatório em MULTIPLA_ESCOLHA; em CERTO_ERRADO não envie — o servidor usa o par fixo ["Certo", "Errado"])',
134
+ correctAlternative: 'int 1-based — gabarito (obrigatório; em CERTO_ERRADO: 1 = Certo, 2 = Errado)',
135
+ commentHtml: 'string? — explicação do gabarito (as regras de qualidade pedem sempre)',
136
+ subjectName: 'string? — matéria da questão (espelhe a do item; o snapshot guarda a dele)',
137
+ subtopicName: 'string? — subtópico da questão',
138
+ };
116
139
  const SCHEMA_JSON = {
117
140
  description: 'Lote de 1..50 itens para POST /api/notes/review-inbox. All-or-nothing: qualquer item inválido rejeita o lote. Os itens nascem PENDING_REVIEW — o usuário aceita/edita/recusa em /cards/inbox.',
118
141
  item: {
119
- type: 'NOTE | FLASHCARD | ONE_SHOT (obrigatório; NORMAL legado é aceito e mapeado para NOTE)',
142
+ type: 'NOTE | FLASHCARD | ONE_SHOT | QUESTION (obrigatório; NORMAL legado é aceito e mapeado para NOTE)',
120
143
  title: 'string (obrigatório)',
121
144
  content: 'string — corpo da nota em markdown (obrigatório quando type=NOTE)',
122
145
  front: 'string — pergunta (obrigatório quando type=FLASHCARD|ONE_SHOT)',
@@ -124,8 +147,11 @@ const SCHEMA_JSON = {
124
147
  subjectName: 'string? — matéria (ex.: "Direito Constitucional")',
125
148
  subtopicName: 'string? — subtópico',
126
149
  tags: 'string[]?',
150
+ deckName: 'string (OBRIGATÓRIO — cartão não fica sem baralho: consulte `memoring deck list`, reutilize um existente ou crie com `memoring deck create`; nome inexistente é criado no envio. A flag --deck do create vale como padrão do lote)',
151
+ question: 'objeto? — snapshot da questão (obrigatório quando type=QUESTION; contrato em questionContract)',
127
152
  source: `string? — origem do material (default: "${DEFAULT_SOURCE}")`,
128
153
  },
154
+ questionContract: QUESTION_CONTRACT,
129
155
  example: [
130
156
  {
131
157
  type: 'FLASHCARD',
@@ -133,28 +159,143 @@ const SCHEMA_JSON = {
133
159
  front: 'Quais são os quatro objetivos fundamentais da República (art. 3º da CF/88)?',
134
160
  back: 'I — construir uma sociedade livre, justa e solidária; II — garantir o desenvolvimento nacional; III — erradicar a pobreza e reduzir as desigualdades; IV — promover o bem de todos.',
135
161
  subjectName: 'Direito Constitucional',
162
+ deckName: 'Direito Constitucional',
136
163
  tags: ['cf88', 'art-3'],
137
164
  source: 'Apostila constitucional.pdf',
138
165
  },
166
+ {
167
+ type: 'QUESTION',
168
+ title: 'IBAMA 2025 — conceito de tributo',
169
+ subjectName: 'Direito Tributário',
170
+ deckName: 'Direito Tributário',
171
+ tags: ['ctn', 'art-3'],
172
+ source: 'aula-00.pdf',
173
+ question: {
174
+ kind: 'CERTO_ERRADO',
175
+ statementHtml: 'Considera-se tributo toda prestação pecuniária compulsória, instituída em lei, cobrada mediante atividade administrativa plenamente vinculada e que não constitua sanção por ato ilícito.',
176
+ correctAlternative: 1,
177
+ commentHtml: 'É a definição do art. 3º do CTN (Lei 5.172/1966).',
178
+ },
179
+ },
139
180
  ],
140
- typeCaps: 'preferência por usuário (`memoring stage caps`, vale para app e CLI) — o `stage schema` mostra os limites ao agente (userCaps: -1 = à vontade, 0 = não gerar o tipo); flags do `stage create` (--max-notes N | --max-flashcards N | --max-one-shots N, ou "livre") vencem a preferência por lote.',
181
+ typeCaps: 'preferência por usuário (`memoring stage caps`, vale para app e CLI) — o `stage schema` mostra os limites ao agente (userCaps: -1 = à vontade, 0 = não gerar o tipo); flags do `stage create` (--max-notes N | --max-flashcards N | --max-one-shots N | --max-questions N, ou "livre") vencem a preferência por lote.',
141
182
  };
142
183
  const SCHEMA_TEXT = `Contrato dos itens da caixa de revisão (POST /api/notes/review-inbox):
143
184
 
144
- type NOTE | FLASHCARD | ONE_SHOT (obrigatório; NORMAL legado é mapeado para NOTE)
185
+ type NOTE | FLASHCARD | ONE_SHOT | QUESTION (obrigatório; NORMAL legado é mapeado para NOTE)
145
186
  title string (obrigatório)
146
187
  content string — corpo da nota (markdown) (obrigatório quando type=NOTE)
147
188
  front, back string — pergunta e resposta (obrigatório quando type=FLASHCARD|ONE_SHOT)
189
+ question objeto — snapshot da questão (obrigatório quando type=QUESTION)
190
+ deckName string — baralho de destino (OBRIGATÓRIO — cartão não fica sem baralho:
191
+ veja \`memoring deck list\`, reutilize ou crie
192
+ com \`memoring deck create\`; --deck = padrão do lote)
148
193
  subjectName? string — matéria (ex.: "Direito Constitucional")
149
194
  subtopicName? string — subtópico
150
195
  tags? string[]
151
196
  source? string — origem do material (default: "${DEFAULT_SOURCE}")
152
197
 
198
+ Snapshot de QUESTION (questões manuais — sem identidade TEC):
199
+ kind MULTIPLA_ESCOLHA | CERTO_ERRADO (obrigatório)
200
+ statementHtml string — enunciado (HTML leve) (obrigatório)
201
+ alternatives string[] — 2 a 5 alternativas (obrigatório em MULTIPLA_ESCOLHA;
202
+ em CERTO_ERRADO não envie — o par fixo é Certo/Errado)
203
+ correctAlternative int 1-based — gabarito (obrigatório; CERTO_ERRADO: 1 = Certo, 2 = Errado)
204
+ commentHtml string — explicação do gabarito (recomendado)
205
+ subjectName/subtopicName string? — matéria/subtópico DA QUESTÃO (o snapshot guarda os próprios)
206
+
153
207
  Lote: 1 a ${MAX_ITEMS} itens, all-or-nothing — qualquer item inválido rejeita o lote inteiro.
154
- Os itens nascem PENDING_REVIEW: o usuário aceita, edita ou recusa em /cards/inbox.
155
- Dica: FLASHCARD entra na fila de revisão espaçada (FSRS); ONE_SHOT é pergunta de resposta única.
208
+ Os itens nascem PENDING_REVIEW: o usuário aceita, edita ou recusa em /cards/inbox
209
+ (agentes podem editar pendentes com \`memoring inbox update <id>\`).
210
+ Dica: FLASHCARD entra na fila de revisão espaçada (FSRS); ONE_SHOT é pergunta de resposta única
211
+ (termo curto — ideal para princípios, leis, datas, prazos, siglas, termos em latim);
212
+ QUESTION treina verificação objetiva (aproveite questões prontas do material).
156
213
  Limites por tipo: a preferência do usuário (abaixo, quando logado) vale por padrão;
157
- --max-notes N | --max-flashcards N | --max-one-shots N no create vencem por lote ("livre" = à vontade).`;
214
+ --max-notes N | --max-flashcards N | --max-one-shots N | --max-questions N no create vencem por lote ("livre" = à vontade).`;
215
+ const NO_DECK_HINT = 'cartão não fica sem baralho: informe deckName no item ou --deck "<nome>" no lote. ' +
216
+ 'Veja `memoring deck list` e reutilize um existente, ou crie com `memoring deck create "<nome>"`.';
217
+ /** Exige baralho em TODO o lote: deckName no item ou a flag --deck como padrão. */
218
+ function enforceDeck(items, flags) {
219
+ const batchDeck = stringFlag(flags, 'deck');
220
+ const index = items.findIndex((it) => !batchDeck && (typeof it.deckName !== 'string' || it.deckName.trim() === ''));
221
+ if (index >= 0) {
222
+ fail(`item #${index + 1} (${items[index].title}) está sem baralho — ${NO_DECK_HINT}`);
223
+ }
224
+ }
225
+ /**
226
+ * Resolve os baralhos do lote ANTES do POST: referências existentes são reutilizadas
227
+ * (id numérico ou nome exato case-insensitive) e nomes novos viram baralhos criados
228
+ * na hora — o agente nunca precisa sair do fluxo para criar estrutura.
229
+ */
230
+ async function resolveBatchDecks(items, flags) {
231
+ const batchDeck = stringFlag(flags, 'deck')?.trim();
232
+ const refOf = (it) => typeof it.deckName === 'string' && it.deckName.trim() !== '' ? it.deckName.trim() : batchDeck;
233
+ const refs = new Set();
234
+ for (const it of items) {
235
+ const ref = refOf(it);
236
+ if (ref) {
237
+ refs.add(ref);
238
+ }
239
+ }
240
+ if (refs.size === 0) {
241
+ return { deckIds: items.map(() => null), createdNames: [] };
242
+ }
243
+ const decks = await listDecks();
244
+ const idByRef = new Map();
245
+ const createdNames = [];
246
+ for (const ref of refs) {
247
+ const existing = resolveDeck(decks, ref);
248
+ if (existing) {
249
+ idByRef.set(ref, existing.id);
250
+ continue;
251
+ }
252
+ if (Number.isInteger(Number(ref)) && Number(ref) > 0) {
253
+ fail(`baralho #${ref} não existe — ${NO_DECK_HINT}`);
254
+ }
255
+ const deck = await createDeck(ref);
256
+ decks.push(deck);
257
+ idByRef.set(ref, deck.id);
258
+ createdNames.push(ref);
259
+ }
260
+ return { deckIds: items.map((it) => idByRef.get(refOf(it) ?? '') ?? null), createdNames };
261
+ }
262
+ /**
263
+ * Atribui o baralho aos itens recém-estagiados (o POST da caixa de revisão não tem
264
+ * deckId — a atribuição é um PUT /notes/{id} por item, com o payload completo do
265
+ * item + deckId). Devolve os rótulos dos itens cuja atribuição falhou.
266
+ */
267
+ async function assignDecks(items, created, deckIds) {
268
+ const failures = [];
269
+ for (let i = 0; i < created.length; i++) {
270
+ const deckId = deckIds[i];
271
+ if (deckId === null || deckId === undefined) {
272
+ continue;
273
+ }
274
+ const item = items[i];
275
+ try {
276
+ await apiFetch(`/notes/${created[i].id}`, {
277
+ method: 'PUT',
278
+ body: {
279
+ type: item.type,
280
+ title: item.title,
281
+ content: item.content ?? null,
282
+ front: item.front ?? null,
283
+ back: item.back ?? null,
284
+ subjectName: item.subjectName ?? null,
285
+ subtopicName: item.subtopicName ?? null,
286
+ tags: item.tags ?? [],
287
+ source: item.source ?? null,
288
+ deckId,
289
+ question: item.question ?? null,
290
+ },
291
+ });
292
+ }
293
+ catch {
294
+ failures.push(`#${created[i].id} (${item.title})`);
295
+ }
296
+ }
297
+ return failures;
298
+ }
158
299
  async function readStdin() {
159
300
  const chunks = [];
160
301
  for await (const chunk of process.stdin) {
@@ -162,6 +303,68 @@ async function readStdin() {
162
303
  }
163
304
  return Buffer.concat(chunks).toString('utf8');
164
305
  }
306
+ /**
307
+ * Valida o snapshot de questão manual espelhando QuestionSnapshot.manual do servidor:
308
+ * modalidade, enunciado, gabarito obrigatório apontando para alternativa existente e,
309
+ * em múltipla escolha, 2 a 5 alternativas não vazias. Certo/errado não leva
310
+ * alternatives — o servidor normaliza para o par fixo Certo/Errado.
311
+ */
312
+ function validateQuestion(raw, where) {
313
+ if (typeof raw !== 'object' || raw === null) {
314
+ fail(`${where}: type=QUESTION exige o objeto "question" (veja o contrato em \`memoring stage schema\`).`);
315
+ }
316
+ const q = raw;
317
+ if ('tecQuestionId' in q || 'tecCadernoId' in q) {
318
+ fail(`${where}: a CLI só estagia questões manuais — não envie tecQuestionId/tecCadernoId (a identidade TEC nasce no servidor).`);
319
+ }
320
+ if (typeof q.kind !== 'string' || !QUESTION_KINDS.includes(q.kind)) {
321
+ fail(`${where}: question.kind deve ser ${QUESTION_KINDS.join(' | ')} (veio ${JSON.stringify(q.kind)}).`);
322
+ }
323
+ if (typeof q.statementHtml !== 'string' || q.statementHtml.trim() === '') {
324
+ fail(`${where}: question.statementHtml (enunciado) é obrigatório.`);
325
+ }
326
+ if (typeof q.correctAlternative !== 'number' || !Number.isInteger(q.correctAlternative)) {
327
+ fail(`${where}: question.correctAlternative (gabarito, int 1-based) é obrigatório.`);
328
+ }
329
+ const correct = q.correctAlternative;
330
+ const subjects = {
331
+ ...(typeof q.subjectName === 'string' ? { subjectName: q.subjectName } : {}),
332
+ ...(typeof q.subtopicName === 'string' ? { subtopicName: q.subtopicName } : {}),
333
+ };
334
+ if (q.kind === 'CERTO_ERRADO') {
335
+ if (correct < 1 || correct > 2) {
336
+ fail(`${where}: em CERTO_ERRADO o gabarito deve ser 1 (Certo) ou 2 (Errado) — veio ${correct}.`);
337
+ }
338
+ return {
339
+ kind: q.kind,
340
+ statementHtml: q.statementHtml,
341
+ correctAlternative: correct,
342
+ ...(typeof q.commentHtml === 'string' ? { commentHtml: q.commentHtml } : {}),
343
+ ...subjects,
344
+ };
345
+ }
346
+ if (!Array.isArray(q.alternatives) || q.alternatives.some((a) => typeof a !== 'string')) {
347
+ fail(`${where}: MULTIPLA_ESCOLHA exige question.alternatives como array de strings.`);
348
+ }
349
+ const alternatives = q.alternatives.map((a) => a.trim());
350
+ if (alternatives.length < 2 || alternatives.length > 5) {
351
+ fail(`${where}: MULTIPLA_ESCOLHA exige de 2 a 5 alternativas (vieram ${alternatives.length}).`);
352
+ }
353
+ if (alternatives.some((a) => a === '')) {
354
+ fail(`${where}: alternativas não podem ser vazias.`);
355
+ }
356
+ if (correct < 1 || correct > alternatives.length) {
357
+ fail(`${where}: gabarito ${correct} não aponta para alternativa existente (1..${alternatives.length}).`);
358
+ }
359
+ return {
360
+ kind: q.kind,
361
+ statementHtml: q.statementHtml,
362
+ alternatives,
363
+ correctAlternative: correct,
364
+ ...(typeof q.commentHtml === 'string' ? { commentHtml: q.commentHtml } : {}),
365
+ ...subjects,
366
+ };
367
+ }
165
368
  function validateItems(raw) {
166
369
  const items = Array.isArray(raw) ? raw : raw?.items;
167
370
  if (!Array.isArray(items) || items.length === 0) {
@@ -192,6 +395,9 @@ function validateItems(raw) {
192
395
  (typeof it.front !== 'string' || it.front.trim() === '' || typeof it.back !== 'string' || it.back.trim() === '')) {
193
396
  fail(`${where} (${it.title}): type=${it.type} exige front e back.`);
194
397
  }
398
+ if (it.type === 'QUESTION') {
399
+ it.question = validateQuestion(it.question, `${where} (${it.title})`);
400
+ }
195
401
  if (it.tags !== undefined && (!Array.isArray(it.tags) || it.tags.some((t) => typeof t !== 'string'))) {
196
402
  fail(`${where} (${it.title}): tags deve ser um array de strings.`);
197
403
  }
@@ -217,32 +423,41 @@ async function emptyInbox() {
217
423
  }
218
424
  return pending.length;
219
425
  }
426
+ const NO_SESSION_WARNING = 'SEM SESSÃO: userCaps, qualityRules e customInstructions ficam indisponíveis. ' +
427
+ 'NÃO gere itens assim — rode `memoring auth login` (device flow, o usuário aprova no browser) e releia o schema antes de gerar.';
220
428
  export async function stageCommand(parsed) {
221
429
  const [sub] = parsed._;
222
430
  switch (sub) {
223
431
  case 'schema': {
432
+ const authenticated = hasSession();
224
433
  const [caps, guidance] = await Promise.all([fetchStoredCaps(), fetchAgentGuidance()]);
434
+ if (!authenticated) {
435
+ process.stderr.write(yellow(`aviso: ${NO_SESSION_WARNING}\n`));
436
+ }
225
437
  if (parsed.flags.json) {
226
438
  printJson({
439
+ authenticated,
227
440
  ...SCHEMA_JSON,
228
- ...(caps
441
+ userCaps: caps
229
442
  ? {
230
- userCaps: {
231
- maxNotes: caps.maxNotes,
232
- maxFlashcards: caps.maxFlashcards,
233
- maxOneShots: caps.maxOneShots,
234
- },
443
+ maxNotes: caps.maxNotes,
444
+ maxFlashcards: caps.maxFlashcards,
445
+ maxOneShots: caps.maxOneShots,
446
+ maxQuestions: caps.maxQuestions,
235
447
  }
236
- : {}),
237
- ...(guidance ?? {}),
448
+ : null,
449
+ qualityRules: guidance?.qualityRules ?? null,
450
+ customInstructions: guidance?.customInstructions ?? null,
451
+ ...(!authenticated ? { warning: NO_SESSION_WARNING } : {}),
238
452
  });
239
453
  }
240
454
  else {
241
455
  process.stdout.write(SCHEMA_TEXT + '\n');
242
456
  if (caps) {
243
457
  process.stdout.write(`\nLimites por tipo configurados por você — o agente DEVE respeitá-los ao gerar o lote: ` +
244
- `notas: ${capLabel(caps.maxNotes)}, flashcards: ${capLabel(caps.maxFlashcards)}, 1-shots: ${capLabel(caps.maxOneShots)}.\n` +
245
- dim('("à vontade" = crie quantos o material justificar; altere com `memoring stage caps`; QUESTION não é estagiável pela CLI)\n'));
458
+ `notas: ${capLabel(caps.maxNotes)}, flashcards: ${capLabel(caps.maxFlashcards)}, ` +
459
+ `1-shots: ${capLabel(caps.maxOneShots)}, questões: ${capLabel(caps.maxQuestions)}.\n` +
460
+ dim('("à vontade" = crie quantos o material justificar; altere com `memoring stage caps`)\n'));
246
461
  }
247
462
  else {
248
463
  process.stdout.write(dim('\n(limites por tipo indisponíveis — faça `memoring auth login` ou verifique a conexão)\n'));
@@ -257,6 +472,9 @@ export async function stageCommand(parsed) {
257
472
  '\n');
258
473
  }
259
474
  }
475
+ else if (authenticated) {
476
+ process.stdout.write(dim('\n(regras de qualidade indisponíveis — verifique a conexão e repita antes de gerar)\n'));
477
+ }
260
478
  }
261
479
  return;
262
480
  }
@@ -308,18 +526,24 @@ export async function stageCommand(parsed) {
308
526
  fail(`JSON inválido: ${e.message}`);
309
527
  }
310
528
  const items = validateItems(parsedJson);
529
+ enforceDeck(items, parsed.flags);
311
530
  enforceTypeCaps(items, parsed.flags, await fetchStoredCaps());
531
+ const batchDecks = await resolveBatchDecks(items, parsed.flags);
312
532
  if (parsed.flags.replace) {
313
533
  const removed = await emptyInbox();
314
534
  if (!parsed.flags.json) {
315
535
  process.stdout.write(dim(`${removed} item(ns) pendente(s) removido(s) da caixa de revisão.\n`));
316
536
  }
317
537
  }
538
+ if (batchDecks.createdNames.length > 0 && !parsed.flags.json) {
539
+ process.stdout.write(dim(`Baralho(s) criado(s): ${batchDecks.createdNames.join(', ')}.\n`));
540
+ }
318
541
  let created;
319
542
  try {
320
543
  const res = (await apiFetch('/notes/review-inbox', {
321
544
  method: 'POST',
322
- body: { items },
545
+ // deckName não faz parte do contrato do POST — o baralho é atribuído via PUT abaixo
546
+ body: { items: items.map(({ deckName, ...rest }) => (void deckName, rest)) },
323
547
  requestId: batchRequestId(raw),
324
548
  }));
325
549
  created = res.created;
@@ -327,7 +551,7 @@ export async function stageCommand(parsed) {
327
551
  catch (e) {
328
552
  if (e instanceof ApiDedupedError) {
329
553
  if (parsed.flags.json) {
330
- printJson({ created: [], deduplicated: true });
554
+ printJson({ created: [], ids: [], deduplicated: true });
331
555
  return;
332
556
  }
333
557
  process.stdout.write(green('Este lote já foi enviado antes — nada foi duplicado.\n'));
@@ -341,8 +565,20 @@ export async function stageCommand(parsed) {
341
565
  }
342
566
  throw e;
343
567
  }
568
+ const deckFailures = await assignDecks(items, created, batchDecks.deckIds);
569
+ if (deckFailures.length > 0) {
570
+ process.stderr.write(yellow(`aviso: ${deckFailures.length} item(ns) ficaram sem baralho por erro no PUT — corrija com \`memoring inbox update <id> --deck\`: ${deckFailures.join(', ')}\n`));
571
+ }
344
572
  if (parsed.flags.json) {
345
- printJson({ created });
573
+ printJson({
574
+ created,
575
+ ids: created.map((note) => note.id),
576
+ decks: {
577
+ created: batchDecks.createdNames,
578
+ assigned: created.length - deckFailures.length - batchDecks.deckIds.filter((id) => id === null).length,
579
+ failed: deckFailures,
580
+ },
581
+ });
346
582
  return;
347
583
  }
348
584
  process.stdout.write(green(`OK — ${created.length} item(ns) aguardando revisão em /cards/inbox:\n`));
@@ -354,12 +590,16 @@ export async function stageCommand(parsed) {
354
590
  default:
355
591
  process.stdout.write('Uso: memoring stage <schema|caps|create> [--file itens.json] [--replace] [--json]\n' +
356
592
  ' schema imprime o contrato JSON dos itens (para humanos e agentes)\n' +
357
- ' logado, inclui seus limites por tipo — o agente deve respeitá-los\n' +
593
+ ' logado, inclui seus limites por tipo e as regras de qualidade —\n' +
594
+ ' o agente deve ler o schema LOGADO, antes de gerar\n' +
358
595
  ' caps mostra/grava seus limites por tipo (--max-notes N | --max-flashcards N |\n' +
359
596
  ' --max-one-shots N | --max-questions N; 0 = não gerar o tipo)\n' +
360
597
  ' create envia o lote para a caixa de revisão (arquivo ou stdin)\n' +
598
+ ' tipos: NOTE | FLASHCARD | ONE_SHOT | QUESTION\n' +
599
+ ' baralho OBRIGATÓRIO: deckName por item ou --deck "<nome|id>" no lote\n' +
600
+ ' (nomes inexistentes são criados; veja `memoring deck list`)\n' +
361
601
  ' --replace esvazia a caixa antes de enviar\n' +
362
- ' --max-notes N | --max-flashcards N | --max-one-shots N\n' +
602
+ ' --max-notes N | --max-flashcards N | --max-one-shots N | --max-questions N\n' +
363
603
  ' vencem a preferência gravada por lote (validação local)\n');
364
604
  if (sub) {
365
605
  process.exitCode = 1;
package/dist/main.js CHANGED
@@ -1,14 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import { ApiError } from './api.js';
3
- import { setColorEnabled, yellow } from './output.js';
3
+ import { CliError, setColorEnabled, yellow } from './output.js';
4
4
  import { authCommand } from './commands/auth.js';
5
+ import { deckCommand } from './commands/deck.js';
5
6
  import { inboxCommand } from './commands/inbox.js';
6
7
  import { stageCommand } from './commands/stage.js';
7
8
  import { updateCommand } from './commands/update.js';
8
9
  import { skillCommand } from './commands/skill.js';
9
- import { fail, parseArgs as parse } from './commands/shared.js';
10
+ import { parseArgs as parse } from './commands/shared.js';
10
11
  import { newerVersionAvailable, scheduleUpdateCheck } from './update.js';
11
- const VERSION = '0.6.0';
12
+ const VERSION = '0.9.0';
12
13
  const HELP = `Memoring CLI — flashcards e notas por IA, direto do terminal.
13
14
 
14
15
  Uso: memoring <comando> [subcomando] [flags]
@@ -16,12 +17,19 @@ Uso: memoring <comando> [subcomando] [flags]
16
17
  Comandos:
17
18
  auth login|logout|status autenticação (device flow — sem senha no terminal)
18
19
  stage schema contrato JSON dos itens da caixa de revisão
20
+ (logado: inclui limites por tipo + regras de qualidade —
21
+ agentes devem ler o schema LOGADO, antes de gerar)
19
22
  stage create [--file f] envia itens (arquivo ou stdin) para a caixa de revisão
20
23
  [--replace] esvazia a caixa antes de enviar
21
24
  stage caps [--max-* N] seus limites por tipo (valem pro app e pros agentes)
22
25
  inbox list itens pendentes de revisão
23
26
  inbox accept <id>|accept-all aceita itens (entram nas listagens e na fila FSRS)
24
27
  inbox discard <id> descarta um item pendente
28
+ inbox discard-all descarta todos os itens pendentes
29
+ inbox update <id> edita um item pendente (--file parcial e/ou flags,
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)
25
33
  update atualiza a CLI para a versão mais recente (via npm)
26
34
  skill guia completo da CLI para agentes de IA (imprime o SKILL.md)
27
35
  version versão da CLI
@@ -84,6 +92,9 @@ async function main() {
84
92
  case 'inbox':
85
93
  await inboxCommand(sub);
86
94
  return;
95
+ case 'deck':
96
+ await deckCommand(sub);
97
+ return;
87
98
  case 'update':
88
99
  await updateCommand(VERSION);
89
100
  return;
@@ -100,8 +111,16 @@ async function main() {
100
111
  }
101
112
  }
102
113
  main().catch((error) => {
114
+ if (error instanceof CliError) {
115
+ // mensagem já impressa pelo fail() — só converte em exit code, sem process.exit (ver CliError)
116
+ process.exitCode = 1;
117
+ return;
118
+ }
103
119
  if (error instanceof ApiError) {
104
- fail(`falha na API (${error.status}): ${error.body}`);
120
+ process.stderr.write(`Erro: falha na API (${error.status}): ${error.body}\n`);
121
+ process.exitCode = 1;
122
+ return;
105
123
  }
106
- fail(error instanceof Error ? error.message : String(error));
124
+ process.stderr.write(`Erro: ${error instanceof Error ? error.message : String(error)}\n`);
125
+ process.exitCode = 1;
107
126
  });
package/dist/output.js CHANGED
@@ -25,7 +25,15 @@ export function printTable(rows, columns) {
25
25
  process.stdout.write(renderRow(columns.map((c) => String(row[c.key] ?? ''))) + '\n');
26
26
  }
27
27
  }
28
+ /**
29
+ * Erro de uso/validação já reportado no stderr. O `main` o converte em exitCode 1
30
+ * SEM process.exit(): no Windows, sair via process.exit() com o timer do
31
+ * AbortSignal.timeout (apiFetch) ainda pendente derruba o processo com assert do
32
+ * libuv (exit 127 em vez de 1). A saída natural drena o loop e evita o crash.
33
+ */
34
+ export class CliError extends Error {
35
+ }
28
36
  export function fail(message) {
29
37
  process.stderr.write(red(`Erro: ${message}`) + '\n');
30
- process.exit(1);
38
+ throw new CliError(message);
31
39
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memoring/cli",
3
- "version": "0.6.0",
3
+ "version": "0.9.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",
@@ -7,22 +7,26 @@ description: Gere flashcards e notas a partir de materiais de estudo locais (PDF
7
7
 
8
8
  O Memoring é um app de estudos com revisão espaçada (FSRS). A CLI `memoring` é a ponte entre você (o agente, que lê o material e gera o conteúdo) e a conta do usuário no app. Você NÃO chama a API HTTP diretamente — use sempre a CLI. Este guia está sempre disponível via `memoring skill`.
9
9
 
10
- ## Pré-requisitos
10
+ ## Pré-requisitos (NESTA ORDEM — não pule)
11
11
 
12
12
  1. CLI instalada: `memoring version` deve responder. Se não, peça ao usuário: `npm install -g @memoring/cli`.
13
- 2. Sessão ativa: `memoring auth status --json` → `authenticated: true`. Se não, peça ao usuário para rodar `memoring auth login` (device flow — ele aprova no browser; você não consegue fazer isso por ele).
13
+ 2. **Sessão ativa ANTES de qualquer geração**: `memoring auth status --json` → `authenticated: true`. Se não, peça ao usuário para rodar `memoring auth login` (device flow — ele aprova no browser; você não consegue fazer isso por ele) e ESPERE o login concluir.
14
+ 3. **NUNCA gere itens sem sessão.** Deslogado, o `stage schema` omite as regras de qualidade (`qualityRules`), os limites do usuário (`userCaps`) e as instruções custom (`customInstructions`) — o JSON traz `authenticated: false` + `warning`. Gerar sem essas regras produz lotes fora do padrão do app. Se o usuário insistir em gerar offline, avise que a qualidade ficará comprometida e que o lote precisará ser refeito.
14
15
 
15
16
  ## Mapa dos comandos
16
17
 
17
18
  | Comando | O que faz |
18
19
  | --- | --- |
19
20
  | `memoring auth login\|logout\|status` | sessão (device flow — sem senha no terminal) |
20
- | `memoring stage schema [--json]` | O CONTRATO dos itens + os limites por tipo do usuário — leia SEMPRE antes de gerar |
21
+ | `memoring stage schema [--json]` | O CONTRATO dos itens + limites por tipo + regras de qualidade — leia SEMPRE, logado, antes de gerar |
21
22
  | `memoring stage caps [--max-* N\|livre]` | limites por tipo do usuário (preferência DELE — nunca escreva sem ele pedir) |
22
- | `memoring stage create --file f [--replace] [--max-* N]` | envia o lote (1 a 50 itens) para a caixa de revisão |
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 |
25
+ | `memoring deck create <nome> [--parent id]` | cria baralho (idempotente) |
23
26
  | `memoring inbox list` | itens pendentes de revisão |
24
27
  | `memoring inbox accept <id>\|accept-all` | aceita itens (entram nas listagens e na fila FSRS) |
25
- | `memoring inbox discard <id>` | descarta um item pendente |
28
+ | `memoring inbox discard <id>\|discard-all` | descarta item(ns) pendente(s) |
29
+ | `memoring inbox update <id> [--file parcial.json] [--title ...] [--front ...] [--back ...] [--tags a,b] ...` | edita um item AINDA PENDENTE (merge com o estado atual) |
26
30
  | `memoring skill` | imprime este guia |
27
31
  | `memoring update` | atualiza a CLI (decisão do usuário — nunca rode por conta própria) |
28
32
 
@@ -31,16 +35,21 @@ Flags globais: `--json` (saída legível por máquina), `--no-color`, `--api-bas
31
35
  ## Fluxo padrão
32
36
 
33
37
  1. **Entenda o pedido** — qual pasta/arquivo/material vira flashcards/notas e se o usuário pediu quantidades.
34
- 2. **Leia o contrato, os limites e as regras**: `memoring stage schema --json`. 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.
35
- 3. **Leia os materiais** e gere os itens no contrato, num arquivo JSON (array ou `{ "items": [...] }`).
36
- 4. **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.
37
- 5. **Avise o usuário**: quantos itens foram para a caixa de revisão e onde aceitar — no app em `/cards/inbox` ou `memoring inbox list` / `memoring inbox accept-all`.
38
+ 2. **Confira a sessão**: `memoring auth status --json`. Sem `authenticated: true`, PARE e peça o login (ver Pré-requisitos).
39
+ 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
+ 5. **Leia os materiais** e gere os itens no contrato, num arquivo JSON (array ou `{ "items": [...] }`).
42
+ 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
+ 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.
44
+ 8. **Avise o usuário**: quantos itens foram para a caixa de revisão e onde aceitar — no app em `/cards/inbox` ou `memoring inbox list` / `memoring inbox accept-all`.
38
45
 
39
46
  ## O contrato (a fonte da verdade é `stage schema`)
40
47
 
41
- - `type`: `NOTE` (nota, exige `content` markdown; `NORMAL` legado é aceito e mapeado para `NOTE`), `FLASHCARD` ou `ONE_SHOT` (exigem `front` + `back`).
48
+ - `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).
42
49
  - `title` sempre obrigatório; lote de 1 a 50 itens, all-or-nothing — qualquer item inválido rejeita o lote inteiro.
43
- - `ONE_SHOT` = pergunta de resposta única (uma palavra ou termo curto); prefira `FLASHCARD` para memorização.
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
+ - `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
+ - `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.
44
53
  - Preencha `source` com a origem real do material (nome do arquivo/pasta) — não use o default quando souber a origem.
45
54
  - `subjectName`/`subtopicName` quando o material tiver matéria/subtópico claros; `tags` curtas e em minúsculas.
46
55
  - NUNCA invente campos fora do contrato — o servidor rejeita o lote inteiro.
@@ -54,11 +63,12 @@ Flags globais: `--json` (saída legível por máquina), `--no-color`, `--api-bas
54
63
 
55
64
  ## Qualidade dos flashcards (o padrão que o app espera)
56
65
 
57
- Cartões atômicos: UMA ideia por cartão — se um conceito tem 3 partes cobráveis, crie 3 cartões, nunca um cartão com lista como resposta. A frente é uma pergunta direta que força LEMBRAR (não reconhecer). A resposta é auto-contida (faz sentido sem o material) e curta. Pares confundíveis (prazos, siglas, exceções) levam na frente o traço que os diferencia. Não invente conteúdo que não esteja no material.
66
+ Cartões atômicos: UMA ideia por cartão — se um conceito tem 3 partes cobráveis, crie 3 cartões, nunca um cartão com lista como resposta. A frente é uma pergunta direta que força LEMBRAR (não reconhecer). A resposta é auto-contida (faz sentido sem o material) e curta. Pares confundíveis (prazos, siglas, exceções) levam na frente o traço que os diferencia. Não invente conteúdo que não esteja no material. Distribua os tipos: um bom lote usa NOTE, FLASHCARD, ONE_SHOT e QUESTION conforme o material pedir — não zere um tipo por timidez (termos, leis, datas e prazos cobráveis viram ONE_SHOT).
58
67
 
59
68
  ## Armadilhas
60
69
 
61
- - **Nunca** use `--replace` sem o usuário pedir explicitamente — ele apaga os itens pendentes existentes da caixa.
70
+ - **Nunca** use `--replace` ou `inbox discard-all` sem o usuário pedir explicitamente — eles apagam os itens pendentes existentes da caixa.
62
71
  - Se aparecer um aviso de versão nova no stderr, ignore e siga o fluxo — **não** rode `memoring update` por conta própria; atualizar a CLI é decisão do usuário.
63
72
  - **Não** altere os limites do usuário (`stage caps`) por conta própria — é preferência dele, compartilhada com o app.
64
73
  - Se `stage create` falhar com erro de validação do servidor, corrija os itens indicados e reenvie — não relaxe o contrato.
74
+ - Errou um item já enviado? Não reenvie o lote (duplicaria): use `memoring inbox update <id>` enquanto ele estiver PENDING_REVIEW.