@memoring/cli 0.12.1 → 0.15.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 +163 -8
- package/dist/api.js +90 -4
- package/dist/args.js +14 -1
- package/dist/auth.js +1 -1
- package/dist/commands/batch.js +327 -0
- package/dist/commands/inbox.js +3 -1
- package/dist/commands/materialReferences.js +359 -0
- package/dist/commands/plan-analyze.js +256 -0
- package/dist/commands/plan-diagnostics.js +65 -0
- package/dist/commands/plan-exams.js +122 -0
- package/dist/commands/plan-goal.js +131 -0
- package/dist/commands/plan-shared.js +133 -0
- package/dist/commands/plan.js +219 -0
- package/dist/commands/planDrafts.js +162 -0
- package/dist/commands/scratch.js +632 -0
- package/dist/commands/stage.js +274 -73
- package/dist/config.js +2 -0
- package/dist/main.js +43 -2
- package/dist/pdf/batchValidation.js +72 -0
- package/dist/pdf/documentCache.js +68 -0
- package/dist/pdf/quoteLocate.js +181 -0
- package/package.json +8 -2
- package/skills/memoring-cli/SKILL.md +58 -7
package/dist/commands/stage.js
CHANGED
|
@@ -2,14 +2,27 @@ 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 { validateBatchReferences } from '../pdf/batchValidation.js';
|
|
6
|
+
import { PdfDocumentCache } from '../pdf/documentCache.js';
|
|
5
7
|
import { createDeck, listDecks, resolveDeck } from './deck.js';
|
|
8
|
+
import { MATERIAL_REFERENCE_CONTRACT, toWireMaterialReference, validateMaterialReferences, } from './materialReferences.js';
|
|
6
9
|
import { dim, fail, green, printJson, stringFlag, yellow } from './shared.js';
|
|
7
|
-
const TYPES = ['NOTE', '
|
|
8
|
-
// Retrocompat: itens.json antigos com "type": "
|
|
9
|
-
|
|
10
|
+
const TYPES = ['NOTE', 'NORMAL', 'ONE_SHOT', 'QUESTION'];
|
|
11
|
+
// Retrocompat: itens.json antigos com "type": "FLASHCARD" são normalizados para "NORMAL"
|
|
12
|
+
// (rename da espécie V238 — o backend também aceita o token legado na entrada).
|
|
13
|
+
const LEGACY_TYPE_ALIASES = { FLASHCARD: 'NORMAL' };
|
|
10
14
|
const MAX_ITEMS = 50;
|
|
11
15
|
const DEFAULT_SOURCE = 'Importação assistida (CLI)';
|
|
12
16
|
const QUESTION_KINDS = ['MULTIPLA_ESCOLHA', 'CERTO_ERRADO'];
|
|
17
|
+
/** MaterialReferenceInput e validação de referências vivem em materialReferences.ts
|
|
18
|
+
* (fronteira única usada pelo stage e pelo `memoring reference`). */
|
|
19
|
+
/**
|
|
20
|
+
* Slug minúsculo (letras, números, hífens, até 80 chars) que identifica o conceito
|
|
21
|
+
* do item. Segmentos alfanuméricos separados por hífen único — sem hífen no
|
|
22
|
+
* início/fim nem duplo, espelhando a validação do backend (`assignConceptKey`).
|
|
23
|
+
*/
|
|
24
|
+
const CONCEPT_KEY_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
25
|
+
const CONCEPT_KEY_MAX = 80;
|
|
13
26
|
const UNLIMITED = -1;
|
|
14
27
|
/**
|
|
15
28
|
* GET /notes/import/agent-guidance, fail-open como o fetchStoredCaps: sem sessão/rede
|
|
@@ -66,13 +79,13 @@ async function fetchStoredCaps() {
|
|
|
66
79
|
/** Flags --max-<tipo>: no `stage create` limitam o lote; no `stage caps` gravam a preferência. */
|
|
67
80
|
const MAX_FLAG_BY_TYPE = {
|
|
68
81
|
NOTE: 'max-notes',
|
|
69
|
-
|
|
82
|
+
NORMAL: 'max-flashcards',
|
|
70
83
|
ONE_SHOT: 'max-one-shots',
|
|
71
84
|
QUESTION: 'max-questions',
|
|
72
85
|
};
|
|
73
86
|
const TYPE_LABEL = {
|
|
74
87
|
NOTE: 'notas',
|
|
75
|
-
|
|
88
|
+
NORMAL: 'flashcards',
|
|
76
89
|
ONE_SHOT: '1-shots',
|
|
77
90
|
QUESTION: 'questões',
|
|
78
91
|
};
|
|
@@ -92,7 +105,7 @@ function enforceTypeCaps(items, flags, stored) {
|
|
|
92
105
|
const storedByType = stored
|
|
93
106
|
? {
|
|
94
107
|
NOTE: stored.maxNotes,
|
|
95
|
-
|
|
108
|
+
NORMAL: stored.maxFlashcards,
|
|
96
109
|
ONE_SHOT: stored.maxOneShots,
|
|
97
110
|
QUESTION: stored.maxQuestions,
|
|
98
111
|
}
|
|
@@ -139,29 +152,42 @@ const QUESTION_CONTRACT = {
|
|
|
139
152
|
const SCHEMA_JSON = {
|
|
140
153
|
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.',
|
|
141
154
|
item: {
|
|
142
|
-
type: 'NOTE |
|
|
155
|
+
type: 'NOTE | NORMAL | ONE_SHOT | QUESTION (obrigatório; FLASHCARD legado é aceito e mapeado para NORMAL)',
|
|
143
156
|
title: 'string (obrigatório)',
|
|
144
157
|
content: 'string — corpo da nota em markdown (obrigatório quando type=NOTE)',
|
|
145
|
-
front: 'string — pergunta (obrigatório quando type=
|
|
146
|
-
back: 'string — resposta (obrigatório quando type=
|
|
158
|
+
front: 'string — pergunta (obrigatório quando type=NORMAL|ONE_SHOT)',
|
|
159
|
+
back: 'string — resposta (obrigatório quando type=NORMAL|ONE_SHOT)',
|
|
147
160
|
subjectName: 'string? — matéria (ex.: "Direito Constitucional")',
|
|
148
161
|
subtopicName: 'string? — subtópico',
|
|
149
162
|
tags: 'string[]?',
|
|
163
|
+
conceptKey: `string? — conceito do item em slug minúsculo (letras/números/hífens, até ${CONCEPT_KEY_MAX} chars). Opcional por compatibilidade, mas FORTEMENTE RECOMENDADO em NORMAL/ONE_SHOT/QUESTION: cartões sobre o MESMO conceito compartilham a chave (ex.: "trib-art-4" num flashcard E numa questão do art. 4º do CTN — o whiteboard do app agrupa por conceito) e conceitos DIFERENTES usam chaves diferentes — nunca reuse a chave para outro conceito`,
|
|
150
164
|
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 como PENDING — oculto no app até o 1º item aceite, que ativa o baralho e os ancestrais pendentes. A flag --deck do create vale como padrão do lote)',
|
|
151
165
|
question: 'objeto? — snapshot da questão (obrigatório quando type=QUESTION; contrato em questionContract)',
|
|
152
166
|
source: `string? — origem do material (default: "${DEFAULT_SOURCE}")`,
|
|
167
|
+
materialReferences: 'array? — referências ao material de origem (contrato em materialReferenceContract): vinculam o item à página do PDF no app — ao aceitar, o link abre o material NA PÁGINA e destaca o trecho exato (quote para PDF com texto, region para escaneado). Não confunda com source (texto livre) — use os dois juntos',
|
|
153
168
|
},
|
|
154
169
|
questionContract: QUESTION_CONTRACT,
|
|
170
|
+
materialReferenceContract: MATERIAL_REFERENCE_CONTRACT,
|
|
155
171
|
example: [
|
|
156
172
|
{
|
|
157
|
-
type: '
|
|
173
|
+
type: 'NORMAL',
|
|
158
174
|
title: 'CF/88 — objetivos fundamentais',
|
|
159
175
|
front: 'Quais são os quatro objetivos fundamentais da República (art. 3º da CF/88)?',
|
|
160
176
|
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.',
|
|
161
177
|
subjectName: 'Direito Constitucional',
|
|
162
178
|
deckName: 'Direito Constitucional',
|
|
163
179
|
tags: ['cf88', 'art-3'],
|
|
180
|
+
conceptKey: 'const-cf88-art-3',
|
|
164
181
|
source: 'Apostila constitucional.pdf',
|
|
182
|
+
materialReferences: [
|
|
183
|
+
{
|
|
184
|
+
driveFileId: '1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms',
|
|
185
|
+
page: 35,
|
|
186
|
+
excerpt: 'Objetivos fundamentais (art. 3º, CF/88).',
|
|
187
|
+
quote: 'Art. 3º — Constituem objetivos fundamentais da República Federativa do Brasil construir uma sociedade livre, justa e solidária [...].',
|
|
188
|
+
contextBefore: 'CAPÍTULO I — DOS PRINCÍPIOS FUNDAMENTAIS',
|
|
189
|
+
},
|
|
190
|
+
],
|
|
165
191
|
},
|
|
166
192
|
{
|
|
167
193
|
type: 'QUESTION',
|
|
@@ -169,6 +195,7 @@ const SCHEMA_JSON = {
|
|
|
169
195
|
subjectName: 'Direito Tributário',
|
|
170
196
|
deckName: 'Direito Tributário',
|
|
171
197
|
tags: ['ctn', 'art-3'],
|
|
198
|
+
conceptKey: 'trib-ctn-art-3',
|
|
172
199
|
source: 'aula-00.pdf',
|
|
173
200
|
question: {
|
|
174
201
|
kind: 'CERTO_ERRADO',
|
|
@@ -177,15 +204,41 @@ const SCHEMA_JSON = {
|
|
|
177
204
|
commentHtml: 'É a definição do art. 3º do CTN (Lei 5.172/1966).',
|
|
178
205
|
},
|
|
179
206
|
},
|
|
207
|
+
{
|
|
208
|
+
type: 'NORMAL',
|
|
209
|
+
title: 'CTN art. 4º — territorialidade',
|
|
210
|
+
front: 'Qual a regra de incidência dos tributos quanto à aplicação da lei (art. 4º, caput, do CTN)?',
|
|
211
|
+
back: 'Regra geral: a lei que institui o tributo aplica-se a todos os fatos geradores ocorridos no território nacional.',
|
|
212
|
+
subjectName: 'Direito Tributário',
|
|
213
|
+
deckName: 'Direito Tributário',
|
|
214
|
+
tags: ['ctn', 'art-4'],
|
|
215
|
+
conceptKey: 'trib-art-4',
|
|
216
|
+
source: 'aula-00.pdf',
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
type: 'QUESTION',
|
|
220
|
+
title: 'CTN art. 4º — exceção à territorialidade',
|
|
221
|
+
subjectName: 'Direito Tributário',
|
|
222
|
+
deckName: 'Direito Tributário',
|
|
223
|
+
tags: ['ctn', 'art-4'],
|
|
224
|
+
conceptKey: 'trib-art-4',
|
|
225
|
+
source: 'aula-00.pdf',
|
|
226
|
+
question: {
|
|
227
|
+
kind: 'CERTO_ERRADO',
|
|
228
|
+
statementHtml: 'Nos termos do art. 4º do CTN, a lei tributária brasileira NUNCA se aplica a fatos geradores ocorridos no exterior, ainda que o fato tenha produzido efeitos no Brasil.',
|
|
229
|
+
correctAlternative: 2,
|
|
230
|
+
commentHtml: 'Parágrafo único do art. 4º: a lei brasileira pode reger fatos ocorridos no exterior quando a eles se refiram dispositivos expressos da Constituição.',
|
|
231
|
+
},
|
|
232
|
+
},
|
|
180
233
|
],
|
|
181
234
|
typeCaps: 'preferência por usuário (`memoring stage caps`, vale para app e CLI) — limites são TETOS, nunca cotas: 50 é só o limite técnico por requisição, não a meta do lote; userCaps -1 = sem teto preferencial (não é incentivo a inflar), 0 = não gerar o tipo. Sem quantidade explícita no pedido, selecione só pontos realmente cobráveis, com cobertura seletiva e carga de revisão sustentável. 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.',
|
|
182
235
|
};
|
|
183
236
|
const SCHEMA_TEXT = `Contrato dos itens da caixa de revisão (POST /api/notes/review-inbox):
|
|
184
237
|
|
|
185
|
-
type NOTE |
|
|
238
|
+
type NOTE | NORMAL | ONE_SHOT | QUESTION (obrigatório; FLASHCARD legado é mapeado para NORMAL)
|
|
186
239
|
title string (obrigatório)
|
|
187
240
|
content string — corpo da nota (markdown) (obrigatório quando type=NOTE)
|
|
188
|
-
front, back string — pergunta e resposta (obrigatório quando type=
|
|
241
|
+
front, back string — pergunta e resposta (obrigatório quando type=NORMAL|ONE_SHOT)
|
|
189
242
|
question objeto — snapshot da questão (obrigatório quando type=QUESTION)
|
|
190
243
|
deckName string — baralho de destino (OBRIGATÓRIO — cartão não fica sem baralho:
|
|
191
244
|
veja \`memoring deck list\`, reutilize ou crie
|
|
@@ -199,7 +252,39 @@ const SCHEMA_TEXT = `Contrato dos itens da caixa de revisão (POST /api/notes/re
|
|
|
199
252
|
subjectName? string — matéria (ex.: "Direito Constitucional")
|
|
200
253
|
subtopicName? string — subtópico
|
|
201
254
|
tags? string[]
|
|
255
|
+
conceptKey? string — conceito do item em slug minúsculo (letras, números
|
|
256
|
+
e hífens, até 80 chars). Opcional por compatibilidade, mas
|
|
257
|
+
FORTEMENTE RECOMENDADO em NORMAL/ONE_SHOT/QUESTION: cartões
|
|
258
|
+
sobre o MESMO conceito compartilham a MESMA chave — ex.:
|
|
259
|
+
"trib-art-4" num flashcard E numa questão do art. 4º do CTN
|
|
260
|
+
(o whiteboard do app agrupa por conceito, não por cartão) —
|
|
261
|
+
e conceitos DIFERENTES usam chaves diferentes: nunca reuse
|
|
262
|
+
uma chave para outro conceito
|
|
202
263
|
source? string — origem do material (default: "${DEFAULT_SOURCE}")
|
|
264
|
+
materialReferences? array — referências ao material de origem (opcional):
|
|
265
|
+
vinculam o item à página do PDF no app; ao aceitar, o link abre o material
|
|
266
|
+
NA PÁGINA e destaca o trecho exato. Não confunda com source (texto livre) —
|
|
267
|
+
use os dois juntos.
|
|
268
|
+
[ { driveFileId, page, excerpt?, quote?, contextBefore?, contextAfter?, region? } ]
|
|
269
|
+
driveFileId string (obrigatório) — id do arquivo no Drive
|
|
270
|
+
(\`memoring scratch list --json\`); trim, não vazio, <= 255
|
|
271
|
+
page int >= 1 (obrigatório) — posição 1-based no documento
|
|
272
|
+
(mesma numeração de \`scratch extract --pages\`); o POST
|
|
273
|
+
converte para pageIndex 0-based — o campo page nunca vai no fio
|
|
274
|
+
excerpt? string — dica CURTA do trecho que originou o item (trim,
|
|
275
|
+
<= 500; vazio = omitido). É SÓ um rótulo humano: NÃO gera
|
|
276
|
+
destaque no PDF
|
|
277
|
+
quote? string — trecho LITERAL do material (trim, <= 1000; vazio =
|
|
278
|
+
omitido). RECOMENDADO para PDF com texto: o app localiza e
|
|
279
|
+
destaca esse trecho na página
|
|
280
|
+
contextBefore?/contextAfter? string — texto colado antes/depois do quote no
|
|
281
|
+
material (trim, <= 500 cada) — desempata ocorrências repetidas
|
|
282
|
+
region? objeto { x, y, w, h } — região visual da página, normalizada
|
|
283
|
+
0..1 com origem no topo-esquerdo (x,y) e tamanho (w,h); números
|
|
284
|
+
FINITOS, w/h > 0, contida no quadrado 0..1 (x+w <= 1, y+h <= 1).
|
|
285
|
+
Para PDF ESCANEADO/sem texto, use a região em vez de quote.
|
|
286
|
+
PDF com texto → quote literal; escaneado → região; excerpt é
|
|
287
|
+
só a dica curta — sozinho ele NÃO destaca nada
|
|
203
288
|
|
|
204
289
|
Snapshot de QUESTION (questões manuais — sem identidade TEC):
|
|
205
290
|
kind MULTIPLA_ESCOLHA | CERTO_ERRADO (obrigatório)
|
|
@@ -213,7 +298,7 @@ Snapshot de QUESTION (questões manuais — sem identidade TEC):
|
|
|
213
298
|
Lote: 1 a ${MAX_ITEMS} itens, all-or-nothing — qualquer item inválido rejeita o lote inteiro.
|
|
214
299
|
Os itens nascem PENDING_REVIEW: o usuário aceita, edita ou recusa em /cards/inbox
|
|
215
300
|
(agentes podem editar pendentes com \`memoring inbox update <id>\`).
|
|
216
|
-
Dica:
|
|
301
|
+
Dica: NORMAL entra na fila de revisão espaçada (FSRS); ONE_SHOT é pergunta de resposta única
|
|
217
302
|
(termo curto — ideal para princípios, leis, datas, prazos, siglas, termos em latim);
|
|
218
303
|
QUESTION treina verificação objetiva (aproveite questões prontas do material).
|
|
219
304
|
Limites por tipo: a preferência do usuário (abaixo, quando logado) vale por padrão;
|
|
@@ -268,43 +353,6 @@ async function resolveBatchDecks(items, flags) {
|
|
|
268
353
|
}
|
|
269
354
|
return { deckIds: items.map((it) => idByRef.get(refOf(it) ?? '') ?? null), createdNames };
|
|
270
355
|
}
|
|
271
|
-
/**
|
|
272
|
-
* Atribui o baralho aos itens recém-estagiados (o POST da caixa de revisão não tem
|
|
273
|
-
* deckId — a atribuição é um PUT /notes/{id} por item, com o payload completo do
|
|
274
|
-
* item + deckId). Devolve os rótulos dos itens cuja atribuição falhou.
|
|
275
|
-
*/
|
|
276
|
-
async function assignDecks(items, created, deckIds) {
|
|
277
|
-
const failures = [];
|
|
278
|
-
for (let i = 0; i < created.length; i++) {
|
|
279
|
-
const deckId = deckIds[i];
|
|
280
|
-
if (deckId === null || deckId === undefined) {
|
|
281
|
-
continue;
|
|
282
|
-
}
|
|
283
|
-
const item = items[i];
|
|
284
|
-
try {
|
|
285
|
-
await apiFetch(`/notes/${created[i].id}`, {
|
|
286
|
-
method: 'PUT',
|
|
287
|
-
body: {
|
|
288
|
-
type: item.type,
|
|
289
|
-
title: item.title,
|
|
290
|
-
content: item.content ?? null,
|
|
291
|
-
front: item.front ?? null,
|
|
292
|
-
back: item.back ?? null,
|
|
293
|
-
subjectName: item.subjectName ?? null,
|
|
294
|
-
subtopicName: item.subtopicName ?? null,
|
|
295
|
-
tags: item.tags ?? [],
|
|
296
|
-
source: item.source ?? null,
|
|
297
|
-
deckId,
|
|
298
|
-
question: item.question ?? null,
|
|
299
|
-
},
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
catch {
|
|
303
|
-
failures.push(`#${created[i].id} (${item.title})`);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
return failures;
|
|
307
|
-
}
|
|
308
356
|
async function readStdin() {
|
|
309
357
|
const chunks = [];
|
|
310
358
|
for await (const chunk of process.stdin) {
|
|
@@ -374,6 +422,10 @@ function validateQuestion(raw, where) {
|
|
|
374
422
|
...subjects,
|
|
375
423
|
};
|
|
376
424
|
}
|
|
425
|
+
/**
|
|
426
|
+
* Validação das referências de material vive em materialReferences.ts
|
|
427
|
+
* (validateMaterialReferences — fronteira única com o `memoring reference`).
|
|
428
|
+
*/
|
|
377
429
|
function validateItems(raw) {
|
|
378
430
|
const items = Array.isArray(raw) ? raw : raw?.items;
|
|
379
431
|
if (!Array.isArray(items) || items.length === 0) {
|
|
@@ -400,7 +452,7 @@ function validateItems(raw) {
|
|
|
400
452
|
if (it.type === 'NOTE' && (typeof it.content !== 'string' || it.content.trim() === '')) {
|
|
401
453
|
fail(`${where} (${it.title}): type=NOTE exige content.`);
|
|
402
454
|
}
|
|
403
|
-
if ((it.type === '
|
|
455
|
+
if ((it.type === 'NORMAL' || it.type === 'ONE_SHOT') &&
|
|
404
456
|
(typeof it.front !== 'string' || it.front.trim() === '' || typeof it.back !== 'string' || it.back.trim() === '')) {
|
|
405
457
|
fail(`${where} (${it.title}): type=${it.type} exige front e back.`);
|
|
406
458
|
}
|
|
@@ -410,16 +462,81 @@ function validateItems(raw) {
|
|
|
410
462
|
if (it.tags !== undefined && (!Array.isArray(it.tags) || it.tags.some((t) => typeof t !== 'string'))) {
|
|
411
463
|
fail(`${where} (${it.title}): tags deve ser um array de strings.`);
|
|
412
464
|
}
|
|
413
|
-
|
|
465
|
+
let conceptKey = null;
|
|
466
|
+
if (it.conceptKey !== undefined && it.conceptKey !== null) {
|
|
467
|
+
if (typeof it.conceptKey !== 'string') {
|
|
468
|
+
fail(`${where} (${it.title}): conceptKey deve ser uma string (slug minúsculo) ou null.`);
|
|
469
|
+
}
|
|
470
|
+
const key = it.conceptKey.trim();
|
|
471
|
+
if (key !== '') {
|
|
472
|
+
if (key.length > CONCEPT_KEY_MAX || !CONCEPT_KEY_PATTERN.test(key)) {
|
|
473
|
+
fail(`${where} (${it.title}): conceptKey "${key}" inválido — use slug minúsculo com letras, ` +
|
|
474
|
+
`números e hífens, até ${CONCEPT_KEY_MAX} chars (ex.: "trib-art-4"). Mesmo conceito, mesma chave; conceito diferente, chave diferente.`);
|
|
475
|
+
}
|
|
476
|
+
conceptKey = key;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
const materialReferences = validateMaterialReferences(it.materialReferences, `${where} (${it.title})`);
|
|
480
|
+
const stageItem = {
|
|
481
|
+
...it,
|
|
482
|
+
conceptKey,
|
|
483
|
+
source: typeof it.source === 'string' && it.source.trim() !== '' ? it.source : DEFAULT_SOURCE,
|
|
484
|
+
};
|
|
485
|
+
if (materialReferences) {
|
|
486
|
+
stageItem.materialReferences = materialReferences;
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
delete stageItem.materialReferences;
|
|
490
|
+
}
|
|
491
|
+
return stageItem;
|
|
414
492
|
});
|
|
415
493
|
}
|
|
494
|
+
/** Item no formato do POST /notes/review-inbox, com baralho e referências na mesma transação. */
|
|
495
|
+
function toReviewInboxItem({ deckName, materialReferences, ...rest }, deckId) {
|
|
496
|
+
void deckName;
|
|
497
|
+
return {
|
|
498
|
+
...rest,
|
|
499
|
+
deckId,
|
|
500
|
+
...(materialReferences && materialReferences.length > 0
|
|
501
|
+
? { materialReferences: materialReferences.map(toWireMaterialReference) }
|
|
502
|
+
: {}),
|
|
503
|
+
};
|
|
504
|
+
}
|
|
416
505
|
/**
|
|
417
506
|
* Idempotência do lote: UUID derivado do conteúdo bruto. Re-rodar o mesmo arquivo
|
|
418
507
|
* após uma falha ambígua (timeout pós-commit) reenvia o mesmo X-Client-Request-Id —
|
|
419
508
|
* o servidor devolve o replay deduplicado em vez de duplicar os itens.
|
|
420
509
|
*/
|
|
421
|
-
function
|
|
422
|
-
const
|
|
510
|
+
function readGeneration(flags) {
|
|
511
|
+
const file = stringFlag(flags, 'generation-file');
|
|
512
|
+
if (!file) {
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
const raw = readFileSync(file, 'utf8');
|
|
516
|
+
let value;
|
|
517
|
+
try {
|
|
518
|
+
value = JSON.parse(raw);
|
|
519
|
+
}
|
|
520
|
+
catch (error) {
|
|
521
|
+
fail(`generation-file inválido: ${error.message}`);
|
|
522
|
+
}
|
|
523
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
524
|
+
fail('generation-file deve conter um objeto JSON com provider e requestedModel.');
|
|
525
|
+
}
|
|
526
|
+
const generation = value;
|
|
527
|
+
for (const field of ['provider', 'requestedModel']) {
|
|
528
|
+
if (typeof generation[field] !== 'string' || generation[field].trim() === '') {
|
|
529
|
+
fail(`generation-file: ${field} é obrigatório e não pode ser vazio.`);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return { value: generation, raw };
|
|
533
|
+
}
|
|
534
|
+
function batchRequestId(raw, generationRaw = '') {
|
|
535
|
+
const hash = createHash('sha256').update(raw);
|
|
536
|
+
if (generationRaw) {
|
|
537
|
+
hash.update('\0').update(generationRaw);
|
|
538
|
+
}
|
|
539
|
+
const digest = hash.digest();
|
|
423
540
|
digest[6] = (digest[6] & 0x0f) | 0x80;
|
|
424
541
|
digest[8] = (digest[8] & 0x3f) | 0x80;
|
|
425
542
|
const hex = digest.subarray(0, 16).toString('hex');
|
|
@@ -437,6 +554,61 @@ const NO_SESSION_WARNING = 'SEM SESSÃO: userCaps, qualityRules e customInstruct
|
|
|
437
554
|
export async function stageCommand(parsed) {
|
|
438
555
|
const [sub] = parsed._;
|
|
439
556
|
switch (sub) {
|
|
557
|
+
case 'validate': {
|
|
558
|
+
const file = stringFlag(parsed.flags, 'file');
|
|
559
|
+
const pdfMapFile = stringFlag(parsed.flags, 'pdf-map');
|
|
560
|
+
if (!file || file === '-' || !pdfMapFile) {
|
|
561
|
+
fail('informe --file lote.json e --pdf-map pdf-map.json (a validação não envia nada ao servidor).');
|
|
562
|
+
}
|
|
563
|
+
let items;
|
|
564
|
+
let pdfMap;
|
|
565
|
+
try {
|
|
566
|
+
items = validateItems(JSON.parse(readFileSync(file, 'utf8')));
|
|
567
|
+
const rawMap = JSON.parse(readFileSync(pdfMapFile, 'utf8'));
|
|
568
|
+
if (typeof rawMap !== 'object' || rawMap === null || Array.isArray(rawMap)) {
|
|
569
|
+
throw new Error('esperado objeto driveFileId → caminho');
|
|
570
|
+
}
|
|
571
|
+
pdfMap = Object.fromEntries(Object.entries(rawMap).map(([key, value]) => {
|
|
572
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
573
|
+
throw new Error(`caminho inválido para ${key}`);
|
|
574
|
+
}
|
|
575
|
+
return [key, value];
|
|
576
|
+
}));
|
|
577
|
+
}
|
|
578
|
+
catch (error) {
|
|
579
|
+
if (error instanceof Error && error.name === 'CliError') {
|
|
580
|
+
throw error;
|
|
581
|
+
}
|
|
582
|
+
fail(`não consegui ler o lote/pdf-map: ${error.message}`);
|
|
583
|
+
}
|
|
584
|
+
enforceDeck(items, parsed.flags);
|
|
585
|
+
enforceTypeCaps(items, parsed.flags, await fetchStoredCaps());
|
|
586
|
+
const cache = new PdfDocumentCache();
|
|
587
|
+
let result;
|
|
588
|
+
try {
|
|
589
|
+
result = await validateBatchReferences(items, pdfMap, cache);
|
|
590
|
+
}
|
|
591
|
+
finally {
|
|
592
|
+
await cache.close();
|
|
593
|
+
}
|
|
594
|
+
if (parsed.flags.json) {
|
|
595
|
+
printJson(result);
|
|
596
|
+
}
|
|
597
|
+
else {
|
|
598
|
+
for (const warning of result.warnings) {
|
|
599
|
+
process.stdout.write(yellow(`aviso: ${warning}\n`));
|
|
600
|
+
}
|
|
601
|
+
for (const error of result.errors) {
|
|
602
|
+
process.stdout.write(`erro: ${error}\n`);
|
|
603
|
+
}
|
|
604
|
+
process.stdout.write(`${result.valid ? green('Lote válido') : 'Lote inválido'} — ${result.stats.items} item(ns), ` +
|
|
605
|
+
`${result.stats.references} referência(s), ${result.stats.validatedHighlights} destaque(s) conferido(s).\n`);
|
|
606
|
+
}
|
|
607
|
+
if (!result.valid) {
|
|
608
|
+
process.exitCode = 1;
|
|
609
|
+
}
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
440
612
|
case 'schema': {
|
|
441
613
|
const authenticated = hasSession();
|
|
442
614
|
const [caps, guidance] = await Promise.all([fetchStoredCaps(), fetchAgentGuidance()]);
|
|
@@ -547,45 +719,73 @@ export async function stageCommand(parsed) {
|
|
|
547
719
|
if (batchDecks.createdNames.length > 0 && !parsed.flags.json) {
|
|
548
720
|
process.stdout.write(dim(`Baralho(s) criado(s) como PENDING (ocultos até o 1º aceite): ${batchDecks.createdNames.join(', ')}.\n`));
|
|
549
721
|
}
|
|
722
|
+
const generation = readGeneration(parsed.flags);
|
|
550
723
|
let created;
|
|
724
|
+
let importRunId = null;
|
|
725
|
+
let deduplicated = false;
|
|
551
726
|
try {
|
|
552
727
|
const res = (await apiFetch('/notes/review-inbox', {
|
|
553
728
|
method: 'POST',
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
729
|
+
body: {
|
|
730
|
+
items: items.map((item, index) => toReviewInboxItem(item, batchDecks.deckIds[index] ?? null)),
|
|
731
|
+
...(generation ? { generation: generation.value } : {}),
|
|
732
|
+
},
|
|
733
|
+
requestId: batchRequestId(raw, generation?.raw),
|
|
557
734
|
}));
|
|
558
735
|
created = res.created;
|
|
736
|
+
importRunId = res.importRunId ?? null;
|
|
559
737
|
}
|
|
560
738
|
catch (e) {
|
|
561
739
|
if (e instanceof ApiDedupedError) {
|
|
562
|
-
if (parsed.flags.
|
|
563
|
-
|
|
740
|
+
if (parsed.flags.replace) {
|
|
741
|
+
const warning = '--replace removeu os itens antes do replay; os IDs anteriores não são mais válidos. Altere o lote e reenvie.';
|
|
742
|
+
if (parsed.flags.json) {
|
|
743
|
+
printJson({ created: [], ids: [], deduplicated: true, warning });
|
|
744
|
+
}
|
|
745
|
+
else {
|
|
746
|
+
process.stdout.write(yellow(`Atenção: ${warning}\n`));
|
|
747
|
+
}
|
|
564
748
|
return;
|
|
565
749
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
750
|
+
if (!e.body) {
|
|
751
|
+
if (parsed.flags.json) {
|
|
752
|
+
printJson({ created: [], ids: [], deduplicated: true });
|
|
753
|
+
}
|
|
754
|
+
else {
|
|
755
|
+
process.stdout.write(green('Este lote já foi enviado antes — nada foi duplicado.\n'));
|
|
756
|
+
}
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
try {
|
|
760
|
+
const replay = JSON.parse(e.body);
|
|
761
|
+
if (!Array.isArray(replay.created)) {
|
|
762
|
+
throw new Error('created ausente');
|
|
763
|
+
}
|
|
764
|
+
created = replay.created;
|
|
765
|
+
importRunId = replay.importRunId ?? null;
|
|
766
|
+
deduplicated = true;
|
|
767
|
+
}
|
|
768
|
+
catch {
|
|
769
|
+
fail('o replay deduplicado não trouxe uma resposta JSON válida; nenhum ID foi presumido.');
|
|
569
770
|
}
|
|
570
|
-
return;
|
|
571
771
|
}
|
|
572
|
-
if (e instanceof ApiError) {
|
|
772
|
+
else if (e instanceof ApiError) {
|
|
573
773
|
fail(`o servidor rejeitou o lote (${e.status}): ${e.body}`);
|
|
574
774
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
if (deckFailures.length > 0) {
|
|
579
|
-
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`));
|
|
775
|
+
else {
|
|
776
|
+
throw e;
|
|
777
|
+
}
|
|
580
778
|
}
|
|
581
779
|
if (parsed.flags.json) {
|
|
582
780
|
printJson({
|
|
583
781
|
created,
|
|
584
782
|
ids: created.map((note) => note.id),
|
|
783
|
+
importRunId,
|
|
784
|
+
...(deduplicated ? { deduplicated: true } : {}),
|
|
585
785
|
decks: {
|
|
586
786
|
created: batchDecks.createdNames,
|
|
587
|
-
assigned: created.length -
|
|
588
|
-
failed:
|
|
787
|
+
assigned: created.length - batchDecks.deckIds.filter((id) => id === null).length,
|
|
788
|
+
failed: [],
|
|
589
789
|
},
|
|
590
790
|
});
|
|
591
791
|
return;
|
|
@@ -597,14 +797,15 @@ export async function stageCommand(parsed) {
|
|
|
597
797
|
return;
|
|
598
798
|
}
|
|
599
799
|
default:
|
|
600
|
-
process.stdout.write('Uso: memoring stage <schema|caps|create> [--file itens.json] [--replace] [--json]\n' +
|
|
800
|
+
process.stdout.write('Uso: memoring stage <schema|validate|caps|create> [--file itens.json] [--replace] [--json]\n' +
|
|
601
801
|
' schema imprime o contrato JSON dos itens (para humanos e agentes)\n' +
|
|
802
|
+
' validate valida lote e referências sem enviar (--file + --pdf-map)\n' +
|
|
602
803
|
' logado, inclui seus limites por tipo e as regras de qualidade —\n' +
|
|
603
804
|
' o agente deve ler o schema LOGADO, antes de gerar\n' +
|
|
604
805
|
' caps mostra/grava seus limites por tipo (--max-notes N | --max-flashcards N |\n' +
|
|
605
806
|
' --max-one-shots N | --max-questions N; 0 = não gerar o tipo)\n' +
|
|
606
807
|
' create envia o lote para a caixa de revisão (arquivo ou stdin)\n' +
|
|
607
|
-
' tipos: NOTE |
|
|
808
|
+
' tipos: NOTE | NORMAL | ONE_SHOT | QUESTION\n' +
|
|
608
809
|
' baralho OBRIGATÓRIO: deckName por item ou --deck "<nome|id>" no lote\n' +
|
|
609
810
|
' (nomes inexistentes são criados como PENDING — ocultos no app até o\n' +
|
|
610
811
|
' primeiro aceite; veja `memoring deck list`)\n' +
|
package/dist/config.js
CHANGED
|
@@ -6,6 +6,7 @@ const DEFAULTS = {
|
|
|
6
6
|
realm: 'memoring',
|
|
7
7
|
clientId: 'memoring-cli',
|
|
8
8
|
apiBase: 'https://memoring.com.br/api',
|
|
9
|
+
webBase: 'https://memoring.com.br',
|
|
9
10
|
};
|
|
10
11
|
export function configDir() {
|
|
11
12
|
if (process.env.MEMORING_CONFIG_HOME) {
|
|
@@ -35,6 +36,7 @@ export function loadConfig() {
|
|
|
35
36
|
realm: process.env.MEMORING_KC_REALM ?? stored.realm ?? DEFAULTS.realm,
|
|
36
37
|
clientId: process.env.MEMORING_KC_CLIENT ?? stored.clientId ?? DEFAULTS.clientId,
|
|
37
38
|
apiBase: process.env.MEMORING_API_BASE ?? stored.apiBase ?? DEFAULTS.apiBase,
|
|
39
|
+
webBase: process.env.MEMORING_WEB_BASE ?? stored.webBase ?? DEFAULTS.webBase,
|
|
38
40
|
accessToken: stored.accessToken,
|
|
39
41
|
refreshToken: stored.refreshToken,
|
|
40
42
|
accessTokenExpiresAt: stored.accessTokenExpiresAt,
|
package/dist/main.js
CHANGED
|
@@ -2,14 +2,18 @@
|
|
|
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 { batchCommand } from './commands/batch.js';
|
|
5
6
|
import { deckCommand } from './commands/deck.js';
|
|
6
7
|
import { inboxCommand } from './commands/inbox.js';
|
|
8
|
+
import { referenceCommand } from './commands/materialReferences.js';
|
|
9
|
+
import { planCommand } from './commands/plan.js';
|
|
10
|
+
import { scratchCommand } from './commands/scratch.js';
|
|
7
11
|
import { stageCommand } from './commands/stage.js';
|
|
8
12
|
import { updateCommand } from './commands/update.js';
|
|
9
13
|
import { skillCommand } from './commands/skill.js';
|
|
10
14
|
import { parseArgs as parse } from './commands/shared.js';
|
|
11
15
|
import { newerVersionAvailable, scheduleUpdateCheck } from './update.js';
|
|
12
|
-
const VERSION = '0.
|
|
16
|
+
const VERSION = '0.15.0';
|
|
13
17
|
const HELP = `Memoring CLI — flashcards e notas por IA, direto do terminal.
|
|
14
18
|
|
|
15
19
|
Uso: memoring <comando> [subcomando] [flags]
|
|
@@ -19,19 +23,44 @@ Comandos:
|
|
|
19
23
|
stage schema contrato JSON dos itens da caixa de revisão
|
|
20
24
|
(logado: inclui limites por tipo + regras de qualidade —
|
|
21
25
|
agentes devem ler o schema LOGADO, antes de gerar)
|
|
26
|
+
stage validate --file f valida lote e referências localmente, sem enviar
|
|
27
|
+
--pdf-map map.json mapa driveFileId → PDF local
|
|
22
28
|
stage create [--file f] envia itens (arquivo ou stdin) para a caixa de revisão
|
|
23
29
|
[--replace] esvazia a caixa antes de enviar
|
|
30
|
+
[--generation-file f] proveniência provider/modelo do lote
|
|
24
31
|
stage caps [--max-* N] seus limites por tipo (valem pro app e pros agentes)
|
|
32
|
+
batch check|run|status|verify plan.json
|
|
33
|
+
preflight, envio retomável e auditoria de atividades
|
|
25
34
|
inbox list itens pendentes de revisão
|
|
26
35
|
inbox accept <id>|accept-all aceita itens (entram nas listagens e na fila FSRS)
|
|
27
36
|
inbox discard <id> descarta um item pendente
|
|
28
37
|
inbox discard-all descarta todos os itens pendentes
|
|
29
38
|
inbox update <id> edita um item pendente (--file parcial e/ou flags,
|
|
30
39
|
inclui --deck <id|nome>)
|
|
40
|
+
reference list <noteId> referências de material de um card (fontes de estudo)
|
|
41
|
+
reference update <noteId> <refId> --file parcial.json
|
|
42
|
+
corrige excerpt/quote/contextos/região de uma referência
|
|
43
|
+
(merge: campo ausente mantém; null limpa; não troca
|
|
44
|
+
nota, arquivo nem página)
|
|
45
|
+
scratch list [--all] rascunhos do caderno no Google Drive (via proxy do
|
|
46
|
+
backend — conecte o Drive pelo app; a CLI não faz OAuth)
|
|
47
|
+
listados pelo CAMINHO da biblioteca (pasta/título)
|
|
48
|
+
scratch download <caminho|driveFileId> baixa o .scratch.json + o PDF de fundo
|
|
49
|
+
[--out <dir>] (se houver) — aceita o caminho do list ou o driveFileId
|
|
50
|
+
[--metadata-only] só o JSON + metadados do PDF, sem baixar o binário
|
|
51
|
+
scratch extract <arquivo.pdf> recorta páginas A-B num sub-PDF (pdf-lib)
|
|
52
|
+
[--pages A-B] [--all] PDF > ~30 páginas sem --pages exige --all explícito
|
|
31
53
|
deck list seus baralhos (id, nome, status, pai, a revisar, relevância, meta, recorde)
|
|
32
54
|
deck create <nome> cria um baralho (--parent <id|nome> p/ sub-baralho; --pending p/ importação)
|
|
33
55
|
deck update <id|nome> metadados do baralho (--relevance/--goal/--tone;
|
|
34
56
|
--clear-* limpa um campo)
|
|
57
|
+
plan analyze <pdfs...> analisa materiais; --web abre revisão sem criar plano
|
|
58
|
+
plan review <draftId> lê/aguarda o rascunho (--wait) e exporta payload (--out f)
|
|
59
|
+
plan create --file f cria o planejamento revisado; --draft uuid usa revisão web
|
|
60
|
+
plan current|next <planId> Meta ativa / próxima Meta (--activities N no fluxo direto;
|
|
61
|
+
--web revisa no app; --draft uuid conclui após REVIEWED)
|
|
62
|
+
plan start|complete|result executa atividades e registra desempenho
|
|
63
|
+
plan exams list|create|delete gerencia provas; diagnostics e guia no subcomando plan
|
|
35
64
|
update atualiza a CLI para a versão mais recente (via npm)
|
|
36
65
|
skill guia completo da CLI para agentes de IA (imprime o SKILL.md)
|
|
37
66
|
version versão da CLI
|
|
@@ -43,7 +72,7 @@ Flags globais:
|
|
|
43
72
|
--version imprime a versão da CLI
|
|
44
73
|
--help, -h ajuda
|
|
45
74
|
|
|
46
|
-
Ambiente (dev): MEMORING_API_BASE, MEMORING_KEYCLOAK_URL, MEMORING_KC_REALM, MEMORING_KC_CLIENT
|
|
75
|
+
Ambiente (dev): MEMORING_API_BASE, MEMORING_WEB_BASE, MEMORING_KEYCLOAK_URL, MEMORING_KC_REALM, MEMORING_KC_CLIENT
|
|
47
76
|
|
|
48
77
|
Atualização: a CLI verifica em background (1x/24h) se há versão nova e avisa no stderr.
|
|
49
78
|
MEMORING_NO_UPDATE_CHECK=1 desativa a verificação.
|
|
@@ -91,12 +120,24 @@ async function main() {
|
|
|
91
120
|
case 'stage':
|
|
92
121
|
await stageCommand(sub);
|
|
93
122
|
return;
|
|
123
|
+
case 'batch':
|
|
124
|
+
await batchCommand(sub);
|
|
125
|
+
return;
|
|
94
126
|
case 'inbox':
|
|
95
127
|
await inboxCommand(sub);
|
|
96
128
|
return;
|
|
129
|
+
case 'reference':
|
|
130
|
+
await referenceCommand(sub);
|
|
131
|
+
return;
|
|
132
|
+
case 'scratch':
|
|
133
|
+
await scratchCommand(sub);
|
|
134
|
+
return;
|
|
97
135
|
case 'deck':
|
|
98
136
|
await deckCommand(sub);
|
|
99
137
|
return;
|
|
138
|
+
case 'plan':
|
|
139
|
+
await planCommand(sub);
|
|
140
|
+
return;
|
|
100
141
|
case 'update':
|
|
101
142
|
await updateCommand(VERSION);
|
|
102
143
|
return;
|