@jmtrin/opencode-kevin 1.4.0 → 2.0.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,13 @@ 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", "sources_enabled", "source_claude_memory", "source_codex_memories", "source_opencode_native", "okf_write_version"];
21
+ export declare const REMOVED_SETTINGS: {
22
+ readonly import_host_memory: {
23
+ readonly since: "2.0.0";
24
+ readonly replacement: "sources_enabled + source_claude_memory/source_codex_memories";
25
+ };
26
+ };
21
27
  export declare const ERROR_LESSON_MODE_VALUES: readonly ["all", "triage_only"];
22
28
  /** Plugin release version — single source is @jmtrin/kevin-core (B-003 drift fix). */
23
29
  export { KEVIN_VERSION };
@@ -128,7 +128,25 @@ 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
+ // v2.0.0 (K16-013 / plan §4.4) — Commonwealth settings (retirement of import_host_memory handled via removals)
137
+ "sources_enabled",
138
+ "source_claude_memory",
139
+ "source_codex_memories",
140
+ "source_opencode_native",
141
+ "okf_write_version",
131
142
  ];
143
+ // v2.0.0 (K16-004 / plan §5.1) — removed settings contract
144
+ export const REMOVED_SETTINGS = {
145
+ import_host_memory: {
146
+ since: "2.0.0",
147
+ replacement: "sources_enabled + source_claude_memory/source_codex_memories",
148
+ },
149
+ };
132
150
  // v1.1.0 (K11-007 / D11-08) — no new settings in 1.1.0; thresholds are constants (D11-03)
133
151
  // v0.7.0 (K7-003 / plan §5.6, D7-12) — the explicit VALUE domain for
134
152
  // `error_lesson_mode`. The setting is TEXT and must be compared with
@@ -175,6 +193,8 @@ function resolveMigrationsDir() {
175
193
  }
176
194
  const REKEY_TABLES = ["memories", "shared_entries", "okf_imports"];
177
195
  export function performRekey(store, toRepoId, opts) {
196
+ if (!/^[0-9a-f]{16}$/.test(toRepoId))
197
+ return { action: "rekey", ok: false, reason: "invalid repo_id" };
178
198
  // The 009 migration carries the repo_id column AND the shared-layer
179
199
  // tables; without it there is nothing to re-key.
180
200
  if (!hasRepoIdColumn(store)) {
@@ -315,6 +335,17 @@ export const KevinPlugin = async (input, options) => {
315
335
  catch {
316
336
  // pre-003 DB without kevin_settings — nothing to seed
317
337
  }
338
+ // v1.5.0 (K15-001) — Diaspora settings: ensure defaults on existing DBs.
339
+ // v2.0.0: import_host_memory retired — seeded via 014 translation then deleted; no longer seeded here.
340
+ try {
341
+ const ins = store.prepare("INSERT OR IGNORE INTO kevin_settings (key, value) VALUES (?, ?)");
342
+ ins.run("skills_canonical_dir", ".agents/skills");
343
+ ins.run("skills_mirror_claude", "0");
344
+ ins.run("skills_mirror_cursor", "0");
345
+ }
346
+ catch {
347
+ // pre-003 DB without kevin_settings — nothing to seed
348
+ }
318
349
  const metrics = new Metrics(store);
319
350
  // v0.4.0 (K4-019): the plugin hooks expose no project field, so the
320
351
  // project id is derived once from the plugin host's working directory
@@ -1344,18 +1375,28 @@ export const KevinPlugin = async (input, options) => {
1344
1375
  },
1345
1376
  }),
1346
1377
  kevin_export: tool({
1347
- description: "Exporta memorias curadas (decision, rule, pattern) como markdown o formato OKF. v0.3.0.",
1378
+ description: "Exporta memorias curadas (decision, rule, pattern) como markdown, OKF o MIF (v1.5.0). v0.3.0.",
1348
1379
  args: {
1349
1380
  format: tool.schema
1350
- .enum(["okf", "markdown"])
1381
+ .enum(["okf", "markdown", "mif"])
1351
1382
  .default("okf")
1352
- .describe("okf | markdown"),
1383
+ .describe("okf | markdown | mif"),
1384
+ 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
1385
  },
1354
1386
  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.
1387
+ // v1.5.0 (K15-009) — MIF branch
1388
+ if (args.format === "mif") {
1389
+ const { toMif } = await import("@jmtrin/kevin-core");
1390
+ const rows = store.prepare("SELECT * FROM memories WHERE status='active'").all();
1391
+ // scope filter to projectId like okf-export does? Use same as exportOkf: filter by project_id = ?
1392
+ const scoped = projectId ? rows.filter((r) => r.project_id === projectId || r.project_id === projectId) : rows;
1393
+ const envelope = toMif(scoped, { redactPii: args.redact_pii === true });
1394
+ try {
1395
+ metrics.incr("mif_exports_total", 1);
1396
+ }
1397
+ catch { }
1398
+ return { title: "Export completado", output: JSON.stringify(envelope, null, 2) };
1399
+ }
1359
1400
  const output = args.format === "markdown"
1360
1401
  ? exportMarkdown(store, projectId)
1361
1402
  : exportOkf(store, projectId);
@@ -1366,11 +1407,53 @@ export const KevinPlugin = async (input, options) => {
1366
1407
  },
1367
1408
  }),
1368
1409
  kevin_import: tool({
1369
- description: "Importa un bundle markdown de conocimiento (v0.3.0). Cada entrada se guarda como context con origin='imported'.",
1410
+ description: "Importa un bundle markdown de conocimiento (v0.3.0) o MIF / host memories (v2.0.0 retira import_host_memory; requiere sources_enabled=1 + source_claude_memory/codex-memories=1).",
1370
1411
  args: {
1371
- bundle: tool.schema.string().min(1),
1412
+ bundle: tool.schema.string().optional(),
1413
+ format: tool.schema.enum(["okf", "markdown", "mif"]).optional().describe("okf | markdown | mif (mif requiere bundle JSON)"),
1414
+ source: tool.schema.enum(["bundle", "claude-memory", "codex-memories"]).optional().describe("bundle (default) | claude-memory | codex-memories (requiere gate)"),
1372
1415
  },
1373
1416
  async execute(args) {
1417
+ const src = args.source ?? "bundle";
1418
+ if (src === "claude-memory" || src === "codex-memories") {
1419
+ const { importHostMemories } = await import("@jmtrin/kevin-core");
1420
+ const report = importHostMemories({ store, memoryService, metrics: metrics, env: kevinEnv, source: src });
1421
+ if (report.error === "disabled") {
1422
+ return { title: "Import no realizado (gate deshabilitado)", output: JSON.stringify({ error: "disabled", hint: report.hint }) };
1423
+ }
1424
+ return { title: "Import completado", output: JSON.stringify(report) };
1425
+ }
1426
+ if (args.format === "mif") {
1427
+ if (!args.bundle)
1428
+ return { title: "Import error", output: JSON.stringify({ error: "missing_bundle", hint: "bundle requerido para formato mif" }) };
1429
+ try {
1430
+ const env = JSON.parse(args.bundle);
1431
+ const { fromMif } = await import("@jmtrin/kevin-core");
1432
+ const { candidates } = fromMif(env);
1433
+ let imported = 0;
1434
+ let duplicates = 0;
1435
+ for (const c of candidates) {
1436
+ const fp = c.content ? (await import("@jmtrin/kevin-core")).fingerprint(c.content) : "";
1437
+ const exists = store.prepare("SELECT 1 FROM memories WHERE fingerprint = ? LIMIT 1").get(fp);
1438
+ if (exists) {
1439
+ duplicates++;
1440
+ continue;
1441
+ }
1442
+ memoryService.save({ type: c.type ?? "context", content: c.content, scope: "project", origin: "imported", fingerprint: fp, metadata: c.metadata });
1443
+ imported++;
1444
+ }
1445
+ try {
1446
+ metrics.incr("mif_imports_total", 1);
1447
+ }
1448
+ catch { }
1449
+ return { title: "Import completado", output: JSON.stringify({ imported, duplicates, unknownFieldsPreserved: candidates[0]?.unknownFields ? Object.keys(candidates[0].unknownFields) : [] }) };
1450
+ }
1451
+ catch (e) {
1452
+ return { title: "Import error", output: JSON.stringify({ error: "bad_json", message: e.message }) };
1453
+ }
1454
+ }
1455
+ if (!args.bundle)
1456
+ return { title: "Import error", output: JSON.stringify({ error: "missing_bundle" }) };
1374
1457
  const result = importOkf(args.bundle, memoryService);
1375
1458
  return {
1376
1459
  title: "Import completado",
@@ -1378,6 +1461,56 @@ export const KevinPlugin = async (input, options) => {
1378
1461
  };
1379
1462
  },
1380
1463
  }),
1464
+ kevin_sources: tool({
1465
+ description: "Fuentes de memoria (v2.0.0): lista estado de cada MemorySource (enabled, precedence, last_sync). Solo lectura.",
1466
+ args: {
1467
+ action: tool.schema.enum(["list"]).default("list").describe("list"),
1468
+ },
1469
+ async execute(_args) {
1470
+ const settings = {};
1471
+ try {
1472
+ const rows = store
1473
+ .prepare("SELECT key, value FROM kevin_settings WHERE key LIKE 'source%' OR key='sources_enabled' OR key='okf_write_version'")
1474
+ .all();
1475
+ for (const r of rows)
1476
+ settings[r.key] = r.value;
1477
+ }
1478
+ catch { }
1479
+ let sources = [];
1480
+ try {
1481
+ sources = store
1482
+ .prepare("SELECT name, enabled, precedence FROM memory_sources ORDER BY precedence")
1483
+ .all();
1484
+ }
1485
+ catch {
1486
+ sources = [
1487
+ { name: "opencode-plugin", enabled: 1, precedence: 10 },
1488
+ {
1489
+ name: "claude-memory",
1490
+ enabled: Number(settings["source_claude_memory"] === "1" ? 1 : 0),
1491
+ precedence: 20,
1492
+ },
1493
+ {
1494
+ name: "codex-memories",
1495
+ enabled: Number(settings["source_codex_memories"] === "1" ? 1 : 0),
1496
+ precedence: 30,
1497
+ },
1498
+ {
1499
+ name: "opencode-native",
1500
+ enabled: Number(settings["source_opencode_native"] === "1" ? 1 : 0),
1501
+ precedence: 40,
1502
+ },
1503
+ ];
1504
+ }
1505
+ return {
1506
+ title: "Fuentes de memoria",
1507
+ output: JSON.stringify({
1508
+ sources_enabled: settings["sources_enabled"] ?? "1",
1509
+ sources,
1510
+ }),
1511
+ };
1512
+ },
1513
+ }),
1381
1514
  kevin_config: tool({
1382
1515
  description: "Lee o modifica settings de Kevin sin SQL (v0.4.0). list: todas las settings de kevin_settings. set: upsert de una key conocida (rechaza keys desconocidas salvo strict:false).",
1383
1516
  args: {
@@ -1418,6 +1551,19 @@ export const KevinPlugin = async (input, options) => {
1418
1551
  }),
1419
1552
  };
1420
1553
  }
1554
+ // v2.0.0 (K16-004) — removed keys return structured removed_in_2.0.0 before unknown_key
1555
+ if (args.key in REMOVED_SETTINGS) {
1556
+ const meta = REMOVED_SETTINGS[args.key];
1557
+ return {
1558
+ title: "kevin_config",
1559
+ output: JSON.stringify({
1560
+ error: "removed_in_2.0.0",
1561
+ key: args.key,
1562
+ replacement: meta.replacement,
1563
+ since: meta.since,
1564
+ }),
1565
+ };
1566
+ }
1421
1567
  const known = KEVIN_CONFIG_KEYS.includes(args.key);
1422
1568
  if (!known && args.strict !== false) {
1423
1569
  return {
@@ -2148,6 +2294,28 @@ export const KevinPlugin = async (input, options) => {
2148
2294
  catch {
2149
2295
  // best-effort — snapshot failure must not break idle
2150
2296
  }
2297
+ // v1.5.0 (K15-007) — skill bundle refresh after snapshots flush
2298
+ try {
2299
+ const manifestPath = join(materializerRoot, "skills-manifest.json");
2300
+ 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);
2301
+ // 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
2302
+ const hasManifest = existsSync(manifestPath);
2303
+ const mirrorActive = memoryService.getSetting("skills_mirror_claude", "0") === "1" || memoryService.getSetting("skills_mirror_cursor", "0") === "1";
2304
+ if (hasManifest || mirrorActive) {
2305
+ const { refreshSkillBundle } = await import("@jmtrin/kevin-core");
2306
+ const bundles = materializer.topicBundles();
2307
+ const canonicalDir = memoryService.getSetting("skills_canonical_dir", ".agents/skills");
2308
+ const mirrors = [];
2309
+ if (memoryService.getSetting("skills_mirror_claude", "0") === "1")
2310
+ mirrors.push("claude");
2311
+ if (memoryService.getSetting("skills_mirror_cursor", "0") === "1")
2312
+ mirrors.push("cursor");
2313
+ refreshSkillBundle({ projectRoot, canonicalDir, mirrors, topics: bundles, repoId: sessionIdentity.repoId, manifestPath, metrics: metrics });
2314
+ }
2315
+ }
2316
+ catch {
2317
+ // best-effort
2318
+ }
2151
2319
  // v1.0.0 (K10-013 / D10-08) — the session recorded work:
2152
2320
  // arm the deferred dispose settlement. The ISO timestamp
2153
2321
  // lets the next session.start compare it against
package/package.json CHANGED
@@ -1,58 +1,50 @@
1
1
  {
2
- "name": "@jmtrin/opencode-kevin",
3
- "version": "1.4.0",
4
- "description": "Kevin — Observa y aprende: capa de aprendizaje para OpenCode",
5
- "type": "module",
6
- "main": "dist/plugin/index.js",
7
- "types": "dist/plugin/index.d.ts",
8
- "exports": {
9
- ".": {
10
- "types": "./dist/plugin/index.d.ts",
11
- "import": "./dist/plugin/index.js"
12
- },
13
- "./tui": {
14
- "types": "@jmtrin/opencode-kevin-tui/dist/index.d.ts",
15
- "import": "@jmtrin/opencode-kevin-tui/dist/index.js"
16
- }
17
- },
18
- "files": [
19
- "dist/plugin"
20
- ],
21
- "scripts": {
22
- "build": "tsc -p tsconfig.json",
23
- "prepublishOnly": "npm run build",
24
- "typecheck": "tsc --noEmit"
25
- },
26
- "dependencies": {
27
- "@opencode-ai/plugin": "^1.18.16",
28
- "@jmtrin/kevin-core": "1.4.0",
29
- "@jmtrin/opencode-kevin-tui": "1.4.0"
30
- },
31
- "optionalDependencies": {
32
- "better-sqlite3": "^12.11.0"
33
- },
34
- "engines": {
35
- "node": ">=22.5.0",
36
- "opencode": "^1.18.0"
37
- },
38
- "repository": {
39
- "type": "git",
40
- "url": "https://github.com/jmtrin/opencode-kevin.git"
41
- },
42
- "homepage": "https://github.com/jmtrin/opencode-kevin#readme",
43
- "bugs": {
44
- "url": "https://github.com/jmtrin/opencode-kevin/issues"
45
- },
46
- "keywords": [
47
- "opencode",
48
- "plugin",
49
- "memory",
50
- "learning",
51
- "ai"
52
- ],
53
- "author": "jmtrin",
54
- "publishConfig": {
55
- "access": "public"
56
- },
57
- "license": "MIT"
2
+ "name": "@jmtrin/opencode-kevin",
3
+ "version": "2.0.0",
4
+ "description": "Kevin — Observa y aprende: capa de aprendizaje para OpenCode",
5
+ "type": "module",
6
+ "main": "dist/plugin/index.js",
7
+ "types": "dist/plugin/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/plugin/index.d.ts",
11
+ "import": "./dist/plugin/index.js"
12
+ },
13
+ "./tui": {
14
+ "types": "@jmtrin/opencode-kevin-tui/dist/index.d.ts",
15
+ "import": "@jmtrin/opencode-kevin-tui/dist/index.js"
16
+ }
17
+ },
18
+ "files": ["dist/plugin"],
19
+ "scripts": {
20
+ "build": "tsc -p tsconfig.json",
21
+ "prepublishOnly": "npm run build",
22
+ "typecheck": "tsc --noEmit"
23
+ },
24
+ "dependencies": {
25
+ "@opencode-ai/plugin": "^1.18.16",
26
+ "@jmtrin/kevin-core": "2.0.0",
27
+ "@jmtrin/opencode-kevin-tui": "2.0.0"
28
+ },
29
+ "optionalDependencies": {
30
+ "better-sqlite3": "^12.11.0"
31
+ },
32
+ "engines": {
33
+ "node": ">=22.5.0",
34
+ "opencode": "^1.18.0"
35
+ },
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/jmtrin/opencode-kevin.git"
39
+ },
40
+ "homepage": "https://github.com/jmtrin/opencode-kevin#readme",
41
+ "bugs": {
42
+ "url": "https://github.com/jmtrin/opencode-kevin/issues"
43
+ },
44
+ "keywords": ["opencode", "plugin", "memory", "learning", "ai"],
45
+ "author": "jmtrin",
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "license": "MIT"
58
50
  }