@memoring/cli 0.8.0 → 0.10.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 +3 -1
- package/dist/commands/deck.js +281 -0
- package/dist/commands/inbox.js +11 -1
- package/dist/commands/stage.js +117 -2
- package/dist/main.js +11 -2
- package/package.json +1 -1
- package/skills/memoring-cli/SKILL.md +10 -5
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>` / discard-all / update `<id>` (edita item PENDENTE — merge do `--file` parcial e/ou flags `--title --front --back --tags a,b ...`) |
|
|
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 |
|
|
@@ -92,6 +93,7 @@ Cada item do lote (1 a 50, all-or-nothing):
|
|
|
92
93
|
```
|
|
93
94
|
|
|
94
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.
|
|
97
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.
|
|
@@ -0,0 +1,281 @@
|
|
|
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
|
+
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
|
+
/** Countdown da data-alvo em dias: futuro "42d", atrasado "-3d" (base UTC, sem fuso). */
|
|
39
|
+
function goalCountdown(goalDate) {
|
|
40
|
+
if (!goalDate) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const target = Date.parse(`${goalDate}T00:00:00Z`);
|
|
44
|
+
if (Number.isNaN(target)) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
const now = new Date();
|
|
48
|
+
const today = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
|
|
49
|
+
return `${Math.round((target - today) / 86_400_000)}d`;
|
|
50
|
+
}
|
|
51
|
+
/** META da listagem/update: goal + countdown da data-alvo, célula de ~30 chars. */
|
|
52
|
+
function formatGoal(deck) {
|
|
53
|
+
const suffix = (() => {
|
|
54
|
+
const countdown = goalCountdown(deck.goalDate);
|
|
55
|
+
return countdown === null ? '' : ` (${countdown})`;
|
|
56
|
+
})();
|
|
57
|
+
const budget = GOAL_DISPLAY_MAX - suffix.length;
|
|
58
|
+
let goal = (deck.goal ?? '').trim();
|
|
59
|
+
if (goal.length > budget) {
|
|
60
|
+
goal = `${goal.slice(0, Math.max(1, budget - 1)).trimEnd()}…`;
|
|
61
|
+
}
|
|
62
|
+
const label = `${goal}${suffix}`.trim();
|
|
63
|
+
return label === '' ? '—' : label;
|
|
64
|
+
}
|
|
65
|
+
/** 45s | 12min | 1h05 — segundos inteiros; médias chegam já arredondadas. */
|
|
66
|
+
function formatRunSeconds(seconds) {
|
|
67
|
+
if (seconds < 60) {
|
|
68
|
+
return `${seconds}s`;
|
|
69
|
+
}
|
|
70
|
+
const minutes = Math.round(seconds / 60);
|
|
71
|
+
if (minutes < 60) {
|
|
72
|
+
return `${minutes}min`;
|
|
73
|
+
}
|
|
74
|
+
return `${Math.floor(minutes / 60)}h${String(minutes % 60).padStart(2, '0')}`;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Recorde exibido: para o pai é a MÉDIA ARITMÉTICA dos filhos com recorde (o
|
|
78
|
+
* fastestRunSeconds do DeckView é só do próprio baralho — quem tem a árvore
|
|
79
|
+
* toda calcula a média); sem filho com recorde, cai no próprio do baralho.
|
|
80
|
+
*/
|
|
81
|
+
function recordOf(deck, childrenByParent) {
|
|
82
|
+
const records = (childrenByParent.get(deck.id) ?? [])
|
|
83
|
+
.map((child) => child.fastestRunSeconds)
|
|
84
|
+
.filter((seconds) => seconds !== null);
|
|
85
|
+
if (records.length > 0) {
|
|
86
|
+
return Math.round(records.reduce((sum, seconds) => sum + seconds, 0) / records.length);
|
|
87
|
+
}
|
|
88
|
+
return deck.fastestRunSeconds;
|
|
89
|
+
}
|
|
90
|
+
function parseRelevanceFlag(raw) {
|
|
91
|
+
const normalized = raw.replace(',', '.');
|
|
92
|
+
if (!/^\d+(\.\d)?$/.test(normalized)) {
|
|
93
|
+
fail(`--relevance inválido: use 0 a 5 com no máximo 1 casa decimal (ex.: 4 ou 4,5 — veio ${JSON.stringify(raw)}).`);
|
|
94
|
+
}
|
|
95
|
+
const value = Number(normalized);
|
|
96
|
+
if (value > 5) {
|
|
97
|
+
fail(`--relevance deve estar entre 0 e 5 (veio ${raw}).`);
|
|
98
|
+
}
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
function parseGoalFlag(raw) {
|
|
102
|
+
const goal = raw.trim();
|
|
103
|
+
if (goal === '') {
|
|
104
|
+
fail('--goal vazio — para limpar a meta use --clear-goal.');
|
|
105
|
+
}
|
|
106
|
+
if (goal.length > GOAL_MAX_LENGTH) {
|
|
107
|
+
fail(`--goal excede ${GOAL_MAX_LENGTH} caracteres (veio ${goal.length}).`);
|
|
108
|
+
}
|
|
109
|
+
return goal;
|
|
110
|
+
}
|
|
111
|
+
function parseGoalDateFlag(raw) {
|
|
112
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(raw)) {
|
|
113
|
+
fail(`--goal-date inválido: use o formato AAAA-MM-DD (veio ${JSON.stringify(raw)}).`);
|
|
114
|
+
}
|
|
115
|
+
const time = Date.parse(`${raw}T00:00:00Z`);
|
|
116
|
+
if (Number.isNaN(time) || new Date(time).toISOString().slice(0, 10) !== raw) {
|
|
117
|
+
fail(`--goal-date inválido: ${raw} não é uma data real.`);
|
|
118
|
+
}
|
|
119
|
+
return raw;
|
|
120
|
+
}
|
|
121
|
+
function parseToneFlag(raw) {
|
|
122
|
+
const tone = raw.trim().toLowerCase();
|
|
123
|
+
if (!COVER_TONES.includes(tone)) {
|
|
124
|
+
fail(`--tone inválido: use ${COVER_TONES.join(' | ')} (veio ${JSON.stringify(raw)}).`);
|
|
125
|
+
}
|
|
126
|
+
return tone;
|
|
127
|
+
}
|
|
128
|
+
const METADATA_SPECS = [
|
|
129
|
+
{ valueFlag: 'relevance', clearFlag: 'clear-relevance', field: 'relevance', parse: parseRelevanceFlag },
|
|
130
|
+
{ valueFlag: 'goal', clearFlag: 'clear-goal', field: 'goal', parse: parseGoalFlag },
|
|
131
|
+
{ valueFlag: 'goal-date', clearFlag: 'clear-goal-date', field: 'goalDate', parse: parseGoalDateFlag },
|
|
132
|
+
{ valueFlag: 'tone', clearFlag: 'clear-tone', field: 'coverTone', parse: parseToneFlag },
|
|
133
|
+
];
|
|
134
|
+
/**
|
|
135
|
+
* Monta o body do PUT /metadata com SÓ os campos informados: flag de valor
|
|
136
|
+
* envia o valor validado, --clear-* envia null explícito (limpa), os dois
|
|
137
|
+
* juntos é erro. Ausente dos dois = campo intacto (o PUT não toca).
|
|
138
|
+
*/
|
|
139
|
+
function metadataBody(flags) {
|
|
140
|
+
const body = {};
|
|
141
|
+
for (const spec of METADATA_SPECS) {
|
|
142
|
+
const raw = stringFlag(flags, spec.valueFlag);
|
|
143
|
+
const clear = flags[spec.clearFlag] === true;
|
|
144
|
+
if (raw !== undefined && clear) {
|
|
145
|
+
fail(`--${spec.valueFlag} e --${spec.clearFlag} não podem ir juntos — use um ou outro.`);
|
|
146
|
+
}
|
|
147
|
+
if (raw !== undefined) {
|
|
148
|
+
body[spec.field] = spec.parse(raw);
|
|
149
|
+
}
|
|
150
|
+
else if (clear) {
|
|
151
|
+
body[spec.field] = null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (Object.keys(body).length === 0) {
|
|
155
|
+
fail('nada a atualizar — informe ao menos um campo: --relevance, --goal, --goal-date, --tone ou um --clear-*.');
|
|
156
|
+
}
|
|
157
|
+
return body;
|
|
158
|
+
}
|
|
159
|
+
function printDeckSummary(deck) {
|
|
160
|
+
process.stdout.write(green(`Baralho "${deck.name}" (#${deck.id}) atualizado.\n`));
|
|
161
|
+
process.stdout.write(` relevância: ${formatRelevance(deck.relevance)}\n`);
|
|
162
|
+
process.stdout.write(` meta: ${formatGoal(deck)}\n`);
|
|
163
|
+
process.stdout.write(` tom de capa: ${deck.coverTone ?? '—'}\n`);
|
|
164
|
+
}
|
|
165
|
+
export async function deckCommand(parsed) {
|
|
166
|
+
const [sub, ...rest] = parsed._;
|
|
167
|
+
switch (sub) {
|
|
168
|
+
case 'list': {
|
|
169
|
+
const decks = await listDecks();
|
|
170
|
+
if (parsed.flags.json) {
|
|
171
|
+
printJson(decks);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (decks.length === 0) {
|
|
175
|
+
process.stdout.write('Nenhum baralho — crie com `memoring deck create "<nome>"`.\n');
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const nameById = new Map(decks.map((d) => [d.id, d.name]));
|
|
179
|
+
const childrenByParent = new Map();
|
|
180
|
+
for (const d of decks) {
|
|
181
|
+
if (d.parentId !== null) {
|
|
182
|
+
const siblings = childrenByParent.get(d.parentId) ?? [];
|
|
183
|
+
siblings.push(d);
|
|
184
|
+
childrenByParent.set(d.parentId, siblings);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
printTable(decks.map((d) => ({
|
|
188
|
+
id: `#${d.id}`,
|
|
189
|
+
name: d.name,
|
|
190
|
+
parent: d.parentId !== null ? (nameById.get(d.parentId) ?? `#${d.parentId}`) : '—',
|
|
191
|
+
due: d.dueCount,
|
|
192
|
+
relevance: formatRelevance(d.relevance),
|
|
193
|
+
goal: formatGoal(d),
|
|
194
|
+
record: (() => {
|
|
195
|
+
const record = recordOf(d, childrenByParent);
|
|
196
|
+
return record === null ? '—' : formatRunSeconds(record);
|
|
197
|
+
})(),
|
|
198
|
+
})), [
|
|
199
|
+
{ key: 'id', label: 'ID' },
|
|
200
|
+
{ key: 'name', label: 'NOME' },
|
|
201
|
+
{ key: 'parent', label: 'BARALHO-PAI' },
|
|
202
|
+
{ key: 'due', label: 'A REVISAR' },
|
|
203
|
+
{ key: 'relevance', label: 'RELEVÂNCIA' },
|
|
204
|
+
{ key: 'goal', label: 'META' },
|
|
205
|
+
{ key: 'record', label: 'RECORDE' },
|
|
206
|
+
]);
|
|
207
|
+
process.stdout.write(`\n${decks.length} baralho(s).\n`);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
case 'create': {
|
|
211
|
+
const name = rest.join(' ').trim();
|
|
212
|
+
if (name === '') {
|
|
213
|
+
fail('informe o nome: memoring deck create "Direito Tributário" [--parent <id>].');
|
|
214
|
+
}
|
|
215
|
+
let parentId = null;
|
|
216
|
+
const parentRef = stringFlag(parsed.flags, 'parent');
|
|
217
|
+
if (parentRef !== undefined) {
|
|
218
|
+
const parent = resolveDeck(await listDecks(), parentRef);
|
|
219
|
+
if (!parent) {
|
|
220
|
+
fail(`baralho-pai "${parentRef}" não encontrado — veja os ids/nomes com \`memoring deck list\`.`);
|
|
221
|
+
}
|
|
222
|
+
parentId = parent.id;
|
|
223
|
+
}
|
|
224
|
+
const existing = resolveDeck(await listDecks(), name);
|
|
225
|
+
if (existing) {
|
|
226
|
+
if (parsed.flags.json) {
|
|
227
|
+
printJson({ deck: existing, alreadyExisted: true });
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
process.stdout.write(`Baralho "${existing.name}" já existe (#${existing.id}).\n`);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const deck = await createDeck(name, parentId);
|
|
234
|
+
if (parsed.flags.json) {
|
|
235
|
+
printJson({ deck, alreadyExisted: false });
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
process.stdout.write(green(`Baralho "${deck.name}" criado (#${deck.id}).\n`));
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
case 'update': {
|
|
242
|
+
const ref = rest.join(' ').trim();
|
|
243
|
+
if (ref === '') {
|
|
244
|
+
fail('informe o baralho: memoring deck update <id|nome> [--relevance 0-5] [--goal "txt"] ...');
|
|
245
|
+
}
|
|
246
|
+
const deck = resolveDeck(await listDecks(), ref);
|
|
247
|
+
if (!deck) {
|
|
248
|
+
fail(`baralho "${ref}" não encontrado — veja os ids/nomes com \`memoring deck list\`.`);
|
|
249
|
+
}
|
|
250
|
+
const body = metadataBody(parsed.flags);
|
|
251
|
+
let updated;
|
|
252
|
+
try {
|
|
253
|
+
updated = (await apiFetch(`/notes/decks/${deck.id}/metadata`, { method: 'PUT', body }));
|
|
254
|
+
}
|
|
255
|
+
catch (e) {
|
|
256
|
+
if (e instanceof ApiError) {
|
|
257
|
+
fail(`o servidor rejeitou a atualização do baralho "${deck.name}" (#${deck.id}) (${e.status}): ${e.body}`);
|
|
258
|
+
}
|
|
259
|
+
throw e;
|
|
260
|
+
}
|
|
261
|
+
if (parsed.flags.json) {
|
|
262
|
+
printJson(updated);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
printDeckSummary(updated);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
default:
|
|
269
|
+
process.stdout.write('Uso: memoring deck <list|create|update> [id|nome] [flags]\n' +
|
|
270
|
+
' list lista seus baralhos (id, nome, pai, a revisar, relevância, meta, recorde)\n' +
|
|
271
|
+
' create <nome> cria um baralho (idempotente: nome existente só retorna o atual)\n' +
|
|
272
|
+
' --parent <id|nome> cria como sub-baralho\n' +
|
|
273
|
+
' update <id|nome> define metadados do baralho\n' +
|
|
274
|
+
' --relevance 0-5 (1 casa) | --goal "txt" (<=160) | --goal-date AAAA-MM-DD\n' +
|
|
275
|
+
' --tone gray|orange|green|blue|purple | --clear-relevance --clear-goal\n' +
|
|
276
|
+
' --clear-goal-date --clear-tone (limpa o campo) | --json\n');
|
|
277
|
+
if (sub) {
|
|
278
|
+
process.exitCode = 1;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
package/dist/commands/inbox.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { apiFetch, ApiError } from '../api.js';
|
|
3
|
+
import { listDecks, resolveDeck } from './deck.js';
|
|
3
4
|
import { fail, green, printJson, printTable, stringFlag } from './shared.js';
|
|
4
5
|
async function listInbox() {
|
|
5
6
|
return (await apiFetch('/notes/review-inbox'));
|
|
@@ -40,6 +41,15 @@ async function updateInboxItem(parsed, id) {
|
|
|
40
41
|
}
|
|
41
42
|
const flags = parsed.flags;
|
|
42
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
|
+
}
|
|
43
53
|
const merged = {
|
|
44
54
|
type: overrides.type ?? stringFlag(flags, 'type') ?? current.type,
|
|
45
55
|
title: overrides.title ?? stringFlag(flags, 'title') ?? current.title,
|
|
@@ -50,7 +60,7 @@ async function updateInboxItem(parsed, id) {
|
|
|
50
60
|
subtopicName: overrides.subtopicName ?? stringFlag(flags, 'subtopic') ?? current.subtopicName ?? null,
|
|
51
61
|
tags: overrides.tags ?? (tagsFlag !== undefined ? parseTagsFlag(tagsFlag) : current.tags ?? []),
|
|
52
62
|
source: overrides.source ?? stringFlag(flags, 'source') ?? current.source ?? null,
|
|
53
|
-
deckId: overrides.deckId ?? current.deckId ?? null,
|
|
63
|
+
deckId: deckId ?? overrides.deckId ?? current.deckId ?? null,
|
|
54
64
|
question: overrides.question ?? current.question ?? null,
|
|
55
65
|
};
|
|
56
66
|
let updated;
|
package/dist/commands/stage.js
CHANGED
|
@@ -2,6 +2,7 @@ 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
7
|
const TYPES = ['NOTE', 'FLASHCARD', 'ONE_SHOT', 'QUESTION'];
|
|
7
8
|
// Retrocompat: itens.json antigos com "type": "NORMAL" são normalizados para "NOTE".
|
|
@@ -146,6 +147,7 @@ const SCHEMA_JSON = {
|
|
|
146
147
|
subjectName: 'string? — matéria (ex.: "Direito Constitucional")',
|
|
147
148
|
subtopicName: 'string? — subtópico',
|
|
148
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)',
|
|
149
151
|
question: 'objeto? — snapshot da questão (obrigatório quando type=QUESTION; contrato em questionContract)',
|
|
150
152
|
source: `string? — origem do material (default: "${DEFAULT_SOURCE}")`,
|
|
151
153
|
},
|
|
@@ -157,6 +159,7 @@ const SCHEMA_JSON = {
|
|
|
157
159
|
front: 'Quais são os quatro objetivos fundamentais da República (art. 3º da CF/88)?',
|
|
158
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.',
|
|
159
161
|
subjectName: 'Direito Constitucional',
|
|
162
|
+
deckName: 'Direito Constitucional',
|
|
160
163
|
tags: ['cf88', 'art-3'],
|
|
161
164
|
source: 'Apostila constitucional.pdf',
|
|
162
165
|
},
|
|
@@ -164,6 +167,7 @@ const SCHEMA_JSON = {
|
|
|
164
167
|
type: 'QUESTION',
|
|
165
168
|
title: 'IBAMA 2025 — conceito de tributo',
|
|
166
169
|
subjectName: 'Direito Tributário',
|
|
170
|
+
deckName: 'Direito Tributário',
|
|
167
171
|
tags: ['ctn', 'art-3'],
|
|
168
172
|
source: 'aula-00.pdf',
|
|
169
173
|
question: {
|
|
@@ -183,6 +187,13 @@ const SCHEMA_TEXT = `Contrato dos itens da caixa de revisão (POST /api/notes/re
|
|
|
183
187
|
content string — corpo da nota (markdown) (obrigatório quando type=NOTE)
|
|
184
188
|
front, back string — pergunta e resposta (obrigatório quando type=FLASHCARD|ONE_SHOT)
|
|
185
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.
|
|
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)
|
|
186
197
|
subjectName? string — matéria (ex.: "Direito Constitucional")
|
|
187
198
|
subtopicName? string — subtópico
|
|
188
199
|
tags? string[]
|
|
@@ -205,6 +216,90 @@ Dica: FLASHCARD entra na fila de revisão espaçada (FSRS); ONE_SHOT é pergunta
|
|
|
205
216
|
QUESTION treina verificação objetiva (aproveite questões prontas do material).
|
|
206
217
|
Limites por tipo: a preferência do usuário (abaixo, quando logado) vale por padrão;
|
|
207
218
|
--max-notes N | --max-flashcards N | --max-one-shots N | --max-questions N no create vencem por lote ("livre" = à vontade).`;
|
|
219
|
+
const NO_DECK_HINT = 'cartão não fica sem baralho: informe deckName no item ou --deck "<nome>" no lote. ' +
|
|
220
|
+
'Veja `memoring deck list` e reutilize um existente, ou crie com `memoring deck create "<nome>"`.';
|
|
221
|
+
/** Exige baralho em TODO o lote: deckName no item ou a flag --deck como padrão. */
|
|
222
|
+
function enforceDeck(items, flags) {
|
|
223
|
+
const batchDeck = stringFlag(flags, 'deck');
|
|
224
|
+
const index = items.findIndex((it) => !batchDeck && (typeof it.deckName !== 'string' || it.deckName.trim() === ''));
|
|
225
|
+
if (index >= 0) {
|
|
226
|
+
fail(`item #${index + 1} (${items[index].title}) está sem baralho — ${NO_DECK_HINT}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Resolve os baralhos do lote ANTES do POST: referências existentes são reutilizadas
|
|
231
|
+
* (id numérico ou nome exato case-insensitive) e nomes novos viram baralhos criados
|
|
232
|
+
* na hora — o agente nunca precisa sair do fluxo para criar estrutura.
|
|
233
|
+
*/
|
|
234
|
+
async function resolveBatchDecks(items, flags) {
|
|
235
|
+
const batchDeck = stringFlag(flags, 'deck')?.trim();
|
|
236
|
+
const refOf = (it) => typeof it.deckName === 'string' && it.deckName.trim() !== '' ? it.deckName.trim() : batchDeck;
|
|
237
|
+
const refs = new Set();
|
|
238
|
+
for (const it of items) {
|
|
239
|
+
const ref = refOf(it);
|
|
240
|
+
if (ref) {
|
|
241
|
+
refs.add(ref);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (refs.size === 0) {
|
|
245
|
+
return { deckIds: items.map(() => null), createdNames: [] };
|
|
246
|
+
}
|
|
247
|
+
const decks = await listDecks();
|
|
248
|
+
const idByRef = new Map();
|
|
249
|
+
const createdNames = [];
|
|
250
|
+
for (const ref of refs) {
|
|
251
|
+
const existing = resolveDeck(decks, ref);
|
|
252
|
+
if (existing) {
|
|
253
|
+
idByRef.set(ref, existing.id);
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (Number.isInteger(Number(ref)) && Number(ref) > 0) {
|
|
257
|
+
fail(`baralho #${ref} não existe — ${NO_DECK_HINT}`);
|
|
258
|
+
}
|
|
259
|
+
const deck = await createDeck(ref);
|
|
260
|
+
decks.push(deck);
|
|
261
|
+
idByRef.set(ref, deck.id);
|
|
262
|
+
createdNames.push(ref);
|
|
263
|
+
}
|
|
264
|
+
return { deckIds: items.map((it) => idByRef.get(refOf(it) ?? '') ?? null), createdNames };
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Atribui o baralho aos itens recém-estagiados (o POST da caixa de revisão não tem
|
|
268
|
+
* deckId — a atribuição é um PUT /notes/{id} por item, com o payload completo do
|
|
269
|
+
* item + deckId). Devolve os rótulos dos itens cuja atribuição falhou.
|
|
270
|
+
*/
|
|
271
|
+
async function assignDecks(items, created, deckIds) {
|
|
272
|
+
const failures = [];
|
|
273
|
+
for (let i = 0; i < created.length; i++) {
|
|
274
|
+
const deckId = deckIds[i];
|
|
275
|
+
if (deckId === null || deckId === undefined) {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
const item = items[i];
|
|
279
|
+
try {
|
|
280
|
+
await apiFetch(`/notes/${created[i].id}`, {
|
|
281
|
+
method: 'PUT',
|
|
282
|
+
body: {
|
|
283
|
+
type: item.type,
|
|
284
|
+
title: item.title,
|
|
285
|
+
content: item.content ?? null,
|
|
286
|
+
front: item.front ?? null,
|
|
287
|
+
back: item.back ?? null,
|
|
288
|
+
subjectName: item.subjectName ?? null,
|
|
289
|
+
subtopicName: item.subtopicName ?? null,
|
|
290
|
+
tags: item.tags ?? [],
|
|
291
|
+
source: item.source ?? null,
|
|
292
|
+
deckId,
|
|
293
|
+
question: item.question ?? null,
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
failures.push(`#${created[i].id} (${item.title})`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return failures;
|
|
302
|
+
}
|
|
208
303
|
async function readStdin() {
|
|
209
304
|
const chunks = [];
|
|
210
305
|
for await (const chunk of process.stdin) {
|
|
@@ -435,18 +530,24 @@ export async function stageCommand(parsed) {
|
|
|
435
530
|
fail(`JSON inválido: ${e.message}`);
|
|
436
531
|
}
|
|
437
532
|
const items = validateItems(parsedJson);
|
|
533
|
+
enforceDeck(items, parsed.flags);
|
|
438
534
|
enforceTypeCaps(items, parsed.flags, await fetchStoredCaps());
|
|
535
|
+
const batchDecks = await resolveBatchDecks(items, parsed.flags);
|
|
439
536
|
if (parsed.flags.replace) {
|
|
440
537
|
const removed = await emptyInbox();
|
|
441
538
|
if (!parsed.flags.json) {
|
|
442
539
|
process.stdout.write(dim(`${removed} item(ns) pendente(s) removido(s) da caixa de revisão.\n`));
|
|
443
540
|
}
|
|
444
541
|
}
|
|
542
|
+
if (batchDecks.createdNames.length > 0 && !parsed.flags.json) {
|
|
543
|
+
process.stdout.write(dim(`Baralho(s) criado(s): ${batchDecks.createdNames.join(', ')}.\n`));
|
|
544
|
+
}
|
|
445
545
|
let created;
|
|
446
546
|
try {
|
|
447
547
|
const res = (await apiFetch('/notes/review-inbox', {
|
|
448
548
|
method: 'POST',
|
|
449
|
-
|
|
549
|
+
// deckName não faz parte do contrato do POST — o baralho é atribuído via PUT abaixo
|
|
550
|
+
body: { items: items.map(({ deckName, ...rest }) => (void deckName, rest)) },
|
|
450
551
|
requestId: batchRequestId(raw),
|
|
451
552
|
}));
|
|
452
553
|
created = res.created;
|
|
@@ -468,8 +569,20 @@ export async function stageCommand(parsed) {
|
|
|
468
569
|
}
|
|
469
570
|
throw e;
|
|
470
571
|
}
|
|
572
|
+
const deckFailures = await assignDecks(items, created, batchDecks.deckIds);
|
|
573
|
+
if (deckFailures.length > 0) {
|
|
574
|
+
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`));
|
|
575
|
+
}
|
|
471
576
|
if (parsed.flags.json) {
|
|
472
|
-
printJson({
|
|
577
|
+
printJson({
|
|
578
|
+
created,
|
|
579
|
+
ids: created.map((note) => note.id),
|
|
580
|
+
decks: {
|
|
581
|
+
created: batchDecks.createdNames,
|
|
582
|
+
assigned: created.length - deckFailures.length - batchDecks.deckIds.filter((id) => id === null).length,
|
|
583
|
+
failed: deckFailures,
|
|
584
|
+
},
|
|
585
|
+
});
|
|
473
586
|
return;
|
|
474
587
|
}
|
|
475
588
|
process.stdout.write(green(`OK — ${created.length} item(ns) aguardando revisão em /cards/inbox:\n`));
|
|
@@ -487,6 +600,8 @@ export async function stageCommand(parsed) {
|
|
|
487
600
|
' --max-one-shots N | --max-questions N; 0 = não gerar o tipo)\n' +
|
|
488
601
|
' create envia o lote para a caixa de revisão (arquivo ou stdin)\n' +
|
|
489
602
|
' tipos: NOTE | FLASHCARD | ONE_SHOT | QUESTION\n' +
|
|
603
|
+
' baralho OBRIGATÓRIO: deckName por item ou --deck "<nome|id>" no lote\n' +
|
|
604
|
+
' (nomes inexistentes são criados; veja `memoring deck list`)\n' +
|
|
490
605
|
' --replace esvazia a caixa antes de enviar\n' +
|
|
491
606
|
' --max-notes N | --max-flashcards N | --max-one-shots N | --max-questions N\n' +
|
|
492
607
|
' vencem a preferência gravada por lote (validação local)\n');
|
package/dist/main.js
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
import { ApiError } from './api.js';
|
|
3
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
10
|
import { parseArgs as parse } from './commands/shared.js';
|
|
10
11
|
import { newerVersionAvailable, scheduleUpdateCheck } from './update.js';
|
|
11
|
-
const VERSION = '0.
|
|
12
|
+
const VERSION = '0.10.0';
|
|
12
13
|
const HELP = `Memoring CLI — flashcards e notas por IA, direto do terminal.
|
|
13
14
|
|
|
14
15
|
Uso: memoring <comando> [subcomando] [flags]
|
|
@@ -25,7 +26,12 @@ Comandos:
|
|
|
25
26
|
inbox accept <id>|accept-all aceita itens (entram nas listagens e na fila FSRS)
|
|
26
27
|
inbox discard <id> descarta um item pendente
|
|
27
28
|
inbox discard-all descarta todos os itens pendentes
|
|
28
|
-
inbox update <id> edita um item pendente (--file parcial e/ou flags
|
|
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, 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/--goal-date/--tone;
|
|
34
|
+
--clear-* limpa um campo)
|
|
29
35
|
update atualiza a CLI para a versão mais recente (via npm)
|
|
30
36
|
skill guia completo da CLI para agentes de IA (imprime o SKILL.md)
|
|
31
37
|
version versão da CLI
|
|
@@ -88,6 +94,9 @@ async function main() {
|
|
|
88
94
|
case 'inbox':
|
|
89
95
|
await inboxCommand(sub);
|
|
90
96
|
return;
|
|
97
|
+
case 'deck':
|
|
98
|
+
await deckCommand(sub);
|
|
99
|
+
return;
|
|
91
100
|
case 'update':
|
|
92
101
|
await updateCommand(VERSION);
|
|
93
102
|
return;
|
package/package.json
CHANGED
|
@@ -20,7 +20,10 @@ O Memoring é um app de estudos com revisão espaçada (FSRS). A CLI `memoring`
|
|
|
20
20
|
| `memoring auth login\|logout\|status` | sessão (device flow — sem senha no terminal) |
|
|
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
|
-
| `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
|
+
| `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, a revisar, relevância, meta, recorde) — consulte ANTES de gerar |
|
|
25
|
+
| `memoring deck create <nome> [--parent id]` | cria baralho (idempotente) |
|
|
26
|
+
| `memoring deck update <id\|nome> [--relevance <0-5>] [--goal "<txt>"] [--goal-date AAAA-MM-DD] [--tone <gray\|orange\|green\|blue\|purple>] [--clear-*]` | define metadados do baralho (relevância, meta, data-alvo, tom de capa; `--clear-*` limpa o campo) |
|
|
24
27
|
| `memoring inbox list` | itens pendentes de revisão |
|
|
25
28
|
| `memoring inbox accept <id>\|accept-all` | aceita itens (entram nas listagens e na fila FSRS) |
|
|
26
29
|
| `memoring inbox discard <id>\|discard-all` | descarta item(ns) pendente(s) |
|
|
@@ -35,15 +38,17 @@ Flags globais: `--json` (saída legível por máquina), `--no-color`, `--api-bas
|
|
|
35
38
|
1. **Entenda o pedido** — qual pasta/arquivo/material vira flashcards/notas e se o usuário pediu quantidades.
|
|
36
39
|
2. **Confira a sessão**: `memoring auth status --json`. Sem `authenticated: true`, PARE e peça o login (ver Pré-requisitos).
|
|
37
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.
|
|
38
|
-
4. **
|
|
39
|
-
5. **
|
|
40
|
-
6. **
|
|
41
|
-
7. **
|
|
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.
|
|
42
|
+
5. **Leia os materiais** e gere os itens no contrato, num arquivo JSON (array ou `{ "items": [...] }`).
|
|
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.
|
|
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.
|
|
45
|
+
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`.
|
|
42
46
|
|
|
43
47
|
## O contrato (a fonte da verdade é `stage schema`)
|
|
44
48
|
|
|
45
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).
|
|
46
50
|
- `title` sempre obrigatório; lote de 1 a 50 itens, all-or-nothing — qualquer item inválido rejeita o lote inteiro.
|
|
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).
|
|
47
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`.
|
|
48
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.
|
|
49
54
|
- Preencha `source` com a origem real do material (nome do arquivo/pasta) — não use o default quando souber a origem.
|