@jmtrin/kevin-core 1.5.0 → 2.1.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.
@@ -0,0 +1,135 @@
1
+ // K16-016 — OpencodeNativeSource (native opencode memories, e.g. .opencode/memory)
2
+ // K21-006 — Relay probe activation: single-source location list, absent-safe
3
+ import { readdirSync, readFileSync, statSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ export const NATIVE_CANDIDATE_PATHS = [
6
+ ".opencode/memory/*.md",
7
+ ".opencode/MEMORY.md",
8
+ ];
9
+ function globMdFiles(dir) {
10
+ try {
11
+ const entries = readdirSync(dir);
12
+ return entries.filter((f) => f.endsWith(".md")).map((f) => join(dir, f));
13
+ }
14
+ catch {
15
+ return [];
16
+ }
17
+ }
18
+ export class OpencodeNativeSource {
19
+ enabledFlag;
20
+ projectRoot;
21
+ name = "opencode-native";
22
+ precedence = 40;
23
+ constructor(enabledFlag, projectRoot = process.cwd()) {
24
+ this.enabledFlag = enabledFlag;
25
+ this.projectRoot = projectRoot;
26
+ }
27
+ enabled() {
28
+ return this.enabledFlag();
29
+ }
30
+ async fetch() {
31
+ if (!this.enabled())
32
+ return [];
33
+ const out = [];
34
+ const seen = new Set();
35
+ for (const pattern of NATIVE_CANDIDATE_PATHS) {
36
+ try {
37
+ if (pattern.includes("*")) {
38
+ // e.g. .opencode/memory/*.md
39
+ const base = pattern.split("*")[0].replace(/\/$/, "");
40
+ const dir = join(this.projectRoot, base);
41
+ let st = null;
42
+ try {
43
+ st = statSync(dir);
44
+ }
45
+ catch {
46
+ continue;
47
+ }
48
+ if (!st.isDirectory())
49
+ continue;
50
+ for (const file of globMdFiles(dir)) {
51
+ if (seen.has(file))
52
+ continue;
53
+ seen.add(file);
54
+ try {
55
+ const txt = readFileSync(file, "utf8");
56
+ for (const line of txt.split("\n").map((s) => s.trim()).filter(Boolean).slice(0, 50)) {
57
+ if (out.length >= 100)
58
+ break;
59
+ out.push({ statement: line, type: "rule", scope: null, source: this.name });
60
+ }
61
+ }
62
+ catch {
63
+ // absent-safe: skip unreadable file
64
+ }
65
+ }
66
+ }
67
+ else {
68
+ const abs = join(this.projectRoot, pattern);
69
+ let st = null;
70
+ try {
71
+ st = statSync(abs);
72
+ }
73
+ catch {
74
+ continue;
75
+ }
76
+ if (!st.isFile())
77
+ continue;
78
+ if (seen.has(abs))
79
+ continue;
80
+ seen.add(abs);
81
+ try {
82
+ const txt = readFileSync(abs, "utf8");
83
+ for (const line of txt.split("\n").map((s) => s.trim()).filter(Boolean).slice(0, 100)) {
84
+ if (out.length >= 100)
85
+ break;
86
+ out.push({ statement: line, type: "rule", scope: null, source: this.name });
87
+ }
88
+ }
89
+ catch {
90
+ // skip
91
+ }
92
+ }
93
+ }
94
+ catch {
95
+ // never throw
96
+ }
97
+ }
98
+ return out;
99
+ }
100
+ health() {
101
+ let found = 0;
102
+ for (const pattern of NATIVE_CANDIDATE_PATHS) {
103
+ try {
104
+ if (pattern.includes("*")) {
105
+ const base = pattern.split("*")[0].replace(/\/$/, "");
106
+ const dir = join(this.projectRoot, base);
107
+ try {
108
+ const st = statSync(dir);
109
+ if (!st.isDirectory())
110
+ continue;
111
+ found += globMdFiles(dir).length;
112
+ }
113
+ catch {
114
+ continue;
115
+ }
116
+ }
117
+ else {
118
+ const abs = join(this.projectRoot, pattern);
119
+ try {
120
+ const st = statSync(abs);
121
+ if (st.isFile())
122
+ found++;
123
+ }
124
+ catch {
125
+ continue;
126
+ }
127
+ }
128
+ }
129
+ catch { }
130
+ }
131
+ if (found > 0)
132
+ return { status: "ok", detail: `found ${found} files` };
133
+ return { status: "absent", detail: "no native memory found at probe paths" };
134
+ }
135
+ }
@@ -0,0 +1,9 @@
1
+ import type { MemorySource, SourceEntry } from "./MemorySource.js";
2
+ export declare class OpencodePluginSource implements MemorySource {
3
+ private enabledFlag;
4
+ name: string;
5
+ precedence: number;
6
+ constructor(enabledFlag: () => boolean);
7
+ enabled(): boolean;
8
+ fetch(): Promise<SourceEntry[]>;
9
+ }
@@ -0,0 +1,15 @@
1
+ export class OpencodePluginSource {
2
+ enabledFlag;
3
+ name = "opencode-plugin";
4
+ precedence = 10;
5
+ constructor(enabledFlag) {
6
+ this.enabledFlag = enabledFlag;
7
+ }
8
+ enabled() {
9
+ return this.enabledFlag();
10
+ }
11
+ async fetch() {
12
+ // Plugin source is the local DB itself — no fetch, handled elsewhere
13
+ return [];
14
+ }
15
+ }
@@ -0,0 +1,18 @@
1
+ export interface DeletedInfo {
2
+ source: string;
3
+ fingerprint: string;
4
+ file?: string;
5
+ }
6
+ /**
7
+ * Pure diff: fingerprints present in prev but absent from current.
8
+ * Both sets are expected to be non-null; empty sets return [].
9
+ * Malformed inputs (null, non-Set) return [] never throw.
10
+ */
11
+ export declare function collectDeletions(prevFingerprints: Set<string> | string[] | null | undefined, currentFingerprints: Set<string> | string[] | null | undefined, source: string): DeletedInfo[];
12
+ /**
13
+ * Variant that parses memory_sources.meta_json shape if present.
14
+ * Expected shape: {"files":{"path":{"mtime":123,"size":456}}} or legacy null/string.
15
+ * For v2.1, meta_json may be absent (no per-file tracking yet) — returns [] gracefully.
16
+ * Kept for spec compatibility (plan §4.3 collectDeletions(prevMetaJson, currentFiles)).
17
+ */
18
+ export declare function collectDeletionsFromMeta(prevMetaJson: string | null, currentFiles: Set<string>, source: string, fingerprintByFile?: Map<string, string>): DeletedInfo[];
@@ -0,0 +1,54 @@
1
+ // K21-005 — Source deletion sync helper
2
+ // Detects memories from a source that have disappeared from the source's current fetch.
3
+ // The source of truth is fingerprints: a memory whose fingerprint is no longer in the
4
+ // fetched set is a candidate for archival + tombstone.
5
+ // File-level meta_json diff (plan §4.3) is not present in v2.0's IdleSync — this layer
6
+ // diffs at fingerprint granularity, which is source-agnostic and cross-file safe.
7
+ // Cross-source protection is enforced by the caller: only memories with matching source
8
+ // are considered deletions.
9
+ /**
10
+ * Pure diff: fingerprints present in prev but absent from current.
11
+ * Both sets are expected to be non-null; empty sets return [].
12
+ * Malformed inputs (null, non-Set) return [] never throw.
13
+ */
14
+ export function collectDeletions(prevFingerprints, currentFingerprints, source) {
15
+ if (!prevFingerprints || !currentFingerprints)
16
+ return [];
17
+ const prev = prevFingerprints instanceof Set ? prevFingerprints : new Set(prevFingerprints);
18
+ const curr = currentFingerprints instanceof Set ? currentFingerprints : new Set(currentFingerprints);
19
+ const out = [];
20
+ for (const fp of prev) {
21
+ if (typeof fp !== "string" || fp.length === 0)
22
+ continue;
23
+ if (!curr.has(fp)) {
24
+ out.push({ source, fingerprint: fp });
25
+ }
26
+ }
27
+ return out;
28
+ }
29
+ /**
30
+ * Variant that parses memory_sources.meta_json shape if present.
31
+ * Expected shape: {"files":{"path":{"mtime":123,"size":456}}} or legacy null/string.
32
+ * For v2.1, meta_json may be absent (no per-file tracking yet) — returns [] gracefully.
33
+ * Kept for spec compatibility (plan §4.3 collectDeletions(prevMetaJson, currentFiles)).
34
+ */
35
+ export function collectDeletionsFromMeta(prevMetaJson, currentFiles, source, fingerprintByFile) {
36
+ if (!prevMetaJson)
37
+ return [];
38
+ try {
39
+ const parsed = JSON.parse(prevMetaJson);
40
+ if (!parsed || typeof parsed.files !== "object" || parsed.files === null)
41
+ return [];
42
+ const out = [];
43
+ for (const file of Object.keys(parsed.files)) {
44
+ if (!currentFiles.has(file)) {
45
+ const fp = fingerprintByFile?.get(file) ?? file;
46
+ out.push({ source, file, fingerprint: fp });
47
+ }
48
+ }
49
+ return out;
50
+ }
51
+ catch {
52
+ return [];
53
+ }
54
+ }
package/package.json CHANGED
@@ -1,28 +1,26 @@
1
1
  {
2
- "name": "@jmtrin/kevin-core",
3
- "version": "1.5.0",
4
- "description": "Kevin core — host-agnostic deterministic brain (Bedrock)",
5
- "type": "module",
6
- "main": "dist/index.js",
7
- "types": "dist/index.d.ts",
8
- "exports": {
9
- ".": {
10
- "types": "./dist/index.d.ts",
11
- "import": "./dist/index.js"
12
- }
13
- },
14
- "files": [
15
- "dist"
16
- ],
17
- "scripts": {
18
- "build": "tsc -p tsconfig.json && node ./scripts/copy-migrations.mjs",
19
- "typecheck": "tsc --noEmit"
20
- },
21
- "engines": {
22
- "node": ">=22.5.0"
23
- },
24
- "publishConfig": {
25
- "access": "public"
26
- },
27
- "license": "MIT"
2
+ "name": "@jmtrin/kevin-core",
3
+ "version": "2.1.0",
4
+ "description": "Kevin core — host-agnostic deterministic brain (Bedrock)",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": ["dist"],
15
+ "scripts": {
16
+ "build": "tsc -p tsconfig.json && node ./scripts/copy-migrations.mjs",
17
+ "typecheck": "tsc --noEmit"
18
+ },
19
+ "engines": {
20
+ "node": ">=22.5.0"
21
+ },
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "license": "MIT"
28
26
  }