kodelyth-ecc 2.24.11 → 2.29.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/CHANGELOG.md +208 -0
- package/CLAUDE.md +2 -2
- package/README.md +7 -4
- package/agents/super-agent.md +102 -0
- package/bin/kodelyth-ecc.js +57 -2
- package/commands/super-agent.md +47 -0
- package/hooks/memory/capture-stop.js +1 -1
- package/install.ps1 +12 -1
- package/install.sh +14 -1
- package/package.json +5 -2
- package/rules/common/agent-intent-routing.md +6 -1
- package/scripts/cli/menu.js +1 -1
- package/scripts/codebase/index.js +59 -2
- package/scripts/hooks/post-bash-command-log.js +4 -11
- package/scripts/hooks/session-start.js +25 -0
- package/scripts/lib/redact.js +58 -0
- package/scripts/lib/state-store/index.js +36 -1
- package/scripts/lib/state-store/schema.js +18 -1
- package/scripts/mcp/register-self.js +120 -3
- package/scripts/memory/cli.js +35 -0
- package/scripts/memory/embedding-providers/local-transformers.js +44 -0
- package/scripts/memory/embeddings.js +98 -0
- package/scripts/memory/instinct-inject.js +67 -0
- package/scripts/memory/pending.js +125 -0
- package/scripts/memory/project-id.js +93 -0
- package/scripts/memory/store.js +123 -13
- package/scripts/rtk/index.js +43 -1
- package/scripts/super-agent/cli.js +135 -0
- package/scripts/super-agent/dag.js +123 -0
- package/scripts/super-agent/integrate.js +91 -0
- package/scripts/super-agent/orchestrator.js +175 -0
- package/scripts/super-agent/state.js +136 -0
- package/scripts/terse/ledger.js +16 -5
- package/skills/terse-mode/SKILL.md +1 -1
|
@@ -22,6 +22,52 @@ function getVersion() {
|
|
|
22
22
|
catch { return null; }
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
// The binary's `--version` string has proven unreliable: builds have reported an
|
|
26
|
+
// old number (e.g. 0.8.1) while exposing much newer tools and the `--ui` (CBM
|
|
27
|
+
// Lens) flag. So detect what the binary can actually DO from its own help text
|
|
28
|
+
// rather than trusting the version. Pure parser (unit-tested) + a thin probe.
|
|
29
|
+
const KNOWN_CAPABILITIES = {
|
|
30
|
+
lens: /--ui\b/, // CBM Lens UI (v0.12+)
|
|
31
|
+
traces: /\bingest_traces\b/, // runtime-trace ingestion
|
|
32
|
+
cypher: /\bquery_graph\b/, // Cypher + complexity/hot-path metrics
|
|
33
|
+
smart_ci: /\bdetect_changes\b/, // Smart CI test selection
|
|
34
|
+
adr: /\bmanage_adr\b/, // ADR nodes
|
|
35
|
+
cross_repo: /cross[-_]repo/, // cross-repo intelligence
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function parseCapabilities(helpText) {
|
|
39
|
+
const h = String(helpText || '');
|
|
40
|
+
const caps = {};
|
|
41
|
+
for (const [name, re] of Object.entries(KNOWN_CAPABILITIES)) caps[name] = re.test(h);
|
|
42
|
+
return caps;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// When the binary exposes clearly-newer capabilities but reports an old version,
|
|
46
|
+
// the version string is not trustworthy — say so rather than displaying a number
|
|
47
|
+
// ECC would otherwise treat as fact. Returns a note string, or null.
|
|
48
|
+
function versionNote(reportedVersion, caps) {
|
|
49
|
+
const hasNew = !!(caps && (caps.lens || caps.traces || caps.cross_repo));
|
|
50
|
+
const looksOld = !!(reportedVersion && /\b0\.[0-8]\./.test(reportedVersion));
|
|
51
|
+
if (hasNew && looksOld) {
|
|
52
|
+
return `binary reports "${reportedVersion}" but exposes newer capabilities — the version string is unreliable; capability detection is authoritative. Run \`kodelythecc codebase install\` to refresh.`;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Probe the binary's help (different builds print tools under `--help` or
|
|
58
|
+
// `cli --help`) and parse once. Returns the detected capability map, or all-false
|
|
59
|
+
// if the binary is absent.
|
|
60
|
+
function capabilities() {
|
|
61
|
+
if (!isInstalled()) return parseCapabilities('');
|
|
62
|
+
let help = '';
|
|
63
|
+
for (const argv of [['--help'], ['cli', '--help']]) {
|
|
64
|
+
try {
|
|
65
|
+
help += '\n' + execFileSync(BIN, argv, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
66
|
+
} catch { /* try the next help surface */ }
|
|
67
|
+
}
|
|
68
|
+
return parseCapabilities(help);
|
|
69
|
+
}
|
|
70
|
+
|
|
25
71
|
function install({ log = () => {} } = {}) {
|
|
26
72
|
if (isInstalled()) return { installed: false, skipped: true, reason: 'already installed', version: getVersion() };
|
|
27
73
|
if (os.platform() === 'win32') {
|
|
@@ -95,7 +141,15 @@ function status() {
|
|
|
95
141
|
const home = os.homedir();
|
|
96
142
|
const c = path.join(home, '.cache', 'codebase-memory-mcp');
|
|
97
143
|
if (fs.existsSync(c)) cacheDir = c;
|
|
98
|
-
|
|
144
|
+
const caps = capabilities();
|
|
145
|
+
return {
|
|
146
|
+
installed: true,
|
|
147
|
+
version,
|
|
148
|
+
version_note: versionNote(version, caps),
|
|
149
|
+
capabilities: caps,
|
|
150
|
+
indexed_projects,
|
|
151
|
+
cache_dir: cacheDir,
|
|
152
|
+
};
|
|
99
153
|
}
|
|
100
154
|
|
|
101
155
|
// ── Structural query proxy (thin wrapper over `codebase-memory-mcp cli`) ────
|
|
@@ -140,4 +194,7 @@ function dashboardSnapshot() {
|
|
|
140
194
|
return out;
|
|
141
195
|
}
|
|
142
196
|
|
|
143
|
-
module.exports = {
|
|
197
|
+
module.exports = {
|
|
198
|
+
isInstalled, getVersion, install, autoConfigureAgents, status, query, dashboardSnapshot,
|
|
199
|
+
capabilities, parseCapabilities, versionNote,
|
|
200
|
+
};
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const os = require('os');
|
|
6
6
|
const path = require('path');
|
|
7
|
+
const { redact } = require('../lib/redact');
|
|
7
8
|
|
|
8
9
|
const MAX_STDIN = 1024 * 1024;
|
|
9
10
|
let raw = '';
|
|
@@ -20,17 +21,9 @@ const MODE_CONFIG = {
|
|
|
20
21
|
};
|
|
21
22
|
|
|
22
23
|
function sanitizeCommand(command) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
.replace(/Authorization:[: ]*[^ ]*[: ]*[^ ]*/gi, 'Authorization:<REDACTED>')
|
|
27
|
-
.replace(/\bAKIA[A-Z0-9]{16}\b/g, '<REDACTED>')
|
|
28
|
-
.replace(/\bASIA[A-Z0-9]{16}\b/g, '<REDACTED>')
|
|
29
|
-
.replace(/password[= ][^ ]*/gi, 'password=<REDACTED>')
|
|
30
|
-
.replace(/\bghp_[A-Za-z0-9_]+\b/g, '<REDACTED>')
|
|
31
|
-
.replace(/\bgho_[A-Za-z0-9_]+\b/g, '<REDACTED>')
|
|
32
|
-
.replace(/\bghs_[A-Za-z0-9_]+\b/g, '<REDACTED>')
|
|
33
|
-
.replace(/\bgithub_pat_[A-Za-z0-9_]+\b/g, '<REDACTED>');
|
|
24
|
+
// Flatten to one log line, then run the shared credential redactor (which now
|
|
25
|
+
// covers more token formats than this hook's old inline list did).
|
|
26
|
+
return redact(String(command || '').replace(/\n/g, ' '));
|
|
34
27
|
}
|
|
35
28
|
|
|
36
29
|
function appendLine(filePath, line) {
|
|
@@ -22,6 +22,7 @@ const {
|
|
|
22
22
|
log
|
|
23
23
|
} = require('../lib/utils');
|
|
24
24
|
const { resolveProjectContext, writeSessionLease, resolveSessionId } = require('../lib/observer-sessions');
|
|
25
|
+
const { summarizeStructuredInstincts } = require('../memory/instinct-inject');
|
|
25
26
|
const { getPackageManager, getSelectionPrompt } = require('../lib/package-manager');
|
|
26
27
|
const { listAliases } = require('../lib/session-aliases');
|
|
27
28
|
const { detectProjectType } = require('../lib/project-detect');
|
|
@@ -380,6 +381,30 @@ async function main() {
|
|
|
380
381
|
additionalContextParts.push(instinctSummary);
|
|
381
382
|
}
|
|
382
383
|
|
|
384
|
+
// Also inject from the structured instinct store (scripts/memory/instincts.js).
|
|
385
|
+
// It was written on every correction but never read — this activates the
|
|
386
|
+
// dormant self-learning loop. Fully fail-safe: never breaks session start.
|
|
387
|
+
const structuredInstinctSummary = summarizeStructuredInstincts(
|
|
388
|
+
(observerContext && observerContext.projectRoot) || process.cwd(),
|
|
389
|
+
{ log }
|
|
390
|
+
);
|
|
391
|
+
if (structuredInstinctSummary) {
|
|
392
|
+
additionalContextParts.push(structuredInstinctSummary);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Surface the auto-capture review queue so candidates the last session proposed
|
|
396
|
+
// are discoverable now, not just at the end of that session. Consent-first:
|
|
397
|
+
// this only points at the review command; nothing is stored without the user
|
|
398
|
+
// running promote-pending.
|
|
399
|
+
try {
|
|
400
|
+
const n = require('../memory/pending').countPending();
|
|
401
|
+
if (n > 0) {
|
|
402
|
+
additionalContextParts.push(
|
|
403
|
+
`${n} memory candidate(s) await review — list with "kodelythecc memory list-pending", confirm with "promote-pending <id|all>".`
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
} catch { /* never block session start */ }
|
|
407
|
+
|
|
383
408
|
// Check for recent session files (last 7 days)
|
|
384
409
|
const recentSessions = dedupeRecentSessions(sessionSearchDirs);
|
|
385
410
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// scripts/lib/redact.js
|
|
2
|
+
//
|
|
3
|
+
// One place to scrub credentials out of text before it is stored or logged.
|
|
4
|
+
// Used by the memory capture path (so a leaked key is never written into
|
|
5
|
+
// memories.jsonl, where it would then be surfaced by recall and injected into a
|
|
6
|
+
// future session) and by the bash-command logger.
|
|
7
|
+
//
|
|
8
|
+
// Two rule families:
|
|
9
|
+
// - High-confidence provider token FORMATS, matched anywhere (prose or a
|
|
10
|
+
// command line) — AWS, GitHub, OpenAI/Anthropic, Stripe, Slack, Google, npm,
|
|
11
|
+
// JWTs, PEM private-key blocks.
|
|
12
|
+
// - Assignment / header forms — `--token=`, `Authorization:`, `password=`,
|
|
13
|
+
// `api_key=`, etc.
|
|
14
|
+
//
|
|
15
|
+
// Every pattern is linear (no nested quantifiers, no adjacent `\s*\s*`) so none
|
|
16
|
+
// can backtrack catastrophically on hostile input — the ReDoS lesson from the
|
|
17
|
+
// prompt-injection guard applies here too.
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const RULES = [
|
|
21
|
+
// ── Provider token formats (match anywhere) ──
|
|
22
|
+
[/-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/g, '<REDACTED:private-key>'],
|
|
23
|
+
[/\bAKIA[0-9A-Z]{16}\b/g, '<REDACTED:aws-key>'],
|
|
24
|
+
[/\bASIA[0-9A-Z]{16}\b/g, '<REDACTED:aws-temp-key>'],
|
|
25
|
+
[/\bghp_[A-Za-z0-9]{20,}\b/g, '<REDACTED:github-token>'],
|
|
26
|
+
[/\bgho_[A-Za-z0-9]{20,}\b/g, '<REDACTED:github-token>'],
|
|
27
|
+
[/\bghs_[A-Za-z0-9]{20,}\b/g, '<REDACTED:github-token>'],
|
|
28
|
+
[/\bghr_[A-Za-z0-9]{20,}\b/g, '<REDACTED:github-token>'],
|
|
29
|
+
[/\bgithub_pat_[A-Za-z0-9_]{22,}\b/g, '<REDACTED:github-pat>'],
|
|
30
|
+
[/\bsk-ant-[A-Za-z0-9_-]{20,}/g, '<REDACTED:anthropic-key>'],
|
|
31
|
+
[/\bsk-[A-Za-z0-9_-]{20,}/g, '<REDACTED:openai-key>'],
|
|
32
|
+
[/\b(?:sk|rk|pk)_(?:live|test)_[A-Za-z0-9]{16,}\b/g, '<REDACTED:stripe-key>'],
|
|
33
|
+
[/\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g, '<REDACTED:slack-token>'],
|
|
34
|
+
[/\bAIza[0-9A-Za-z_-]{35}\b/g, '<REDACTED:google-key>'],
|
|
35
|
+
[/\bnpm_[A-Za-z0-9]{36}\b/g, '<REDACTED:npm-token>'],
|
|
36
|
+
[/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g, '<REDACTED:jwt>'],
|
|
37
|
+
// ── Assignment / header forms ──
|
|
38
|
+
[/(--token[= ])\S+/g, '$1<REDACTED>'],
|
|
39
|
+
[/(Authorization:\s*)(?:Bearer\s+)?\S+/gi, '$1<REDACTED>'],
|
|
40
|
+
[/(\bpassword\s*[=:]\s*)\S+/gi, '$1<REDACTED>'],
|
|
41
|
+
[/(\b(?:api[_-]?key|secret|access[_-]?token|auth[_-]?token)\s*[=:]\s*)["']?[A-Za-z0-9\-_.]{8,}["']?/gi, '$1<REDACTED>'],
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
// Replace any credentials in `text` with labelled placeholders. Non-strings pass
|
|
45
|
+
// through unchanged (null/undefined stay as they are).
|
|
46
|
+
function redact(text) {
|
|
47
|
+
if (typeof text !== 'string') return text;
|
|
48
|
+
let out = text;
|
|
49
|
+
for (const [re, repl] of RULES) out = out.replace(re, repl);
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// True when redaction would change the text — i.e. a credential is present.
|
|
54
|
+
function hasSecret(text) {
|
|
55
|
+
return typeof text === 'string' && redact(text) !== text;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = { redact, hasSecret, RULES };
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const path = require('path');
|
|
6
|
-
const initSqlJs = require('sql.js');
|
|
7
6
|
|
|
8
7
|
const { applyMigrations, getAppliedMigrations } = require('./migrations');
|
|
9
8
|
const { createQueryApi } = require('./queries');
|
|
@@ -11,6 +10,40 @@ const { assertValidEntity, validateEntity } = require('./schema');
|
|
|
11
10
|
|
|
12
11
|
const DEFAULT_STATE_STORE_RELATIVE_PATH = path.join('.claude', 'ecc', 'state.db');
|
|
13
12
|
|
|
13
|
+
// sql.js is OPTIONAL. It powers the session store, the status DB, and the
|
|
14
|
+
// skill-evolution tracker — not the core (memory, RTK, Terse, codebase graph),
|
|
15
|
+
// which stay pure-JS and need nothing. It used to be required at module load, so
|
|
16
|
+
// importing state-store threw "Cannot find module 'sql.js'" for every install
|
|
17
|
+
// that did not have it (which, since it was never a declared dependency, was all
|
|
18
|
+
// of them) — before any caller's try/catch could run. Load it lazily and turn a
|
|
19
|
+
// missing module into a clean, catchable error instead of an import-time crash.
|
|
20
|
+
function loadSqlJs() {
|
|
21
|
+
try {
|
|
22
|
+
return require('sql.js');
|
|
23
|
+
} catch (cause) {
|
|
24
|
+
const err = new Error(
|
|
25
|
+
'SQLite support is unavailable: the optional dependency "sql.js" is not installed. ' +
|
|
26
|
+
'Install it (npm i sql.js, or reinstall kodelyth-ecc) to enable the session store, ' +
|
|
27
|
+
'status DB, and skill-evolution tracker. Core memory, RTK, Terse, and the codebase ' +
|
|
28
|
+
'graph do not need it.'
|
|
29
|
+
);
|
|
30
|
+
err.code = 'ECC_SQLITE_UNAVAILABLE';
|
|
31
|
+
err.cause = cause;
|
|
32
|
+
throw err;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Non-throwing probe for callers and `doctor`: is the optional SQLite layer
|
|
37
|
+
// usable on this install?
|
|
38
|
+
function isSqliteAvailable() {
|
|
39
|
+
try {
|
|
40
|
+
require.resolve('sql.js');
|
|
41
|
+
return true;
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
14
47
|
function resolveStateStorePath(options = {}) {
|
|
15
48
|
if (options.dbPath) {
|
|
16
49
|
if (options.dbPath === ':memory:') {
|
|
@@ -163,6 +196,7 @@ async function openDatabase(SQL, dbPath) {
|
|
|
163
196
|
|
|
164
197
|
async function createStateStore(options = {}) {
|
|
165
198
|
const dbPath = resolveStateStorePath(options);
|
|
199
|
+
const initSqlJs = loadSqlJs();
|
|
166
200
|
const SQL = await initSqlJs();
|
|
167
201
|
const db = await openDatabase(SQL, dbPath);
|
|
168
202
|
const appliedMigrations = applyMigrations(db);
|
|
@@ -188,4 +222,5 @@ module.exports = {
|
|
|
188
222
|
DEFAULT_STATE_STORE_RELATIVE_PATH,
|
|
189
223
|
createStateStore,
|
|
190
224
|
resolveStateStorePath,
|
|
225
|
+
isSqliteAvailable,
|
|
191
226
|
};
|
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
-
|
|
5
|
+
// `ajv` is loaded lazily inside getAjv(), NOT at module top. It is an OPTIONAL
|
|
6
|
+
// dependency (it validates the optional SQLite state-store's entities), and
|
|
7
|
+
// requiring it here eagerly made importing state-store throw "Cannot find module
|
|
8
|
+
// 'ajv'" on any install without it — which is every default install, and which
|
|
9
|
+
// broke the pre-publish test step in CI. Same bug class as the sql.js one.
|
|
6
10
|
|
|
7
11
|
const SCHEMA_PATH = path.join(__dirname, '..', '..', '..', 'schemas', 'state-store.schema.json');
|
|
8
12
|
|
|
@@ -33,6 +37,19 @@ function getAjv() {
|
|
|
33
37
|
return cachedAjv;
|
|
34
38
|
}
|
|
35
39
|
|
|
40
|
+
let Ajv;
|
|
41
|
+
try {
|
|
42
|
+
Ajv = require('ajv');
|
|
43
|
+
} catch (cause) {
|
|
44
|
+
const err = new Error(
|
|
45
|
+
'state-store schema validation needs the optional dependency "ajv", which is not installed. ' +
|
|
46
|
+
'Install it (or reinstall kodelyth-ecc) to enable session-store / status-DB validation.'
|
|
47
|
+
);
|
|
48
|
+
err.code = 'ECC_AJV_UNAVAILABLE';
|
|
49
|
+
err.cause = cause;
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
|
|
36
53
|
cachedAjv = new Ajv({
|
|
37
54
|
allErrors: true,
|
|
38
55
|
strict: false,
|
|
@@ -25,6 +25,108 @@ function claudeDesktopConfigPath() {
|
|
|
25
25
|
return path.join(home, '.config', 'Claude', 'claude_desktop_config.json');
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
// MCP config paths for the non-Claude IDEs ECC can auto-wire via JSON. Every
|
|
29
|
+
// target below uses the SAME `{ mcpServers: { <name>: {...} } }` shape as Claude
|
|
30
|
+
// Code (Kiro included — it is MCP-native), so the hardened readConfig +
|
|
31
|
+
// registerInFile merge works unchanged; only the file location differs. Codex is
|
|
32
|
+
// TOML and handled separately (registerCodexToml).
|
|
33
|
+
//
|
|
34
|
+
// Still manual (fragile, editor-version-specific path): Cline / Roo Code store
|
|
35
|
+
// MCP settings in VS Code globalStorage, so ECC does not guess that path.
|
|
36
|
+
function mcpPathsForTarget(target, cwd = process.cwd()) {
|
|
37
|
+
const home = os.homedir();
|
|
38
|
+
switch (target) {
|
|
39
|
+
case 'claude-code':
|
|
40
|
+
case 'claude-home':
|
|
41
|
+
return [claudeCodeConfigPath()];
|
|
42
|
+
case 'cursor':
|
|
43
|
+
case 'cursor-project':
|
|
44
|
+
return [path.join(cwd, '.cursor', 'mcp.json')];
|
|
45
|
+
case 'windsurf':
|
|
46
|
+
case 'windsurf-home':
|
|
47
|
+
case 'windsurf-project':
|
|
48
|
+
return [path.join(home, '.codeium', 'windsurf', 'mcp_config.json')];
|
|
49
|
+
case 'gemini-cli':
|
|
50
|
+
case 'gemini-home':
|
|
51
|
+
case 'gemini-project':
|
|
52
|
+
return [path.join(home, '.gemini', 'settings.json')];
|
|
53
|
+
case 'kiro':
|
|
54
|
+
case 'kiro-project':
|
|
55
|
+
return [path.join(cwd, '.kiro', 'settings', 'mcp.json')];
|
|
56
|
+
case 'kiro-home':
|
|
57
|
+
return [path.join(home, '.kiro', 'settings', 'mcp.json')];
|
|
58
|
+
default:
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function codexConfigPath() {
|
|
64
|
+
return path.join(os.homedir(), '.codex', 'config.toml');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Codex CLI stores MCP servers in TOML as [mcp_servers.<name>] tables. Without
|
|
68
|
+
// pulling in a TOML parser, add our table only when it is absent (idempotent),
|
|
69
|
+
// and otherwise leave the file exactly as it is. Appending a table is
|
|
70
|
+
// order-independent in TOML, so this never clobbers existing servers or settings.
|
|
71
|
+
function registerCodexToml(file, { dryRun = false } = {}) {
|
|
72
|
+
let raw = '';
|
|
73
|
+
try {
|
|
74
|
+
raw = fs.readFileSync(file, 'utf8');
|
|
75
|
+
} catch (err) {
|
|
76
|
+
if (err.code !== 'ENOENT') {
|
|
77
|
+
return { file, action: 'error', error: `cannot read ${file}: ${err.message}` };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (/\[mcp_servers\.(?:"?kodelyth-ecc"?)\]/.test(raw)) {
|
|
81
|
+
return { file, action: 'unchanged' };
|
|
82
|
+
}
|
|
83
|
+
const table = `\n[mcp_servers."${SERVER_NAME}"]\ncommand = "kodelythecc"\nargs = ["mcp"]\n`;
|
|
84
|
+
const next = raw.length && !raw.endsWith('\n') ? `${raw}\n${table}` : `${raw}${table}`;
|
|
85
|
+
if (!dryRun) {
|
|
86
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
87
|
+
safeFs.replaceFilePreservingMode(file, next);
|
|
88
|
+
}
|
|
89
|
+
return { file, action: 'added' };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Returns the list of targets this module can auto-wire (for help text / docs).
|
|
93
|
+
function supportedMcpTargets() {
|
|
94
|
+
return ['claude-code', 'cursor', 'windsurf', 'gemini-cli', 'kiro', 'codex'];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Register ECC's MCP server for one install target. JSON targets reuse the same
|
|
98
|
+
// merge that protects ~/.claude.json (absent → created, present → merged without
|
|
99
|
+
// clobbering, unparseable → refused); Codex uses the TOML writer above. Never
|
|
100
|
+
// throws — a bad config for one IDE must not fail an install.
|
|
101
|
+
function registerForTarget(target, { cwd = process.cwd(), log = () => {}, dryRun = false } = {}) {
|
|
102
|
+
if (target === 'codex' || target === 'codex-home') {
|
|
103
|
+
try {
|
|
104
|
+
const r = registerCodexToml(codexConfigPath(), { dryRun });
|
|
105
|
+
log(` ${r.action}: ${r.file}`);
|
|
106
|
+
return [{ target, ...r }];
|
|
107
|
+
} catch (e) {
|
|
108
|
+
log(` skipped codex: ${e.message}`);
|
|
109
|
+
return [{ target, file: codexConfigPath(), action: 'error', error: e.message }];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const paths = mcpPathsForTarget(target, cwd);
|
|
113
|
+
if (paths.length === 0) {
|
|
114
|
+
return [{ target, action: 'unsupported', reason: `no known MCP config path for "${target}"` }];
|
|
115
|
+
}
|
|
116
|
+
const results = [];
|
|
117
|
+
for (const p of paths) {
|
|
118
|
+
try {
|
|
119
|
+
const r = registerInFile(p, { dryRun });
|
|
120
|
+
log(` ${r.action}: ${r.file}`);
|
|
121
|
+
results.push({ target, ...r });
|
|
122
|
+
} catch (e) {
|
|
123
|
+
log(` skipped ${p}: ${e.message}`);
|
|
124
|
+
results.push({ target, file: p, action: 'error', error: e.message });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return results;
|
|
128
|
+
}
|
|
129
|
+
|
|
28
130
|
// Reading this file has THREE outcomes, and collapsing them is destructive.
|
|
29
131
|
//
|
|
30
132
|
// readJson used to return null for every failure, and the caller did
|
|
@@ -145,9 +247,19 @@ function unregisterAll({ log = () => {} } = {}) {
|
|
|
145
247
|
function statusAll() {
|
|
146
248
|
const rows = [];
|
|
147
249
|
for (const p of [claudeCodeConfigPath(), claudeDesktopConfigPath()]) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
250
|
+
// Was `readJson(p)` — a function that no longer exists after the
|
|
251
|
+
// absent/present/unreadable refactor, so statusAll() threw
|
|
252
|
+
// "readJson is not defined" and took `kodelythecc doctor` and the `mcp`
|
|
253
|
+
// status command down with it. Use the real reader and its tri-state result.
|
|
254
|
+
const cfg = readConfig(p);
|
|
255
|
+
const data = (cfg && cfg.data) || {};
|
|
256
|
+
const present = !!(data.mcpServers && data.mcpServers[SERVER_NAME]);
|
|
257
|
+
rows.push({
|
|
258
|
+
file: p,
|
|
259
|
+
present,
|
|
260
|
+
exists: cfg.state !== 'absent',
|
|
261
|
+
readable: cfg.state !== 'unreadable',
|
|
262
|
+
});
|
|
151
263
|
}
|
|
152
264
|
return rows;
|
|
153
265
|
}
|
|
@@ -157,6 +269,11 @@ module.exports = {
|
|
|
157
269
|
serverEntry,
|
|
158
270
|
claudeCodeConfigPath,
|
|
159
271
|
claudeDesktopConfigPath,
|
|
272
|
+
mcpPathsForTarget,
|
|
273
|
+
supportedMcpTargets,
|
|
274
|
+
registerForTarget,
|
|
275
|
+
registerCodexToml,
|
|
276
|
+
codexConfigPath,
|
|
160
277
|
registerAll,
|
|
161
278
|
unregisterAll,
|
|
162
279
|
statusAll,
|
package/scripts/memory/cli.js
CHANGED
|
@@ -19,6 +19,7 @@ const path = require('path');
|
|
|
19
19
|
const store = require('./store');
|
|
20
20
|
const { buildContextBlock } = require('./inject');
|
|
21
21
|
const { extractCandidates } = require('./extract');
|
|
22
|
+
const pending = require('./pending');
|
|
22
23
|
|
|
23
24
|
function parseArgs(argv) {
|
|
24
25
|
const args = argv.slice(2);
|
|
@@ -190,6 +191,40 @@ function main() {
|
|
|
190
191
|
return;
|
|
191
192
|
}
|
|
192
193
|
|
|
194
|
+
// ── Auto-capture review queue ────────────────────────────────────────────
|
|
195
|
+
// The Stop hook auto-extracts candidates from each session into the queue;
|
|
196
|
+
// these three commands are the consent gate that turns them into memories.
|
|
197
|
+
case 'list-pending':
|
|
198
|
+
case 'review-pending': {
|
|
199
|
+
const items = pending.listPending();
|
|
200
|
+
if (items.length === 0) { console.log('No memory candidates pending review.'); return; }
|
|
201
|
+
console.log(`${items.length} candidate(s) pending review:\n`);
|
|
202
|
+
items.forEach((c) => {
|
|
203
|
+
console.log(`[${c.id}] (score ${c.score ?? '-'}) ${c.problem}`);
|
|
204
|
+
console.log(` tags: ${(c.tags || []).join(', ') || '-'}`);
|
|
205
|
+
console.log(` approach: ${String(c.approach).slice(0, 200)}`);
|
|
206
|
+
console.log('');
|
|
207
|
+
});
|
|
208
|
+
console.log('Confirm with: memory promote-pending <id|all> · drop with: memory discard-pending <id|all>');
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
case 'promote-pending': {
|
|
213
|
+
const sel = positional[0] || 'all';
|
|
214
|
+
const stored = pending.promotePending(sel === 'all' ? 'all' : sel);
|
|
215
|
+
if (stored.length === 0) { console.log('Nothing promoted (no matching pending candidate).'); return; }
|
|
216
|
+
console.log(`Stored ${stored.length} memor${stored.length === 1 ? 'y' : 'ies'}:`);
|
|
217
|
+
stored.forEach(m => console.log(` ${m.id} ${m.problem}`));
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
case 'discard-pending': {
|
|
222
|
+
const sel = positional[0] || 'all';
|
|
223
|
+
const n = pending.discardPending(sel === 'all' ? 'all' : sel);
|
|
224
|
+
console.log(`Discarded ${n} pending candidate(s).`);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
193
228
|
default:
|
|
194
229
|
console.error(`Unknown command: ${cmd}`);
|
|
195
230
|
help();
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// scripts/memory/embedding-providers/local-transformers.js
|
|
2
|
+
//
|
|
3
|
+
// Local, on-device embedding provider. Runs a small sentence-embedding model
|
|
4
|
+
// through the OPTIONAL `@xenova/transformers` dependency (WASM/ONNX — no native
|
|
5
|
+
// build). After the model is cached once, inference is fully on-device, so the
|
|
6
|
+
// "100% local, zero-network" property holds at recall time; only the first-ever
|
|
7
|
+
// load fetches the model.
|
|
8
|
+
//
|
|
9
|
+
// This module is required ONLY when the user has opted into embeddings
|
|
10
|
+
// (KODELYTH_MEMORY_EMBEDDINGS=on). If the optional dependency is not installed,
|
|
11
|
+
// the require throws and embeddings.getProvider() catches it and falls back to
|
|
12
|
+
// BM25 — so a default install never needs this and never loads it.
|
|
13
|
+
//
|
|
14
|
+
// Not exercised by the CI suite (it needs the heavy optional dependency and a
|
|
15
|
+
// model download); the fusion/gating logic it feeds IS covered, via a stub
|
|
16
|
+
// provider, in tests/memory/embeddings.test.js.
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const MODEL = process.env.KODELYTH_EMBEDDING_MODEL || 'Xenova/all-MiniLM-L6-v2';
|
|
20
|
+
|
|
21
|
+
let _pipelinePromise = null;
|
|
22
|
+
|
|
23
|
+
async function createProvider() {
|
|
24
|
+
// require is inside the function so a missing optional dependency surfaces to
|
|
25
|
+
// the caller (embeddings.getProvider) as a catchable error, not an import-time
|
|
26
|
+
// crash of the memory module.
|
|
27
|
+
const { pipeline } = require('@xenova/transformers');
|
|
28
|
+
if (!_pipelinePromise) {
|
|
29
|
+
_pipelinePromise = pipeline('feature-extraction', MODEL);
|
|
30
|
+
}
|
|
31
|
+
const extractor = await _pipelinePromise;
|
|
32
|
+
return {
|
|
33
|
+
model: MODEL,
|
|
34
|
+
async embed(text) {
|
|
35
|
+
if (!text) return null;
|
|
36
|
+
// Mean-pooled, L2-normalized sentence embedding — the standard recipe for
|
|
37
|
+
// all-MiniLM-style models. output.data is a Float32Array.
|
|
38
|
+
const output = await extractor(String(text), { pooling: 'mean', normalize: true });
|
|
39
|
+
return Array.from(output.data);
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { createProvider, MODEL };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// scripts/memory/embeddings.js
|
|
2
|
+
//
|
|
3
|
+
// OPTIONAL, OFF BY DEFAULT semantic layer for memory recall. The default recall
|
|
4
|
+
// path stays pure BM25 — sync, zero-dependency, zero-network — exactly as
|
|
5
|
+
// advertised. This module only does anything when a user opts in
|
|
6
|
+
// (KODELYTH_MEMORY_EMBEDDINGS=on) AND a local embedding provider is actually
|
|
7
|
+
// available. If either is false, every function here degrades to "no semantics",
|
|
8
|
+
// and recall falls back to BM25 unchanged.
|
|
9
|
+
//
|
|
10
|
+
// The provider is pluggable: the real one (scripts/memory/embedding-providers/
|
|
11
|
+
// local-transformers.js) runs a local model via the optional `@xenova/
|
|
12
|
+
// transformers` dependency, so inference is on-device. Tests inject a
|
|
13
|
+
// deterministic stub instead, so the fusion/gating logic is verified without the
|
|
14
|
+
// heavy dependency or a model download.
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
// Test seam: when set (including to null), overrides provider resolution.
|
|
18
|
+
let _providerOverride;
|
|
19
|
+
let _hasOverride = false;
|
|
20
|
+
|
|
21
|
+
function _setProviderForTest(provider) { _providerOverride = provider; _hasOverride = true; }
|
|
22
|
+
function _clearProviderForTest() { _providerOverride = undefined; _hasOverride = false; }
|
|
23
|
+
|
|
24
|
+
// Off unless explicitly enabled. Keeps the default session zero-network/zero-dep.
|
|
25
|
+
function isEnabled() {
|
|
26
|
+
const v = String(process.env.KODELYTH_MEMORY_EMBEDDINGS || '').trim().toLowerCase();
|
|
27
|
+
return v === '1' || v === 'on' || v === 'true' || v === 'yes';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Resolve the embedding provider, or null. Null means "no semantics" — a
|
|
31
|
+
// disabled flag, a missing optional dependency, or a load failure all collapse
|
|
32
|
+
// to the same safe BM25-only behaviour.
|
|
33
|
+
async function getProvider() {
|
|
34
|
+
if (_hasOverride) return _providerOverride;
|
|
35
|
+
if (!isEnabled()) return null;
|
|
36
|
+
try {
|
|
37
|
+
const { createProvider } = require('./embedding-providers/local-transformers');
|
|
38
|
+
return await createProvider();
|
|
39
|
+
} catch {
|
|
40
|
+
return null; // optional dep not installed, or model load failed
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Cosine similarity of two equal-length numeric vectors. Returns 0 for missing,
|
|
45
|
+
// mismatched, or zero-magnitude vectors rather than NaN, so it is always safe to
|
|
46
|
+
// sort by.
|
|
47
|
+
function cosineSim(a, b) {
|
|
48
|
+
if (!a || !b || a.length === 0 || a.length !== b.length) return 0;
|
|
49
|
+
let dot = 0, na = 0, nb = 0;
|
|
50
|
+
for (let i = 0; i < a.length; i++) {
|
|
51
|
+
dot += a[i] * b[i];
|
|
52
|
+
na += a[i] * a[i];
|
|
53
|
+
nb += b[i] * b[i];
|
|
54
|
+
}
|
|
55
|
+
if (na === 0 || nb === 0) return 0;
|
|
56
|
+
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Fuse BM25 relevance with semantic similarity and re-rank. Pure function.
|
|
60
|
+
//
|
|
61
|
+
// results: BM25 results, each { id, score, ... }, already relevance-ranked
|
|
62
|
+
// queryVec: the query embedding (or null → returns results unchanged)
|
|
63
|
+
// vecById: { [id]: vector } for whichever results have a stored embedding
|
|
64
|
+
// weight: semantic weight in [0,1] (default 0.5). final =
|
|
65
|
+
// weight*semantic + (1-weight)*bm25norm, with BM25 min-max normalized
|
|
66
|
+
// across this result set. A result with no stored vector keeps its
|
|
67
|
+
// BM25-normalized score (semantic contribution 0), so missing
|
|
68
|
+
// embeddings never push a relevant memory down unfairly.
|
|
69
|
+
function fuseHybrid(results, { queryVec, vecById = {}, weight = 0.5 } = {}) {
|
|
70
|
+
if (!Array.isArray(results) || results.length === 0) return results || [];
|
|
71
|
+
if (!queryVec) return results;
|
|
72
|
+
|
|
73
|
+
const scores = results.map(r => r.score || 0);
|
|
74
|
+
const min = Math.min(...scores);
|
|
75
|
+
const max = Math.max(...scores);
|
|
76
|
+
const span = max - min;
|
|
77
|
+
const norm = (s) => (span === 0 ? 1 : (s - min) / span);
|
|
78
|
+
|
|
79
|
+
const w = Math.min(1, Math.max(0, weight));
|
|
80
|
+
return results
|
|
81
|
+
.map(r => {
|
|
82
|
+
const vec = vecById[r.id];
|
|
83
|
+
const bm25n = norm(r.score || 0);
|
|
84
|
+
const sem = vec ? Math.max(0, cosineSim(queryVec, vec)) : bm25n;
|
|
85
|
+
const fused = w * sem + (1 - w) * bm25n;
|
|
86
|
+
return { ...r, _bm25norm: bm25n, _semantic: vec ? sem : null, _hybrid: fused };
|
|
87
|
+
})
|
|
88
|
+
.sort((a, b) => b._hybrid - a._hybrid);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = {
|
|
92
|
+
isEnabled,
|
|
93
|
+
getProvider,
|
|
94
|
+
cosineSim,
|
|
95
|
+
fuseHybrid,
|
|
96
|
+
_setProviderForTest,
|
|
97
|
+
_clearProviderForTest,
|
|
98
|
+
};
|