@memoring/cli 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,6 +10,16 @@ A CLI não tem LLM própria: quem lê o material e gera os flashcards é o seu a
10
10
  npm install -g @memoring/cli
11
11
  ```
12
12
 
13
+ ## Atualização
14
+
15
+ A CLI verifica em background (no máximo 1x a cada 24h) se há versão nova no npm e avisa no stderr — nunca bloqueia o comando nem polui a saída `--json`. Para atualizar:
16
+
17
+ ```
18
+ memoring update # equivale a npm i -g @memoring/cli@latest
19
+ ```
20
+
21
+ `MEMORING_NO_UPDATE_CHECK=1` desativa a verificação.
22
+
13
23
  ## Quickstart
14
24
 
15
25
  Autentique (abre o browser — sem senha no terminal):
@@ -44,8 +54,9 @@ Os itens enviados nascem **PENDING_REVIEW** — nada entra nas suas listagens ne
44
54
  | Command | Purpose |
45
55
  | --- | --- |
46
56
  | `memoring auth` | login / logout / status |
47
- | `memoring stage` | schema (o contrato) / create (envia o lote, `--file` ou stdin, `--replace` esvazia a caixa antes) |
57
+ | `memoring stage` | schema (o contrato) / create (envia o lote, `--file` ou stdin, `--replace` esvazia a caixa antes, `--max-notes\|--max-flashcards\|--max-one-shots` limitam cada tipo) |
48
58
  | `memoring inbox` | list / accept `<id>` / accept-all / discard `<id>` |
59
+ | `memoring update` | atualiza a CLI para a última versão (via npm) |
49
60
  | `memoring version` | versão da CLI |
50
61
 
51
62
  ## Flags
@@ -79,7 +90,7 @@ Cada item do lote (1 a 50, all-or-nothing):
79
90
  }
80
91
  ```
81
92
 
82
- - `type`: `NORMAL` (nota, exige `content` em markdown), `FLASHCARD` ou `ONE_SHOT` (exigem `front` + `back`).
93
+ - `type`: `NOTE` (nota, exige `content` em markdown; `NORMAL` legado é aceito e mapeado para `NOTE`), `FLASHCARD` ou `ONE_SHOT` (exigem `front` + `back`).
83
94
  - `source`: origem do material — default `"Importação assistida (CLI)"`.
84
95
 
85
96
  ## Para agentes de IA
@@ -95,5 +106,6 @@ memoring auth login
95
106
  ```
96
107
 
97
108
  Variáveis: `MEMORING_API_BASE`, `MEMORING_KEYCLOAK_URL`, `MEMORING_KC_REALM`, `MEMORING_KC_CLIENT`, `MEMORING_CONFIG_HOME` (isola o config.json).
109
+ Override de ambiente vale por sessão: o `auth login` não o grava no config.json — exporte as variáveis em cada shell (ou use `memoring --api-base ...` por comando).
98
110
 
99
111
  Build a partir do repo: `cd cli && npm install && npm run build && node dist/main.js --help`.
@@ -1,15 +1,53 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { apiFetch, ApiError } from '../api.js';
3
3
  import { dim, fail, green, printJson, stringFlag } from './shared.js';
4
- const TYPES = ['NORMAL', 'FLASHCARD', 'ONE_SHOT'];
4
+ const TYPES = ['NOTE', 'FLASHCARD', 'ONE_SHOT'];
5
+ // Retrocompat: itens.json antigos com "type": "NORMAL" são normalizados para "NOTE".
6
+ const LEGACY_TYPE_ALIASES = { NORMAL: 'NOTE' };
5
7
  const MAX_ITEMS = 50;
6
8
  const DEFAULT_SOURCE = 'Importação assistida (CLI)';
9
+ /** Flags --max-<tipo> do `stage create`: teto por tipo no lote (validação local). */
10
+ const MAX_FLAG_BY_TYPE = {
11
+ NOTE: 'max-notes',
12
+ FLASHCARD: 'max-flashcards',
13
+ ONE_SHOT: 'max-one-shots',
14
+ };
15
+ const TYPE_LABEL = {
16
+ NOTE: 'notas',
17
+ FLASHCARD: 'flashcards',
18
+ ONE_SHOT: '1-shots',
19
+ };
20
+ function enforceTypeCaps(items, flags) {
21
+ const caps = Object.entries(MAX_FLAG_BY_TYPE)
22
+ .map(([type, flag]) => {
23
+ const raw = stringFlag(flags, flag);
24
+ if (raw === undefined) {
25
+ return null;
26
+ }
27
+ const max = Number(raw);
28
+ if (!Number.isInteger(max) || max < 0) {
29
+ fail(`--${flag} inválido: use um inteiro >= 0 (veio ${JSON.stringify(raw)}).`);
30
+ }
31
+ return { type, flag, max };
32
+ })
33
+ .filter((cap) => cap !== null);
34
+ if (caps.length === 0) {
35
+ return;
36
+ }
37
+ for (const cap of caps) {
38
+ const count = items.filter((item) => item.type === cap.type).length;
39
+ if (count > cap.max) {
40
+ fail(`lote tem ${count} ${TYPE_LABEL[cap.type]} e --${cap.flag}=${cap.max}: ajuste o lote ou o limite. ` +
41
+ 'Se um agente gerou este lote, peça a ele para respeitar os limites por tipo.');
42
+ }
43
+ }
44
+ }
7
45
  const SCHEMA_JSON = {
8
46
  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.',
9
47
  item: {
10
- type: 'NORMAL | FLASHCARD | ONE_SHOT (obrigatório)',
48
+ type: 'NOTE | FLASHCARD | ONE_SHOT (obrigatório; NORMAL legado é aceito e mapeado para NOTE)',
11
49
  title: 'string (obrigatório)',
12
- content: 'string — corpo da nota em markdown (obrigatório quando type=NORMAL)',
50
+ content: 'string — corpo da nota em markdown (obrigatório quando type=NOTE)',
13
51
  front: 'string — pergunta (obrigatório quando type=FLASHCARD|ONE_SHOT)',
14
52
  back: 'string — resposta (obrigatório quando type=FLASHCARD|ONE_SHOT)',
15
53
  subjectName: 'string? — matéria (ex.: "Direito Constitucional")',
@@ -28,12 +66,13 @@ const SCHEMA_JSON = {
28
66
  source: 'Apostila constitucional.pdf',
29
67
  },
30
68
  ],
69
+ typeCaps: 'flags opcionais do stage create: --max-notes N | --max-flashcards N | --max-one-shots N — o lote é rejeitado se algum tipo exceder o limite.',
31
70
  };
32
71
  const SCHEMA_TEXT = `Contrato dos itens da caixa de revisão (POST /api/notes/review-inbox):
33
72
 
34
- type NORMAL | FLASHCARD | ONE_SHOT (obrigatório)
73
+ type NOTE | FLASHCARD | ONE_SHOT (obrigatório; NORMAL legado é mapeado para NOTE)
35
74
  title string (obrigatório)
36
- content string — corpo da nota (markdown) (obrigatório quando type=NORMAL)
75
+ content string — corpo da nota (markdown) (obrigatório quando type=NOTE)
37
76
  front, back string — pergunta e resposta (obrigatório quando type=FLASHCARD|ONE_SHOT)
38
77
  subjectName? string — matéria (ex.: "Direito Constitucional")
39
78
  subtopicName? string — subtópico
@@ -42,7 +81,8 @@ const SCHEMA_TEXT = `Contrato dos itens da caixa de revisão (POST /api/notes/re
42
81
 
43
82
  Lote: 1 a ${MAX_ITEMS} itens, all-or-nothing — qualquer item inválido rejeita o lote inteiro.
44
83
  Os itens nascem PENDING_REVIEW: o usuário aceita, edita ou recusa em /cards/inbox.
45
- Dica: FLASHCARD entra na fila de revisão espaçada (FSRS); ONE_SHOT é pergunta de resposta única.`;
84
+ Dica: FLASHCARD entra na fila de revisão espaçada (FSRS); ONE_SHOT é pergunta de resposta única.
85
+ Limites por tipo: --max-notes N | --max-flashcards N | --max-one-shots N (o lote é rejeitado se exceder).`;
46
86
  async function readStdin() {
47
87
  const chunks = [];
48
88
  for await (const chunk of process.stdin) {
@@ -64,14 +104,17 @@ function validateItems(raw) {
64
104
  fail(`${where}: esperado objeto, veio ${typeof item}.`);
65
105
  }
66
106
  const it = item;
107
+ if (typeof it.type === 'string' && LEGACY_TYPE_ALIASES[it.type] !== undefined) {
108
+ it.type = LEGACY_TYPE_ALIASES[it.type];
109
+ }
67
110
  if (typeof it.type !== 'string' || !TYPES.includes(it.type)) {
68
111
  fail(`${where}: type deve ser ${TYPES.join(' | ')} (veio ${JSON.stringify(it.type)}).`);
69
112
  }
70
113
  if (typeof it.title !== 'string' || it.title.trim() === '') {
71
114
  fail(`${where}: title é obrigatório.`);
72
115
  }
73
- if (it.type === 'NORMAL' && (typeof it.content !== 'string' || it.content.trim() === '')) {
74
- fail(`${where} (${it.title}): type=NORMAL exige content.`);
116
+ if (it.type === 'NOTE' && (typeof it.content !== 'string' || it.content.trim() === '')) {
117
+ fail(`${where} (${it.title}): type=NOTE exige content.`);
75
118
  }
76
119
  if ((it.type === 'FLASHCARD' || it.type === 'ONE_SHOT') &&
77
120
  (typeof it.front !== 'string' || it.front.trim() === '' || typeof it.back !== 'string' || it.back.trim() === '')) {
@@ -122,6 +165,7 @@ export async function stageCommand(parsed) {
122
165
  fail(`JSON inválido: ${e.message}`);
123
166
  }
124
167
  const items = validateItems(parsedJson);
168
+ enforceTypeCaps(items, parsed.flags);
125
169
  if (parsed.flags.replace) {
126
170
  const removed = await emptyInbox();
127
171
  if (!parsed.flags.json) {
@@ -153,7 +197,9 @@ export async function stageCommand(parsed) {
153
197
  process.stdout.write('Uso: memoring stage <schema|create> [--file itens.json] [--replace] [--json]\n' +
154
198
  ' schema imprime o contrato JSON dos itens (para humanos e agentes)\n' +
155
199
  ' create envia o lote para a caixa de revisão (arquivo ou stdin)\n' +
156
- ' --replace esvazia a caixa antes de enviar\n');
200
+ ' --replace esvazia a caixa antes de enviar\n' +
201
+ ' --max-notes N | --max-flashcards N | --max-one-shots N\n' +
202
+ ' limitam a quantidade de cada tipo no lote (validação local)\n');
157
203
  if (sub) {
158
204
  process.exitCode = 1;
159
205
  }
@@ -0,0 +1,35 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { bold, dim, fail, green, yellow } from './shared.js';
3
+ import { fetchLatestVersion, isNewerVersion, recordUpdateCheck } from '../update.js';
4
+ const PACKAGE = '@memoring/cli';
5
+ export async function updateCommand(current) {
6
+ process.stdout.write(dim('consultando o registry npm...') + '\n');
7
+ const latest = await fetchLatestVersion();
8
+ if (latest && !isNewerVersion(latest, current)) {
9
+ recordUpdateCheck(latest);
10
+ process.stdout.write(green(`o memoring já está na versão mais recente (${current}).`) + '\n');
11
+ return;
12
+ }
13
+ if (latest) {
14
+ process.stdout.write(`atualizando ${current} → ${bold(latest)}...\n`);
15
+ }
16
+ else {
17
+ process.stdout.write(yellow('registry indisponível — tentando atualizar via npm mesmo assim...') + '\n');
18
+ }
19
+ const code = await runNpmGlobalInstall();
20
+ if (code !== 0) {
21
+ fail(`a atualização via npm falhou (código ${code}). Tente manualmente: npm install -g ${PACKAGE}@latest`);
22
+ }
23
+ recordUpdateCheck(latest ?? undefined);
24
+ process.stdout.write(green('memoring atualizado — rode `memoring version` para conferir.') + '\n');
25
+ }
26
+ function runNpmGlobalInstall() {
27
+ return new Promise((resolve) => {
28
+ const child = spawn('npm', ['install', '-g', `${PACKAGE}@latest`], {
29
+ stdio: 'inherit',
30
+ shell: process.platform === 'win32',
31
+ });
32
+ child.on('error', () => resolve(-1));
33
+ child.on('close', (code) => resolve(code ?? -1));
34
+ });
35
+ }
package/dist/config.js CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
3
  import { join } from 'node:path';
4
4
  const DEFAULTS = {
@@ -7,7 +7,7 @@ const DEFAULTS = {
7
7
  clientId: 'memoring-cli',
8
8
  apiBase: 'https://memoring.com.br/api',
9
9
  };
10
- function configDir() {
10
+ export function configDir() {
11
11
  if (process.env.MEMORING_CONFIG_HOME) {
12
12
  return process.env.MEMORING_CONFIG_HOME;
13
13
  }
@@ -41,10 +41,47 @@ export function loadConfig() {
41
41
  email: stored.email,
42
42
  };
43
43
  }
44
+ /**
45
+ * Endpoints para PERSISTIR: só o que veio do arquivo ou do default. Override de
46
+ * ambiente (MEMORING_API_BASE etc.) é efêmero por definição — gravá-lo no
47
+ * config.json faria comandos futuros, sem a variável, apontarem para o
48
+ * ambiente errado (ex.: dev).
49
+ */
50
+ export function storedEndpoints() {
51
+ const stored = readStored();
52
+ return {
53
+ keycloakUrl: stored.keycloakUrl ?? DEFAULTS.keycloakUrl,
54
+ realm: stored.realm ?? DEFAULTS.realm,
55
+ clientId: stored.clientId ?? DEFAULTS.clientId,
56
+ apiBase: stored.apiBase ?? DEFAULTS.apiBase,
57
+ };
58
+ }
59
+ function writeAtomic(json) {
60
+ const path = configPath();
61
+ const tmp = `${path}.${process.pid}.tmp`;
62
+ writeFileSync(tmp, json, { mode: 0o600 });
63
+ renameSync(tmp, path);
64
+ }
44
65
  function writeStored(patch) {
45
66
  const merged = { ...readStored(), ...patch };
46
67
  mkdirSync(configDir(), { recursive: true });
47
- writeFileSync(configPath(), JSON.stringify(merged, null, 2), { mode: 0o600 });
68
+ writeAtomic(JSON.stringify(merged, null, 2));
69
+ }
70
+ /** Lockfile que serializa o refresh de token entre processos da CLI (ver auth.ts). */
71
+ export function refreshLockPath() {
72
+ return join(configDir(), 'refresh.lock');
73
+ }
74
+ export function ensureConfigDir() {
75
+ mkdirSync(configDir(), { recursive: true });
76
+ }
77
+ /** Remove o lockfile sem erro quando ele já foi liberado por outro processo. */
78
+ export function releaseRefreshLock() {
79
+ try {
80
+ rmSync(refreshLockPath());
81
+ }
82
+ catch {
83
+ // já removido por outro processo
84
+ }
48
85
  }
49
86
  export function saveSession(endpoints, tokens) {
50
87
  writeStored({ ...endpoints, ...tokens });
@@ -60,7 +97,7 @@ export function clearSession() {
60
97
  void accessTokenExpiresAt;
61
98
  void email;
62
99
  mkdirSync(configDir(), { recursive: true });
63
- writeFileSync(configPath(), JSON.stringify(rest, null, 2), { mode: 0o600 });
100
+ writeAtomic(JSON.stringify(rest, null, 2));
64
101
  return existed;
65
102
  }
66
103
  export function hasSession() {
package/dist/main.js CHANGED
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import { ApiError } from './api.js';
3
- import { setColorEnabled } from './output.js';
3
+ import { 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
+ import { updateCommand } from './commands/update.js';
7
8
  import { fail, parseArgs as parse } from './commands/shared.js';
8
- const VERSION = '0.1.0';
9
+ import { newerVersionAvailable, scheduleUpdateCheck } from './update.js';
10
+ const VERSION = '0.3.0';
9
11
  const HELP = `Memoring CLI — flashcards e notas por IA, direto do terminal.
10
12
 
11
13
  Uso: memoring <comando> [subcomando] [flags]
@@ -18,16 +20,21 @@ Comandos:
18
20
  inbox list itens pendentes de revisão
19
21
  inbox accept <id>|accept-all aceita itens (entram nas listagens e na fila FSRS)
20
22
  inbox discard <id> descarta um item pendente
23
+ update atualiza a CLI para a versão mais recente (via npm)
21
24
  version versão da CLI
22
25
 
23
26
  Flags globais:
24
27
  --json saída legível por máquina
25
28
  --no-color desativa cores
26
29
  --api-base sobrescreve a URL da API (default: https://memoring.com.br/api)
30
+ --version imprime a versão da CLI
27
31
  --help, -h ajuda
28
32
 
29
33
  Ambiente (dev): MEMORING_API_BASE, MEMORING_KEYCLOAK_URL, MEMORING_KC_REALM, MEMORING_KC_CLIENT
30
34
 
35
+ Atualização: a CLI verifica em background (1x/24h) se há versão nova e avisa no stderr.
36
+ MEMORING_NO_UPDATE_CHECK=1 desativa a verificação.
37
+
31
38
  Exemplos:
32
39
  memoring auth login
33
40
  memoring stage schema
@@ -45,8 +52,21 @@ async function main() {
45
52
  if (apiBase) {
46
53
  process.env.MEMORING_API_BASE = apiBase;
47
54
  }
55
+ if (parsed._[0] !== 'update') {
56
+ scheduleUpdateCheck();
57
+ }
58
+ process.on('exit', () => {
59
+ const latest = newerVersionAvailable(VERSION);
60
+ if (latest) {
61
+ process.stderr.write(yellow(`\nnova versão do memoring disponível: ${VERSION} → ${latest} — rode \`memoring update\` para atualizar.\n`));
62
+ }
63
+ });
48
64
  const [command, ...rest] = parsed._;
49
65
  const sub = { _: rest, flags: parsed.flags };
66
+ if (parsed.flags.version) {
67
+ process.stdout.write(`memoring-cli ${VERSION}\n`);
68
+ return;
69
+ }
50
70
  if (parsed.flags.help || !command) {
51
71
  process.stdout.write(HELP);
52
72
  return;
@@ -61,6 +81,9 @@ async function main() {
61
81
  case 'inbox':
62
82
  await inboxCommand(sub);
63
83
  return;
84
+ case 'update':
85
+ await updateCommand(VERSION);
86
+ return;
64
87
  case 'version':
65
88
  process.stdout.write(`memoring-cli ${VERSION}\n`);
66
89
  return;
package/dist/update.js ADDED
@@ -0,0 +1,126 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { configDir } from './config.js';
5
+ const REGISTRY_LATEST_URL = 'https://registry.npmjs.org/@memoring%2Fcli/latest';
6
+ const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
7
+ const FETCH_TIMEOUT_MS = 8000;
8
+ function cachePath() {
9
+ return join(configDir(), 'update-check.json');
10
+ }
11
+ function readCache() {
12
+ try {
13
+ const parsed = JSON.parse(readFileSync(cachePath(), 'utf8'));
14
+ if (typeof parsed.checkedAt !== 'number') {
15
+ return null;
16
+ }
17
+ return {
18
+ checkedAt: parsed.checkedAt,
19
+ latest: typeof parsed.latest === 'string' ? parsed.latest : undefined,
20
+ };
21
+ }
22
+ catch {
23
+ return null;
24
+ }
25
+ }
26
+ function writeCache(latest) {
27
+ try {
28
+ mkdirSync(dirname(cachePath()), { recursive: true });
29
+ const tmp = `${cachePath()}.${process.pid}.tmp`;
30
+ writeFileSync(tmp, JSON.stringify({ checkedAt: Date.now(), latest }));
31
+ renameSync(tmp, cachePath());
32
+ }
33
+ catch {
34
+ // cache é best-effort — nunca atrapalhar o comando em execução
35
+ }
36
+ }
37
+ /** true quando `latest` é semver maior que `current` (comparação numérica de x.y.z). */
38
+ export function isNewerVersion(latest, current) {
39
+ const parts = (v) => v.replace(/^v/, '').split('.').map((p) => Number.parseInt(p, 10) || 0);
40
+ const a = parts(latest);
41
+ const b = parts(current);
42
+ for (let i = 0; i < 3; i++) {
43
+ const x = a[i] ?? 0;
44
+ const y = b[i] ?? 0;
45
+ if (x !== y) {
46
+ return x > y;
47
+ }
48
+ }
49
+ return false;
50
+ }
51
+ /** Versão mais nova segundo o cache local, ou null (cache ausente/stale não é erro). */
52
+ export function newerVersionAvailable(current) {
53
+ const cache = readCache();
54
+ return cache?.latest && isNewerVersion(cache.latest, current) ? cache.latest : null;
55
+ }
56
+ /** Consulta o registry na hora (usado pelo `memoring update`). Null quando offline/indisponível. */
57
+ export async function fetchLatestVersion() {
58
+ const controller = new AbortController();
59
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
60
+ try {
61
+ const response = await fetch(REGISTRY_LATEST_URL, {
62
+ signal: controller.signal,
63
+ headers: { accept: 'application/json' },
64
+ });
65
+ if (!response.ok) {
66
+ return null;
67
+ }
68
+ const body = (await response.json());
69
+ return typeof body.version === 'string' ? body.version : null;
70
+ }
71
+ catch {
72
+ return null;
73
+ }
74
+ finally {
75
+ clearTimeout(timer);
76
+ }
77
+ }
78
+ /** Grava o cache (após self-update bem-sucedido, suprime o aviso na hora). */
79
+ export function recordUpdateCheck(latest) {
80
+ writeCache(latest);
81
+ }
82
+ /**
83
+ * Agenda a verificação periódica: um processo filho detached faz o fetch e grava
84
+ * o cache — o comando em execução nunca espera a rede e nunca vê erro (offline ok).
85
+ * No máximo uma verificação a cada 24h; MEMORING_NO_UPDATE_CHECK=1 desativa.
86
+ */
87
+ export function scheduleUpdateCheck() {
88
+ if (process.env.MEMORING_NO_UPDATE_CHECK) {
89
+ return;
90
+ }
91
+ const cache = readCache();
92
+ if (cache && Date.now() - cache.checkedAt < CHECK_INTERVAL_MS) {
93
+ return;
94
+ }
95
+ const script = `
96
+ const [cachePath, previous] = process.argv.slice(1);
97
+ const fs = require('node:fs');
98
+ const path = require('node:path');
99
+ const writeCache = (latest) => {
100
+ try {
101
+ fs.mkdirSync(path.dirname(cachePath), { recursive: true });
102
+ const tmp = cachePath + '.' + process.pid + '.tmp';
103
+ fs.writeFileSync(tmp, JSON.stringify({ checkedAt: Date.now(), latest: latest || undefined }));
104
+ fs.renameSync(tmp, cachePath);
105
+ } catch {}
106
+ };
107
+ const controller = new AbortController();
108
+ const timer = setTimeout(() => controller.abort(), ${FETCH_TIMEOUT_MS});
109
+ fetch(${JSON.stringify(REGISTRY_LATEST_URL)}, { signal: controller.signal, headers: { accept: 'application/json' } })
110
+ .then((r) => (r.ok ? r.json() : null))
111
+ .then((b) => writeCache(b && typeof b.version === 'string' ? b.version : previous))
112
+ .catch(() => writeCache(previous))
113
+ .finally(() => clearTimeout(timer));
114
+ `;
115
+ try {
116
+ const child = spawn(process.execPath, ['-e', script, cachePath(), cache?.latest ?? ''], {
117
+ detached: true,
118
+ stdio: 'ignore',
119
+ windowsHide: true,
120
+ });
121
+ child.unref();
122
+ }
123
+ catch {
124
+ // verificação é best-effort
125
+ }
126
+ }
package/package.json CHANGED
@@ -1,7 +1,12 @@
1
1
  {
2
2
  "name": "@memoring/cli",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "CLI do Memoring — envie flashcards/notas gerados por IA para a sua caixa de revisão, direto do terminal.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/caiomestres/memoring.git",
8
+ "directory": "cli"
9
+ },
5
10
  "type": "module",
6
11
  "bin": {
7
12
  "memoring": "dist/main.js"
@@ -22,8 +22,9 @@ O Memoring é um app de estudos com revisão espaçada (FSRS). A CLI `memoring`
22
22
 
23
23
  ## Regras do contrato (resumo — a fonte da verdade é `stage schema`)
24
24
 
25
- - `type`: `NORMAL` (nota, exige `content` markdown), `FLASHCARD` ou `ONE_SHOT` (exigem `front` + `back`).
25
+ - `type`: `NOTE` (nota, exige `content` markdown; `NORMAL` legado é aceito e mapeado para `NOTE`), `FLASHCARD` ou `ONE_SHOT` (exigem `front` + `back`).
26
26
  - `title` sempre obrigatório; lote de 1 a 50 itens, all-or-nothing.
27
+ - Se o usuário pedir limites por tipo (ex.: "no máximo 20 flashcards e 5 notas"), respeite-os ao gerar e valide com `--max-notes N --max-flashcards N --max-one-shots N` no `stage create` — o lote é rejeitado se exceder.
27
28
  - `ONE_SHOT` = pergunta de resposta única; prefira `FLASHCARD` para memorização.
28
29
  - Preencha `source` com a origem real do material (nome do arquivo/pasta) — não use o default quando souber a origem.
29
30
  - `subjectName`/`subtopicName` quando o material tiver matéria/subtópico claros; `tags` curtas e em minúsculas.
@@ -32,6 +33,7 @@ O Memoring é um app de estudos com revisão espaçada (FSRS). A CLI `memoring`
32
33
  ## Armadilhas
33
34
 
34
35
  - **Nunca** use `--replace` sem o usuário pedir explicitamente — ele apaga os itens pendentes existentes da caixa.
36
+ - 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.
35
37
  - Se `stage create` falhar com erro de validação do servidor, corrija os itens indicados e reenvie — não relaxe o contrato.
36
38
  - Lotes grandes: divida em vários arquivos de até 50 itens e envie sequencialmente.
37
39
  - Flashcards bons são atômicos: uma ideia por cartão, pergunta direta, resposta completa mas curta.