ozali 0.8.0 → 0.9.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 +1 -0
- package/cli/bin/ozali.mjs +3 -2
- package/cli/lib/commands.mjs +194 -11
- package/cli/lib/detect.mjs +27 -0
- package/cli/lib/util.mjs +55 -0
- package/cli/test/smoke.test.mjs +44 -1
- package/docs/obsidian-integration.md +93 -0
- package/package.json +1 -1
- package/templates/obsidian-vault/.obsidian/app.json +12 -0
- package/templates/obsidian-vault/.obsidian/core-plugins.json +9 -0
- package/templates/obsidian-vault/MOCs/Bugfixes Recientes.md +9 -0
- package/templates/obsidian-vault/MOCs/Decisiones del Equipo.md +9 -0
- package/templates/obsidian-vault/MOCs/M/303/251tricas y Tokens.md" +9 -0
- package/templates/obsidian-vault/MOCs/Proyectos.md +17 -0
- package/templates/obsidian-vault/MOCs//303/215ndice Global.md" +19 -0
- package/templates/obsidian-vault/README.md +22 -0
package/README.md
CHANGED
|
@@ -167,6 +167,7 @@ Claude Code y opencode (perfiles de permisos para ambos en
|
|
|
167
167
|
| Histórico aislado y memoria de equipo | [docs/team-history.md](docs/team-history.md) |
|
|
168
168
|
| Desplegar Engram Cloud (VPS) | [docs/deploy-cloud-vps.md](docs/deploy-cloud-vps.md) |
|
|
169
169
|
| Desplegar Engram Cloud (Google Cloud) | [docs/deploy-cloud-gcloud.md](docs/deploy-cloud-gcloud.md) |
|
|
170
|
+
| Integración con Obsidian | [docs/obsidian-integration.md](docs/obsidian-integration.md) |
|
|
170
171
|
| Skill bootstrap | [skill/SKILL.md](skill/SKILL.md) |
|
|
171
172
|
| Calibración de testing + TDD | [skill/references/calibration-blueprint.md](skill/references/calibration-blueprint.md) |
|
|
172
173
|
| Contrato y versión de `cdk` | [skill/references/cdk-contract.md](skill/references/cdk-contract.md) |
|
package/cli/bin/ozali.mjs
CHANGED
|
@@ -63,6 +63,7 @@ function parseArgs(argv) {
|
|
|
63
63
|
else if (a === "--import") opts.import = true;
|
|
64
64
|
else if (a === "--push") opts.push = true;
|
|
65
65
|
else if (a === "--cloud") opts.cloud = true;
|
|
66
|
+
else if (a === "--obsidian") opts.obsidian = true;
|
|
66
67
|
else if (a === "--no-engram") opts.noEngram = true;
|
|
67
68
|
else if (a === "--no-trust") opts.noTrust = true;
|
|
68
69
|
else if (a === "--no-jarvis") opts.noJarvis = true;
|
|
@@ -100,8 +101,8 @@ async function main() {
|
|
|
100
101
|
case "init": return await init(cwd, opts);
|
|
101
102
|
case "workspace": return await workspace(cwd, opts);
|
|
102
103
|
case "doctor": return doctor(cwd);
|
|
103
|
-
case "update": return update(cwd, opts);
|
|
104
|
-
case "sync": return sync(cwd, opts);
|
|
104
|
+
case "update": return await update(cwd, opts);
|
|
105
|
+
case "sync": return await sync(cwd, opts);
|
|
105
106
|
case "audit": return await audit(cwd, opts);
|
|
106
107
|
case "cloud": return await cloud(cwd, opts);
|
|
107
108
|
default:
|
package/cli/lib/commands.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
SKILL_SRC, COMMIT_SKILL_SRC, TEMPLATES_SRC, exists, ensureDir, copyDir, readJSON, writeJSON,
|
|
8
8
|
ensureGitignore, tryExec, spawnCmd, which, engramAssetName, pickEngramAsset,
|
|
9
9
|
projectName, pkgVersion, DEFAULT_KNOWLEDGE, HOME, openURL, gitInfo,
|
|
10
|
+
toPortablePath, fromPortablePath,
|
|
10
11
|
} from "./util.mjs";
|
|
11
12
|
import { detectAll, detectWorkspace, detectReferences } from "./detect.mjs";
|
|
12
13
|
import { ask, confirm, select } from "./prompt.mjs";
|
|
@@ -115,7 +116,8 @@ export async function init(cwd, opts) {
|
|
|
115
116
|
], 0);
|
|
116
117
|
|
|
117
118
|
// Repo de conocimiento (histórico aislado)
|
|
118
|
-
const
|
|
119
|
+
const knowledgeRepoRaw = opts.knowledgeRepo || await ask("Ruta del repo de conocimiento (histórico aislado)", DEFAULT_KNOWLEDGE);
|
|
120
|
+
const knowledgeRepo = fromPortablePath(knowledgeRepoRaw, cwd);
|
|
119
121
|
|
|
120
122
|
// Engram
|
|
121
123
|
let memoryMode = "docs";
|
|
@@ -124,6 +126,24 @@ export async function init(cwd, opts) {
|
|
|
124
126
|
} else if (env.engram.available) {
|
|
125
127
|
memoryMode = "hybrid";
|
|
126
128
|
ok(`Engram disponible (${env.engram.bin}). Modo de memoria: ${c.bold("hybrid")} (docs + Engram).`);
|
|
129
|
+
// Verificar si hay versión nueva con cooldown de seguridad
|
|
130
|
+
const versionCheck = checkEngramVersion();
|
|
131
|
+
if (versionCheck && versionCheck.canUpgrade) {
|
|
132
|
+
warn(`Hay una nueva versión de Engram: ${c.bold(versionCheck.latest)} (tienes ${versionCheck.current}).`);
|
|
133
|
+
if (await confirm("¿Actualizar Engram ahora?", false)) {
|
|
134
|
+
info("Actualizando Engram…");
|
|
135
|
+
if (process.platform === "darwin" && which("brew")) {
|
|
136
|
+
spawnCmd("brew", ["upgrade", "gentleman-programming/tap/engram"]);
|
|
137
|
+
} else if (which("go")) {
|
|
138
|
+
spawnCmd("go", ["install", "github.com/Gentleman-Programming/engram/cmd/engram@latest"]);
|
|
139
|
+
} else {
|
|
140
|
+
warn("No se puede auto-actualizar sin Homebrew (macOS) o Go. Descarga manual:");
|
|
141
|
+
info(" " + c.cyan(versionCheck.url));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
} else if (versionCheck && versionCheck.cooldown) {
|
|
145
|
+
info(`Engram ${c.bold(versionCheck.latest)} está disponible pero aún en cooldown de seguridad (24h). Se activará el ${new Date(new Date(versionCheck.publishedAt).getTime() + 24*60*60*1000).toLocaleDateString()}.`);
|
|
146
|
+
}
|
|
127
147
|
} else {
|
|
128
148
|
warn("Engram no está instalado.");
|
|
129
149
|
// --dry-run no instala; --yes usa el default (sí); interactivo pregunta.
|
|
@@ -155,6 +175,23 @@ export async function init(cwd, opts) {
|
|
|
155
175
|
}
|
|
156
176
|
}
|
|
157
177
|
|
|
178
|
+
// Obsidian (opt-in) — ofrecer instalación si no detectado
|
|
179
|
+
if (!env.obsidian.installed) {
|
|
180
|
+
warn("Obsidian no detectado. Es el visualizador recomendado para el vault de conocimiento.");
|
|
181
|
+
if (!opts.dryRun) {
|
|
182
|
+
const installObsidian = opts.yes ? false : await confirm("¿Abrir la página de descarga de Obsidian?", false);
|
|
183
|
+
if (installObsidian) {
|
|
184
|
+
const url = process.platform === "darwin" ? "https://obsidian.md/download"
|
|
185
|
+
: process.platform === "win32" ? "https://obsidian.md/download"
|
|
186
|
+
: "https://obsidian.md/download";
|
|
187
|
+
openURL(url);
|
|
188
|
+
info("Descarga e instala Obsidian, luego corre " + c.bold("ozali sync --obsidian") + " para generar el vault.");
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
ok(`Obsidian detectado (${c.dim(env.obsidian.path)}).`);
|
|
193
|
+
}
|
|
194
|
+
|
|
158
195
|
// Engram Cloud (opt-in) — réplica de equipo además del git-sync. Solo si Engram quedó disponible.
|
|
159
196
|
let cloud = { enabled: false };
|
|
160
197
|
if (memoryMode === "hybrid" && !opts.dryRun) {
|
|
@@ -220,7 +257,9 @@ export async function init(cwd, opts) {
|
|
|
220
257
|
|
|
221
258
|
// 5) config local (gitignored)
|
|
222
259
|
const config = {
|
|
223
|
-
version: pkgVersion(), agent, scope,
|
|
260
|
+
version: pkgVersion(), agent, scope,
|
|
261
|
+
knowledgeRepo: toPortablePath(knowledgeRepo, cwd),
|
|
262
|
+
memoryMode, cloud,
|
|
224
263
|
project: projectName(cwd), createdAt: new Date().toISOString(),
|
|
225
264
|
};
|
|
226
265
|
writeJSON(CONFIG_PATH(cwd), config);
|
|
@@ -244,6 +283,53 @@ export async function init(cwd, opts) {
|
|
|
244
283
|
return 0;
|
|
245
284
|
}
|
|
246
285
|
|
|
286
|
+
/**
|
|
287
|
+
* Consulta la API de releases de Engram y compara con la versión instalada.
|
|
288
|
+
* Si hay una versión estable más reciente cuyo release tenga >24h de antigüedad,
|
|
289
|
+
* advierte al usuario y ofrece upgrade. Si la versión nueva tiene <24h, ignora
|
|
290
|
+
* (cooldown de seguridad contra supply-chain attacks).
|
|
291
|
+
* Devuelve { current, latest, url, canUpgrade } o null si no hay info.
|
|
292
|
+
*/
|
|
293
|
+
function checkEngramVersion() {
|
|
294
|
+
const currentRaw = tryExec("engram", ["version"]);
|
|
295
|
+
if (!currentRaw) return null;
|
|
296
|
+
const current = currentRaw.trim().replace(/^engram\s+/, "");
|
|
297
|
+
const raw = fetchText(ENGRAM_RELEASES_LIST);
|
|
298
|
+
if (!raw) return null;
|
|
299
|
+
let releases;
|
|
300
|
+
try { releases = JSON.parse(raw); } catch { return null; }
|
|
301
|
+
if (!Array.isArray(releases)) return null;
|
|
302
|
+
const now = Date.now();
|
|
303
|
+
const COOLDOWN_MS = 24 * 60 * 60 * 1000;
|
|
304
|
+
for (const r of releases) {
|
|
305
|
+
if (!r || r.draft || r.prerelease) continue;
|
|
306
|
+
const m = /^v(\d+\.\d+\.\d+)$/.exec(r.tag_name || "");
|
|
307
|
+
if (!m) continue;
|
|
308
|
+
const latest = m[1];
|
|
309
|
+
if (compareSemver(latest, current) <= 0) break; // no hay nada más nuevo
|
|
310
|
+
const published = r.published_at ? new Date(r.published_at).getTime() : 0;
|
|
311
|
+
if (!published || now - published < COOLDOWN_MS) {
|
|
312
|
+
// Versión muy reciente — mostrar como disponible pero con cooldown activo
|
|
313
|
+
return { current, latest, url: r.html_url, canUpgrade: false, cooldown: true, publishedAt: r.published_at };
|
|
314
|
+
}
|
|
315
|
+
// Versión estable con cooldown cumplido
|
|
316
|
+
const asset = pickEngramAsset([r], process.platform, process.arch);
|
|
317
|
+
return { current, latest, url: asset ? asset.url : r.html_url, canUpgrade: true, cooldown: false, publishedAt: r.published_at };
|
|
318
|
+
}
|
|
319
|
+
return { current, latest: current, canUpgrade: false };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Comparación semver simple: devuelve >0 si a>b, <0 si a<b, 0 si iguales. */
|
|
323
|
+
function compareSemver(a, b) {
|
|
324
|
+
const pa = a.split(".").map((n) => parseInt(n, 10));
|
|
325
|
+
const pb = b.split(".").map((n) => parseInt(n, 10));
|
|
326
|
+
for (let i = 0; i < 3; i++) {
|
|
327
|
+
const na = pa[i] || 0, nb = pb[i] || 0;
|
|
328
|
+
if (na !== nb) return na - nb;
|
|
329
|
+
}
|
|
330
|
+
return 0;
|
|
331
|
+
}
|
|
332
|
+
|
|
247
333
|
/**
|
|
248
334
|
* Instala Engram con la mejor ruta disponible para el SO actual.
|
|
249
335
|
* Linux: binario precompilado (auto-descarga, sin toolchain) con brew/go como alternativas.
|
|
@@ -795,7 +881,7 @@ export async function workspace(cwd, opts = {}) {
|
|
|
795
881
|
|
|
796
882
|
// Modos batch (Track 1): operan sobre los miembros del workspace ya existente y salen.
|
|
797
883
|
if (opts.wsDoctor) return workspaceDoctor(ws.members);
|
|
798
|
-
if (opts.wsUpdate) return workspaceUpdate(ws.members, opts);
|
|
884
|
+
if (opts.wsUpdate) return await workspaceUpdate(ws.members, opts);
|
|
799
885
|
|
|
800
886
|
// Fase B — remediación de los que no tienen ozali init
|
|
801
887
|
const missing = ws.members.filter((m) => m.status === "missing-init");
|
|
@@ -807,7 +893,7 @@ export async function workspace(cwd, opts = {}) {
|
|
|
807
893
|
const shared = {
|
|
808
894
|
agent: opts.agent || (base && base.agent),
|
|
809
895
|
scope: opts.scope || (base && base.scope),
|
|
810
|
-
knowledgeRepo: opts.knowledgeRepo || (base && base.knowledgeRepo),
|
|
896
|
+
knowledgeRepo: fromPortablePath(opts.knowledgeRepo || (base && base.knowledgeRepo), root),
|
|
811
897
|
};
|
|
812
898
|
for (const m of missing) {
|
|
813
899
|
const go = opts.yes ? true : await confirm(`¿Correr ${c.bold("ozali init")} en ${c.bold(m.dir)}?`, true);
|
|
@@ -888,7 +974,7 @@ function workspaceDoctor(members) {
|
|
|
888
974
|
}
|
|
889
975
|
|
|
890
976
|
/** Track 1 — update de todos los miembros ozali (skills/permisos/jarvis) + resumen. */
|
|
891
|
-
function workspaceUpdate(members, opts) {
|
|
977
|
+
async function workspaceUpdate(members, opts) {
|
|
892
978
|
const results = [];
|
|
893
979
|
let failed = 0;
|
|
894
980
|
for (const m of members) {
|
|
@@ -899,7 +985,7 @@ function workspaceUpdate(members, opts) {
|
|
|
899
985
|
results.push({ dir: m.dir, mark: c.yellow("—"), note: "sin init (saltado)" });
|
|
900
986
|
continue;
|
|
901
987
|
}
|
|
902
|
-
const code = update(m.path, opts);
|
|
988
|
+
const code = await update(m.path, opts);
|
|
903
989
|
if (code !== 0) failed++;
|
|
904
990
|
results.push({ dir: m.dir, mark: code === 0 ? c.green("✔") : c.yellow("✖"), note: code === 0 ? "actualizado" : "revisar" });
|
|
905
991
|
}
|
|
@@ -956,7 +1042,10 @@ function writeWorkspaceManifest(root, members, references, opts) {
|
|
|
956
1042
|
version: pkgVersion(),
|
|
957
1043
|
root,
|
|
958
1044
|
agent,
|
|
959
|
-
knowledgeRepo:
|
|
1045
|
+
knowledgeRepo: toPortablePath(
|
|
1046
|
+
opts.knowledgeRepo || existing.knowledgeRepo || base.knowledgeRepo || DEFAULT_KNOWLEDGE,
|
|
1047
|
+
root
|
|
1048
|
+
),
|
|
960
1049
|
cloud: base.cloud || existing.cloud || { enabled: false },
|
|
961
1050
|
members: members.map((m) => ({ path: m.dir, project: m.project, status: m.status, sot: m.sot.found ? m.sot.variant : null })),
|
|
962
1051
|
references: references.map((e) => ({ from: e.fromDir, to: e.toDir, kind: e.kind })),
|
|
@@ -1056,7 +1145,9 @@ export function doctor(cwd) {
|
|
|
1056
1145
|
cloudMeta && cloudMeta.dashboard ? c.cyan(cloudMeta.dashboard) : ""].filter(Boolean).join(" · ")
|
|
1057
1146
|
: "off (opt-in, git-sync activo)";
|
|
1058
1147
|
add("Engram Cloud", true, cloudDetail);
|
|
1059
|
-
|
|
1148
|
+
const kRepoPortable = cfg && cfg.knowledgeRepo;
|
|
1149
|
+
const kRepoResolved = kRepoPortable ? fromPortablePath(kRepoPortable, cwd) : null;
|
|
1150
|
+
add("Repo de conocimiento", !!(kRepoResolved && exists(kRepoResolved)), kRepoResolved || "sin configurar (ozali init)");
|
|
1060
1151
|
|
|
1061
1152
|
// Strict TDD (de la fuente de verdad)
|
|
1062
1153
|
const tdd = readStrictTdd(cwd, env.sot);
|
|
@@ -1166,7 +1257,7 @@ function readStrictTdd(cwd, sot) {
|
|
|
1166
1257
|
// references), los perfiles de permisos y **crea/refresca ozali-jarvis** (clave para repos
|
|
1167
1258
|
// inicializados antes de 0.4.0). La skill `cdk` la regenera el AGENTE (no el CLI): se detecta
|
|
1168
1259
|
// y se guía la regeneración.
|
|
1169
|
-
export function update(cwd, opts = {}) {
|
|
1260
|
+
export async function update(cwd, opts = {}) {
|
|
1170
1261
|
step("ozali update — actualizar la instalación al paquete actual");
|
|
1171
1262
|
const env = detectAll(cwd);
|
|
1172
1263
|
const cfgPath = CONFIG_PATH(cwd);
|
|
@@ -1232,6 +1323,37 @@ export function update(cwd, opts = {}) {
|
|
|
1232
1323
|
info("cdk aún no generada en este repo. Corre la skill " + c.bold("ozali") + " en tu agente para crearla.");
|
|
1233
1324
|
}
|
|
1234
1325
|
|
|
1326
|
+
// 4.5) Engram version check (cooldown 24h)
|
|
1327
|
+
if (env.engram.available) {
|
|
1328
|
+
const versionCheck = checkEngramVersion();
|
|
1329
|
+
if (versionCheck && versionCheck.canUpgrade) {
|
|
1330
|
+
warn(`Hay una nueva versión de Engram: ${c.bold(versionCheck.latest)} (tienes ${versionCheck.current}).`);
|
|
1331
|
+
if (await confirm("¿Actualizar Engram ahora?", false)) {
|
|
1332
|
+
info("Actualizando Engram…");
|
|
1333
|
+
if (process.platform === "darwin" && which("brew")) {
|
|
1334
|
+
spawnCmd("brew", ["upgrade", "gentleman-programming/tap/engram"]);
|
|
1335
|
+
} else if (which("go")) {
|
|
1336
|
+
spawnCmd("go", ["install", "github.com/Gentleman-Programming/engram/cmd/engram@latest"]);
|
|
1337
|
+
} else {
|
|
1338
|
+
warn("No se puede auto-actualizar sin Homebrew (macOS) o Go. Descarga manual:");
|
|
1339
|
+
info(" " + c.cyan(versionCheck.url));
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
} else if (versionCheck && versionCheck.cooldown) {
|
|
1343
|
+
info(`Engram ${c.bold(versionCheck.latest)} está disponible pero aún en cooldown de seguridad (24h). Se activará el ${new Date(new Date(versionCheck.publishedAt).getTime() + 24*60*60*1000).toLocaleDateString()}.`);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// 4.6) Obsidian check
|
|
1348
|
+
if (!env.obsidian.installed) {
|
|
1349
|
+
warn("Obsidian no detectado. Es el visualizador recomendado para el vault de conocimiento.");
|
|
1350
|
+
const installObsidian = opts.yes ? false : await confirm("¿Abrir la página de descarga de Obsidian?", false);
|
|
1351
|
+
if (installObsidian) {
|
|
1352
|
+
openURL("https://obsidian.md/download");
|
|
1353
|
+
info("Descarga e instala Obsidian, luego corre " + c.bold("ozali sync --obsidian") + " para generar el vault.");
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1235
1357
|
// 5) versión del config
|
|
1236
1358
|
if (cfg) { cfg.version = pkgVersion(); cfg.updatedAt = new Date().toISOString(); writeJSON(cfgPath, cfg); }
|
|
1237
1359
|
ok(`Instalación al día con ozali v${pkgVersion()}.`);
|
|
@@ -1274,11 +1396,11 @@ function cdkCanonicalVersion() {
|
|
|
1274
1396
|
}
|
|
1275
1397
|
|
|
1276
1398
|
// ============================================================= sync ===========
|
|
1277
|
-
export function sync(cwd, opts) {
|
|
1399
|
+
export async function sync(cwd, opts) {
|
|
1278
1400
|
step(`ozali sync${opts.import ? " --import" : ""}${opts.cloud ? " --cloud" : ""} — histórico ↔ repo de conocimiento`);
|
|
1279
1401
|
const cfg = readJSON(CONFIG_PATH(cwd));
|
|
1280
1402
|
if (!cfg || !cfg.knowledgeRepo) { warn("Sin repo de conocimiento configurado. Corre " + c.bold("ozali init") + "."); return 1; }
|
|
1281
|
-
const kRepo = cfg.knowledgeRepo;
|
|
1403
|
+
const kRepo = fromPortablePath(cfg.knowledgeRepo, cwd);
|
|
1282
1404
|
if (!exists(kRepo)) { err(`El repo de conocimiento no existe: ${kRepo}`); return 1; }
|
|
1283
1405
|
const project = cfg.project || projectName(cwd);
|
|
1284
1406
|
const projDir = path.join(kRepo, "projects", project);
|
|
@@ -1383,6 +1505,16 @@ export function sync(cwd, opts) {
|
|
|
1383
1505
|
if (exists(docsLocal)) { copyDir(docsLocal, path.join(projDir, "docs")); ok(`Docs copiados a projects/${project}/docs/.`); }
|
|
1384
1506
|
else info("No hay .ozali/docs/ que sincronizar todavía.");
|
|
1385
1507
|
|
|
1508
|
+
// 2.5) Obsidian vault export
|
|
1509
|
+
const vaultPath = path.join(kRepo, "obsidian");
|
|
1510
|
+
if (opts.obsidian) {
|
|
1511
|
+
await exportObsidianVault(kRepo, project, vaultPath);
|
|
1512
|
+
} else if (exists(vaultPath) && !opts.yes) {
|
|
1513
|
+
if (await confirm("¿Exportar memoria a Obsidian vault?", true)) {
|
|
1514
|
+
await exportObsidianVault(kRepo, project, vaultPath);
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1386
1518
|
// 3) commit (push solo si hay remoto)
|
|
1387
1519
|
if (exists(path.join(kRepo, ".git"))) {
|
|
1388
1520
|
tryExec("git", ["add", "-A"], { cwd: kRepo });
|
|
@@ -1400,6 +1532,57 @@ export function sync(cwd, opts) {
|
|
|
1400
1532
|
return 0;
|
|
1401
1533
|
}
|
|
1402
1534
|
|
|
1535
|
+
/**
|
|
1536
|
+
* Exporta la memoria de Engram a un vault de Obsidian compatible.
|
|
1537
|
+
* 1) Copia templates base si no existen.
|
|
1538
|
+
* 2) Genera MOCs dinámicos (proyectos) desde knowledgeRepo/projects/.
|
|
1539
|
+
* 3) Ejecuta `engram obsidian-export`.
|
|
1540
|
+
*/
|
|
1541
|
+
async function exportObsidianVault(kRepo, project, vaultPath) {
|
|
1542
|
+
if (!tryExec("engram", ["--version"])) {
|
|
1543
|
+
warn("Engram no está disponible. No se puede exportar a Obsidian.");
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
ensureDir(vaultPath);
|
|
1547
|
+
// 1) Templates base
|
|
1548
|
+
const templateDir = path.join(TEMPLATES_SRC, "obsidian-vault");
|
|
1549
|
+
if (exists(templateDir)) {
|
|
1550
|
+
for (const entry of fs.readdirSync(templateDir, { withFileTypes: true })) {
|
|
1551
|
+
const src = path.join(templateDir, entry.name);
|
|
1552
|
+
const dst = path.join(vaultPath, entry.name);
|
|
1553
|
+
if (entry.isDirectory()) {
|
|
1554
|
+
if (!exists(dst)) copyDir(src, dst);
|
|
1555
|
+
} else if (!exists(dst)) {
|
|
1556
|
+
fs.copyFileSync(src, dst);
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
// 2) MOC dinámico — Proyectos
|
|
1561
|
+
const projectsDir = path.join(kRepo, "projects");
|
|
1562
|
+
const projectsMoc = path.join(vaultPath, "MOCs", "Proyectos.md");
|
|
1563
|
+
if (exists(projectsDir)) {
|
|
1564
|
+
const projects = [];
|
|
1565
|
+
for (const entry of fs.readdirSync(projectsDir, { withFileTypes: true })) {
|
|
1566
|
+
if (entry.isDirectory()) projects.push(entry.name);
|
|
1567
|
+
}
|
|
1568
|
+
const list = projects.map((p) => `- [[${p}]] — proyecto activo`).join("\n");
|
|
1569
|
+
const body = fs.readFileSync(projectsMoc, "utf8");
|
|
1570
|
+
const updated = body.replace(
|
|
1571
|
+
/<!-- PROJECTS_START -->[\s\S]*?<!-- PROJECTS_END -->/,
|
|
1572
|
+
`<!-- PROJECTS_START -->\n${list || "- Sin proyectos activos todavía."}\n<!-- PROJECTS_END -->`
|
|
1573
|
+
);
|
|
1574
|
+
fs.writeFileSync(projectsMoc, updated);
|
|
1575
|
+
}
|
|
1576
|
+
// 3) Export Engram
|
|
1577
|
+
info("Exportando memoria a Obsidian vault…");
|
|
1578
|
+
const out = tryExec("engram", ["obsidian-export", "--vault", vaultPath, "--project", project, "--graph-config", "preserve"]);
|
|
1579
|
+
if (out !== null) {
|
|
1580
|
+
ok("Obsidian vault actualizado.");
|
|
1581
|
+
} else {
|
|
1582
|
+
warn("engram obsidian-export falló. Revisa que el vault esté cerrado en Obsidian.");
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1403
1586
|
// ============================================================ audit ===========
|
|
1404
1587
|
// Navega/audita la memoria de Engram: del proyecto actual o general (todos los
|
|
1405
1588
|
// proyectos). Sin contexto de proyecto en la ruta → general. Sin Engram → audita
|
package/cli/lib/detect.mjs
CHANGED
|
@@ -47,6 +47,32 @@ export function detectEngram() {
|
|
|
47
47
|
return { available: !!bin, bin: bin || null };
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/** Detecta si Obsidian está instalado. Devuelve { installed, path } por SO. */
|
|
51
|
+
export function detectObsidian() {
|
|
52
|
+
const plat = process.platform;
|
|
53
|
+
if (plat === "darwin") {
|
|
54
|
+
const appPaths = [
|
|
55
|
+
"/Applications/Obsidian.app",
|
|
56
|
+
path.join(HOME, "Applications", "Obsidian.app"),
|
|
57
|
+
];
|
|
58
|
+
for (const p of appPaths) if (exists(p)) return { installed: true, path: p };
|
|
59
|
+
const bin = which("obsidian");
|
|
60
|
+
if (bin) return { installed: true, path: bin };
|
|
61
|
+
} else if (plat === "linux") {
|
|
62
|
+
const bin = which("obsidian");
|
|
63
|
+
if (bin) return { installed: true, path: bin };
|
|
64
|
+
const flatpak = path.join(HOME, ".var", "app", "md.obsidian.Obsidian");
|
|
65
|
+
if (exists(flatpak)) return { installed: true, path: flatpak };
|
|
66
|
+
const snap = "/snap/bin/obsidian";
|
|
67
|
+
if (exists(snap)) return { installed: true, path: snap };
|
|
68
|
+
} else if (plat === "win32") {
|
|
69
|
+
const localAppData = process.env.LOCALAPPDATA || path.join(HOME, "AppData", "Local");
|
|
70
|
+
const exe = path.join(localAppData, "Obsidian", "Obsidian.exe");
|
|
71
|
+
if (exists(exe)) return { installed: true, path: exe };
|
|
72
|
+
}
|
|
73
|
+
return { installed: false, path: null };
|
|
74
|
+
}
|
|
75
|
+
|
|
50
76
|
/** Metadatos compartibles de Engram Cloud del proyecto (sin secretos). */
|
|
51
77
|
export function detectCloud(cwd) {
|
|
52
78
|
const metaPath = path.join(cwd, ".ozali", "cloud.json");
|
|
@@ -105,6 +131,7 @@ export function detectAll(cwd) {
|
|
|
105
131
|
agents: detectAgents(cwd),
|
|
106
132
|
skill: detectInstalledSkill(cwd),
|
|
107
133
|
engram: detectEngram(),
|
|
134
|
+
obsidian: detectObsidian(),
|
|
108
135
|
cloud: detectCloud(cwd),
|
|
109
136
|
testing: detectTesting(cwd),
|
|
110
137
|
};
|
package/cli/lib/util.mjs
CHANGED
|
@@ -193,5 +193,60 @@ export function nodeMajor() {
|
|
|
193
193
|
return parseInt(process.versions.node.split(".")[0], 10);
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
// ---- rutas portables (cross-platform / cross-team) --------------------------
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Convierte un path absoluto a formato portable para guardar en config.json.
|
|
200
|
+
* Reglas (en orden de prioridad):
|
|
201
|
+
* 1. Si está bajo `os.homedir()`, reemplazar prefijo por `~`.
|
|
202
|
+
* 2. Si `base` (cwd del proyecto) está definido y el path está bajo `base`,
|
|
203
|
+
* devolver relativo a `base`.
|
|
204
|
+
* 3. Dejar absoluto (legacy; emitir warning en el caller si es necesario).
|
|
205
|
+
*/
|
|
206
|
+
export function toPortablePath(absPath, base = null) {
|
|
207
|
+
if (!absPath) return absPath;
|
|
208
|
+
// Normalizar con realpath para resolver symlinks (macOS: /var → /private/var)
|
|
209
|
+
let normalized;
|
|
210
|
+
try { normalized = fs.realpathSync(absPath); } catch { normalized = path.resolve(absPath); }
|
|
211
|
+
const home = os.homedir();
|
|
212
|
+
// 1) home-relative → ~
|
|
213
|
+
if (home && (normalized === home || normalized.startsWith(home + path.sep))) {
|
|
214
|
+
return "~" + normalized.slice(home.length);
|
|
215
|
+
}
|
|
216
|
+
// 2) base-relative
|
|
217
|
+
if (base) {
|
|
218
|
+
let baseNorm;
|
|
219
|
+
try { baseNorm = fs.realpathSync(base); } catch { baseNorm = path.resolve(base); }
|
|
220
|
+
if (normalized.startsWith(baseNorm + path.sep)) {
|
|
221
|
+
return path.relative(baseNorm, normalized);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// 3) absoluto legacy
|
|
225
|
+
return normalized;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Expande un path portable a absoluto.
|
|
230
|
+
* Reglas:
|
|
231
|
+
* 1. Si empieza con `~`, expandir a `os.homedir()`.
|
|
232
|
+
* 2. Si es relativo, resolver contra `base` (cwd del proyecto).
|
|
233
|
+
* 3. Si ya es absoluto (legacy), devolver tal cual.
|
|
234
|
+
*/
|
|
235
|
+
export function fromPortablePath(portablePath, base = null) {
|
|
236
|
+
if (!portablePath) return portablePath;
|
|
237
|
+
// 1) expandir ~
|
|
238
|
+
if (portablePath.startsWith("~")) {
|
|
239
|
+
const home = os.homedir();
|
|
240
|
+
const rest = portablePath.slice(1);
|
|
241
|
+
return home + (rest.startsWith(path.sep) || rest === "" ? rest : path.sep + rest);
|
|
242
|
+
}
|
|
243
|
+
// 2) relativo → absoluto contra base
|
|
244
|
+
if (base && !path.isAbsolute(portablePath)) {
|
|
245
|
+
return path.resolve(base, portablePath);
|
|
246
|
+
}
|
|
247
|
+
// 3) absoluto legacy o ya resuelto
|
|
248
|
+
return path.resolve(portablePath);
|
|
249
|
+
}
|
|
250
|
+
|
|
196
251
|
export const HOME = os.homedir();
|
|
197
252
|
export const DEFAULT_KNOWLEDGE = path.join(HOME, ".ozali", "knowledge");
|
package/cli/test/smoke.test.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import fs from "node:fs";
|
|
|
6
6
|
import os from "node:os";
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
|
-
import { engramAssetName, pickEngramAsset } from "../lib/util.mjs";
|
|
9
|
+
import { engramAssetName, pickEngramAsset, toPortablePath, fromPortablePath } from "../lib/util.mjs";
|
|
10
10
|
|
|
11
11
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
12
12
|
const BIN = path.resolve(HERE, "..", "bin", "ozali.mjs");
|
|
@@ -383,6 +383,49 @@ test("engramAssetName respeta la convención de release por SO/arch", () => {
|
|
|
383
383
|
assert.equal(engramAssetName("freebsd", "x64", "1.17.0"), null, "SO no soportado → null");
|
|
384
384
|
});
|
|
385
385
|
|
|
386
|
+
test("toPortablePath convierte absolutos a ~ o relativo", () => {
|
|
387
|
+
const home = os.homedir();
|
|
388
|
+
const cwd = "/project";
|
|
389
|
+
assert.equal(toPortablePath(path.join(home, ".ozali", "knowledge"), cwd), "~/.ozali/knowledge");
|
|
390
|
+
assert.equal(toPortablePath("/project/.k", "/project"), ".k");
|
|
391
|
+
assert.equal(toPortablePath("/outside", "/project"), "/outside");
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
test("fromPortablePath expande ~ y resuelve relativos", () => {
|
|
395
|
+
const home = os.homedir();
|
|
396
|
+
const cwd = "/project";
|
|
397
|
+
assert.equal(fromPortablePath("~/.ozali/knowledge", cwd), path.join(home, ".ozali", "knowledge"));
|
|
398
|
+
assert.equal(fromPortablePath(".k", cwd), "/project/.k");
|
|
399
|
+
assert.equal(fromPortablePath("/abs", cwd), "/abs");
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
test("init guarda knowledgeRepo como path portable", () => {
|
|
403
|
+
const dir = tmpProject();
|
|
404
|
+
try {
|
|
405
|
+
run(["init", "--yes", "--no-engram", "--no-trust", "--no-jarvis", "--agent", "claude-code", "--scope", "project", "--knowledge-repo", path.join(dir, ".k")], dir);
|
|
406
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(dir, ".ozali", "config.json"), "utf8"));
|
|
407
|
+
assert.ok(cfg.knowledgeRepo, "config tiene knowledgeRepo");
|
|
408
|
+
assert.ok(!path.isAbsolute(cfg.knowledgeRepo), "knowledgeRepo NO debe ser absoluto");
|
|
409
|
+
assert.equal(cfg.knowledgeRepo, ".k", "knowledgeRepo debe ser relativo al proyecto");
|
|
410
|
+
} finally {
|
|
411
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test("sync resuelve knowledgeRepo portable correctamente", () => {
|
|
416
|
+
const dir = tmpProject();
|
|
417
|
+
try {
|
|
418
|
+
run(["init", "--yes", "--no-engram", "--no-trust", "--no-jarvis", "--agent", "claude-code", "--scope", "project", "--knowledge-repo", path.join(dir, ".k")], dir);
|
|
419
|
+
// Crear algo en .ozali/docs/ para que sync tenga qué copiar
|
|
420
|
+
fs.mkdirSync(path.join(dir, ".ozali", "docs"), { recursive: true });
|
|
421
|
+
fs.writeFileSync(path.join(dir, ".ozali", "docs", "test.md"), "# test\n");
|
|
422
|
+
const { stdout } = run(["sync", "--yes"], dir);
|
|
423
|
+
assert.match(stdout, /copiados|Docs copiados/, "sync debe encontrar el repo de conocimiento");
|
|
424
|
+
} finally {
|
|
425
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
|
|
386
429
|
test("pickEngramAsset ignora tags no-semver sin binarios (pi-v*) y draft/prerelease", () => {
|
|
387
430
|
// Shape real de la API de GitHub: el 'latest' es pi-v0.1.9 (0 assets); los binarios
|
|
388
431
|
// viven en tags vX.Y.Z. Además metemos un prerelease más nuevo que debe saltarse.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Integración con Obsidian
|
|
2
|
+
|
|
3
|
+
← [README](README.md)
|
|
4
|
+
|
|
5
|
+
Ozali puede exportar la memoria de equipo (Engram) y el histórico de hitos a un **vault de Obsidian**: un espacio navegable con wikilinks, grafo de conocimiento y Mapas de Contenido (MOCs).
|
|
6
|
+
|
|
7
|
+
## ¿Qué es el vault?
|
|
8
|
+
|
|
9
|
+
Es una carpeta `obsidian/` dentro de tu **repo de conocimiento** (`ozali-knowledge/` o `~/.ozali/knowledge/`). Contiene:
|
|
10
|
+
|
|
11
|
+
- **README.md** — índice humano del vault
|
|
12
|
+
- **MOCs/** — Mapas de Contenido auto-generados (proyectos, decisiones, bugfixes, métricas)
|
|
13
|
+
- **Memoria/** — exportación de Engram (decisiones, bugfixes, resúmenes técnicos)
|
|
14
|
+
- **.obsidian/** — configuración compartida del equipo (plugins core, hotkeys)
|
|
15
|
+
|
|
16
|
+
## Requisitos
|
|
17
|
+
|
|
18
|
+
- [Obsidian](https://obsidian.md) instalado (opt-in; ozali lo detecta y ofrece descargar)
|
|
19
|
+
- Engram instalado (`engram obsidian-export` es el motor de exportación)
|
|
20
|
+
|
|
21
|
+
## Uso
|
|
22
|
+
|
|
23
|
+
### Generar / actualizar el vault
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# Desde cualquier proyecto ozali
|
|
27
|
+
ozali sync --obsidian
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Esto:
|
|
31
|
+
1. Exporta Engram al vault (`engram obsidian-export`)
|
|
32
|
+
2. Actualiza los MOCs dinámicos (lista de proyectos, etc.)
|
|
33
|
+
3. Commit en el repo de conocimiento
|
|
34
|
+
|
|
35
|
+
### Sin el flag `--obsidian`
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
ozali sync
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Si el vault ya existe, ozali pregunta: *"¿Exportar memoria a Obsidian vault?"*. Responde `sí` para actualizar.
|
|
42
|
+
|
|
43
|
+
### Abrir el vault en Obsidian
|
|
44
|
+
|
|
45
|
+
1. Abre Obsidian
|
|
46
|
+
2. Selecciona **"Open folder as vault"**
|
|
47
|
+
3. Navega a tu `ozali-knowledge/obsidian/` (o `~/.ozali/knowledge/obsidian/`)
|
|
48
|
+
|
|
49
|
+
### Estructura recomendada del repo de conocimiento
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
ozali-knowledge/
|
|
53
|
+
.git/
|
|
54
|
+
engram/ ← export chunks de Engram
|
|
55
|
+
projects/
|
|
56
|
+
{proyecto}/
|
|
57
|
+
docs/ ← docs por hito
|
|
58
|
+
obsidian/ ← 🆕 VAULT DE OBSIDIAN
|
|
59
|
+
.obsidian/
|
|
60
|
+
README.md
|
|
61
|
+
MOCs/
|
|
62
|
+
Memoria/
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Configuración compartida
|
|
66
|
+
|
|
67
|
+
La carpeta `.obsidian/` del vault **sí se commitea** en el repo de conocimiento. Contiene:
|
|
68
|
+
- Plugins core habilitados (graph, backlinks, quick switcher)
|
|
69
|
+
- Config de lectura (readable line length)
|
|
70
|
+
|
|
71
|
+
**No se commitean** settings personales: temas, snippets, `workspace.json`, notas `Daily/`.
|
|
72
|
+
|
|
73
|
+
## Flujo de trabajo típico
|
|
74
|
+
|
|
75
|
+
1. Trabajas en un hito con `cdk` → ozali genera docs en `.ozali/docs/`
|
|
76
|
+
2. `ozali sync` → lleva docs + Engram al repo de conocimiento
|
|
77
|
+
3. `ozali sync --obsidian` → regenera el vault con la memoria actualizada
|
|
78
|
+
4. Abres Obsidian → exploras el grafo, wikilinks, decisiones del equipo
|
|
79
|
+
|
|
80
|
+
## Troubleshooting
|
|
81
|
+
|
|
82
|
+
| Síntoma | Causa | Solución |
|
|
83
|
+
|---|---|---|
|
|
84
|
+
| `engram obsidian-export` falla | Vault abierto en Obsidian (lock de archivos) | Cierra el vault en Obsidian y reintenta |
|
|
85
|
+
| MOCs vacíos | Sin proyectos en `knowledgeRepo/projects/` | Corre `ozali sync` primero para crear la estructura |
|
|
86
|
+
| No se detecta Obsidian | Instalado en path no estándar | Ignora la advertencia; abre el vault manualmente |
|
|
87
|
+
|
|
88
|
+
## Cerebro vs Vault
|
|
89
|
+
|
|
90
|
+
- **Cerebro** (`AI.md` + `.ai/`) vive en **cada repo** — es la fuente de verdad.
|
|
91
|
+
- **Vault** (`obsidian/`) vive en el **repo de conocimiento** — es la lectura humana acumulada.
|
|
92
|
+
|
|
93
|
+
No duplicamos el cerebro en el vault. Los MOCs usan **markdown links** para referenciar `AI.md` fuera del vault, y **wikilinks** para todo lo que está dentro (`Memoria/`, `MOCs/`).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ozali",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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": {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"alwaysUpdateLinks": true,
|
|
3
|
+
"newFileLocation": "folder",
|
|
4
|
+
"newFileFolderPath": "Memoria",
|
|
5
|
+
"attachmentFolderPath": "Memoria",
|
|
6
|
+
"readableLineLength": true,
|
|
7
|
+
"strictLineBreaks": false,
|
|
8
|
+
"showLineNumber": false,
|
|
9
|
+
"spellcheck": false,
|
|
10
|
+
"tabSize": 2,
|
|
11
|
+
"useTab": false
|
|
12
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# 🗺️ Bugfixes Recientes
|
|
2
|
+
|
|
3
|
+
Correcciones con impacto en otros módulos o que revelan gotchas del repo.
|
|
4
|
+
|
|
5
|
+
> Este MOC se regenera automáticamente con `ozali sync --obsidian`.
|
|
6
|
+
|
|
7
|
+
<!-- BUGFIXES_START -->
|
|
8
|
+
<!-- Se rellena automáticamente por ozali sync --obsidian desde Engram -->
|
|
9
|
+
<!-- BUGFIXES_END -->
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# 🗺️ Decisiones del Equipo
|
|
2
|
+
|
|
3
|
+
Decisiones de arquitectura, trade-offs y convenciones que afectan a todos.
|
|
4
|
+
|
|
5
|
+
> Este MOC se regenera automáticamente con `ozali sync --obsidian`.
|
|
6
|
+
|
|
7
|
+
<!-- DECISIONS_START -->
|
|
8
|
+
<!-- Se rellena automáticamente por ozali sync --obsidian desde Engram -->
|
|
9
|
+
<!-- DECISIONS_END -->
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# 🗺️ Métricas y Tokens
|
|
2
|
+
|
|
3
|
+
Resumen agregado de uso de tokens y eficiencia del equipo.
|
|
4
|
+
|
|
5
|
+
> Este MOC se regenera automáticamente con `ozali sync --obsidian`.
|
|
6
|
+
|
|
7
|
+
<!-- METRICS_START -->
|
|
8
|
+
<!-- Se rellena automáticamente por ozali sync --obsidian desde Engram -->
|
|
9
|
+
<!-- METRICS_END -->
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# 🗺️ Proyectos
|
|
2
|
+
|
|
3
|
+
Proyectos activos en el workspace ozali.
|
|
4
|
+
|
|
5
|
+
> Este MOC se regenera automáticamente con `ozali sync --obsidian`.
|
|
6
|
+
|
|
7
|
+
## Activos
|
|
8
|
+
|
|
9
|
+
<!-- PROJECTS_START -->
|
|
10
|
+
<!-- Se rellena automáticamente por ozali sync --obsidian -->
|
|
11
|
+
<!-- PROJECTS_END -->
|
|
12
|
+
|
|
13
|
+
## Agregar un nuevo proyecto
|
|
14
|
+
|
|
15
|
+
1. Corre `ozali init` en el repo del proyecto.
|
|
16
|
+
2. Corre `ozali sync --obsidian` desde cualquier proyecto.
|
|
17
|
+
3. El proyecto aparecerá aquí automáticamente.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# 🗺️ Índice Global
|
|
2
|
+
|
|
3
|
+
Bienvenido al vault de ozali. Usa este índice para navegar la memoria del equipo.
|
|
4
|
+
|
|
5
|
+
## Mapas de Contenido (MOCs)
|
|
6
|
+
|
|
7
|
+
- [[MOCs/Proyectos]] — Todos los proyectos del equipo
|
|
8
|
+
- [[MOCs/Decisiones del Equipo]] — Decisiones arquitectónicas y trade-offs
|
|
9
|
+
- [[MOCs/Bugfixes Recientes]] — Correcciones con impacto cruzado
|
|
10
|
+
- [[MOCs/Métricas y Tokens]] — Consumo de tokens y eficiencia
|
|
11
|
+
|
|
12
|
+
## Cómo actualizar este vault
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
# Desde cualquier proyecto ozali
|
|
16
|
+
ozali sync --obsidian
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
> El vault se regenera automáticamente. No edites `Memoria/` a mano.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Vault de Ozali — Memoria del Equipo
|
|
2
|
+
|
|
3
|
+
Este vault contiene la memoria acumulada de los proyectos gestionados con ozali.
|
|
4
|
+
Se actualiza automáticamente con `ozali sync --obsidian`.
|
|
5
|
+
|
|
6
|
+
## 📁 Estructura
|
|
7
|
+
|
|
8
|
+
- [[MOCs/Índice Global]] — Navegación principal
|
|
9
|
+
- [[MOCs/Proyectos]] — Lista de proyectos activos
|
|
10
|
+
- [[MOCs/Decisiones del Equipo]] — Decisiones de arquitectura y trade-offs
|
|
11
|
+
- [[MOCs/Bugfixes Recientes]] — Correcciones que afectan a otros
|
|
12
|
+
- [[MOCs/Métricas y Tokens]] — Uso de tokens y ahorro por recall
|
|
13
|
+
- `Memoria/` — Exportación de Engram (decisiones, bugfixes, métricas)
|
|
14
|
+
|
|
15
|
+
## 🔗 Fuentes de verdad (cerebros)
|
|
16
|
+
|
|
17
|
+
Los documentos de arquitectura y reglas de cada proyecto viven en su repo:
|
|
18
|
+
|
|
19
|
+
- Abre el workspace en tu editor para acceder al `AI.md` / `IA.md` de cada proyecto.
|
|
20
|
+
|
|
21
|
+
> No edites los archivos en `Memoria/` manualmente — se regeneran automáticamente.
|
|
22
|
+
> Usa `MOCs/` y `Daily/` para tus notas personales.
|