@memoring/cli 0.5.0 → 0.8.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 +4 -3
- package/dist/commands/inbox.js +93 -10
- package/dist/commands/stage.js +179 -22
- package/dist/main.js +17 -5
- package/dist/output.js +9 -1
- package/package.json +1 -1
- package/skills/memoring-cli/SKILL.md +19 -13
package/README.md
CHANGED
|
@@ -54,8 +54,8 @@ Os itens enviados nascem **PENDING_REVIEW** — nada entra nas suas listagens ne
|
|
|
54
54
|
| Command | Purpose |
|
|
55
55
|
| --- | --- |
|
|
56
56
|
| `memoring auth` | login / logout / status |
|
|
57
|
-
| `memoring stage` | schema (o contrato + seus limites por tipo) / 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>` |
|
|
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>` / discard-all / update `<id>` (edita item PENDENTE — merge do `--file` parcial e/ou flags `--title --front --back --tags a,b ...`) |
|
|
59
59
|
| `memoring update` | atualiza a CLI para a última versão (via npm) |
|
|
60
60
|
| `memoring skill` | imprime o guia completo da CLI para agentes de IA (o SKILL.md empacotado) |
|
|
61
61
|
| `memoring version` | versão da CLI |
|
|
@@ -91,9 +91,10 @@ Cada item do lote (1 a 50, all-or-nothing):
|
|
|
91
91
|
}
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
-
- `type`: `NOTE` (nota, exige `content` em markdown; `NORMAL` legado é aceito e mapeado para `NOTE`), `FLASHCARD` ou `ONE_SHOT` (exigem `front` + `back`).
|
|
94
|
+
- `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`).
|
|
95
95
|
- `source`: origem do material — default `"Importação assistida (CLI)"`.
|
|
96
96
|
- 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.
|
|
97
|
+
- 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
98
|
|
|
98
99
|
## Para agentes de IA
|
|
99
100
|
|
package/dist/commands/inbox.js
CHANGED
|
@@ -1,8 +1,74 @@
|
|
|
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 { fail, green, printJson, printTable, stringFlag } from './shared.js';
|
|
3
4
|
async function listInbox() {
|
|
4
5
|
return (await apiFetch('/notes/review-inbox'));
|
|
5
6
|
}
|
|
7
|
+
function parseId(idArg, sub) {
|
|
8
|
+
const id = Number(idArg);
|
|
9
|
+
if (!idArg || !Number.isInteger(id) || id <= 0) {
|
|
10
|
+
fail(`informe o id numérico do item: memoring inbox ${sub} <id> (veja os ids com \`memoring inbox list\`).`);
|
|
11
|
+
}
|
|
12
|
+
return id;
|
|
13
|
+
}
|
|
14
|
+
function parseTagsFlag(raw) {
|
|
15
|
+
return raw
|
|
16
|
+
.split(',')
|
|
17
|
+
.map((t) => t.trim())
|
|
18
|
+
.filter((t) => t !== '');
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Edição de item PENDENTE (merge + PUT /api/notes/{id}): o PUT exige o objeto completo,
|
|
22
|
+
* então o estado atual vem do `inbox list` — que só retorna PENDING_REVIEW, o que já
|
|
23
|
+
* garante que o update não toca item aceito. Overrides vêm de --file (JSON parcial)
|
|
24
|
+
* e/ou flags; flags vencem o arquivo.
|
|
25
|
+
*/
|
|
26
|
+
async function updateInboxItem(parsed, id) {
|
|
27
|
+
const current = (await listInbox()).find((n) => n.id === id);
|
|
28
|
+
if (!current) {
|
|
29
|
+
fail(`item #${id} não está pendente de revisão — update só vale para PENDING_REVIEW (veja \`memoring inbox list\`).`);
|
|
30
|
+
}
|
|
31
|
+
let overrides = {};
|
|
32
|
+
const file = stringFlag(parsed.flags, 'file');
|
|
33
|
+
if (file) {
|
|
34
|
+
try {
|
|
35
|
+
overrides = JSON.parse(readFileSync(file, 'utf8'));
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
fail(`JSON inválido em --file: ${e.message}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const flags = parsed.flags;
|
|
42
|
+
const tagsFlag = stringFlag(flags, 'tags');
|
|
43
|
+
const merged = {
|
|
44
|
+
type: overrides.type ?? stringFlag(flags, 'type') ?? current.type,
|
|
45
|
+
title: overrides.title ?? stringFlag(flags, 'title') ?? current.title,
|
|
46
|
+
content: overrides.content ?? stringFlag(flags, 'content') ?? current.content ?? null,
|
|
47
|
+
front: overrides.front ?? stringFlag(flags, 'front') ?? current.front ?? null,
|
|
48
|
+
back: overrides.back ?? stringFlag(flags, 'back') ?? current.back ?? null,
|
|
49
|
+
subjectName: overrides.subjectName ?? stringFlag(flags, 'subject') ?? current.subjectName ?? null,
|
|
50
|
+
subtopicName: overrides.subtopicName ?? stringFlag(flags, 'subtopic') ?? current.subtopicName ?? null,
|
|
51
|
+
tags: overrides.tags ?? (tagsFlag !== undefined ? parseTagsFlag(tagsFlag) : current.tags ?? []),
|
|
52
|
+
source: overrides.source ?? stringFlag(flags, 'source') ?? current.source ?? null,
|
|
53
|
+
deckId: overrides.deckId ?? current.deckId ?? null,
|
|
54
|
+
question: overrides.question ?? current.question ?? null,
|
|
55
|
+
};
|
|
56
|
+
let updated;
|
|
57
|
+
try {
|
|
58
|
+
updated = (await apiFetch(`/notes/${id}`, { method: 'PUT', body: merged }));
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
if (e instanceof ApiError) {
|
|
62
|
+
fail(`o servidor rejeitou a edição (${e.status}): ${e.body}`);
|
|
63
|
+
}
|
|
64
|
+
throw e;
|
|
65
|
+
}
|
|
66
|
+
if (parsed.flags.json) {
|
|
67
|
+
printJson(updated);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
process.stdout.write(green(`Item #${updated.id} [${updated.type}] "${updated.title}" atualizado — continua pendente de revisão.\n`));
|
|
71
|
+
}
|
|
6
72
|
export async function inboxCommand(parsed) {
|
|
7
73
|
const [sub, idArg] = parsed._;
|
|
8
74
|
switch (sub) {
|
|
@@ -26,10 +92,7 @@ export async function inboxCommand(parsed) {
|
|
|
26
92
|
}
|
|
27
93
|
case 'accept':
|
|
28
94
|
case 'discard': {
|
|
29
|
-
const id =
|
|
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
|
-
}
|
|
95
|
+
const id = parseId(idArg, sub);
|
|
33
96
|
try {
|
|
34
97
|
if (sub === 'accept') {
|
|
35
98
|
const note = (await apiFetch(`/notes/review-inbox/${id}/accept`, { method: 'POST' }));
|
|
@@ -68,12 +131,32 @@ export async function inboxCommand(parsed) {
|
|
|
68
131
|
process.stdout.write(green(`${res.accepted} item(ns) aceito(s).\n`));
|
|
69
132
|
return;
|
|
70
133
|
}
|
|
134
|
+
case 'discard-all': {
|
|
135
|
+
const pending = await listInbox();
|
|
136
|
+
for (const note of pending) {
|
|
137
|
+
await apiFetch(`/notes/review-inbox/${note.id}`, { method: 'DELETE' });
|
|
138
|
+
}
|
|
139
|
+
if (parsed.flags.json) {
|
|
140
|
+
printJson({ discarded: pending.length });
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
process.stdout.write(green(`${pending.length} item(ns) pendente(s) descartado(s).\n`));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
case 'update': {
|
|
147
|
+
await updateInboxItem(parsed, parseId(idArg, sub));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
71
150
|
default:
|
|
72
|
-
process.stdout.write('Uso: memoring inbox <list|accept|accept-all|discard> [id] [--json]\n' +
|
|
73
|
-
' list
|
|
74
|
-
' accept <id>
|
|
75
|
-
' accept-all
|
|
76
|
-
' discard <id> descarta um item pendente\n'
|
|
151
|
+
process.stdout.write('Uso: memoring inbox <list|accept|accept-all|discard|discard-all|update> [id] [--json]\n' +
|
|
152
|
+
' list lista os itens pendentes de revisão\n' +
|
|
153
|
+
' accept <id> aceita um item (entra nas listagens e na fila FSRS)\n' +
|
|
154
|
+
' accept-all aceita todos os itens pendentes\n' +
|
|
155
|
+
' discard <id> descarta um item pendente\n' +
|
|
156
|
+
' discard-all descarta todos os itens pendentes\n' +
|
|
157
|
+
' update <id> edita um item PENDENTE (merge: --file parcial.json e/ou flags\n' +
|
|
158
|
+
' --title --content --front --back --subject --subtopic\n' +
|
|
159
|
+
' --tags a,b,c --source --type)\n');
|
|
77
160
|
if (sub) {
|
|
78
161
|
process.exitCode = 1;
|
|
79
162
|
}
|
package/dist/commands/stage.js
CHANGED
|
@@ -3,12 +3,28 @@ import { readFileSync } from 'node:fs';
|
|
|
3
3
|
import { apiFetch, ApiDedupedError, ApiError } from '../api.js';
|
|
4
4
|
import { hasSession } from '../config.js';
|
|
5
5
|
import { dim, fail, green, printJson, stringFlag, yellow } from './shared.js';
|
|
6
|
-
const TYPES = ['NOTE', 'FLASHCARD', 'ONE_SHOT'];
|
|
6
|
+
const TYPES = ['NOTE', 'FLASHCARD', 'ONE_SHOT', 'QUESTION'];
|
|
7
7
|
// Retrocompat: itens.json antigos com "type": "NORMAL" são normalizados para "NOTE".
|
|
8
8
|
const LEGACY_TYPE_ALIASES = { NORMAL: 'NOTE' };
|
|
9
9
|
const MAX_ITEMS = 50;
|
|
10
10
|
const DEFAULT_SOURCE = 'Importação assistida (CLI)';
|
|
11
|
+
const QUESTION_KINDS = ['MULTIPLA_ESCOLHA', 'CERTO_ERRADO'];
|
|
11
12
|
const UNLIMITED = -1;
|
|
13
|
+
/**
|
|
14
|
+
* GET /notes/import/agent-guidance, fail-open como o fetchStoredCaps: sem sessão/rede
|
|
15
|
+
* devolve null e o schema sai só com o contrato.
|
|
16
|
+
*/
|
|
17
|
+
async function fetchAgentGuidance() {
|
|
18
|
+
if (!hasSession()) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
return (await apiFetch('/notes/import/agent-guidance'));
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
12
28
|
/** Rótulo de exibição de um teto: -1 → "à vontade". */
|
|
13
29
|
function capLabel(max) {
|
|
14
30
|
return max === UNLIMITED ? 'à vontade' : `até ${max}`;
|
|
@@ -51,11 +67,13 @@ const MAX_FLAG_BY_TYPE = {
|
|
|
51
67
|
NOTE: 'max-notes',
|
|
52
68
|
FLASHCARD: 'max-flashcards',
|
|
53
69
|
ONE_SHOT: 'max-one-shots',
|
|
70
|
+
QUESTION: 'max-questions',
|
|
54
71
|
};
|
|
55
72
|
const TYPE_LABEL = {
|
|
56
73
|
NOTE: 'notas',
|
|
57
74
|
FLASHCARD: 'flashcards',
|
|
58
75
|
ONE_SHOT: '1-shots',
|
|
76
|
+
QUESTION: 'questões',
|
|
59
77
|
};
|
|
60
78
|
/** Campo do ImportCaps ↔ flag (o `stage caps` também aceita --max-questions, usado pelo app). */
|
|
61
79
|
const CAPS_FLAG_BY_FIELD = {
|
|
@@ -71,7 +89,12 @@ const CAPS_FLAG_BY_FIELD = {
|
|
|
71
89
|
*/
|
|
72
90
|
function enforceTypeCaps(items, flags, stored) {
|
|
73
91
|
const storedByType = stored
|
|
74
|
-
? {
|
|
92
|
+
? {
|
|
93
|
+
NOTE: stored.maxNotes,
|
|
94
|
+
FLASHCARD: stored.maxFlashcards,
|
|
95
|
+
ONE_SHOT: stored.maxOneShots,
|
|
96
|
+
QUESTION: stored.maxQuestions,
|
|
97
|
+
}
|
|
75
98
|
: {};
|
|
76
99
|
for (const [type, flag] of Object.entries(MAX_FLAG_BY_TYPE)) {
|
|
77
100
|
const fromFlag = parseCapFlag(stringFlag(flags, flag), flag);
|
|
@@ -98,10 +121,24 @@ function enforceTypeCaps(items, flags, stored) {
|
|
|
98
121
|
}
|
|
99
122
|
}
|
|
100
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Contrato do snapshot de questão estagiável pela CLI: apenas questões MANUAIS
|
|
126
|
+
* (sem identidade TEC — tecQuestionId e cia. nascem no servidor, nunca num payload
|
|
127
|
+
* de client). Espelha a validação de QuestionSnapshot.manual do backend.
|
|
128
|
+
*/
|
|
129
|
+
const QUESTION_CONTRACT = {
|
|
130
|
+
kind: 'MULTIPLA_ESCOLHA | CERTO_ERRADO (obrigatório)',
|
|
131
|
+
statementHtml: 'string — enunciado da questão (obrigatório; HTML leve)',
|
|
132
|
+
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"])',
|
|
133
|
+
correctAlternative: 'int 1-based — gabarito (obrigatório; em CERTO_ERRADO: 1 = Certo, 2 = Errado)',
|
|
134
|
+
commentHtml: 'string? — explicação do gabarito (as regras de qualidade pedem sempre)',
|
|
135
|
+
subjectName: 'string? — matéria da questão (espelhe a do item; o snapshot guarda a dele)',
|
|
136
|
+
subtopicName: 'string? — subtópico da questão',
|
|
137
|
+
};
|
|
101
138
|
const SCHEMA_JSON = {
|
|
102
139
|
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.',
|
|
103
140
|
item: {
|
|
104
|
-
type: 'NOTE | FLASHCARD | ONE_SHOT (obrigatório; NORMAL legado é aceito e mapeado para NOTE)',
|
|
141
|
+
type: 'NOTE | FLASHCARD | ONE_SHOT | QUESTION (obrigatório; NORMAL legado é aceito e mapeado para NOTE)',
|
|
105
142
|
title: 'string (obrigatório)',
|
|
106
143
|
content: 'string — corpo da nota em markdown (obrigatório quando type=NOTE)',
|
|
107
144
|
front: 'string — pergunta (obrigatório quando type=FLASHCARD|ONE_SHOT)',
|
|
@@ -109,8 +146,10 @@ const SCHEMA_JSON = {
|
|
|
109
146
|
subjectName: 'string? — matéria (ex.: "Direito Constitucional")',
|
|
110
147
|
subtopicName: 'string? — subtópico',
|
|
111
148
|
tags: 'string[]?',
|
|
149
|
+
question: 'objeto? — snapshot da questão (obrigatório quando type=QUESTION; contrato em questionContract)',
|
|
112
150
|
source: `string? — origem do material (default: "${DEFAULT_SOURCE}")`,
|
|
113
151
|
},
|
|
152
|
+
questionContract: QUESTION_CONTRACT,
|
|
114
153
|
example: [
|
|
115
154
|
{
|
|
116
155
|
type: 'FLASHCARD',
|
|
@@ -121,25 +160,51 @@ const SCHEMA_JSON = {
|
|
|
121
160
|
tags: ['cf88', 'art-3'],
|
|
122
161
|
source: 'Apostila constitucional.pdf',
|
|
123
162
|
},
|
|
163
|
+
{
|
|
164
|
+
type: 'QUESTION',
|
|
165
|
+
title: 'IBAMA 2025 — conceito de tributo',
|
|
166
|
+
subjectName: 'Direito Tributário',
|
|
167
|
+
tags: ['ctn', 'art-3'],
|
|
168
|
+
source: 'aula-00.pdf',
|
|
169
|
+
question: {
|
|
170
|
+
kind: 'CERTO_ERRADO',
|
|
171
|
+
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.',
|
|
172
|
+
correctAlternative: 1,
|
|
173
|
+
commentHtml: 'É a definição do art. 3º do CTN (Lei 5.172/1966).',
|
|
174
|
+
},
|
|
175
|
+
},
|
|
124
176
|
],
|
|
125
|
-
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.',
|
|
177
|
+
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.',
|
|
126
178
|
};
|
|
127
179
|
const SCHEMA_TEXT = `Contrato dos itens da caixa de revisão (POST /api/notes/review-inbox):
|
|
128
180
|
|
|
129
|
-
type NOTE | FLASHCARD | ONE_SHOT
|
|
181
|
+
type NOTE | FLASHCARD | ONE_SHOT | QUESTION (obrigatório; NORMAL legado é mapeado para NOTE)
|
|
130
182
|
title string (obrigatório)
|
|
131
183
|
content string — corpo da nota (markdown) (obrigatório quando type=NOTE)
|
|
132
184
|
front, back string — pergunta e resposta (obrigatório quando type=FLASHCARD|ONE_SHOT)
|
|
185
|
+
question objeto — snapshot da questão (obrigatório quando type=QUESTION)
|
|
133
186
|
subjectName? string — matéria (ex.: "Direito Constitucional")
|
|
134
187
|
subtopicName? string — subtópico
|
|
135
188
|
tags? string[]
|
|
136
189
|
source? string — origem do material (default: "${DEFAULT_SOURCE}")
|
|
137
190
|
|
|
191
|
+
Snapshot de QUESTION (questões manuais — sem identidade TEC):
|
|
192
|
+
kind MULTIPLA_ESCOLHA | CERTO_ERRADO (obrigatório)
|
|
193
|
+
statementHtml string — enunciado (HTML leve) (obrigatório)
|
|
194
|
+
alternatives string[] — 2 a 5 alternativas (obrigatório em MULTIPLA_ESCOLHA;
|
|
195
|
+
em CERTO_ERRADO não envie — o par fixo é Certo/Errado)
|
|
196
|
+
correctAlternative int 1-based — gabarito (obrigatório; CERTO_ERRADO: 1 = Certo, 2 = Errado)
|
|
197
|
+
commentHtml string — explicação do gabarito (recomendado)
|
|
198
|
+
subjectName/subtopicName string? — matéria/subtópico DA QUESTÃO (o snapshot guarda os próprios)
|
|
199
|
+
|
|
138
200
|
Lote: 1 a ${MAX_ITEMS} itens, all-or-nothing — qualquer item inválido rejeita o lote inteiro.
|
|
139
|
-
Os itens nascem PENDING_REVIEW: o usuário aceita, edita ou recusa em /cards/inbox
|
|
140
|
-
|
|
201
|
+
Os itens nascem PENDING_REVIEW: o usuário aceita, edita ou recusa em /cards/inbox
|
|
202
|
+
(agentes podem editar pendentes com \`memoring inbox update <id>\`).
|
|
203
|
+
Dica: FLASHCARD entra na fila de revisão espaçada (FSRS); ONE_SHOT é pergunta de resposta única
|
|
204
|
+
(termo curto — ideal para princípios, leis, datas, prazos, siglas, termos em latim);
|
|
205
|
+
QUESTION treina verificação objetiva (aproveite questões prontas do material).
|
|
141
206
|
Limites por tipo: a preferência do usuário (abaixo, quando logado) vale por padrão;
|
|
142
|
-
--max-notes N | --max-flashcards N | --max-one-shots N no create vencem por lote ("livre" = à vontade).`;
|
|
207
|
+
--max-notes N | --max-flashcards N | --max-one-shots N | --max-questions N no create vencem por lote ("livre" = à vontade).`;
|
|
143
208
|
async function readStdin() {
|
|
144
209
|
const chunks = [];
|
|
145
210
|
for await (const chunk of process.stdin) {
|
|
@@ -147,6 +212,68 @@ async function readStdin() {
|
|
|
147
212
|
}
|
|
148
213
|
return Buffer.concat(chunks).toString('utf8');
|
|
149
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* Valida o snapshot de questão manual espelhando QuestionSnapshot.manual do servidor:
|
|
217
|
+
* modalidade, enunciado, gabarito obrigatório apontando para alternativa existente e,
|
|
218
|
+
* em múltipla escolha, 2 a 5 alternativas não vazias. Certo/errado não leva
|
|
219
|
+
* alternatives — o servidor normaliza para o par fixo Certo/Errado.
|
|
220
|
+
*/
|
|
221
|
+
function validateQuestion(raw, where) {
|
|
222
|
+
if (typeof raw !== 'object' || raw === null) {
|
|
223
|
+
fail(`${where}: type=QUESTION exige o objeto "question" (veja o contrato em \`memoring stage schema\`).`);
|
|
224
|
+
}
|
|
225
|
+
const q = raw;
|
|
226
|
+
if ('tecQuestionId' in q || 'tecCadernoId' in q) {
|
|
227
|
+
fail(`${where}: a CLI só estagia questões manuais — não envie tecQuestionId/tecCadernoId (a identidade TEC nasce no servidor).`);
|
|
228
|
+
}
|
|
229
|
+
if (typeof q.kind !== 'string' || !QUESTION_KINDS.includes(q.kind)) {
|
|
230
|
+
fail(`${where}: question.kind deve ser ${QUESTION_KINDS.join(' | ')} (veio ${JSON.stringify(q.kind)}).`);
|
|
231
|
+
}
|
|
232
|
+
if (typeof q.statementHtml !== 'string' || q.statementHtml.trim() === '') {
|
|
233
|
+
fail(`${where}: question.statementHtml (enunciado) é obrigatório.`);
|
|
234
|
+
}
|
|
235
|
+
if (typeof q.correctAlternative !== 'number' || !Number.isInteger(q.correctAlternative)) {
|
|
236
|
+
fail(`${where}: question.correctAlternative (gabarito, int 1-based) é obrigatório.`);
|
|
237
|
+
}
|
|
238
|
+
const correct = q.correctAlternative;
|
|
239
|
+
const subjects = {
|
|
240
|
+
...(typeof q.subjectName === 'string' ? { subjectName: q.subjectName } : {}),
|
|
241
|
+
...(typeof q.subtopicName === 'string' ? { subtopicName: q.subtopicName } : {}),
|
|
242
|
+
};
|
|
243
|
+
if (q.kind === 'CERTO_ERRADO') {
|
|
244
|
+
if (correct < 1 || correct > 2) {
|
|
245
|
+
fail(`${where}: em CERTO_ERRADO o gabarito deve ser 1 (Certo) ou 2 (Errado) — veio ${correct}.`);
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
kind: q.kind,
|
|
249
|
+
statementHtml: q.statementHtml,
|
|
250
|
+
correctAlternative: correct,
|
|
251
|
+
...(typeof q.commentHtml === 'string' ? { commentHtml: q.commentHtml } : {}),
|
|
252
|
+
...subjects,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
if (!Array.isArray(q.alternatives) || q.alternatives.some((a) => typeof a !== 'string')) {
|
|
256
|
+
fail(`${where}: MULTIPLA_ESCOLHA exige question.alternatives como array de strings.`);
|
|
257
|
+
}
|
|
258
|
+
const alternatives = q.alternatives.map((a) => a.trim());
|
|
259
|
+
if (alternatives.length < 2 || alternatives.length > 5) {
|
|
260
|
+
fail(`${where}: MULTIPLA_ESCOLHA exige de 2 a 5 alternativas (vieram ${alternatives.length}).`);
|
|
261
|
+
}
|
|
262
|
+
if (alternatives.some((a) => a === '')) {
|
|
263
|
+
fail(`${where}: alternativas não podem ser vazias.`);
|
|
264
|
+
}
|
|
265
|
+
if (correct < 1 || correct > alternatives.length) {
|
|
266
|
+
fail(`${where}: gabarito ${correct} não aponta para alternativa existente (1..${alternatives.length}).`);
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
kind: q.kind,
|
|
270
|
+
statementHtml: q.statementHtml,
|
|
271
|
+
alternatives,
|
|
272
|
+
correctAlternative: correct,
|
|
273
|
+
...(typeof q.commentHtml === 'string' ? { commentHtml: q.commentHtml } : {}),
|
|
274
|
+
...subjects,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
150
277
|
function validateItems(raw) {
|
|
151
278
|
const items = Array.isArray(raw) ? raw : raw?.items;
|
|
152
279
|
if (!Array.isArray(items) || items.length === 0) {
|
|
@@ -177,6 +304,9 @@ function validateItems(raw) {
|
|
|
177
304
|
(typeof it.front !== 'string' || it.front.trim() === '' || typeof it.back !== 'string' || it.back.trim() === '')) {
|
|
178
305
|
fail(`${where} (${it.title}): type=${it.type} exige front e back.`);
|
|
179
306
|
}
|
|
307
|
+
if (it.type === 'QUESTION') {
|
|
308
|
+
it.question = validateQuestion(it.question, `${where} (${it.title})`);
|
|
309
|
+
}
|
|
180
310
|
if (it.tags !== undefined && (!Array.isArray(it.tags) || it.tags.some((t) => typeof t !== 'string'))) {
|
|
181
311
|
fail(`${where} (${it.title}): tags deve ser um array de strings.`);
|
|
182
312
|
}
|
|
@@ -202,33 +332,58 @@ async function emptyInbox() {
|
|
|
202
332
|
}
|
|
203
333
|
return pending.length;
|
|
204
334
|
}
|
|
335
|
+
const NO_SESSION_WARNING = 'SEM SESSÃO: userCaps, qualityRules e customInstructions ficam indisponíveis. ' +
|
|
336
|
+
'NÃO gere itens assim — rode `memoring auth login` (device flow, o usuário aprova no browser) e releia o schema antes de gerar.';
|
|
205
337
|
export async function stageCommand(parsed) {
|
|
206
338
|
const [sub] = parsed._;
|
|
207
339
|
switch (sub) {
|
|
208
340
|
case 'schema': {
|
|
209
|
-
const
|
|
341
|
+
const authenticated = hasSession();
|
|
342
|
+
const [caps, guidance] = await Promise.all([fetchStoredCaps(), fetchAgentGuidance()]);
|
|
343
|
+
if (!authenticated) {
|
|
344
|
+
process.stderr.write(yellow(`aviso: ${NO_SESSION_WARNING}\n`));
|
|
345
|
+
}
|
|
210
346
|
if (parsed.flags.json) {
|
|
211
|
-
printJson(
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
347
|
+
printJson({
|
|
348
|
+
authenticated,
|
|
349
|
+
...SCHEMA_JSON,
|
|
350
|
+
userCaps: caps
|
|
351
|
+
? {
|
|
215
352
|
maxNotes: caps.maxNotes,
|
|
216
353
|
maxFlashcards: caps.maxFlashcards,
|
|
217
354
|
maxOneShots: caps.maxOneShots,
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
355
|
+
maxQuestions: caps.maxQuestions,
|
|
356
|
+
}
|
|
357
|
+
: null,
|
|
358
|
+
qualityRules: guidance?.qualityRules ?? null,
|
|
359
|
+
customInstructions: guidance?.customInstructions ?? null,
|
|
360
|
+
...(!authenticated ? { warning: NO_SESSION_WARNING } : {}),
|
|
361
|
+
});
|
|
221
362
|
}
|
|
222
363
|
else {
|
|
223
364
|
process.stdout.write(SCHEMA_TEXT + '\n');
|
|
224
365
|
if (caps) {
|
|
225
366
|
process.stdout.write(`\nLimites por tipo configurados por você — o agente DEVE respeitá-los ao gerar o lote: ` +
|
|
226
|
-
`notas: ${capLabel(caps.maxNotes)}, flashcards: ${capLabel(caps.maxFlashcards)},
|
|
227
|
-
|
|
367
|
+
`notas: ${capLabel(caps.maxNotes)}, flashcards: ${capLabel(caps.maxFlashcards)}, ` +
|
|
368
|
+
`1-shots: ${capLabel(caps.maxOneShots)}, questões: ${capLabel(caps.maxQuestions)}.\n` +
|
|
369
|
+
dim('("à vontade" = crie quantos o material justificar; altere com `memoring stage caps`)\n'));
|
|
228
370
|
}
|
|
229
371
|
else {
|
|
230
372
|
process.stdout.write(dim('\n(limites por tipo indisponíveis — faça `memoring auth login` ou verifique a conexão)\n'));
|
|
231
373
|
}
|
|
374
|
+
if (guidance) {
|
|
375
|
+
process.stdout.write('\nRegras de qualidade — SEMPRE aplicadas ao gerar (as mesmas que o app envia à LLM):\n' +
|
|
376
|
+
guidance.qualityRules.trim() +
|
|
377
|
+
'\n');
|
|
378
|
+
if (guidance.customInstructions) {
|
|
379
|
+
process.stdout.write('\nInstruções adicionais do usuário — também SEMPRE aplicadas:\n' +
|
|
380
|
+
guidance.customInstructions.trim() +
|
|
381
|
+
'\n');
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
else if (authenticated) {
|
|
385
|
+
process.stdout.write(dim('\n(regras de qualidade indisponíveis — verifique a conexão e repita antes de gerar)\n'));
|
|
386
|
+
}
|
|
232
387
|
}
|
|
233
388
|
return;
|
|
234
389
|
}
|
|
@@ -299,7 +454,7 @@ export async function stageCommand(parsed) {
|
|
|
299
454
|
catch (e) {
|
|
300
455
|
if (e instanceof ApiDedupedError) {
|
|
301
456
|
if (parsed.flags.json) {
|
|
302
|
-
printJson({ created: [], deduplicated: true });
|
|
457
|
+
printJson({ created: [], ids: [], deduplicated: true });
|
|
303
458
|
return;
|
|
304
459
|
}
|
|
305
460
|
process.stdout.write(green('Este lote já foi enviado antes — nada foi duplicado.\n'));
|
|
@@ -314,7 +469,7 @@ export async function stageCommand(parsed) {
|
|
|
314
469
|
throw e;
|
|
315
470
|
}
|
|
316
471
|
if (parsed.flags.json) {
|
|
317
|
-
printJson({ created });
|
|
472
|
+
printJson({ created, ids: created.map((note) => note.id) });
|
|
318
473
|
return;
|
|
319
474
|
}
|
|
320
475
|
process.stdout.write(green(`OK — ${created.length} item(ns) aguardando revisão em /cards/inbox:\n`));
|
|
@@ -326,12 +481,14 @@ export async function stageCommand(parsed) {
|
|
|
326
481
|
default:
|
|
327
482
|
process.stdout.write('Uso: memoring stage <schema|caps|create> [--file itens.json] [--replace] [--json]\n' +
|
|
328
483
|
' schema imprime o contrato JSON dos itens (para humanos e agentes)\n' +
|
|
329
|
-
' logado, inclui seus limites por tipo
|
|
484
|
+
' logado, inclui seus limites por tipo e as regras de qualidade —\n' +
|
|
485
|
+
' o agente deve ler o schema LOGADO, antes de gerar\n' +
|
|
330
486
|
' caps mostra/grava seus limites por tipo (--max-notes N | --max-flashcards N |\n' +
|
|
331
487
|
' --max-one-shots N | --max-questions N; 0 = não gerar o tipo)\n' +
|
|
332
488
|
' create envia o lote para a caixa de revisão (arquivo ou stdin)\n' +
|
|
489
|
+
' tipos: NOTE | FLASHCARD | ONE_SHOT | QUESTION\n' +
|
|
333
490
|
' --replace esvazia a caixa antes de enviar\n' +
|
|
334
|
-
' --max-notes N | --max-flashcards N | --max-one-shots N\n' +
|
|
491
|
+
' --max-notes N | --max-flashcards N | --max-one-shots N | --max-questions N\n' +
|
|
335
492
|
' vencem a preferência gravada por lote (validação local)\n');
|
|
336
493
|
if (sub) {
|
|
337
494
|
process.exitCode = 1;
|
package/dist/main.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
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
5
|
import { inboxCommand } from './commands/inbox.js';
|
|
6
6
|
import { stageCommand } from './commands/stage.js';
|
|
7
7
|
import { updateCommand } from './commands/update.js';
|
|
8
8
|
import { skillCommand } from './commands/skill.js';
|
|
9
|
-
import {
|
|
9
|
+
import { parseArgs as parse } from './commands/shared.js';
|
|
10
10
|
import { newerVersionAvailable, scheduleUpdateCheck } from './update.js';
|
|
11
|
-
const VERSION = '0.
|
|
11
|
+
const VERSION = '0.8.0';
|
|
12
12
|
const HELP = `Memoring CLI — flashcards e notas por IA, direto do terminal.
|
|
13
13
|
|
|
14
14
|
Uso: memoring <comando> [subcomando] [flags]
|
|
@@ -16,12 +16,16 @@ Uso: memoring <comando> [subcomando] [flags]
|
|
|
16
16
|
Comandos:
|
|
17
17
|
auth login|logout|status autenticação (device flow — sem senha no terminal)
|
|
18
18
|
stage schema contrato JSON dos itens da caixa de revisão
|
|
19
|
+
(logado: inclui limites por tipo + regras de qualidade —
|
|
20
|
+
agentes devem ler o schema LOGADO, antes de gerar)
|
|
19
21
|
stage create [--file f] envia itens (arquivo ou stdin) para a caixa de revisão
|
|
20
22
|
[--replace] esvazia a caixa antes de enviar
|
|
21
23
|
stage caps [--max-* N] seus limites por tipo (valem pro app e pros agentes)
|
|
22
24
|
inbox list itens pendentes de revisão
|
|
23
25
|
inbox accept <id>|accept-all aceita itens (entram nas listagens e na fila FSRS)
|
|
24
26
|
inbox discard <id> descarta um item pendente
|
|
27
|
+
inbox discard-all descarta todos os itens pendentes
|
|
28
|
+
inbox update <id> edita um item pendente (--file parcial e/ou flags)
|
|
25
29
|
update atualiza a CLI para a versão mais recente (via npm)
|
|
26
30
|
skill guia completo da CLI para agentes de IA (imprime o SKILL.md)
|
|
27
31
|
version versão da CLI
|
|
@@ -100,8 +104,16 @@ async function main() {
|
|
|
100
104
|
}
|
|
101
105
|
}
|
|
102
106
|
main().catch((error) => {
|
|
107
|
+
if (error instanceof CliError) {
|
|
108
|
+
// mensagem já impressa pelo fail() — só converte em exit code, sem process.exit (ver CliError)
|
|
109
|
+
process.exitCode = 1;
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
103
112
|
if (error instanceof ApiError) {
|
|
104
|
-
|
|
113
|
+
process.stderr.write(`Erro: falha na API (${error.status}): ${error.body}\n`);
|
|
114
|
+
process.exitCode = 1;
|
|
115
|
+
return;
|
|
105
116
|
}
|
|
106
|
-
|
|
117
|
+
process.stderr.write(`Erro: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
118
|
+
process.exitCode = 1;
|
|
107
119
|
});
|
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
|
-
|
|
38
|
+
throw new CliError(message);
|
|
31
39
|
}
|
package/package.json
CHANGED
|
@@ -7,22 +7,24 @@ 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
|
|
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 +
|
|
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]` | envia o lote (1 a 50 itens) para a caixa de revisão; `--json` devolve `created` + `ids` |
|
|
23
24
|
| `memoring inbox list` | itens pendentes de revisão |
|
|
24
25
|
| `memoring inbox accept <id>\|accept-all` | aceita itens (entram nas listagens e na fila FSRS) |
|
|
25
|
-
| `memoring inbox discard <id
|
|
26
|
+
| `memoring inbox discard <id>\|discard-all` | descarta item(ns) pendente(s) |
|
|
27
|
+
| `memoring inbox update <id> [--file parcial.json] [--title ...] [--front ...] [--back ...] [--tags a,b] ...` | edita um item AINDA PENDENTE (merge com o estado atual) |
|
|
26
28
|
| `memoring skill` | imprime este guia |
|
|
27
29
|
| `memoring update` | atualiza a CLI (decisão do usuário — nunca rode por conta própria) |
|
|
28
30
|
|
|
@@ -31,16 +33,19 @@ Flags globais: `--json` (saída legível por máquina), `--no-color`, `--api-bas
|
|
|
31
33
|
## Fluxo padrão
|
|
32
34
|
|
|
33
35
|
1. **Entenda o pedido** — qual pasta/arquivo/material vira flashcards/notas e se o usuário pediu quantidades.
|
|
34
|
-
2. **
|
|
35
|
-
3. **Leia os
|
|
36
|
-
4. **
|
|
37
|
-
5. **
|
|
36
|
+
2. **Confira a sessão**: `memoring auth status --json`. Sem `authenticated: true`, PARE e peça o login (ver Pré-requisitos).
|
|
37
|
+
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.
|
|
38
|
+
4. **Leia os materiais** e gere os itens no contrato, num arquivo JSON (array ou `{ "items": [...] }`).
|
|
39
|
+
5. **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.
|
|
40
|
+
6. **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.
|
|
41
|
+
7. **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
42
|
|
|
39
43
|
## O contrato (a fonte da verdade é `stage schema`)
|
|
40
44
|
|
|
41
|
-
- `type`: `NOTE` (nota, exige `content` markdown; `NORMAL` legado é aceito e mapeado para `NOTE`), `FLASHCARD` ou `ONE_SHOT` (exigem `front` + `back`).
|
|
45
|
+
- `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
46
|
- `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 (
|
|
47
|
+
- `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`.
|
|
48
|
+
- `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
49
|
- Preencha `source` com a origem real do material (nome do arquivo/pasta) — não use o default quando souber a origem.
|
|
45
50
|
- `subjectName`/`subtopicName` quando o material tiver matéria/subtópico claros; `tags` curtas e em minúsculas.
|
|
46
51
|
- NUNCA invente campos fora do contrato — o servidor rejeita o lote inteiro.
|
|
@@ -54,11 +59,12 @@ Flags globais: `--json` (saída legível por máquina), `--no-color`, `--api-bas
|
|
|
54
59
|
|
|
55
60
|
## Qualidade dos flashcards (o padrão que o app espera)
|
|
56
61
|
|
|
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.
|
|
62
|
+
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
63
|
|
|
59
64
|
## Armadilhas
|
|
60
65
|
|
|
61
|
-
- **Nunca** use `--replace` sem o usuário pedir explicitamente —
|
|
66
|
+
- **Nunca** use `--replace` ou `inbox discard-all` sem o usuário pedir explicitamente — eles apagam os itens pendentes existentes da caixa.
|
|
62
67
|
- 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
68
|
- **Não** altere os limites do usuário (`stage caps`) por conta própria — é preferência dele, compartilhada com o app.
|
|
64
69
|
- Se `stage create` falhar com erro de validação do servidor, corrija os itens indicados e reenvie — não relaxe o contrato.
|
|
70
|
+
- Errou um item já enviado? Não reenvie o lote (duplicaria): use `memoring inbox update <id>` enquanto ele estiver PENDING_REVIEW.
|