kontex-core 1.0.0 → 1.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.
package/dist/index.js CHANGED
@@ -16,41 +16,6 @@ var __export = (target, all) => {
16
16
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
17
  var __require = import.meta.require;
18
18
 
19
- // src/secrets.ts
20
- function buildExtraPatterns(extraPatterns) {
21
- const result = [];
22
- for (const pattern of extraPatterns) {
23
- try {
24
- result.push({ name: `custom:${pattern.slice(0, 30)}`, regex: new RegExp(pattern) });
25
- } catch {}
26
- }
27
- return result;
28
- }
29
- function scanForSecrets(content, extraPatterns = []) {
30
- const allPatterns = [...SECRET_PATTERNS, ...buildExtraPatterns(extraPatterns)];
31
- for (const { name, regex } of allPatterns) {
32
- if (regex.test(content)) {
33
- return { blocked: true, pattern: name };
34
- }
35
- }
36
- return { blocked: false };
37
- }
38
- var SECRET_PATTERNS;
39
- var init_secrets = __esm(() => {
40
- SECRET_PATTERNS = [
41
- { name: "generic-long-token", regex: /['"]\w{32,}['"]/ },
42
- { name: "api-key-assignment", regex: /api[_-]?key\s*[:=]\s*['"]?\w+/i },
43
- { name: "secret-key-assignment", regex: /secret[_-]?key\s*[:=]\s*['"]?\w+/i },
44
- { name: "password-assignment", regex: /password\s*[:=]\s*['"]?[^\s'"]{8,}/i },
45
- { name: "postgres-connection", regex: /postgres:\/\/[^@]+:[^@]+@/ },
46
- { name: "mysql-connection", regex: /mysql:\/\/[^@]+:[^@]+@/ },
47
- { name: "mongodb-connection", regex: /mongodb\+srv:\/\/[^@]+:[^@]+@/ },
48
- { name: "openai-key", regex: /sk-[a-zA-Z0-9]{40,}/ },
49
- { name: "github-personal-token", regex: /ghp_[a-zA-Z0-9]{36}/ },
50
- { name: "aws-access-key", regex: /AKIA[A-Z0-9]{16}/ }
51
- ];
52
- });
53
-
54
19
  // src/storage/db.ts
55
20
  var exports_db = {};
56
21
  __export(exports_db, {
@@ -63,8 +28,9 @@ import { join as join2 } from "path";
63
28
  import { existsSync as existsSync2, mkdirSync } from "fs";
64
29
  import * as sqliteVec from "sqlite-vec";
65
30
  function getDatabase(workspaceRoot) {
66
- if (dbInstance)
67
- return dbInstance;
31
+ const existing = dbInstances.get(workspaceRoot);
32
+ if (existing)
33
+ return existing;
68
34
  const indexDir = join2(workspaceRoot, INDEX_DIR);
69
35
  if (!existsSync2(indexDir))
70
36
  mkdirSync(indexDir, { recursive: true });
@@ -73,13 +39,20 @@ function getDatabase(workspaceRoot) {
73
39
  db.exec("PRAGMA synchronous=NORMAL");
74
40
  tryLoadVecExtension(db);
75
41
  runMigrations(db);
76
- dbInstance = db;
42
+ dbInstances.set(workspaceRoot, db);
77
43
  return db;
78
44
  }
79
- function closeDatabase() {
80
- if (dbInstance) {
81
- dbInstance.close();
82
- dbInstance = null;
45
+ function closeDatabase(workspaceRoot) {
46
+ if (workspaceRoot) {
47
+ const db = dbInstances.get(workspaceRoot);
48
+ if (db) {
49
+ db.close();
50
+ dbInstances.delete(workspaceRoot);
51
+ }
52
+ } else {
53
+ for (const db of dbInstances.values())
54
+ db.close();
55
+ dbInstances.clear();
83
56
  }
84
57
  }
85
58
  function runMigrations(db) {
@@ -131,12 +104,240 @@ function tryLoadVecExtension(db) {
131
104
  console.warn(`kontex: sqlite-vec extension not available (${message}). Semantic search will use keyword matching.`);
132
105
  }
133
106
  }
134
- var INDEX_DIR = ".kontex-index", DB_FILENAME = "index.db", EMBEDDING_DIM = 384, dbInstance = null;
135
- var init_db = () => {};
107
+ var INDEX_DIR = ".kontex-index", DB_FILENAME = "index.db", EMBEDDING_DIM = 384, dbInstances;
108
+ var init_db = __esm(() => {
109
+ dbInstances = new Map;
110
+ });
111
+
112
+ // src/llm.ts
113
+ var exports_llm = {};
114
+ __export(exports_llm, {
115
+ createLLMModel: () => createLLMModel
116
+ });
117
+ async function createLLMModel(config, token) {
118
+ const { createOpenAI } = await import("@ai-sdk/openai");
119
+ switch (config.llm.provider) {
120
+ case "github-models":
121
+ return createOpenAI({
122
+ baseURL: "https://models.inference.ai.azure.com",
123
+ apiKey: token ?? ""
124
+ })(config.llm.model);
125
+ case "openai":
126
+ return createOpenAI({
127
+ apiKey: config.llm.apiKey ?? process.env.OPENAI_API_KEY ?? ""
128
+ })(config.llm.model);
129
+ case "ollama":
130
+ return createOpenAI({
131
+ baseURL: "http://localhost:11434/v1",
132
+ apiKey: "ollama"
133
+ })(config.llm.model);
134
+ case "anthropic":
135
+ return null;
136
+ case "none":
137
+ return null;
138
+ default:
139
+ return null;
140
+ }
141
+ }
142
+
143
+ // src/config.ts
144
+ import { readFileSync, writeFileSync, existsSync } from "fs";
145
+ import { join } from "path";
146
+ var DEFAULT_CONFIG = {
147
+ compile: {
148
+ tokenBudget: 3000,
149
+ alwaysInclude: ["memory/project.md"],
150
+ excludePaths: ["memory/sessions/archive/"]
151
+ },
152
+ embedding: {
153
+ provider: "local",
154
+ model: "Xenova/all-MiniLM-L6-v2"
155
+ },
156
+ llm: {
157
+ provider: "github-models",
158
+ model: "gpt-4o-mini"
159
+ },
160
+ quality: {
161
+ minConfidence: 0.6,
162
+ autoVerifyThreshold: 0.85,
163
+ deduplicateThreshold: 0.92,
164
+ contradictionThreshold: 0.75
165
+ },
166
+ hooks: {
167
+ postCommitExtract: true,
168
+ postMergeRecompile: true,
169
+ maxBackgroundRetries: 2
170
+ },
171
+ secrets: {
172
+ scan: true,
173
+ extraPatterns: []
174
+ },
175
+ decay: {
176
+ sessionArchiveDays: 7,
177
+ unverifiedExpireDays: 30,
178
+ maxSessionsDirKB: 500
179
+ }
180
+ };
181
+ var CONFIG_FILENAME = "kontex.config.json";
182
+ function resolveEnvVars(value) {
183
+ return value.replace(/\$\{(\w+)\}/g, (_, envKey) => {
184
+ return process.env[envKey] ?? "";
185
+ });
186
+ }
187
+ function deepMerge(defaults, overrides) {
188
+ const result = { ...defaults };
189
+ for (const key of Object.keys(overrides)) {
190
+ const overrideVal = overrides[key];
191
+ const defaultVal = defaults[key];
192
+ if (overrideVal !== undefined && typeof overrideVal === "object" && !Array.isArray(overrideVal) && typeof defaultVal === "object" && !Array.isArray(defaultVal) && defaultVal !== null) {
193
+ result[key] = deepMerge(defaultVal, overrideVal);
194
+ } else if (overrideVal !== undefined) {
195
+ result[key] = overrideVal;
196
+ }
197
+ }
198
+ return result;
199
+ }
200
+ function loadConfig(workspaceRoot) {
201
+ const configPath = join(workspaceRoot, CONFIG_FILENAME);
202
+ if (!existsSync(configPath)) {
203
+ return { ...DEFAULT_CONFIG };
204
+ }
205
+ try {
206
+ const raw = readFileSync(configPath, "utf-8");
207
+ const parsed = JSON.parse(raw);
208
+ const merged = deepMerge(DEFAULT_CONFIG, parsed);
209
+ if (merged.llm && merged.llm.apiKey) {
210
+ merged.llm.apiKey = resolveEnvVars(merged.llm.apiKey);
211
+ }
212
+ return merged;
213
+ } catch (err) {
214
+ console.warn(`kontex: Failed to parse ${configPath} \u2014 using defaults. Fix the JSON syntax to apply your settings.
215
+ Error: ${err instanceof Error ? err.message : String(err)}`);
216
+ return { ...DEFAULT_CONFIG };
217
+ }
218
+ }
219
+ function writeConfig(workspaceRoot, config) {
220
+ const configPath = join(workspaceRoot, CONFIG_FILENAME);
221
+ const serializable = { ...config };
222
+ writeFileSync(configPath, JSON.stringify(serializable, null, 2) + `
223
+ `, "utf-8");
224
+ }
225
+ // src/secrets.ts
226
+ var SECRET_PATTERNS = [
227
+ { name: "generic-long-token", regex: /['"]\w{32,}['"]/ },
228
+ { name: "api-key-assignment", regex: /api[_-]?key\s*[:=]\s*['"]?\w+/i },
229
+ { name: "secret-key-assignment", regex: /secret[_-]?key\s*[:=]\s*['"]?\w+/i },
230
+ { name: "password-assignment", regex: /password\s*[:=]\s*['"]?[^\s'"]{8,}/i },
231
+ { name: "postgres-connection", regex: /postgres:\/\/[^@]+:[^@]+@/ },
232
+ { name: "mysql-connection", regex: /mysql:\/\/[^@]+:[^@]+@/ },
233
+ { name: "mongodb-connection", regex: /mongodb\+srv:\/\/[^@]+:[^@]+@/ },
234
+ { name: "openai-key", regex: /sk-[a-zA-Z0-9]{40,}/ },
235
+ { name: "github-personal-token", regex: /ghp_[a-zA-Z0-9]{36}/ },
236
+ { name: "aws-access-key", regex: /AKIA[A-Z0-9]{16}/ }
237
+ ];
238
+ var extraPatternCache = new Map;
239
+ function buildExtraPatterns(extraPatterns) {
240
+ if (extraPatterns.length === 0)
241
+ return [];
242
+ const cacheKey = extraPatterns.join("\x00");
243
+ const cached = extraPatternCache.get(cacheKey);
244
+ if (cached)
245
+ return cached;
246
+ const result = [];
247
+ for (const pattern of extraPatterns) {
248
+ try {
249
+ result.push({ name: `custom:${pattern.slice(0, 30)}`, regex: new RegExp(pattern) });
250
+ } catch {}
251
+ }
252
+ extraPatternCache.set(cacheKey, result);
253
+ return result;
254
+ }
255
+ function scanForSecrets(content, extraPatterns = []) {
256
+ const allPatterns = [...SECRET_PATTERNS, ...buildExtraPatterns(extraPatterns)];
257
+ for (const { name, regex } of allPatterns) {
258
+ if (regex.test(content)) {
259
+ return { blocked: true, pattern: name };
260
+ }
261
+ }
262
+ return { blocked: false };
263
+ }
264
+ // src/auth.ts
265
+ var KEYCHAIN_SERVICE = "kontex";
266
+ var KEYCHAIN_ACCOUNT = "github-oauth";
267
+ async function getKeytar() {
268
+ const mod = await import("keytar");
269
+ return mod.default;
270
+ }
271
+ var GITHUB_CLIENT_ID = process.env.KONTEX_GITHUB_CLIENT_ID ?? "Ov23liMXcybhETe03nNJ";
272
+ async function login() {
273
+ const { createOAuthDeviceAuth } = await import("@octokit/auth-oauth-device");
274
+ const auth = createOAuthDeviceAuth({
275
+ clientType: "oauth-app",
276
+ clientId: GITHUB_CLIENT_ID,
277
+ scopes: [],
278
+ onVerification: (verification) => {
279
+ console.log(`
280
+ Visit: ${verification.verification_uri}`);
281
+ console.log(`Code: ${verification.user_code}
282
+ `);
283
+ console.log(`Waiting for authorization...
284
+ `);
285
+ openBrowser(verification.verification_uri).catch(() => {});
286
+ }
287
+ });
288
+ const { token } = await auth({ type: "oauth" });
289
+ const keytar = await getKeytar();
290
+ await keytar.setPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT, token);
291
+ const username = await fetchGitHubUsername(token);
292
+ console.log(`\u2713 Authenticated as @${username}`);
293
+ console.log("\u2713 GitHub Models access confirmed");
294
+ return username;
295
+ }
296
+ async function logout() {
297
+ const keytar = await getKeytar();
298
+ const deleted = await keytar.deletePassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
299
+ console.log(deleted ? "\u2713 Token removed from keychain" : "No token found in keychain");
300
+ }
301
+ async function getToken() {
302
+ const keytar = await getKeytar();
303
+ return keytar.getPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
304
+ }
305
+ async function isAuthenticated() {
306
+ const token = await getToken();
307
+ return token !== null;
308
+ }
309
+ async function fetchGitHubUsername(token) {
310
+ const response = await fetch("https://api.github.com/user", {
311
+ headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json" }
312
+ });
313
+ if (!response.ok)
314
+ throw new Error(`GitHub API returned ${response.status}`);
315
+ const data = await response.json();
316
+ return data.login;
317
+ }
318
+ async function openBrowser(url) {
319
+ const { execFile } = await import("child_process");
320
+ const [cmd, ...args] = process.platform === "darwin" ? ["open", url] : process.platform === "win32" ? ["cmd", "/c", "start", "", url] : ["xdg-open", url];
321
+ return new Promise((resolve, reject) => {
322
+ execFile(cmd, args, (error) => {
323
+ if (error)
324
+ reject(error);
325
+ else
326
+ resolve();
327
+ });
328
+ });
329
+ }
330
+
331
+ // src/index.ts
332
+ init_db();
136
333
 
137
334
  // src/storage/embeddings.ts
138
335
  import { join as join3 } from "path";
139
336
  import { homedir } from "os";
337
+ var DEFAULT_MODEL = "Xenova/all-MiniLM-L6-v2";
338
+ var CACHE_DIR = join3(homedir(), ".cache", "kontex", "models");
339
+ var pipeline = null;
340
+ var modelLoading = null;
140
341
  async function initEmbeddingModel(modelName = DEFAULT_MODEL) {
141
342
  if (pipeline)
142
343
  return;
@@ -157,21 +358,11 @@ async function loadPipeline(modelName) {
157
358
  const pipe = await createPipeline("feature-extraction", modelName);
158
359
  return pipe;
159
360
  }
160
- var DEFAULT_MODEL = "Xenova/all-MiniLM-L6-v2", CACHE_DIR, pipeline = null, modelLoading = null;
161
- var init_embeddings = __esm(() => {
162
- CACHE_DIR = join3(homedir(), ".cache", "kontex", "models");
163
- });
164
-
165
361
  // src/memory/write.ts
166
- var exports_write = {};
167
- __export(exports_write, {
168
- writeMemory: () => writeMemory,
169
- logDecision: () => logDecision,
170
- invalidateMemory: () => invalidateMemory
171
- });
172
362
  import { writeFileSync as writeFileSync2, readFileSync as readFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync2, appendFileSync } from "fs";
173
363
  import { join as join4, dirname } from "path";
174
364
  import matter from "gray-matter";
365
+ init_db();
175
366
  async function writeMemory(entry, workspaceRoot, config) {
176
367
  const logPath = join4(workspaceRoot, ".kontex-log", "quality.log");
177
368
  const secretResult = scanForSecrets(entry.content, config.secrets.extraPatterns);
@@ -233,7 +424,7 @@ ${l1}
233
424
  if (!existsSync3(fileDir))
234
425
  mkdirSync2(fileDir, { recursive: true });
235
426
  writeFileSync2(filePath, fileContent, "utf-8");
236
- await indexMemoryEntry(uri, entry.content, entry.type, verified, entry.confidence, entry.affected_paths ?? [], db, l0, l1, "");
427
+ await indexMemoryEntry(uri, entry.content, entry.type, verified, entry.confidence, entry.affected_paths ?? [], db, l0, l1, "", author, frontmatter.tags, frontmatter.global, frontmatter.stale, frontmatter.ref_count);
237
428
  logQualityEvent(logPath, "WRITTEN", `${uri} (verified: ${verified})`);
238
429
  return { success: true, uri, verified };
239
430
  }
@@ -257,7 +448,11 @@ async function logDecision(adr, workspaceRoot, _config) {
257
448
  mkdirSync2(decisionsDir, { recursive: true });
258
449
  const { readdirSync } = await import("fs");
259
450
  const existing = readdirSync(decisionsDir).filter((f) => f.endsWith(".md"));
260
- const nextNum = String(existing.length + 1).padStart(3, "0");
451
+ const highestNum = existing.reduce((max, filename) => {
452
+ const match = filename.match(/^(\d+)-/);
453
+ return match ? Math.max(max, parseInt(match[1], 10)) : max;
454
+ }, 0);
455
+ const nextNum = String(highestNum + 1).padStart(3, "0");
261
456
  const slug = slugify(adr.title);
262
457
  const uri = `memory/decisions/${nextNum}-${slug}`;
263
458
  const now = new Date().toISOString();
@@ -307,7 +502,7 @@ ${adr.rationale}${alternativesSection}${consequencesSection}
307
502
  writeFileSync2(filePath, fileContent, "utf-8");
308
503
  const db = getDatabase(workspaceRoot);
309
504
  const adrContent = `${adr.title} ${adr.decision} ${adr.context}`;
310
- await indexMemoryEntry(uri, adrContent, "decision", true, 0.95, adr.affected_paths ?? [], db, l0, body, "");
505
+ await indexMemoryEntry(uri, adrContent, "decision", true, 0.95, adr.affected_paths ?? [], db, l0, body, "", author, frontmatter.tags, frontmatter.global, frontmatter.stale, frontmatter.ref_count);
311
506
  return { success: true, uri, verified: true };
312
507
  }
313
508
  async function dedupCheck(content, db, config) {
@@ -327,21 +522,28 @@ async function dedupCheck(content, db, config) {
327
522
  if (similarity > config.quality.contradictionThreshold)
328
523
  return { status: "conflict", existing_uri: top.uri, existing_content: top.content, message: `Similar memory exists at ${top.uri}. If this supersedes it, call kontex_invalidate first.` };
329
524
  return { status: "clear" };
330
- } catch {
525
+ } catch (err) {
526
+ console.warn(`kontex: dedup check failed (embedding or DB error) \u2014 skipping duplicate detection.
527
+ Error: ${err instanceof Error ? err.message : String(err)}`);
331
528
  return { status: "clear" };
332
529
  }
333
530
  }
334
- async function indexMemoryEntry(uri, content, type, verified, confidence, affectedPaths, db, l0 = "", l1 = "", l2 = "") {
335
- db.prepare(`INSERT OR REPLACE INTO memories (uri, content, type, l0, l1, l2, verified, confidence, affected_paths, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))`).run(uri, content, type, l0, l1, l2, verified ? 1 : 0, confidence, JSON.stringify(affectedPaths));
531
+ async function indexMemoryEntry(uri, content, type, verified, confidence, affectedPaths, db, l0 = "", l1 = "", l2 = "", author = "", tags = [], global = false, stale = false, refCount = 0) {
532
+ db.prepare(`INSERT OR REPLACE INTO memories (uri, content, type, l0, l1, l2, verified, confidence, affected_paths, author, tags, global, stale, ref_count, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))`).run(uri, content, type, l0, l1, l2, verified ? 1 : 0, confidence, JSON.stringify(affectedPaths), author, JSON.stringify(tags), global ? 1 : 0, stale ? 1 : 0, refCount);
336
533
  try {
337
534
  const embedding = await embed(content);
338
535
  db.prepare(`INSERT OR REPLACE INTO memory_embeddings (uri, embedding) VALUES (?, ?)`).run(uri, Buffer.from(embedding.buffer));
339
- } catch {}
536
+ } catch (err) {
537
+ console.warn(`kontex: failed to index embedding for ${uri} \u2014 semantic search will not find this entry.
538
+ Error: ${err instanceof Error ? err.message : String(err)}`);
539
+ }
340
540
  }
541
+ var uriCounter = 0;
341
542
  function generateUri(type, content) {
342
543
  const slug = slugify(content.split(`
343
544
  `)[0]?.slice(0, 60) ?? "entry");
344
- return `memory/${type}s/${slug}-${Date.now().toString(36)}`;
545
+ const uniqueSuffix = `${Date.now().toString(36)}-${(uriCounter++).toString(36)}`;
546
+ return `memory/${type}s/${slug}-${uniqueSuffix}`;
345
547
  }
346
548
  function slugify(text) {
347
549
  return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50);
@@ -371,199 +573,11 @@ function logQualityEvent(logPath, event, message) {
371
573
  function logSecurityEvent(workspaceRoot, pattern) {
372
574
  logQualityEvent(join4(workspaceRoot, ".kontex-log", "security.log"), "SECRET_BLOCKED", pattern);
373
575
  }
374
- var init_write = __esm(() => {
375
- init_secrets();
376
- init_embeddings();
377
- init_db();
378
- });
379
-
380
- // src/llm.ts
381
- var exports_llm = {};
382
- __export(exports_llm, {
383
- createLLMModel: () => createLLMModel
384
- });
385
- async function createLLMModel(config, token) {
386
- const { createOpenAI } = await import("@ai-sdk/openai");
387
- switch (config.llm.provider) {
388
- case "github-models":
389
- return createOpenAI({
390
- baseURL: "https://models.inference.ai.azure.com",
391
- apiKey: token ?? ""
392
- })(config.llm.model);
393
- case "openai":
394
- return createOpenAI({
395
- apiKey: config.llm.apiKey ?? process.env.OPENAI_API_KEY ?? ""
396
- })(config.llm.model);
397
- case "ollama":
398
- return createOpenAI({
399
- baseURL: "http://localhost:11434/v1",
400
- apiKey: "ollama"
401
- })(config.llm.model);
402
- case "anthropic":
403
- return null;
404
- case "none":
405
- return null;
406
- default:
407
- return null;
408
- }
409
- }
410
-
411
- // src/config.ts
412
- import { readFileSync, writeFileSync, existsSync } from "fs";
413
- import { join } from "path";
414
- var DEFAULT_CONFIG = {
415
- compile: {
416
- tokenBudget: 3000,
417
- alwaysInclude: ["memory/project.md"],
418
- excludePaths: ["memory/sessions/archive/"]
419
- },
420
- embedding: {
421
- provider: "local",
422
- model: "Xenova/all-MiniLM-L6-v2"
423
- },
424
- llm: {
425
- provider: "github-models",
426
- model: "gpt-4o-mini"
427
- },
428
- quality: {
429
- minConfidence: 0.6,
430
- autoVerifyThreshold: 0.85,
431
- deduplicateThreshold: 0.92,
432
- contradictionThreshold: 0.75
433
- },
434
- hooks: {
435
- postCommitExtract: true,
436
- postMergeRecompile: true,
437
- maxBackgroundRetries: 2
438
- },
439
- secrets: {
440
- scan: true,
441
- extraPatterns: []
442
- },
443
- decay: {
444
- sessionArchiveDays: 7,
445
- unverifiedExpireDays: 30,
446
- maxSessionsDirKB: 500
447
- }
448
- };
449
- var CONFIG_FILENAME = "kontex.config.json";
450
- function resolveEnvVars(value) {
451
- return value.replace(/\$\{(\w+)\}/g, (_, envKey) => {
452
- return process.env[envKey] ?? "";
453
- });
454
- }
455
- function deepMerge(defaults, overrides) {
456
- const result = { ...defaults };
457
- for (const key of Object.keys(overrides)) {
458
- const overrideVal = overrides[key];
459
- const defaultVal = defaults[key];
460
- if (overrideVal !== undefined && typeof overrideVal === "object" && !Array.isArray(overrideVal) && typeof defaultVal === "object" && !Array.isArray(defaultVal) && defaultVal !== null) {
461
- result[key] = deepMerge(defaultVal, overrideVal);
462
- } else if (overrideVal !== undefined) {
463
- result[key] = overrideVal;
464
- }
465
- }
466
- return result;
467
- }
468
- function loadConfig(workspaceRoot) {
469
- const configPath = join(workspaceRoot, CONFIG_FILENAME);
470
- if (!existsSync(configPath)) {
471
- return { ...DEFAULT_CONFIG };
472
- }
473
- try {
474
- const raw = readFileSync(configPath, "utf-8");
475
- const parsed = JSON.parse(raw);
476
- const merged = deepMerge(DEFAULT_CONFIG, parsed);
477
- if (merged.llm && merged.llm.apiKey) {
478
- merged.llm.apiKey = resolveEnvVars(merged.llm.apiKey);
479
- }
480
- return merged;
481
- } catch {
482
- return { ...DEFAULT_CONFIG };
483
- }
484
- }
485
- function writeConfig(workspaceRoot, config) {
486
- const configPath = join(workspaceRoot, CONFIG_FILENAME);
487
- const serializable = { ...config };
488
- writeFileSync(configPath, JSON.stringify(serializable, null, 2) + `
489
- `, "utf-8");
490
- }
491
-
492
- // src/index.ts
493
- init_secrets();
494
-
495
- // src/auth.ts
496
- import keytar from "keytar";
497
- var KEYCHAIN_SERVICE = "kontex";
498
- var KEYCHAIN_ACCOUNT = "github-oauth";
499
- var GITHUB_CLIENT_ID = "Ov23liMXcybhETe03nNJ";
500
- async function login() {
501
- const { createOAuthDeviceAuth } = await import("@octokit/auth-oauth-device");
502
- const auth = createOAuthDeviceAuth({
503
- clientType: "oauth-app",
504
- clientId: GITHUB_CLIENT_ID,
505
- scopes: [],
506
- onVerification: (verification) => {
507
- console.log(`
508
- Visit: ${verification.verification_uri}`);
509
- console.log(`Code: ${verification.user_code}
510
- `);
511
- console.log(`Waiting for authorization...
512
- `);
513
- openBrowser(verification.verification_uri).catch(() => {});
514
- }
515
- });
516
- const { token } = await auth({ type: "oauth" });
517
- await keytar.setPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT, token);
518
- const username = await fetchGitHubUsername(token);
519
- console.log(`\u2713 Authenticated as @${username}`);
520
- console.log("\u2713 GitHub Models access confirmed");
521
- return username;
522
- }
523
- async function logout() {
524
- const deleted = await keytar.deletePassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
525
- console.log(deleted ? "\u2713 Token removed from keychain" : "No token found in keychain");
526
- }
527
- async function getToken() {
528
- return keytar.getPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
529
- }
530
- async function isAuthenticated() {
531
- const token = await getToken();
532
- return token !== null;
533
- }
534
- async function fetchGitHubUsername(token) {
535
- const response = await fetch("https://api.github.com/user", {
536
- headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json" }
537
- });
538
- if (!response.ok)
539
- throw new Error(`GitHub API returned ${response.status}`);
540
- const data = await response.json();
541
- return data.login;
542
- }
543
- async function openBrowser(url) {
544
- const { exec } = await import("child_process");
545
- const command = process.platform === "darwin" ? `open "${url}"` : process.platform === "win32" ? `start "${url}"` : `xdg-open "${url}"`;
546
- return new Promise((resolve, reject) => {
547
- exec(command, (error) => {
548
- if (error)
549
- reject(error);
550
- else
551
- resolve();
552
- });
553
- });
554
- }
555
-
556
- // src/index.ts
557
- init_db();
558
- init_embeddings();
559
- init_write();
560
-
561
576
  // src/memory/read.ts
562
- init_embeddings();
563
- init_db();
564
577
  import { readFileSync as readFileSync3, readdirSync, existsSync as existsSync4 } from "fs";
565
578
  import { join as join5, relative } from "path";
566
579
  import matter2 from "gray-matter";
580
+ init_db();
567
581
  async function findMemories(query, limit = 5, workspaceRoot) {
568
582
  const db = getDatabase(workspaceRoot);
569
583
  try {
@@ -682,49 +696,52 @@ function splitTiers(content) {
682
696
  import { writeFileSync as writeFileSync3 } from "fs";
683
697
  import { join as join6 } from "path";
684
698
  import { encoding_for_model } from "tiktoken";
685
- var cachedEncoder = null;
686
- function getEncoder() {
687
- if (!cachedEncoder) {
688
- try {
689
- cachedEncoder = encoding_for_model("gpt-4o-mini");
690
- } catch {
691
- return { encode: (text) => new Uint32Array(Math.ceil(text.length / 4)) };
692
- }
699
+ function createEncoder() {
700
+ try {
701
+ return encoding_for_model("gpt-4o-mini");
702
+ } catch {
703
+ return null;
693
704
  }
694
- return cachedEncoder;
695
705
  }
696
706
  async function compile(workspaceRoot, config) {
697
- const allEntries = loadAllEntries(workspaceRoot);
698
- const recentFiles = await getRecentlyModifiedFiles(workspaceRoot, 7);
699
- let tokenCount = 0;
700
- const sections = [];
701
- const systemPrompt = buildSystemPrompt();
702
- sections.push(systemPrompt);
703
- tokenCount += estimateTokens(systemPrompt);
704
- const verifiedEntries = allEntries.filter((e) => e.verified && !e.stale);
705
- const l0Section = buildL0Index(verifiedEntries);
706
- sections.push(l0Section);
707
- tokenCount += estimateTokens(l0Section);
708
- const relevant = verifiedEntries.filter((e) => e.global || isRecentlyTouched(e, recentFiles) || isRecentlyCreated(e, 7)).sort((a, b) => b.ref_count - a.ref_count);
709
- const includedUris = new Set;
710
- for (const entry of relevant) {
711
- const l1Content = formatL1Section(entry);
712
- const tokens = estimateTokens(l1Content);
713
- if (tokenCount + tokens > config.compile.tokenBudget)
714
- break;
715
- sections.push(l1Content);
716
- tokenCount += tokens;
717
- includedUris.add(entry.uri);
718
- }
719
- const l2Available = allEntries.filter((e) => !e.stale && !includedUris.has(e.uri));
720
- if (l2Available.length > 0)
721
- sections.push(buildL2Footer(l2Available));
722
- const output = sections.join(`
707
+ const encoder = createEncoder();
708
+ try {
709
+ const allEntries = loadAllEntries(workspaceRoot);
710
+ const recentFiles = await getRecentlyModifiedFiles(workspaceRoot, 7);
711
+ let tokenCount = 0;
712
+ const sections = [];
713
+ const systemPrompt = buildSystemPrompt();
714
+ sections.push(systemPrompt);
715
+ tokenCount += estimateTokens(systemPrompt, encoder);
716
+ const verifiedEntries = allEntries.filter((e) => e.verified && !e.stale);
717
+ const l0Section = buildL0Index(verifiedEntries);
718
+ sections.push(l0Section);
719
+ tokenCount += estimateTokens(l0Section, encoder);
720
+ const relevant = verifiedEntries.filter((e) => e.global || isRecentlyTouched(e, recentFiles) || isRecentlyCreated(e, 7)).sort((a, b) => b.ref_count - a.ref_count);
721
+ const includedUris = new Set;
722
+ for (const entry of relevant) {
723
+ const l1Content = formatL1Section(entry);
724
+ const tokens = estimateTokens(l1Content, encoder);
725
+ if (tokenCount + tokens > config.compile.tokenBudget)
726
+ break;
727
+ sections.push(l1Content);
728
+ tokenCount += tokens;
729
+ includedUris.add(entry.uri);
730
+ }
731
+ const l2Available = allEntries.filter((e) => !e.stale && !includedUris.has(e.uri));
732
+ if (l2Available.length > 0)
733
+ sections.push(buildL2Footer(l2Available));
734
+ const output = sections.join(`
723
735
 
724
736
  ---
725
737
 
726
738
  `);
727
- writeFileSync3(join6(workspaceRoot, ".context", "KONTEX.md"), output, "utf-8");
739
+ writeFileSync3(join6(workspaceRoot, ".context", "KONTEX.md"), output, "utf-8");
740
+ } finally {
741
+ try {
742
+ encoder?.free();
743
+ } catch {}
744
+ }
728
745
  }
729
746
  function buildSystemPrompt() {
730
747
  return `## Project memory (kontex)
@@ -796,12 +813,12 @@ async function getRecentlyModifiedFiles(workspaceRoot, days) {
796
813
  return [];
797
814
  }
798
815
  }
799
- function estimateTokens(text) {
816
+ function estimateTokens(text, encoder) {
800
817
  try {
801
- return getEncoder().encode(text).length;
802
- } catch {
803
- return Math.ceil(text.length / 4);
804
- }
818
+ if (encoder)
819
+ return encoder.encode(text).length;
820
+ } catch {}
821
+ return Math.ceil(text.length / 4);
805
822
  }
806
823
  // src/memory/decay.ts
807
824
  import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, readdirSync as readdirSync2, statSync, existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
@@ -864,12 +881,14 @@ async function archiveSessions(memoryDir, archiveDays, result) {
864
881
  const filePath = join7(sessionsDir, file.name);
865
882
  try {
866
883
  if (statSync(filePath).mtime < cutoff) {
867
- writeFileSync4(join7(archiveDir, file.name), readFileSync4(filePath, "utf-8"), "utf-8");
884
+ const archiveDest = join7(archiveDir, file.name);
885
+ const finalDest = existsSync5(archiveDest) ? join7(archiveDir, file.name.replace(".md", `-${Date.now()}.md`)) : archiveDest;
886
+ writeFileSync4(finalDest, readFileSync4(filePath, "utf-8"), "utf-8");
868
887
  writeFileSync4(filePath, `---
869
888
  archived: true
870
889
  archived_at: ${new Date().toISOString()}
871
890
  ---
872
- Archived to archive/${file.name}
891
+ Archived to archive/${basename(finalDest)}
873
892
  `, "utf-8");
874
893
  result.archived.push(file.name);
875
894
  }
@@ -908,7 +927,9 @@ async function enforceSessionsSizeCap(memoryDir, maxSizeKB, result) {
908
927
  if (totalKB <= maxSizeKB)
909
928
  break;
910
929
  totalKB -= statSync(file.path).size / 1024;
911
- writeFileSync4(join7(archiveDir, file.name), readFileSync4(file.path, "utf-8"), "utf-8");
930
+ const archiveDest = join7(archiveDir, file.name);
931
+ const finalDest = existsSync5(archiveDest) ? join7(archiveDir, file.name.replace(".md", `-${Date.now()}.md`)) : archiveDest;
932
+ writeFileSync4(finalDest, readFileSync4(file.path, "utf-8"), "utf-8");
912
933
  writeFileSync4(file.path, `---
913
934
  archived: true
914
935
  ---
@@ -1009,7 +1030,6 @@ Do not guess what memory contains \u2014 search it.`,
1009
1030
  ];
1010
1031
 
1011
1032
  // src/mcp/handlers.ts
1012
- init_write();
1013
1033
  import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, existsSync as existsSync6 } from "fs";
1014
1034
  import { join as join8 } from "path";
1015
1035
  import matter4 from "gray-matter";
@@ -1154,7 +1174,6 @@ function findAffectedMemories(stagedFiles, workspaceRoot) {
1154
1174
  import { writeFileSync as writeFileSync7, existsSync as existsSync8, unlinkSync, mkdirSync as mkdirSync4, appendFileSync as appendFileSync2 } from "fs";
1155
1175
  import { join as join10, dirname as dirname2 } from "path";
1156
1176
  import { createHash } from "crypto";
1157
- init_write();
1158
1177
  async function handlePostCommit(commitSha, authorEmail, workspaceRoot) {
1159
1178
  const config = loadConfig(workspaceRoot);
1160
1179
  const logPath = join10(workspaceRoot, ".kontex-log", "hooks.log");
@@ -1184,10 +1203,8 @@ async function handlePostCommit(commitSha, authorEmail, workspaceRoot) {
1184
1203
  await writeMemory({ content: m.content, type: m.type, why_memorable: m.why_memorable, confidence: m.confidence, affected_paths: m.affected_paths }, workspaceRoot, config);
1185
1204
  }
1186
1205
  await writeSessionFile(commitSha, authorEmail, extraction, workspaceRoot);
1187
- if (extraction.stale_uris.length > 0) {
1188
- const { invalidateMemory: invalidateMemory2 } = await Promise.resolve().then(() => (init_write(), exports_write));
1189
- for (const uri of extraction.stale_uris)
1190
- await invalidateMemory2(uri, `Flagged stale by commit ${commitSha.slice(0, 7)}`, workspaceRoot);
1206
+ for (const uri of extraction.stale_uris) {
1207
+ await invalidateMemory(uri, `Flagged stale by commit ${commitSha.slice(0, 7)}`, workspaceRoot);
1191
1208
  }
1192
1209
  }
1193
1210
  await compileAndCommit(workspaceRoot, config);
@@ -1273,6 +1290,10 @@ async function getCommitDiff(sha, workspaceRoot) {
1273
1290
  return "";
1274
1291
  }
1275
1292
  }
1293
+ function isGitOperationInProgress(workspaceRoot) {
1294
+ const gitDir = join10(workspaceRoot, ".git");
1295
+ return ["REBASE_HEAD", "MERGE_HEAD", "CHERRY_PICK_HEAD", "REVERT_HEAD", "BISECT_LOG"].some((f) => existsSync8(join10(gitDir, f)));
1296
+ }
1276
1297
  async function compileAndCommit(workspaceRoot, config) {
1277
1298
  if (!existsSync8(join10(workspaceRoot, ".context")))
1278
1299
  return;
@@ -1280,8 +1301,12 @@ async function compileAndCommit(workspaceRoot, config) {
1280
1301
  try {
1281
1302
  Bun.spawnSync(["git", "add", ".context/"], { cwd: workspaceRoot });
1282
1303
  const status = Bun.spawnSync(["git", "diff", "--cached", "--quiet", ".context/"], { cwd: workspaceRoot });
1283
- if (status.exitCode !== 0)
1304
+ if (status.exitCode !== 0) {
1305
+ if (isGitOperationInProgress(workspaceRoot)) {
1306
+ return;
1307
+ }
1284
1308
  Bun.spawnSync(["git", "commit", "--no-verify", "-m", "chore(kontex): update memory [skip ci]"], { cwd: workspaceRoot });
1309
+ }
1285
1310
  } catch {}
1286
1311
  }
1287
1312
  function logHookEvent(logPath, event, message) {
@@ -1295,7 +1320,6 @@ function logHookEvent(logPath, event, message) {
1295
1320
  import { existsSync as existsSync9 } from "fs";
1296
1321
  import { join as join11 } from "path";
1297
1322
  init_db();
1298
- init_embeddings();
1299
1323
  async function handlePostMerge(workspaceRoot) {
1300
1324
  if (!existsSync9(join11(workspaceRoot, ".context")))
1301
1325
  return;
@@ -1323,10 +1347,8 @@ async function rebuildIndex(workspaceRoot) {
1323
1347
  import { existsSync as existsSync11, readFileSync as readFileSync7, writeFileSync as writeFileSync8, mkdirSync as mkdirSync5, appendFileSync as appendFileSync3, chmodSync } from "fs";
1324
1348
  import { join as join13, resolve } from "path";
1325
1349
  import { homedir as homedir2 } from "os";
1326
- init_embeddings();
1327
1350
 
1328
1351
  // src/memory/extract.ts
1329
- init_write();
1330
1352
  import { readFileSync as readFileSync6, existsSync as existsSync10 } from "fs";
1331
1353
  import { join as join12 } from "path";
1332
1354
  async function runInitAI(workspaceRoot) {
@@ -1619,7 +1641,7 @@ function registerInAITools(root) {
1619
1641
  const home = homedir2();
1620
1642
  const entry = { command: "bunx", args: ["kontex", "mcp"], env: { KONTEX_WORKSPACE: root } };
1621
1643
  const tools = [
1622
- { name: "Claude Code", configPath: join13(home, ".claude", "claude_desktop_config.json"), key: "mcpServers" },
1644
+ { name: "Claude Code", configPath: join13(home, ".claude.json"), key: "mcpServers" },
1623
1645
  { name: "Cursor", configPath: join13(root, ".cursor", "mcp.json"), key: "mcpServers" },
1624
1646
  { name: "Windsurf", configPath: join13(home, ".codeium", "windsurf", "mcp_config.json"), key: "mcpServers" },
1625
1647
  { name: "Zed", configPath: join13(home, ".config", "zed", "settings.json"), key: "mcpServers" }
Binary file
package/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "kontex-core",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Core engine for kontex — quality gate, MCP server, compilation, embedding, storage, hooks, auth",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
+ "engines": {
8
+ "bun": ">=1.1.0"
9
+ },
7
10
  "repository": {
8
11
  "type": "git",
9
12
  "url": "git+https://github.com/ArekBee/kontex.git",