ozali 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
@@ -46,18 +46,38 @@ npx --ignore-scripts ozali@<versión> init
46
46
  git clone <repo> && node ozali/cli/bin/ozali.mjs init
47
47
  ```
48
48
 
49
+ > **`dlx`/`npx` son efímeros:** ejecutan `init` pero **no dejan `ozali` en tu PATH**. Para usar
50
+ > `ozali doctor`/`update`/`sync` después, instala globalmente (el propio `init` te lo recuerda):
51
+ >
52
+ > ```bash
53
+ > pnpm add -g ozali@<versión> # recomendado
54
+ > npm install -g ozali@<versión>
55
+ > ```
56
+
49
57
  ### Uso del CLI
50
58
 
51
59
  ```bash
52
- ozali init # detecta agente, instala la skill, aísla el histórico, configura Engram
53
- ozali doctor # health-check read-only (fuente de verdad, Engram, Strict TDD, runner…)
60
+ ozali init # detecta agente, instala la skill, aísla el histórico, instala+configura Engram
61
+ ozali doctor # health-check read-only (fuente de verdad, Engram, Cloud, Strict TDD, runner…)
54
62
  ozali update # actualiza la skill instalada a la versión del paquete
55
63
  ozali sync # lleva el histórico (docs + Engram) al repo de conocimiento de equipo
56
64
  ```
57
65
 
58
- Flags útiles: `--yes` (no interactivo), `--dry-run` (init sin escribir), `--agent`, `--scope`,
59
- `--knowledge-repo`, `--import`/`--push` (sync). Modelo mental completo en
60
- [docs/intended-usage.md](docs/intended-usage.md).
66
+ `init` también escribe un **perfil base de permisos** (`.claude/settings.json` para Claude Code,
67
+ `opencode.json` para opencode) para reducir confirmaciones: deja libres comandos seguros y bloquea
68
+ los destructivos. Es un template — tus reglas se conservan al re-correr `init`. Como Claude Code
69
+ **ignora** los permisos de un proyecto hasta confiar en él, `init` ofrece marcar el workspace como
70
+ confiable (`hasTrustDialogAccepted` en `~/.claude.json`); usa `--no-trust` para omitirlo.
71
+
72
+ `init` también **instala Engram** y registra su MCP con `engram setup <agente>`: en modo
73
+ interactivo te pregunta (default sí), y con `--yes` lo instala automáticamente. Usa brew en
74
+ macOS/Linux, `go install` en Windows, con binario precompilado como fallback. Si prefieres no
75
+ instalarlo, pasa `--no-engram` (arranca en modo `docs`). Opcionalmente habilita **Engram Cloud**
76
+ (réplica de equipo opt-in) además del git-sync.
77
+
78
+ Flags útiles: `--yes` (no interactivo), `--dry-run` (init sin escribir), `--no-engram`, `--no-trust`,
79
+ `--agent`, `--scope`, `--knowledge-repo`, `--import`/`--push`/`--cloud` (sync). Modelo mental completo
80
+ en [docs/intended-usage.md](docs/intended-usage.md).
61
81
 
62
82
  ## Agentes soportados
63
83
 
@@ -68,6 +88,7 @@ Claude Code y opencode (perfiles de permisos para ambos en
68
88
 
69
89
  | Tema | Doc |
70
90
  |---|---|
91
+ | Guía de uso (usuarios generales) | [docs/guia-de-uso.md](docs/guia-de-uso.md) |
71
92
  | Uso previsto (modelo mental) | [docs/intended-usage.md](docs/intended-usage.md) |
72
93
  | Seguridad del instalador (npx/pnpm) | [docs/security.md](docs/security.md) |
73
94
  | Histórico aislado y memoria de equipo | [docs/team-history.md](docs/team-history.md) |
package/cli/bin/ozali.mjs CHANGED
@@ -24,8 +24,11 @@ ${c.bold("Opciones comunes:")}
24
24
  --agent <a> (init) claude-code | opencode | both.
25
25
  --scope <s> (init) project | global.
26
26
  --knowledge-repo <p> (init) Ruta del repo de conocimiento.
27
+ --no-engram (init) No usar Engram; arranca en modo docs.
28
+ --no-trust (init) No marcar el workspace como confiable en Claude Code.
27
29
  --import (sync) Importa del repo de conocimiento a local.
28
30
  --push (sync) Hace push al remoto del repo de conocimiento.
31
+ --cloud (sync) Replica también a Engram Cloud (si está habilitado).
29
32
  -h, --help Esta ayuda. -v, --version Versión.
30
33
 
31
34
  ${c.bold("Instalación segura:")}
@@ -42,6 +45,9 @@ function parseArgs(argv) {
42
45
  else if (a === "--dry-run") opts.dryRun = true;
43
46
  else if (a === "--import") opts.import = true;
44
47
  else if (a === "--push") opts.push = true;
48
+ else if (a === "--cloud") opts.cloud = true;
49
+ else if (a === "--no-engram") opts.noEngram = true;
50
+ else if (a === "--no-trust") opts.noTrust = true;
45
51
  else if (a === "--agent") opts.agent = argv[++i];
46
52
  else if (a === "--scope") opts.scope = argv[++i];
47
53
  else if (a === "--knowledge-repo") opts.knowledgeRepo = argv[++i];
@@ -4,7 +4,8 @@ import path from "node:path";
4
4
  import {
5
5
  c, ok, warn, err, info, step,
6
6
  SKILL_SRC, exists, ensureDir, copyDir, readJSON, writeJSON,
7
- ensureGitignore, tryExec, projectName, pkgVersion, DEFAULT_KNOWLEDGE,
7
+ ensureGitignore, tryExec, spawnCmd, which,
8
+ projectName, pkgVersion, DEFAULT_KNOWLEDGE, HOME,
8
9
  } from "./util.mjs";
9
10
  import { detectAll } from "./detect.mjs";
10
11
  import { ask, confirm, select } from "./prompt.mjs";
@@ -47,14 +48,47 @@ export async function init(cwd, opts) {
47
48
  const knowledgeRepo = opts.knowledgeRepo || await ask("Ruta del repo de conocimiento (histórico aislado)", DEFAULT_KNOWLEDGE);
48
49
 
49
50
  // Engram
50
- let memoryMode = "hybrid";
51
- if (env.engram.available) {
51
+ let memoryMode = "docs";
52
+ if (opts.noEngram) {
53
+ info("--no-engram: arranco en modo " + c.bold("docs") + " (sin usar Engram).");
54
+ } else if (env.engram.available) {
55
+ memoryMode = "hybrid";
52
56
  ok(`Engram disponible (${env.engram.bin}). Modo de memoria: ${c.bold("hybrid")} (docs + Engram).`);
53
57
  } else {
54
- memoryMode = "docs";
55
- warn("Engram no está instalado. Arrancamos en modo " + c.bold("docs") + " (solo documentos legibles).");
56
- info("Para activar memoria buscable/acumulativa instala Engram: " + c.cyan("https://github.com/Gentleman-Programming/engram"));
57
- info("Tras instalarlo, vuelve a correr " + c.bold("ozali doctor") + " — el modo subirá a hybrid automáticamente.");
58
+ warn("Engram no está instalado.");
59
+ // --dry-run no instala; --yes usa el default (); interactivo pregunta.
60
+ if (opts.dryRun) info("(dry-run) Aquí instalaría y configuraría Engram.");
61
+ const installNow = opts.dryRun ? false
62
+ : (opts.yes ? true : await confirm("¿Instalo y configuro Engram ahora?", true));
63
+ if (installNow) {
64
+ if (opts.yes) info("Modo no interactivo: instalando Engram automáticamente…");
65
+ const installed = installEngram();
66
+ if (installed) {
67
+ if (agent === "claude-code" || agent === "both") {
68
+ info("Registrando MCP en Claude Code…");
69
+ spawnCmd("engram", ["setup", "claude-code"]);
70
+ }
71
+ if (agent === "opencode" || agent === "both") {
72
+ info("Registrando MCP en opencode…");
73
+ spawnCmd("engram", ["setup", "opencode"]);
74
+ }
75
+ memoryMode = "hybrid";
76
+ ok("Engram listo. Modo de memoria: " + c.bold("hybrid") + ".");
77
+ info("Reinicia tu agente para que cargue el servidor MCP de Engram.");
78
+ } else {
79
+ warn("Instalación no completada. Continúo en modo " + c.bold("docs") + ".");
80
+ printEngramManualInstructions(agent);
81
+ }
82
+ } else {
83
+ info("Modo " + c.bold("docs") + " activo. Cuando instales Engram, corre " + c.bold("ozali doctor") + " para activar hybrid.");
84
+ printEngramManualInstructions(agent);
85
+ }
86
+ }
87
+
88
+ // Engram Cloud (opt-in) — réplica de equipo además del git-sync. Solo si Engram quedó disponible.
89
+ let cloud = { enabled: false };
90
+ if (memoryMode === "hybrid" && !opts.dryRun) {
91
+ cloud = await maybeEnableEngramCloud(projectName(cwd), opts);
58
92
  }
59
93
 
60
94
  if (opts.dryRun) { warn("--dry-run: no escribo nada. Plan mostrado arriba."); return 0; }
@@ -67,7 +101,12 @@ export async function init(cwd, opts) {
67
101
  copyDir(SKILL_SRC, target);
68
102
  ok(`Skill instalada en ${c.bold(path.relative(cwd, target) || target)}.`);
69
103
 
70
- // 2) opencode: perfil base de permisos (idempotente, merge mínimo)
104
+ // 2) perfiles base de permisos (idempotentes, merge mínimo) por agente
105
+ if (agent === "claude-code" || agent === "both") {
106
+ ensureClaudeCodeProfile(cwd, scope);
107
+ // Claude Code ignora los permisos de un .claude/settings.json de proyecto hasta confiar en él.
108
+ if (scope === "project" && !opts.noTrust) await ensureClaudeWorkspaceTrust(cwd, opts);
109
+ }
71
110
  if (agent === "opencode" || agent === "both") {
72
111
  ensureOpencodeProfile(cwd);
73
112
  }
@@ -92,7 +131,7 @@ export async function init(cwd, opts) {
92
131
 
93
132
  // 5) config local (gitignored)
94
133
  const config = {
95
- version: pkgVersion(), agent, scope, knowledgeRepo, memoryMode,
134
+ version: pkgVersion(), agent, scope, knowledgeRepo, memoryMode, cloud,
96
135
  project: projectName(cwd), createdAt: new Date().toISOString(),
97
136
  };
98
137
  writeJSON(CONFIG_PATH(cwd), config);
@@ -100,6 +139,15 @@ export async function init(cwd, opts) {
100
139
 
101
140
  // --- siguientes pasos ---
102
141
  step("Siguientes pasos");
142
+ // Detect if ozali is NOT permanently in PATH (i.e., run via pnpm dlx / npx without global install).
143
+ if (!which("ozali")) {
144
+ const v = pkgVersion();
145
+ warn("ozali no está en tu PATH — fue ejecutado via dlx/npx sin instalación permanente.");
146
+ info(`Instala globalmente para usar ${c.bold("ozali doctor")}, ${c.bold("ozali sync")}, etc.:`);
147
+ console.log(` ${c.bold(`pnpm add -g ozali@${v}`)} ${c.dim("← recomendado")}`);
148
+ console.log(` ${c.dim("o")} ${c.bold(`npm install -g ozali@${v}`)}`);
149
+ console.log("");
150
+ }
103
151
  console.log(` 1. Abre tu agente en este proyecto.`);
104
152
  console.log(` 2. Escribe ${c.bold('"diagnostica el proyecto"')} o ${c.bold('"ozali"')} para arrancar el bootstrap (calibración + generación de la skill ${c.bold("cdk")}).`);
105
153
  console.log(` 3. Tras trabajar, corre ${c.bold("ozali sync")} para llevar el histórico al repo de conocimiento.`);
@@ -107,6 +155,165 @@ export async function init(cwd, opts) {
107
155
  return 0;
108
156
  }
109
157
 
158
+ /**
159
+ * Instala Engram con la mejor ruta disponible para el SO actual.
160
+ * macOS/Linux: Homebrew (recomendado) o Go install como fallback.
161
+ * Windows: Go install (recomendado) o binario manual.
162
+ * Devuelve true si el binario quedó disponible en PATH.
163
+ */
164
+ function installEngram() {
165
+ const plat = process.platform;
166
+
167
+ if (plat === "darwin" || plat === "linux") {
168
+ if (which("brew")) {
169
+ info("Instalando: " + c.bold("brew install gentleman-programming/tap/engram"));
170
+ spawnCmd("brew", ["install", "gentleman-programming/tap/engram"]);
171
+ } else if (which("go")) {
172
+ info("Homebrew no encontrado — instalando con Go:");
173
+ info(" " + c.bold("go install github.com/Gentleman-Programming/engram/cmd/engram@latest"));
174
+ spawnCmd("go", ["install", "github.com/Gentleman-Programming/engram/cmd/engram@latest"]);
175
+ } else {
176
+ warn("No se encontró Homebrew ni Go. Opciones de instalación:");
177
+ info(" a) Homebrew " + c.dim("(recomendado)") + ": " + c.cyan("https://brew.sh") + " → " + c.bold("brew install gentleman-programming/tap/engram"));
178
+ info(" b) Binario precompilado: " + c.cyan("https://github.com/Gentleman-Programming/engram/releases"));
179
+ return false;
180
+ }
181
+ } else if (plat === "win32") {
182
+ if (which("go")) {
183
+ info("Instalando: " + c.bold("go install github.com/Gentleman-Programming/engram/cmd/engram@latest"));
184
+ spawnCmd("go", ["install", "github.com/Gentleman-Programming/engram/cmd/engram@latest"]);
185
+ } else {
186
+ warn("Go no encontrado. Opciones de instalación en Windows:");
187
+ info(" a) Go 1.24+ " + c.dim("(recomendado)") + ": " + c.cyan("https://go.dev/dl/") + " → " + c.bold("go install github.com/Gentleman-Programming/engram/cmd/engram@latest"));
188
+ info(" b) Binario .zip: " + c.cyan("https://github.com/Gentleman-Programming/engram/releases"));
189
+ return false;
190
+ }
191
+ } else {
192
+ info("Guía de instalación completa: " + c.cyan("https://github.com/Gentleman-Programming/engram/blob/main/docs/INSTALLATION.md"));
193
+ return false;
194
+ }
195
+
196
+ const bin = which("engram");
197
+ if (bin) { ok("Engram instalado (" + bin + ")."); return true; }
198
+ warn("El binario engram no aparece en PATH tras la instalación.");
199
+ info("Puede que necesites reiniciar la terminal o ajustar tu PATH.");
200
+ return false;
201
+ }
202
+
203
+ /**
204
+ * Engram Cloud opt-in: réplica de equipo en tiempo real, adicional al git-sync.
205
+ * Configura el servidor y enrola el proyecto. Devuelve { enabled, server }.
206
+ */
207
+ async function maybeEnableEngramCloud(project, opts) {
208
+ if (opts.yes) return { enabled: false };
209
+ const enable = await confirm("¿Habilitar Engram Cloud para el equipo? (réplica opt-in, requiere un servidor)", false);
210
+ if (!enable) {
211
+ info("Cloud omitido. El histórico de equipo viaja por git-sync (" + c.bold("ozali sync") + ").");
212
+ return { enabled: false };
213
+ }
214
+ const server = await ask("URL del servidor de Engram Cloud", "http://127.0.0.1:18080");
215
+ info(`Configurando Engram Cloud → ${server}`);
216
+ spawnCmd("engram", ["cloud", "config", "--server", server]);
217
+ info(`Enrolando el proyecto "${project}"…`);
218
+ if (spawnCmd("engram", ["cloud", "enroll", project]) === 0) {
219
+ ok("Engram Cloud habilitado. Replica con " + c.bold("ozali sync --cloud") + ".");
220
+ return { enabled: true, server };
221
+ }
222
+ warn("No se pudo enrolar el proyecto en Engram Cloud. Continúo solo con git-sync.");
223
+ return { enabled: false, server };
224
+ }
225
+
226
+ function printEngramManualInstructions(agent) {
227
+ const plat = process.platform;
228
+ info("Para activar memoria buscable/acumulativa (modo " + c.bold("hybrid") + "):");
229
+ if (plat === "darwin" || plat === "linux") {
230
+ info(" 1. " + c.bold("brew install gentleman-programming/tap/engram") + " " + c.dim("(o binario: github.com/Gentleman-Programming/engram/releases)"));
231
+ } else if (plat === "win32") {
232
+ info(" 1. " + c.bold("go install github.com/Gentleman-Programming/engram/cmd/engram@latest") + " " + c.dim("(requiere Go 1.24+)"));
233
+ info(" " + c.dim("o binario .zip: github.com/Gentleman-Programming/engram/releases"));
234
+ } else {
235
+ info(" 1. " + c.cyan("https://github.com/Gentleman-Programming/engram/blob/main/docs/INSTALLATION.md"));
236
+ }
237
+ if (agent === "claude-code" || agent === "both") info(" 2. " + c.bold("engram setup claude-code"));
238
+ if (agent === "opencode" || agent === "both") info(" 2. " + c.bold("engram setup opencode"));
239
+ info(" 3. Corre " + c.bold("ozali doctor") + " — el modo subirá a hybrid automáticamente.");
240
+ }
241
+
242
+ // Perfil base de permisos para Claude Code: lectura/comandos comunes libres,
243
+ // destructivos denegados. Es un TEMPLATE — el usuario puede añadir más entradas y
244
+ // re-correr init no las pisa (hace unión de listas).
245
+ const CLAUDE_PERMS = {
246
+ allow: [
247
+ "WebFetch", "WebSearch",
248
+ "Bash(python *)", "Bash(python3 *)",
249
+ "Bash(node *)", "Bash(npm *)", "Bash(npx *)", "Bash(pnpm *)", "Bash(yarn *)",
250
+ "Bash(go *)", "Bash(mvn *)", "Bash(java *)",
251
+ "Bash(git status)", "Bash(git diff *)", "Bash(git log *)", "Bash(git add *)", "Bash(git commit *)",
252
+ "Bash(ozali *)", "Bash(engram *)",
253
+ "PowerShell(python *)", "PowerShell(node *)", "PowerShell(npm *)", "PowerShell(npx *)",
254
+ "PowerShell(mvn *)", "PowerShell(java *)",
255
+ ],
256
+ deny: [
257
+ "Bash(rm -rf *)", "Bash(git push *)",
258
+ "PowerShell(Remove-Item *)",
259
+ ],
260
+ };
261
+
262
+ function mergeUnique(existing, additions) {
263
+ const out = Array.isArray(existing) ? [...existing] : [];
264
+ const seen = new Set(out);
265
+ let added = 0;
266
+ for (const item of additions) if (!seen.has(item)) { out.push(item); seen.add(item); added++; }
267
+ return { out, added };
268
+ }
269
+
270
+ function ensureClaudeCodeProfile(cwd, scope) {
271
+ const settingsPath = scope === "global"
272
+ ? path.join(HOME, ".claude", "settings.json")
273
+ : path.join(cwd, ".claude", "settings.json");
274
+ const cfg = readJSON(settingsPath, {});
275
+ cfg.permissions = cfg.permissions || {};
276
+ const allowMerge = mergeUnique(cfg.permissions.allow, CLAUDE_PERMS.allow);
277
+ const denyMerge = mergeUnique(cfg.permissions.deny, CLAUDE_PERMS.deny);
278
+ cfg.permissions.allow = allowMerge.out;
279
+ cfg.permissions.deny = denyMerge.out;
280
+ writeJSON(settingsPath, cfg);
281
+ const rel = path.relative(cwd, settingsPath) || settingsPath;
282
+ if (allowMerge.added + denyMerge.added > 0) {
283
+ ok(`Perfil base de permisos de Claude Code en ${c.bold(rel)} (${allowMerge.added} allow / ${denyMerge.added} deny añadidos; tus reglas se conservan).`);
284
+ } else {
285
+ info(`Permisos de Claude Code ya cubiertos en ${c.bold(rel)} (sin cambios).`);
286
+ }
287
+ }
288
+
289
+ // Marca el proyecto como confiable en Claude Code (~/.claude.json). Sin esto, Claude Code
290
+ // ignora los permisos de un .claude/settings.json de proyecto ("workspace not trusted").
291
+ async function ensureClaudeWorkspaceTrust(cwd, opts) {
292
+ const p = path.join(HOME, ".claude.json");
293
+ if (!exists(p)) {
294
+ info("Claude Code aún no tiene ~/.claude.json; al abrirlo aquí, acepta el diálogo de confianza.");
295
+ return;
296
+ }
297
+ let cfg;
298
+ try { cfg = JSON.parse(fs.readFileSync(p, "utf8")); }
299
+ catch { warn("No pude leer ~/.claude.json; acepta el diálogo de confianza de Claude Code manualmente."); return; }
300
+ cfg.projects = cfg.projects || {};
301
+ cfg.projects[cwd] = cfg.projects[cwd] || {};
302
+ if (cfg.projects[cwd].hasTrustDialogAccepted === true) {
303
+ info("Claude Code ya confía en este workspace.");
304
+ return;
305
+ }
306
+ const doTrust = opts.yes ? true
307
+ : await confirm("¿Marcar este proyecto como confiable en Claude Code? (necesario para que apliquen los permisos)", true);
308
+ if (!doTrust) {
309
+ info("Workspace no marcado como confiable: Claude Code ignorará los permisos hasta que aceptes su diálogo.");
310
+ return;
311
+ }
312
+ cfg.projects[cwd].hasTrustDialogAccepted = true;
313
+ fs.writeFileSync(p, JSON.stringify(cfg, null, 2)); // formato que usa Claude Code (indent 2, sin newline final)
314
+ ok("Workspace marcado como confiable en Claude Code (los permisos de .claude/settings.json ya aplican).");
315
+ }
316
+
110
317
  function ensureOpencodeProfile(cwd) {
111
318
  const p = path.join(cwd, "opencode.json");
112
319
  const base = readJSON(p, {});
@@ -135,6 +342,9 @@ export function doctor(cwd) {
135
342
  add("Fuente de verdad", env.sot.found, env.sot.found ? `${env.sot.doc} + ${env.sot.dir}/` : "ausente (corre la skill 'ozali')");
136
343
  add("Skill ozali instalada", env.skill.installed, env.skill.installed ? env.skill.paths.map((p) => path.relative(cwd, p) || p).join(", ") : "no instalada (ozali init)");
137
344
  add("Engram", env.engram.available, env.engram.available ? env.engram.bin : "no instalado → modo docs");
345
+ const cloudOn = !!(cfg && cfg.cloud && cfg.cloud.enabled);
346
+ // Cloud es opt-in: "off" es un estado válido (no cuenta como fallo).
347
+ add("Engram Cloud", true, cloudOn ? `enrolado → ${cfg.cloud.server || "server por defecto"}` : "off (opt-in, git-sync activo)");
138
348
  add("Repo de conocimiento", !!(cfg && cfg.knowledgeRepo && exists(cfg.knowledgeRepo)), cfg ? cfg.knowledgeRepo : "sin configurar (ozali init)");
139
349
 
140
350
  // Strict TDD (de la fuente de verdad)
@@ -154,6 +364,23 @@ export function doctor(cwd) {
154
364
  console.log("");
155
365
  if (bad === 0) ok("Todo en orden. ozali está listo para trabajar.");
156
366
  else warn(`${bad} punto(s) a atender. Revisa los ✖ de arriba.`);
367
+
368
+ // Auto-upgrade: si Engram acaba de instalarse y el config aún dice "docs", subir a hybrid.
369
+ if (cfg && cfg.memoryMode === "docs" && env.engram.available) {
370
+ cfg.memoryMode = "hybrid";
371
+ writeJSON(CONFIG_PATH(cwd), cfg);
372
+ ok("Engram detectado → modo de memoria actualizado a " + c.bold("hybrid") + " en .ozali/config.json.");
373
+ }
374
+
375
+ // Estado de sync de Engram (informativo).
376
+ if (env.engram.available) {
377
+ const status = tryExec("engram", ["sync", "--status"], { cwd });
378
+ if (status) {
379
+ step("Estado de sync (Engram)");
380
+ for (const line of status.split(/\r?\n/)) console.log(" " + c.dim(line));
381
+ }
382
+ }
383
+
157
384
  return bad === 0 ? 0 : 1;
158
385
  }
159
386
 
@@ -192,13 +419,45 @@ export function sync(cwd, opts) {
192
419
  const docsLocal = path.join(cwd, ".ozali", "docs");
193
420
  const engramLocal = path.join(cwd, ".engram");
194
421
 
422
+ // Engram Cloud (opt-in): réplica bidireccional adicional al git-sync.
423
+ if (opts.cloud) {
424
+ if (cfg.cloud && cfg.cloud.enabled && tryExec("engram", ["--version"])) {
425
+ info(`Replicando con Engram Cloud (proyecto "${project}")…`);
426
+ if (spawnCmd("engram", ["sync", "--cloud", "--project", project], { cwd }) === 0) ok("Réplica con Engram Cloud completada.");
427
+ else warn("engram sync --cloud no terminó correctamente. Revisa el output de arriba.");
428
+ } else if (!(cfg.cloud && cfg.cloud.enabled)) {
429
+ warn("--cloud pedido, pero Engram Cloud no está habilitado. Corre " + c.bold("ozali init") + " para habilitarlo, o usa git-sync sin --cloud.");
430
+ } else {
431
+ warn("--cloud pedido, pero Engram no responde. Omito la réplica cloud.");
432
+ }
433
+ }
434
+
195
435
  if (opts.import) {
196
436
  // Repo de conocimiento → local
437
+ // 0) Traer lo más reciente del equipo (si el knowledge repo tiene remoto).
438
+ if (exists(path.join(kRepo, ".git")) && tryExec("git", ["remote", "get-url", "origin"], { cwd: kRepo })) {
439
+ info("Actualizando el repo de conocimiento (git pull)…");
440
+ if (spawnCmd("git", ["pull", "--ff-only"], { cwd: kRepo }) !== 0) {
441
+ warn("git pull no pudo completarse; importo lo que haya localmente.");
442
+ }
443
+ }
444
+ // 1) Docs
197
445
  const srcDocs = path.join(projDir, "docs");
198
446
  if (exists(srcDocs)) { copyDir(srcDocs, docsLocal); ok("Docs importados a .ozali/docs/."); }
447
+ else info("Aún no hay docs en el repo de conocimiento para este proyecto.");
448
+ // 2) Engram: copiar los chunks del repo de conocimiento → .engram/ ANTES de importar
449
+ // (engram sync --import lee de .engram/ en el cwd; sin esta copia, un dev nuevo
450
+ // no importaría nada).
199
451
  if (cfg.memoryMode === "hybrid" && tryExec("engram", ["--version"])) {
200
- info("Ejecutando engram sync --import…");
201
- tryExec("engram", ["sync", "--import"], { cwd });
452
+ const srcEngram = path.join(kRepo, "engram", project);
453
+ if (exists(srcEngram)) {
454
+ copyDir(srcEngram, engramLocal);
455
+ info("Chunks de Engram copiados a .engram/. Importando…");
456
+ if (spawnCmd("engram", ["sync", "--import"], { cwd }) === 0) ok("Memorias importadas a Engram local.");
457
+ else warn("engram sync --import no terminó correctamente. Revisa el output de arriba.");
458
+ } else {
459
+ info("Aún no hay export de Engram en el repo de conocimiento para este proyecto.");
460
+ }
202
461
  }
203
462
  info("Import completo. Revisa .ozali/docs/.");
204
463
  return 0;
@@ -208,8 +467,12 @@ export function sync(cwd, opts) {
208
467
  // 1) Engram export (si hybrid + disponible)
209
468
  if (cfg.memoryMode === "hybrid" && tryExec("engram", ["--version"])) {
210
469
  info("Exportando memorias con engram sync…");
211
- tryExec("engram", ["sync"], { cwd });
212
- if (exists(engramLocal)) { copyDir(engramLocal, path.join(kRepo, "engram", project)); ok("Export de Engram copiado al repo de conocimiento."); }
470
+ if (spawnCmd("engram", ["sync"], { cwd }) === 0) {
471
+ if (exists(engramLocal)) { copyDir(engramLocal, path.join(kRepo, "engram", project)); ok("Export de Engram copiado al repo de conocimiento."); }
472
+ else info("engram sync no generó .engram/ (sin memorias nuevas).");
473
+ } else {
474
+ warn("engram sync falló; sincronizo solo docs. Revisa el output de arriba.");
475
+ }
213
476
  } else if (cfg.memoryMode === "hybrid") {
214
477
  warn("Modo hybrid pero Engram no responde; sincronizo solo docs.");
215
478
  }
package/cli/lib/util.mjs CHANGED
@@ -79,6 +79,24 @@ export function tryExec(cmd, args = [], opts = {}) {
79
79
  }
80
80
  }
81
81
 
82
+ /** Ejecuta un comando con I/O visible en la terminal. Devuelve el exit code (0 = éxito). */
83
+ export function spawnCmd(cmd, args = [], opts = {}) {
84
+ try {
85
+ execFileSync(cmd, args, { stdio: "inherit", ...opts });
86
+ return 0;
87
+ } catch (e) {
88
+ return e.status ?? 1;
89
+ }
90
+ }
91
+
92
+ /** Detecta el gestor de paquetes disponible (pnpm > bun > yarn > npm). */
93
+ export function detectPkgManager() {
94
+ for (const pm of ["pnpm", "bun", "yarn", "npm"]) {
95
+ if (which(pm)) return pm;
96
+ }
97
+ return "npm";
98
+ }
99
+
82
100
  export function which(bin) {
83
101
  const finder = process.platform === "win32" ? "where" : "which";
84
102
  return tryExec(finder, [bin]);
@@ -57,12 +57,20 @@ test("doctor en proyecto vacío reporta pendientes (exit 1)", () => {
57
57
  test("init --yes instala la skill, aísla el histórico y escribe config", () => {
58
58
  const dir = tmpProject();
59
59
  try {
60
- run(["init", "--yes", "--agent", "claude-code", "--scope", "project", "--knowledge-repo", path.join(dir, ".k")], dir);
60
+ run(["init", "--yes", "--no-engram", "--no-trust", "--agent", "claude-code", "--scope", "project", "--knowledge-repo", path.join(dir, ".k")], dir);
61
61
  assert.ok(fs.existsSync(path.join(dir, ".claude", "skills", "ozali", "SKILL.md")), "SKILL.md instalada");
62
62
  assert.ok(fs.existsSync(path.join(dir, ".ozali", "config.json")), "config escrita");
63
+ const cfg = JSON.parse(fs.readFileSync(path.join(dir, ".ozali", "config.json"), "utf8"));
64
+ assert.equal(cfg.memoryMode, "docs", "--no-engram deja modo docs");
63
65
  const gi = fs.readFileSync(path.join(dir, ".gitignore"), "utf8");
64
66
  assert.match(gi, /\.ozali\//);
65
67
  assert.match(gi, /\.engram\//);
68
+ // perfil base de permisos de Claude Code
69
+ const settingsPath = path.join(dir, ".claude", "settings.json");
70
+ assert.ok(fs.existsSync(settingsPath), "settings.json de Claude Code escrito");
71
+ const settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
72
+ assert.ok(Array.isArray(settings.permissions.allow) && settings.permissions.allow.length > 0, "allow no vacío");
73
+ assert.ok(settings.permissions.deny.includes("Bash(rm -rf *)"), "deny bloquea rm -rf");
66
74
  } finally {
67
75
  fs.rmSync(dir, { recursive: true, force: true });
68
76
  }
@@ -71,7 +79,7 @@ test("init --yes instala la skill, aísla el histórico y escribe config", () =>
71
79
  test("init --dry-run no escribe nada", () => {
72
80
  const dir = tmpProject();
73
81
  try {
74
- run(["init", "--dry-run", "--yes"], dir);
82
+ run(["init", "--dry-run", "--yes", "--no-engram"], dir);
75
83
  assert.ok(!fs.existsSync(path.join(dir, ".ozali")), "no debe crear .ozali en dry-run");
76
84
  assert.ok(!fs.existsSync(path.join(dir, ".claude")), "no debe crear .claude en dry-run");
77
85
  } finally {
@@ -0,0 +1,166 @@
1
+ # Guía de uso — ozali
2
+
3
+ ← [README](../README.md) · ¿buscas el modelo mental? → [intended-usage.md](intended-usage.md)
4
+
5
+ Guía rápida y sin tecnicismos para empezar a usar **ozali** en cualquier proyecto. Si nunca lo
6
+ has usado, lee esto de principio a fin: son 5 minutos.
7
+
8
+ ---
9
+
10
+ ## ¿Qué es ozali, en simple?
11
+
12
+ ozali le enseña a tu asistente de IA (Claude Code u opencode) **cómo trabajar en tu proyecto**:
13
+ entiende tu código, propone un plan, pide tu aprobación antes de ejecutar, y **recuerda** lo que
14
+ el equipo hizo antes. Tú sigues mandando; ozali pone el método y el orden.
15
+
16
+ Piensa en tres piezas:
17
+
18
+ | Pieza | Para qué sirve |
19
+ |---|---|
20
+ | **El comando `ozali`** | Lo corres en la terminal. Instala y mantiene todo. |
21
+ | **La skill `ozali`** | Vive dentro de tu agente. Diagnostica el proyecto y prepara el trabajo. |
22
+ | **Engram** *(opcional)* | La "memoria" del equipo: lo aprendido se guarda y se comparte. |
23
+
24
+ ---
25
+
26
+ ## Paso a paso (primera vez)
27
+
28
+ ### 1. Instala ozali
29
+
30
+ ```bash
31
+ pnpm add -g ozali # recomendado
32
+ # o: npm install -g ozali
33
+ ```
34
+
35
+ > Puedes probarlo sin instalar con `pnpm dlx ozali init`, pero entonces el comando `ozali` **no
36
+ > queda disponible** después. Para usar `doctor`, `sync`, etc. en el día a día, instálalo con `-g`.
37
+
38
+ ### 2. Entra a tu proyecto y arranca
39
+
40
+ ```bash
41
+ cd mi-proyecto
42
+ ozali init
43
+ ```
44
+
45
+ El asistente te hará unas preguntas (qué agente usas, dónde guardar la memoria, si instalar
46
+ Engram). Puedes aceptar los valores por defecto si dudas. Al terminar, deja listo:
47
+
48
+ - la skill instalada en tu agente,
49
+ - los permisos base para que el agente **no te pregunte por cada comando** (ver más abajo),
50
+ - el aislamiento del histórico (no ensucia tu repo).
51
+
52
+ ### 3. Abre tu agente y diagnostica
53
+
54
+ Abre Claude Code (u opencode) en el proyecto y escribe:
55
+
56
+ ```
57
+ diagnostica el proyecto
58
+ ```
59
+
60
+ ozali revisará tu código, preparará la documentación base y te mostrará un **plan**. Nada se
61
+ ejecuta hasta que tú apruebes (eso es el 🛑 **GATE**).
62
+
63
+ ### 4. Trabaja normal
64
+
65
+ Pídele cosas a tu agente como siempre: *"crea el endpoint de login"*, *"corrige este bug"*, etc.
66
+ ozali se encarga de hacerlo con orden y de ir guardando el registro.
67
+
68
+ ### 5. Guarda y comparte (si trabajas en equipo)
69
+
70
+ ```bash
71
+ ozali sync
72
+ ```
73
+
74
+ Lleva el histórico y la memoria al repo de conocimiento del equipo. Tus compañeros lo traen con
75
+ `ozali sync --import`.
76
+
77
+ ---
78
+
79
+ ## Los comandos (chuleta)
80
+
81
+ | Comando | Qué hace |
82
+ |---|---|
83
+ | `ozali init` | Prepara el proyecto (lo corres una vez por repo). |
84
+ | `ozali doctor` | Revisa que todo esté bien. Es solo lectura, no cambia nada. |
85
+ | `ozali update` | Actualiza la skill a la última versión. |
86
+ | `ozali sync` | Sube el histórico/memoria al repo de equipo. |
87
+ | `ozali sync --import` | Baja lo que el equipo ya guardó. |
88
+
89
+ ¿Algo no funciona? Corre **`ozali doctor`** primero: te dice qué falta y cómo arreglarlo.
90
+
91
+ ---
92
+
93
+ ## Sobre los permisos (menos interrupciones)
94
+
95
+ Por defecto, los agentes piden confirmación **antes de cada comando**. Eso cansa. Durante `init`,
96
+ ozali crea un archivo `.claude/settings.json` con una lista de permisos razonable: deja correr
97
+ libremente cosas seguras (instalar dependencias, correr tests, leer archivos, `git status`…) y
98
+ **bloquea las peligrosas** (`rm -rf`, `git push`).
99
+
100
+ Es **un punto de partida, no una jaula**: puedes editar ese archivo y añadir lo que tu proyecto
101
+ necesite. Si vuelves a correr `ozali init`, tus reglas **se conservan** (no se borran ni se
102
+ duplican).
103
+
104
+ > **Confianza del workspace (Claude Code):** por seguridad, Claude Code **ignora** los permisos de
105
+ > un proyecto hasta que confías en él (verás un aviso *"this workspace has not been trusted"*).
106
+ > `init` te ofrece marcarlo como confiable automáticamente; si prefieres hacerlo tú, abre Claude
107
+ > Code en la carpeta y acepta el diálogo de confianza una vez. Con `--no-trust` ozali no lo toca.
108
+
109
+ Ejemplo — permitir Docker además de lo de fábrica:
110
+
111
+ ```json
112
+ {
113
+ "permissions": {
114
+ "allow": ["Bash(docker *)"],
115
+ "deny": []
116
+ }
117
+ }
118
+ ```
119
+
120
+ ---
121
+
122
+ ## Engram: la memoria del equipo (opcional)
123
+
124
+ Durante `init`, ozali **instala Engram por ti**: en modo interactivo te pregunta (basta con aceptar),
125
+ y en modo automático (`--yes`) lo instala solo. Lo baja con brew (macOS/Linux) o `go` (Windows), y si
126
+ no puede, te deja las instrucciones. Con Engram, tu agente **recuerda** decisiones, bugs resueltos y
127
+ convenciones — y los recupera en futuras sesiones.
128
+
129
+ ¿No quieres Engram ahora? Pasa `--no-engram` y ozali arranca en modo `docs` (la memoria se queda en
130
+ documentos legibles). Cuando quieras activarlo, instálalo y corre `ozali doctor`: el modo sube a
131
+ `hybrid` solo.
132
+
133
+ Dos detalles si lo usas en equipo:
134
+
135
+ - **Idioma:** guarda la memoria compartida **en español** (y búscala en español). El buscador no
136
+ mezcla idiomas: si guardas en español y buscas en inglés, no encuentra nada.
137
+ - **Privado vs. compartido:** lo que marques como `scope: project` lo ve todo el equipo. Para notas
138
+ personales usa `scope: personal` (aunque hoy el sync comparte ambos — para algo realmente privado,
139
+ no lo pongas en el proyecto de equipo).
140
+
141
+ Más detalle técnico en [engram-convention.md](../skill/references/engram-convention.md).
142
+
143
+ ---
144
+
145
+ ## Preguntas frecuentes
146
+
147
+ **¿Tengo que usar Engram?**
148
+ No. Es opcional. Sin él, ozali funciona en modo `docs`.
149
+
150
+ **¿ozali toca mi código sin permiso?**
151
+ No. Siempre hay un plan y un GATE de aprobación antes de ejecutar.
152
+
153
+ **Escribí `ozali` y la terminal dice "command not found".**
154
+ Lo corriste con `dlx`/`npx`, que es temporal. Instálalo global: `pnpm add -g ozali`.
155
+
156
+ **¿Esto ensucia mi repositorio?**
157
+ No. El histórico y la memoria se aíslan (`.ozali/`, `.engram/` van al `.gitignore`).
158
+
159
+ **Veo "this workspace has not been trusted" y mis permisos no aplican.**
160
+ Es el aviso de seguridad de Claude Code: ignora los permisos del proyecto hasta confiar en él.
161
+ Deja que `init` lo marque como confiable, o abre Claude Code en la carpeta y acepta el diálogo una
162
+ vez. Tras eso, el aviso desaparece y los permisos surten efecto.
163
+
164
+ **¿Funciona en Windows?**
165
+ Sí. Los permisos base incluyen variantes PowerShell, y Engram se instala con `go install` o un
166
+ binario descargable.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ozali",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Bootstrap interactivo de IA por equipo: calibra el proyecto, genera la skill de ejecución (cdk) con TDD/SDD, y mantiene memoria de equipo (Engram) con histórico aislado.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -33,6 +33,44 @@ Sin remoto, el nombre de carpeta. Mantén UN nombre por proyecto (evita drift `m
33
33
 
34
34
  ---
35
35
 
36
+ ## 1.5. Memoria de equipo (scope + idioma)
37
+
38
+ La memoria de Engram es **compartida**: lo que un dev guarda en `scope: project` lo recuperan los
39
+ agentes de sus compañeros tras `ozali sync` / `ozali sync --import`. Dos convenciones son
40
+ **obligatorias** para que esa memoria compartida sirva como herramienta de equipo.
41
+
42
+ ### Scope: `project` vs `personal`
43
+
44
+ | Guarda como `scope: project` | Guarda como `scope: personal` |
45
+ |---|---|
46
+ | Decisiones de arquitectura y trade-offs | Notas y aprendizajes personales |
47
+ | Bugfixes que afectan a otros | Atajos de editor / dotfiles |
48
+ | Convenciones, naming, *gotchas* del repo | Preferencias de estilo o workflow |
49
+ | Contratos de API, quirks de despliegue | Enlaces para releer luego |
50
+ | Contexto de onboarding | TODOs y recordatorios personales |
51
+
52
+ > Regla rápida: **si el agente de un compañero debería encontrarlo, es `scope: project`.** Todos los
53
+ > artefactos automáticos de `cdk` (§2) van en `scope: project`.
54
+
55
+ > ⚠️ **El sync exporta AMBOS scopes.** Hoy `scope` filtra búsqueda, **no** transporte: al sincronizar
56
+ > un proyecto se comparten también sus observaciones `personal`. Si necesitas notas verdaderamente
57
+ > privadas, guárdalas bajo **otro nombre de proyecto** (p. ej. `tu-nombre-notas`) que no sincronices,
58
+ > o no las pongas en Engram.
59
+
60
+ ### Idioma de la memoria compartida: **español**
61
+
62
+ FTS5 (el buscador de Engram) **no es multilingüe**: una búsqueda en un idioma no matchea memorias
63
+ guardadas en otro. Por eso este equipo fija una **lingua franca** para `scope: project`:
64
+
65
+ - **`scope: project` → siempre en español.** Títulos, `topic_key` semánticos y contenido.
66
+ - **`scope: personal` → cualquier idioma** (nadie más busca tu scope personal).
67
+
68
+ > 🔑 **Regla de consistencia:** como elegimos español, **tanto al guardar como al buscar**
69
+ > `scope: project` se usa español. Una `mem_search` en inglés sobre memoria guardada en español
70
+ > **no devuelve nada** — la búsqueda se fragmenta y la memoria de equipo deja de funcionar.
71
+
72
+ ---
73
+
36
74
  ## 2. Naming determinista
37
75
 
38
76
  Todos los artefactos de `cdk` en Engram siguen: