@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.
- package/dist/commands/deck.js +156 -4
- package/dist/commands/stage.js +5 -1
- package/dist/main.js +5 -3
- package/package.json +1 -1
- package/skills/memoring-cli/SKILL.md +4 -3
package/dist/commands/deck.js
CHANGED
|
@@ -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] [
|
|
92
|
-
' list
|
|
93
|
-
' create <nome>
|
|
94
|
-
'
|
|
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
|
}
|
package/dist/commands/stage.js
CHANGED
|
@@ -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.
|
|
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
|
@@ -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.
|