@jmtrin/opencode-kevin 1.4.0 → 1.5.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.
@@ -17,7 +17,7 @@ export interface KevinPluginOptions {
17
17
  * v0.4.0 (K4-021) — settings surfaced by `kevin_config` (plan §8.8).
18
18
  * Unknown keys are rejected on `set` unless `strict: false`.
19
19
  */
20
- export declare const KEVIN_CONFIG_KEYS: readonly ["quality_gate_enabled", "lesson_snippet_injection", "patternminer_enabled", "cross_project_enabled", "llm_reflection_enabled", "tool_calls_dedup_enabled", "deterministic_retrieval", "pre_prompt_budget_tokens", "archive_after_days", "curation_enabled", "agents_md_path", "skill_emission_enabled", "reference_emission_enabled", "injection_confidence_floor", "repo_truth_enabled", "convention_mining_enabled", "conflict_detection_enabled", "error_lesson_mode", "shared_layer_enabled", "okf_path", "share_requires_approval", "author_identity_mode", "shared_confidence_floor", "hook_liveness_enabled", "native_registration_enabled", "host_probe_history_enabled", "dead_hook_report_threshold", "perf_enabled", "perf_ring_capacity", "perf_flush_on_idle", "contract_report_enabled", "tui_snapshots_enabled", "mcp_write_enabled", "mcp_approve_enabled", "mcp_repo_override"];
20
+ export declare const KEVIN_CONFIG_KEYS: readonly ["quality_gate_enabled", "lesson_snippet_injection", "patternminer_enabled", "cross_project_enabled", "llm_reflection_enabled", "tool_calls_dedup_enabled", "deterministic_retrieval", "pre_prompt_budget_tokens", "archive_after_days", "curation_enabled", "agents_md_path", "skill_emission_enabled", "reference_emission_enabled", "injection_confidence_floor", "repo_truth_enabled", "convention_mining_enabled", "conflict_detection_enabled", "error_lesson_mode", "shared_layer_enabled", "okf_path", "share_requires_approval", "author_identity_mode", "shared_confidence_floor", "hook_liveness_enabled", "native_registration_enabled", "host_probe_history_enabled", "dead_hook_report_threshold", "perf_enabled", "perf_ring_capacity", "perf_flush_on_idle", "contract_report_enabled", "tui_snapshots_enabled", "mcp_write_enabled", "mcp_approve_enabled", "mcp_repo_override", "skills_canonical_dir", "skills_mirror_claude", "skills_mirror_cursor", "import_host_memory"];
21
21
  export declare const ERROR_LESSON_MODE_VALUES: readonly ["all", "triage_only"];
22
22
  /** Plugin release version — single source is @jmtrin/kevin-core (B-003 drift fix). */
23
23
  export { KEVIN_VERSION };
@@ -128,6 +128,12 @@ export const KEVIN_CONFIG_KEYS = [
128
128
  "mcp_write_enabled",
129
129
  "mcp_approve_enabled",
130
130
  "mcp_repo_override",
131
+ // v1.5.0 (K15-001 / plan §4) — the four Diaspora settings (no migration this release)
132
+ // skills_canonical_dir is a path, others are TEXT flags compared with === "1".
133
+ "skills_canonical_dir",
134
+ "skills_mirror_claude",
135
+ "skills_mirror_cursor",
136
+ "import_host_memory",
131
137
  ];
132
138
  // v1.1.0 (K11-007 / D11-08) — no new settings in 1.1.0; thresholds are constants (D11-03)
133
139
  // v0.7.0 (K7-003 / plan §5.6, D7-12) — the explicit VALUE domain for
@@ -175,6 +181,8 @@ function resolveMigrationsDir() {
175
181
  }
176
182
  const REKEY_TABLES = ["memories", "shared_entries", "okf_imports"];
177
183
  export function performRekey(store, toRepoId, opts) {
184
+ if (!/^[0-9a-f]{16}$/.test(toRepoId))
185
+ return { action: "rekey", ok: false, reason: "invalid repo_id" };
178
186
  // The 009 migration carries the repo_id column AND the shared-layer
179
187
  // tables; without it there is nothing to re-key.
180
188
  if (!hasRepoIdColumn(store)) {
@@ -315,6 +323,17 @@ export const KevinPlugin = async (input, options) => {
315
323
  catch {
316
324
  // pre-003 DB without kevin_settings — nothing to seed
317
325
  }
326
+ // v1.5.0 (K15-001) — Diaspora settings: ensure defaults on existing DBs.
327
+ try {
328
+ const ins = store.prepare("INSERT OR IGNORE INTO kevin_settings (key, value) VALUES (?, ?)");
329
+ ins.run("skills_canonical_dir", ".agents/skills");
330
+ ins.run("skills_mirror_claude", "0");
331
+ ins.run("skills_mirror_cursor", "0");
332
+ ins.run("import_host_memory", "0");
333
+ }
334
+ catch {
335
+ // pre-003 DB without kevin_settings — nothing to seed
336
+ }
318
337
  const metrics = new Metrics(store);
319
338
  // v0.4.0 (K4-019): the plugin hooks expose no project field, so the
320
339
  // project id is derived once from the plugin host's working directory
@@ -1344,18 +1363,28 @@ export const KevinPlugin = async (input, options) => {
1344
1363
  },
1345
1364
  }),
1346
1365
  kevin_export: tool({
1347
- description: "Exporta memorias curadas (decision, rule, pattern) como markdown o formato OKF. v0.3.0.",
1366
+ description: "Exporta memorias curadas (decision, rule, pattern) como markdown, OKF o MIF (v1.5.0). v0.3.0.",
1348
1367
  args: {
1349
1368
  format: tool.schema
1350
- .enum(["okf", "markdown"])
1369
+ .enum(["okf", "markdown", "mif"])
1351
1370
  .default("okf")
1352
- .describe("okf | markdown"),
1371
+ .describe("okf | markdown | mif"),
1372
+ redact_pii: tool.schema.boolean().optional().default(false).describe("Cuando true, aplica redaccion SECRET_PATTERNS sobre el contenido (MIF honour-when-flagged, defecto false para preservar 1.x)"),
1353
1373
  },
1354
1374
  async execute(args) {
1355
- // v0.8.0 (K8-027) — the v1 export is scoped to this
1356
- // plugin's project: the global database holds several
1357
- // projects' memories, and a committed export must not
1358
- // leak another project's knowledge.
1375
+ // v1.5.0 (K15-009) — MIF branch
1376
+ if (args.format === "mif") {
1377
+ const { toMif } = await import("@jmtrin/kevin-core");
1378
+ const rows = store.prepare("SELECT * FROM memories WHERE status='active'").all();
1379
+ // scope filter to projectId like okf-export does? Use same as exportOkf: filter by project_id = ?
1380
+ const scoped = projectId ? rows.filter((r) => r.project_id === projectId || r.project_id === projectId) : rows;
1381
+ const envelope = toMif(scoped, { redactPii: args.redact_pii === true });
1382
+ try {
1383
+ metrics.incr("mif_exports_total", 1);
1384
+ }
1385
+ catch { }
1386
+ return { title: "Export completado", output: JSON.stringify(envelope, null, 2) };
1387
+ }
1359
1388
  const output = args.format === "markdown"
1360
1389
  ? exportMarkdown(store, projectId)
1361
1390
  : exportOkf(store, projectId);
@@ -1366,11 +1395,53 @@ export const KevinPlugin = async (input, options) => {
1366
1395
  },
1367
1396
  }),
1368
1397
  kevin_import: tool({
1369
- description: "Importa un bundle markdown de conocimiento (v0.3.0). Cada entrada se guarda como context con origin='imported'.",
1398
+ description: "Importa un bundle markdown de conocimiento (v0.3.0) o MIF / host memories (v1.5.0). Para host, requiere import_host_memory='1' (gate).",
1370
1399
  args: {
1371
- bundle: tool.schema.string().min(1),
1400
+ bundle: tool.schema.string().optional(),
1401
+ format: tool.schema.enum(["okf", "markdown", "mif"]).optional().describe("okf | markdown | mif (mif requiere bundle JSON)"),
1402
+ source: tool.schema.enum(["bundle", "claude-memory", "codex-memories"]).optional().describe("bundle (default) | claude-memory | codex-memories (requiere gate)"),
1372
1403
  },
1373
1404
  async execute(args) {
1405
+ const src = args.source ?? "bundle";
1406
+ if (src === "claude-memory" || src === "codex-memories") {
1407
+ const { importHostMemories } = await import("@jmtrin/kevin-core");
1408
+ const report = importHostMemories({ store, memoryService, metrics: metrics, env: kevinEnv, source: src });
1409
+ if (report.error === "disabled") {
1410
+ return { title: "Import no realizado (gate deshabilitado)", output: JSON.stringify({ error: "disabled", hint: "Ejecuta kevin_config set import_host_memory 1 para habilitar" }) };
1411
+ }
1412
+ return { title: "Import completado", output: JSON.stringify(report) };
1413
+ }
1414
+ if (args.format === "mif") {
1415
+ if (!args.bundle)
1416
+ return { title: "Import error", output: JSON.stringify({ error: "missing_bundle", hint: "bundle requerido para formato mif" }) };
1417
+ try {
1418
+ const env = JSON.parse(args.bundle);
1419
+ const { fromMif } = await import("@jmtrin/kevin-core");
1420
+ const { candidates } = fromMif(env);
1421
+ let imported = 0;
1422
+ let duplicates = 0;
1423
+ for (const c of candidates) {
1424
+ const fp = c.content ? (await import("@jmtrin/kevin-core")).fingerprint(c.content) : "";
1425
+ const exists = store.prepare("SELECT 1 FROM memories WHERE fingerprint = ? LIMIT 1").get(fp);
1426
+ if (exists) {
1427
+ duplicates++;
1428
+ continue;
1429
+ }
1430
+ memoryService.save({ type: c.type ?? "context", content: c.content, scope: "project", origin: "imported", fingerprint: fp, metadata: c.metadata });
1431
+ imported++;
1432
+ }
1433
+ try {
1434
+ metrics.incr("mif_imports_total", 1);
1435
+ }
1436
+ catch { }
1437
+ return { title: "Import completado", output: JSON.stringify({ imported, duplicates, unknownFieldsPreserved: candidates[0]?.unknownFields ? Object.keys(candidates[0].unknownFields) : [] }) };
1438
+ }
1439
+ catch (e) {
1440
+ return { title: "Import error", output: JSON.stringify({ error: "bad_json", message: e.message }) };
1441
+ }
1442
+ }
1443
+ if (!args.bundle)
1444
+ return { title: "Import error", output: JSON.stringify({ error: "missing_bundle" }) };
1374
1445
  const result = importOkf(args.bundle, memoryService);
1375
1446
  return {
1376
1447
  title: "Import completado",
@@ -2148,6 +2219,28 @@ export const KevinPlugin = async (input, options) => {
2148
2219
  catch {
2149
2220
  // best-effort — snapshot failure must not break idle
2150
2221
  }
2222
+ // v1.5.0 (K15-007) — skill bundle refresh after snapshots flush
2223
+ try {
2224
+ const manifestPath = join(materializerRoot, "skills-manifest.json");
2225
+ const shouldRefresh = memoryService.getSetting("skills_mirror_claude", "0") === "1" || memoryService.getSetting("skills_mirror_cursor", "0") === "1" || memoryService.getSetting("skills_canonical_dir", ".agents/skills") !== "" || existsSync(manifestPath);
2226
+ // spec: run when ANY skill setting is on OR manifest exists; canonical_dir always set so we check manifest existence as primary gate plus mirror flags
2227
+ const hasManifest = existsSync(manifestPath);
2228
+ const mirrorActive = memoryService.getSetting("skills_mirror_claude", "0") === "1" || memoryService.getSetting("skills_mirror_cursor", "0") === "1";
2229
+ if (hasManifest || mirrorActive) {
2230
+ const { refreshSkillBundle } = await import("@jmtrin/kevin-core");
2231
+ const bundles = materializer.topicBundles();
2232
+ const canonicalDir = memoryService.getSetting("skills_canonical_dir", ".agents/skills");
2233
+ const mirrors = [];
2234
+ if (memoryService.getSetting("skills_mirror_claude", "0") === "1")
2235
+ mirrors.push("claude");
2236
+ if (memoryService.getSetting("skills_mirror_cursor", "0") === "1")
2237
+ mirrors.push("cursor");
2238
+ refreshSkillBundle({ projectRoot, canonicalDir, mirrors, topics: bundles, repoId: sessionIdentity.repoId, manifestPath, metrics: metrics });
2239
+ }
2240
+ }
2241
+ catch {
2242
+ // best-effort
2243
+ }
2151
2244
  // v1.0.0 (K10-013 / D10-08) — the session recorded work:
2152
2245
  // arm the deferred dispose settlement. The ISO timestamp
2153
2246
  // lets the next session.start compare it against
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jmtrin/opencode-kevin",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Kevin — Observa y aprende: capa de aprendizaje para OpenCode",
5
5
  "type": "module",
6
6
  "main": "dist/plugin/index.js",
@@ -25,8 +25,8 @@
25
25
  },
26
26
  "dependencies": {
27
27
  "@opencode-ai/plugin": "^1.18.16",
28
- "@jmtrin/kevin-core": "1.4.0",
29
- "@jmtrin/opencode-kevin-tui": "1.4.0"
28
+ "@jmtrin/kevin-core": "1.5.0",
29
+ "@jmtrin/opencode-kevin-tui": "1.5.0"
30
30
  },
31
31
  "optionalDependencies": {
32
32
  "better-sqlite3": "^12.11.0"