kodelyth-ecc 1.3.0 → 1.4.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/AGENTS.md +1 -1
- package/CHANGELOG.md +35 -0
- package/CLAUDE.md +5 -5
- package/KODELYTH.md +1 -1
- package/README.md +81 -14
- package/VERSION +1 -1
- package/agents/kodelyth-memory.md +87 -0
- package/bin/kodelyth-ecc.js +18 -12
- package/commands/memory.md +62 -0
- package/hooks/hooks.json +26 -0
- package/hooks/memory/capture-stop.js +88 -0
- package/hooks/memory/inject-start.js +60 -0
- package/install.ps1 +20 -1
- package/install.sh +1 -1
- package/package.json +4 -2
- package/rules/common/memory-protocol.md +56 -0
- package/scripts/memory/cli.js +200 -0
- package/scripts/memory/extract.js +176 -0
- package/scripts/memory/inject.js +145 -0
- package/scripts/memory/store.js +300 -0
- package/skills/kodelyth-memory/SKILL.md +136 -0
- package/tests/memory/store.test.js +121 -0
package/install.sh
CHANGED
|
@@ -47,7 +47,7 @@ echo -e "${RED}${BOLD} ║ DANGER LEVEL: GOD TIER · NOT FOR JUNIOR DEV
|
|
|
47
47
|
echo -e "${RED}${BOLD} ╚══════════════════════════════════════════════════════════════╝${RESET}"
|
|
48
48
|
echo ""
|
|
49
49
|
echo -e "${BOLD} Kodelyth ECC — The most dangerous AI coding toolkit on the planet${RESET}"
|
|
50
|
-
echo -e "${CYAN}
|
|
50
|
+
echo -e "${CYAN} 59 specialist agents · 188 skills · 80 commands · 18+ hooks · intent routing · local memory${RESET}"
|
|
51
51
|
echo -e "${CYAN} Any language · Any framework · Any scale · 300B-level quality${RESET}"
|
|
52
52
|
echo ""
|
|
53
53
|
echo -e " github.com/sifxprime/kodelyth-ecc"
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kodelyth-ecc",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Production-grade AI coding toolkit —
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "Production-grade AI coding toolkit — 59 agents, 188 skills, 79 commands, god-tier intent routing, local self-learning memory. Works with Claude Code, Windsurf, Cursor, Codex, Antigravity, and OpenCode.",
|
|
5
5
|
"author": "Kodelyth <github.com/sifxprime>",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
"ai-agents",
|
|
20
20
|
"coding-toolkit",
|
|
21
21
|
"llm",
|
|
22
|
+
"memory",
|
|
23
|
+
"self-learning",
|
|
22
24
|
"kodelyth"
|
|
23
25
|
],
|
|
24
26
|
"bin": {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Kodelyth Memory Protocol
|
|
2
|
+
|
|
3
|
+
> Auto-loaded every session. Tells the AI when to consult and contribute to local memory.
|
|
4
|
+
|
|
5
|
+
## What is Kodelyth Memory
|
|
6
|
+
|
|
7
|
+
A local file at `~/.kodelyth/memory/memories.jsonl` storing solutions, patterns, and gotchas extracted from past sessions. Retrieval is BM25 (keyword + tag matching). It is **not** a learned model — it is a retrieval store that gives you better context.
|
|
8
|
+
|
|
9
|
+
If the SessionStart hook ran, you have already received a memory block in your initial context with:
|
|
10
|
+
- The user's recurring patterns (tags seen across multiple sessions)
|
|
11
|
+
- Recent solutions in this project
|
|
12
|
+
- Their detected stack
|
|
13
|
+
|
|
14
|
+
## When to recall memory mid-session
|
|
15
|
+
|
|
16
|
+
Trigger a memory recall (run the `kodelyth-memory` agent or `node scripts/memory/cli.js search "<query>"`) when **any** of these is true:
|
|
17
|
+
|
|
18
|
+
1. The user describes a task in a domain that matches a tag in their memory (`payments`, `auth`, `database`, `deployment`, etc.)
|
|
19
|
+
2. The user asks "have I done this before?" or "how did I solve X last time?"
|
|
20
|
+
3. The user mentions a library, framework, or service by name that appears in past memories
|
|
21
|
+
4. You're about to commit to an architectural decision and want to check past precedent
|
|
22
|
+
|
|
23
|
+
## How to surface a recalled memory
|
|
24
|
+
|
|
25
|
+
Naturally, never robotically. Pattern:
|
|
26
|
+
|
|
27
|
+
> I checked your memory — you solved a similar problem in `<project>` on `<date>`. The approach that worked was `<approach>`. Want me to apply the same here, or is this case different?
|
|
28
|
+
|
|
29
|
+
Always end by giving the user the option to override. Memory is a hint, not a command.
|
|
30
|
+
|
|
31
|
+
## When to capture a new memory
|
|
32
|
+
|
|
33
|
+
Capture when **all** are true:
|
|
34
|
+
- The user signals success (`"that worked"`, `"perfect"`, `"fixed it"`, `"thanks"`, `"done"`)
|
|
35
|
+
- The work involved real iteration (not a one-shot trivial fix)
|
|
36
|
+
- The lesson is reusable (would help in another similar problem, not just this exact file)
|
|
37
|
+
|
|
38
|
+
Show the user the draft memory before storing. Never silently capture.
|
|
39
|
+
|
|
40
|
+
## What you must not do
|
|
41
|
+
|
|
42
|
+
- **Never** auto-recall the same memory twice in one session (the user has seen it)
|
|
43
|
+
- **Never** capture without explicit confirmation (`yes`, `store it`, `remember`)
|
|
44
|
+
- **Never** treat a recalled memory as ground truth — it could be stale
|
|
45
|
+
- **Never** transmit memory data anywhere — it is local-only by design
|
|
46
|
+
- **Never** fabricate a memory ("you usually...") if no relevant memory exists — say nothing
|
|
47
|
+
|
|
48
|
+
## Cache-aware behaviour
|
|
49
|
+
|
|
50
|
+
The injected memory block is structured so its prefix is identical across calls in the same project. Do not rewrite or reorder this block during a session — that defeats the prompt cache and burns tokens. If you need to add session-specific notes, append them after the block.
|
|
51
|
+
|
|
52
|
+
## Honest disclosure
|
|
53
|
+
|
|
54
|
+
If the user asks "how do you know that about me?", answer plainly:
|
|
55
|
+
|
|
56
|
+
> "It's in your local Kodelyth Memory at `~/.kodelyth/memory/`. You can inspect it, edit it, or delete it any time. Nothing was sent anywhere."
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// =============================================================================
|
|
3
|
+
// Kodelyth ECC — Memory CLI
|
|
4
|
+
//
|
|
5
|
+
// Usage:
|
|
6
|
+
// node scripts/memory/cli.js list
|
|
7
|
+
// node scripts/memory/cli.js search "<query>"
|
|
8
|
+
// node scripts/memory/cli.js remember "<title>" --approach "<text>" --tags tag1,tag2
|
|
9
|
+
// node scripts/memory/cli.js forget <id>
|
|
10
|
+
// node scripts/memory/cli.js stats
|
|
11
|
+
// node scripts/memory/cli.js inject [--query "<text>"]
|
|
12
|
+
// node scripts/memory/cli.js extract <session.jsonl>
|
|
13
|
+
// node scripts/memory/cli.js rebuild-index
|
|
14
|
+
// =============================================================================
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const store = require('./store');
|
|
20
|
+
const { buildContextBlock } = require('./inject');
|
|
21
|
+
const { extractCandidates } = require('./extract');
|
|
22
|
+
|
|
23
|
+
function parseArgs(argv) {
|
|
24
|
+
const args = argv.slice(2);
|
|
25
|
+
const cmd = args[0];
|
|
26
|
+
const positional = [];
|
|
27
|
+
const flags = {};
|
|
28
|
+
for (let i = 1; i < args.length; i++) {
|
|
29
|
+
const a = args[i];
|
|
30
|
+
if (a.startsWith('--')) {
|
|
31
|
+
const key = a.slice(2);
|
|
32
|
+
const next = args[i + 1];
|
|
33
|
+
if (!next || next.startsWith('--')) {
|
|
34
|
+
flags[key] = true;
|
|
35
|
+
} else {
|
|
36
|
+
flags[key] = next;
|
|
37
|
+
i += 1;
|
|
38
|
+
}
|
|
39
|
+
} else {
|
|
40
|
+
positional.push(a);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return { cmd, positional, flags };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function help() {
|
|
47
|
+
console.log(`
|
|
48
|
+
Kodelyth ECC — Memory CLI
|
|
49
|
+
|
|
50
|
+
Commands:
|
|
51
|
+
list Show all stored memories
|
|
52
|
+
search "<query>" BM25 search across memories
|
|
53
|
+
remember "<title>" Add a memory (use --approach, --tags, --language)
|
|
54
|
+
forget <id> Mark a memory deleted
|
|
55
|
+
stats Show memory store stats
|
|
56
|
+
inject [--query "<text>"] Print the cache-friendly context block
|
|
57
|
+
extract <session.jsonl> Extract memory candidates from a session log
|
|
58
|
+
rebuild-index Rebuild the BM25 index from memories.jsonl
|
|
59
|
+
|
|
60
|
+
Storage: ${store.PATHS.dir}
|
|
61
|
+
`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function fmt(memory, full = false) {
|
|
65
|
+
const date = (memory.captured_at || '').slice(0, 10);
|
|
66
|
+
const tags = (memory.tags || []).join(',');
|
|
67
|
+
if (!full) {
|
|
68
|
+
return `${memory.id} ${date} [${memory.language || '-'}] ${memory.problem.slice(0, 70)} (${tags})`;
|
|
69
|
+
}
|
|
70
|
+
return [
|
|
71
|
+
`id: ${memory.id}`,
|
|
72
|
+
`captured: ${memory.captured_at}`,
|
|
73
|
+
`language: ${memory.language || '-'}`,
|
|
74
|
+
`tags: ${tags || '-'}`,
|
|
75
|
+
`project: ${memory.project_path || '-'}`,
|
|
76
|
+
`problem: ${memory.problem}`,
|
|
77
|
+
`approach:`,
|
|
78
|
+
...(memory.approach || '').split('\n').map(l => ` ${l}`),
|
|
79
|
+
memory.gotchas?.length ? `gotchas:` : '',
|
|
80
|
+
...(memory.gotchas || []).map(g => ` - ${g}`),
|
|
81
|
+
].filter(Boolean).join('\n');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function main() {
|
|
85
|
+
const { cmd, positional, flags } = parseArgs(process.argv);
|
|
86
|
+
|
|
87
|
+
if (!cmd || cmd === 'help' || cmd === '-h' || cmd === '--help') {
|
|
88
|
+
help();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
switch (cmd) {
|
|
93
|
+
case 'list': {
|
|
94
|
+
const all = store.listAll();
|
|
95
|
+
if (all.length === 0) {
|
|
96
|
+
console.log('No memories yet. Add one with: memory remember "<title>" --approach "<text>"');
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
for (const m of all) console.log(fmt(m));
|
|
100
|
+
console.log(`\n${all.length} total`);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
case 'search': {
|
|
105
|
+
const query = positional[0];
|
|
106
|
+
if (!query) { console.error('Usage: search "<query>"'); process.exit(1); }
|
|
107
|
+
const results = store.recall(query, { limit: Number(flags.limit) || 5 });
|
|
108
|
+
if (results.length === 0) { console.log('No matches.'); return; }
|
|
109
|
+
for (const m of results) {
|
|
110
|
+
console.log(`[score ${m.score.toFixed(2)}] ${fmt(m)}`);
|
|
111
|
+
}
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
case 'remember': {
|
|
116
|
+
const title = positional[0];
|
|
117
|
+
const approach = flags.approach;
|
|
118
|
+
if (!title || !approach) {
|
|
119
|
+
console.error('Usage: remember "<title>" --approach "<what worked>" [--tags a,b] [--language ts]');
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
const memory = store.capture({
|
|
123
|
+
problem: title,
|
|
124
|
+
approach,
|
|
125
|
+
tags: (flags.tags || '').split(',').filter(Boolean),
|
|
126
|
+
project: flags.project || process.cwd(),
|
|
127
|
+
language: flags.language || null,
|
|
128
|
+
files: (flags.files || '').split(',').filter(Boolean),
|
|
129
|
+
gotchas: (flags.gotchas || '').split(';').filter(Boolean),
|
|
130
|
+
source: 'cli',
|
|
131
|
+
});
|
|
132
|
+
console.log(`Captured: ${memory.id}`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
case 'forget': {
|
|
137
|
+
const id = positional[0];
|
|
138
|
+
if (!id) { console.error('Usage: forget <id>'); process.exit(1); }
|
|
139
|
+
const ok = store.forget(id);
|
|
140
|
+
console.log(ok ? `Forgotten: ${id}` : `Not found: ${id}`);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
case 'stats': {
|
|
145
|
+
const s = store.stats();
|
|
146
|
+
console.log(`Total memories: ${s.total}`);
|
|
147
|
+
console.log(`Storage: ${s.storageDir}`);
|
|
148
|
+
console.log(`Projects: ${s.projects}`);
|
|
149
|
+
console.log(`By language:`);
|
|
150
|
+
for (const [lang, count] of Object.entries(s.byLanguage)) {
|
|
151
|
+
console.log(` ${lang.padEnd(12)} ${count}`);
|
|
152
|
+
}
|
|
153
|
+
console.log(`Top tags:`);
|
|
154
|
+
for (const [tag, count] of s.topTags) {
|
|
155
|
+
console.log(` ${tag.padEnd(20)} ${count}`);
|
|
156
|
+
}
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
case 'inject': {
|
|
161
|
+
const block = buildContextBlock({
|
|
162
|
+
projectRoot: flags.project || process.cwd(),
|
|
163
|
+
query: flags.query || null,
|
|
164
|
+
});
|
|
165
|
+
if (!block) { console.log(''); return; }
|
|
166
|
+
console.log(block.text);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
case 'extract': {
|
|
171
|
+
const sessionPath = positional[0];
|
|
172
|
+
if (!sessionPath) { console.error('Usage: extract <session.jsonl>'); process.exit(1); }
|
|
173
|
+
const candidates = extractCandidates(path.resolve(sessionPath));
|
|
174
|
+
if (candidates.length === 0) { console.log('No memory candidates found.'); return; }
|
|
175
|
+
console.log(`Found ${candidates.length} candidate memories:\n`);
|
|
176
|
+
candidates.forEach((c, i) => {
|
|
177
|
+
console.log(`[${i + 1}] (score ${c.score}) ${c.problem}`);
|
|
178
|
+
console.log(` tags: ${c.tags.join(', ') || '-'}`);
|
|
179
|
+
console.log(` language: ${c.language || '-'}`);
|
|
180
|
+
console.log(` approach: ${c.approach.slice(0, 200)}...`);
|
|
181
|
+
console.log('');
|
|
182
|
+
});
|
|
183
|
+
console.log('Review and confirm with: memory remember "<problem>" --approach "<approach>"');
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
case 'rebuild-index': {
|
|
188
|
+
const r = store.rebuildIndex();
|
|
189
|
+
console.log(`Rebuilt index for ${r.count} memories.`);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
default:
|
|
194
|
+
console.error(`Unknown command: ${cmd}`);
|
|
195
|
+
help();
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
main();
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Kodelyth ECC — Session Learning Extractor
|
|
3
|
+
// Reads a session transcript (JSONL) and extracts capture-worthy memories.
|
|
4
|
+
//
|
|
5
|
+
// Strategy: heuristic scoring, no LLM call. We look for signals that a real
|
|
6
|
+
// problem was solved:
|
|
7
|
+
// 1. Edit/Write tool followed by a passing test or successful build
|
|
8
|
+
// 2. User saying "that worked", "fixed it", "great", "thanks"
|
|
9
|
+
// 3. Same file edited 3+ times in one session (iteration → solution)
|
|
10
|
+
// 4. Long Bash output ending in exit code 0 after several failures
|
|
11
|
+
//
|
|
12
|
+
// Each candidate becomes a draft memory. The user (or the AI) reviews and
|
|
13
|
+
// confirms via /memory review. Nothing is auto-stored without confirmation —
|
|
14
|
+
// silent capture is how memory systems become noisy and useless.
|
|
15
|
+
// =============================================================================
|
|
16
|
+
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
|
|
21
|
+
const SUCCESS_PHRASES = [
|
|
22
|
+
/\bthat worked\b/i,
|
|
23
|
+
/\bfixed it\b/i,
|
|
24
|
+
/\bperfect\b/i,
|
|
25
|
+
/\bnice\b.*\bworks\b/i,
|
|
26
|
+
/\bthanks\b/i,
|
|
27
|
+
/\bgreat\b/i,
|
|
28
|
+
/\bsolved\b/i,
|
|
29
|
+
/\bdone\b/i,
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const FAILURE_PHRASES = [
|
|
33
|
+
/\bstill broken\b/i,
|
|
34
|
+
/\bdoesn'?t work\b/i,
|
|
35
|
+
/\bnope\b/i,
|
|
36
|
+
/\bsame error\b/i,
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
function readTranscript(jsonlPath) {
|
|
40
|
+
if (!fs.existsSync(jsonlPath)) return [];
|
|
41
|
+
const lines = fs.readFileSync(jsonlPath, 'utf8').split('\n').filter(Boolean);
|
|
42
|
+
const events = [];
|
|
43
|
+
for (const line of lines) {
|
|
44
|
+
try { events.push(JSON.parse(line)); } catch { /* skip malformed */ }
|
|
45
|
+
}
|
|
46
|
+
return events;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function extractText(event) {
|
|
50
|
+
if (typeof event.content === 'string') return event.content;
|
|
51
|
+
if (Array.isArray(event.content)) {
|
|
52
|
+
return event.content
|
|
53
|
+
.filter(c => c && (c.type === 'text' || typeof c.text === 'string'))
|
|
54
|
+
.map(c => c.text || '')
|
|
55
|
+
.join('\n');
|
|
56
|
+
}
|
|
57
|
+
if (event.message?.content) return extractText({ content: event.message.content });
|
|
58
|
+
return '';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function detectLanguage(filesTouched) {
|
|
62
|
+
const exts = filesTouched.map(f => (f.match(/\.[a-z0-9]+$/i) || [''])[0].toLowerCase());
|
|
63
|
+
if (exts.includes('.ts') || exts.includes('.tsx')) return 'typescript';
|
|
64
|
+
if (exts.includes('.js') || exts.includes('.jsx')) return 'javascript';
|
|
65
|
+
if (exts.includes('.py')) return 'python';
|
|
66
|
+
if (exts.includes('.go')) return 'golang';
|
|
67
|
+
if (exts.includes('.rs')) return 'rust';
|
|
68
|
+
if (exts.includes('.java')) return 'java';
|
|
69
|
+
if (exts.includes('.kt')) return 'kotlin';
|
|
70
|
+
if (exts.includes('.swift')) return 'swift';
|
|
71
|
+
if (exts.includes('.rb')) return 'ruby';
|
|
72
|
+
if (exts.includes('.php')) return 'php';
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function extractTags(text) {
|
|
77
|
+
const tags = new Set();
|
|
78
|
+
const taxonomy = {
|
|
79
|
+
'api-integration': /\b(api|endpoint|rest|graphql|grpc|webhook)\b/i,
|
|
80
|
+
'authentication': /\b(auth|jwt|oauth|sso|login|session|token)\b/i,
|
|
81
|
+
'database': /\b(sql|postgres|mysql|mongo|redis|orm|migration|query)\b/i,
|
|
82
|
+
'testing': /\b(test|jest|vitest|pytest|playwright|cypress|coverage)\b/i,
|
|
83
|
+
'deployment': /\b(deploy|vercel|netlify|aws|docker|kubernetes|ci|cd)\b/i,
|
|
84
|
+
'performance': /\b(slow|optimize|perf|cache|n\+1|memory leak)\b/i,
|
|
85
|
+
'security': /\b(secure|xss|csrf|injection|vuln|sanitiz)\b/i,
|
|
86
|
+
'state-management': /\b(redux|zustand|context|signal|state|store)\b/i,
|
|
87
|
+
'styling': /\b(css|tailwind|styled|theme|responsive)\b/i,
|
|
88
|
+
'routing': /\b(router|navigation|route|next-router|react-router)\b/i,
|
|
89
|
+
'forms': /\b(form|validation|zod|yup|formik|hook-form)\b/i,
|
|
90
|
+
'streaming': /\b(stream|sse|websocket|realtime)\b/i,
|
|
91
|
+
'payments': /\b(stripe|paypal|payment|checkout|billing|subscription)\b/i,
|
|
92
|
+
'ai-llm': /\b(openai|anthropic|llm|gpt|claude|gemini|prompt)\b/i,
|
|
93
|
+
};
|
|
94
|
+
for (const [tag, pattern] of Object.entries(taxonomy)) {
|
|
95
|
+
if (pattern.test(text)) tags.add(tag);
|
|
96
|
+
}
|
|
97
|
+
return Array.from(tags);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function scoreCandidate(events, candidateIdx) {
|
|
101
|
+
let score = 0;
|
|
102
|
+
const around = events.slice(Math.max(0, candidateIdx - 5), candidateIdx + 5);
|
|
103
|
+
|
|
104
|
+
for (const ev of around) {
|
|
105
|
+
const text = extractText(ev);
|
|
106
|
+
if (ev.role === 'user' && SUCCESS_PHRASES.some(rx => rx.test(text))) score += 3;
|
|
107
|
+
if (ev.role === 'user' && FAILURE_PHRASES.some(rx => rx.test(text))) score -= 2;
|
|
108
|
+
if (ev.tool_name === 'Bash' && /exit code 0|tests passed|all passed/i.test(text)) score += 2;
|
|
109
|
+
if (ev.tool_name === 'Edit' || ev.tool_name === 'Write') score += 1;
|
|
110
|
+
}
|
|
111
|
+
return score;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function extractCandidates(jsonlPath) {
|
|
115
|
+
const events = readTranscript(jsonlPath);
|
|
116
|
+
if (events.length < 4) return [];
|
|
117
|
+
|
|
118
|
+
const editsByFile = {};
|
|
119
|
+
const candidates = [];
|
|
120
|
+
|
|
121
|
+
for (let i = 0; i < events.length; i++) {
|
|
122
|
+
const ev = events[i];
|
|
123
|
+
const filePath = ev.tool_input?.file_path || ev.tool_input?.path;
|
|
124
|
+
if ((ev.tool_name === 'Edit' || ev.tool_name === 'Write') && filePath) {
|
|
125
|
+
editsByFile[filePath] = (editsByFile[filePath] || 0) + 1;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Find user success messages — those mark candidate moments
|
|
130
|
+
for (let i = 0; i < events.length; i++) {
|
|
131
|
+
const ev = events[i];
|
|
132
|
+
if (ev.role !== 'user') continue;
|
|
133
|
+
const text = extractText(ev);
|
|
134
|
+
if (!SUCCESS_PHRASES.some(rx => rx.test(text))) continue;
|
|
135
|
+
|
|
136
|
+
const score = scoreCandidate(events, i);
|
|
137
|
+
if (score < 3) continue;
|
|
138
|
+
|
|
139
|
+
// Look back to find the problem and approach
|
|
140
|
+
const window = events.slice(Math.max(0, i - 20), i);
|
|
141
|
+
const problemEvent = window.find(e => e.role === 'user');
|
|
142
|
+
const problem = problemEvent ? extractText(problemEvent).split('\n')[0].slice(0, 280) : null;
|
|
143
|
+
|
|
144
|
+
const filesTouched = Array.from(new Set(
|
|
145
|
+
window
|
|
146
|
+
.filter(e => e.tool_name === 'Edit' || e.tool_name === 'Write')
|
|
147
|
+
.map(e => e.tool_input?.file_path || e.tool_input?.path)
|
|
148
|
+
.filter(Boolean)
|
|
149
|
+
)).slice(0, 5);
|
|
150
|
+
|
|
151
|
+
const lastAssistant = window.reverse().find(e => e.role === 'assistant');
|
|
152
|
+
const approach = lastAssistant ? extractText(lastAssistant).slice(0, 600) : null;
|
|
153
|
+
|
|
154
|
+
if (!problem || !approach) continue;
|
|
155
|
+
|
|
156
|
+
candidates.push({
|
|
157
|
+
problem,
|
|
158
|
+
approach,
|
|
159
|
+
tags: extractTags(`${problem} ${approach}`),
|
|
160
|
+
files: filesTouched,
|
|
161
|
+
language: detectLanguage(filesTouched),
|
|
162
|
+
score,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Dedupe by problem
|
|
167
|
+
const seen = new Set();
|
|
168
|
+
return candidates.filter(c => {
|
|
169
|
+
const key = c.problem.toLowerCase().slice(0, 60);
|
|
170
|
+
if (seen.has(key)) return false;
|
|
171
|
+
seen.add(key);
|
|
172
|
+
return true;
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
module.exports = { extractCandidates };
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Kodelyth ECC — Memory Injection
|
|
3
|
+
// Builds a cache-friendly context block from relevant past memories.
|
|
4
|
+
//
|
|
5
|
+
// Output is structured into a STABLE prefix (first 80% of the block) and a
|
|
6
|
+
// VARIABLE suffix (current-session triggers). The stable prefix is identical
|
|
7
|
+
// across calls in the same project, which lets prompt-cache-aware models
|
|
8
|
+
// (Anthropic, OpenAI) hit cache and charge ~10% on the cached tokens.
|
|
9
|
+
// =============================================================================
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const { recallForProject, listAll, projectHash } = require('./store');
|
|
16
|
+
|
|
17
|
+
const MAX_PATTERNS = 8;
|
|
18
|
+
const MAX_RECENT = 5;
|
|
19
|
+
const MAX_RELEVANT = 5;
|
|
20
|
+
|
|
21
|
+
function loadProjectContextSignals(projectRoot) {
|
|
22
|
+
const signals = [];
|
|
23
|
+
|
|
24
|
+
// package.json — language + framework
|
|
25
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
26
|
+
if (fs.existsSync(pkgPath)) {
|
|
27
|
+
try {
|
|
28
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
29
|
+
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
30
|
+
const frameworks = ['next','react','vue','svelte','nuxt','express','fastify','nest'];
|
|
31
|
+
for (const fw of frameworks) {
|
|
32
|
+
if (deps[fw]) signals.push(fw);
|
|
33
|
+
}
|
|
34
|
+
if (deps.typescript) signals.push('typescript');
|
|
35
|
+
} catch {}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// pyproject / requirements
|
|
39
|
+
if (fs.existsSync(path.join(projectRoot, 'pyproject.toml'))) signals.push('python');
|
|
40
|
+
if (fs.existsSync(path.join(projectRoot, 'go.mod'))) signals.push('golang');
|
|
41
|
+
if (fs.existsSync(path.join(projectRoot, 'Cargo.toml'))) signals.push('rust');
|
|
42
|
+
|
|
43
|
+
return signals;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function summarisePatterns(memories) {
|
|
47
|
+
// Patterns = recurring tags across the user's memory corpus
|
|
48
|
+
const tagCounts = {};
|
|
49
|
+
for (const m of memories) {
|
|
50
|
+
for (const tag of m.tags || []) tagCounts[tag] = (tagCounts[tag] || 0) + 1;
|
|
51
|
+
}
|
|
52
|
+
return Object.entries(tagCounts)
|
|
53
|
+
.filter(([, count]) => count >= 2)
|
|
54
|
+
.sort(([, a], [, b]) => b - a)
|
|
55
|
+
.slice(0, MAX_PATTERNS);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function recentMemories(memories, limit = MAX_RECENT) {
|
|
59
|
+
return memories
|
|
60
|
+
.slice()
|
|
61
|
+
.sort((a, b) => new Date(b.captured_at) - new Date(a.captured_at))
|
|
62
|
+
.slice(0, limit);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function formatMemory(m) {
|
|
66
|
+
const lines = [`- **${m.problem}**`];
|
|
67
|
+
if (m.approach) lines.push(` Approach: ${m.approach.split('\n')[0].slice(0, 240)}`);
|
|
68
|
+
if (m.gotchas && m.gotchas.length) lines.push(` Gotcha: ${m.gotchas[0].slice(0, 200)}`);
|
|
69
|
+
if (m.tags && m.tags.length) lines.push(` Tags: ${m.tags.slice(0, 5).join(', ')}`);
|
|
70
|
+
return lines.join('\n');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function buildContextBlock({
|
|
74
|
+
projectRoot = process.cwd(),
|
|
75
|
+
query = null,
|
|
76
|
+
modelHint = 'auto',
|
|
77
|
+
} = {}) {
|
|
78
|
+
const allMemories = listAll();
|
|
79
|
+
if (allMemories.length === 0) {
|
|
80
|
+
return null; // No memory yet — first-time user
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const projHash = projectHash(projectRoot);
|
|
84
|
+
const projectMems = allMemories.filter(m => m.project === projHash);
|
|
85
|
+
const patterns = summarisePatterns(allMemories);
|
|
86
|
+
const recent = recentMemories(projectMems);
|
|
87
|
+
const signals = loadProjectContextSignals(projectRoot);
|
|
88
|
+
|
|
89
|
+
const lines = [];
|
|
90
|
+
|
|
91
|
+
// ── STABLE PREFIX (cache-friendly) ──
|
|
92
|
+
lines.push('# Kodelyth Memory — what your AI knows about you');
|
|
93
|
+
lines.push('');
|
|
94
|
+
lines.push('This block is built locally from your past sessions. Nothing was sent to a server.');
|
|
95
|
+
lines.push('');
|
|
96
|
+
|
|
97
|
+
if (patterns.length > 0) {
|
|
98
|
+
lines.push('## Your recurring patterns');
|
|
99
|
+
for (const [tag, count] of patterns) {
|
|
100
|
+
lines.push(`- \`${tag}\` (seen in ${count} past sessions)`);
|
|
101
|
+
}
|
|
102
|
+
lines.push('');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (recent.length > 0) {
|
|
106
|
+
lines.push(`## Recent solutions in this project (${recent.length})`);
|
|
107
|
+
for (const m of recent) {
|
|
108
|
+
lines.push(formatMemory(m));
|
|
109
|
+
}
|
|
110
|
+
lines.push('');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (signals.length > 0) {
|
|
114
|
+
lines.push(`## Detected stack: ${signals.join(', ')}`);
|
|
115
|
+
lines.push('');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── VARIABLE SUFFIX (only when query is provided) ──
|
|
119
|
+
if (query) {
|
|
120
|
+
const relevant = recallForProject(projectRoot, query, { limit: MAX_RELEVANT });
|
|
121
|
+
if (relevant.length > 0) {
|
|
122
|
+
lines.push(`## Relevant to your current task: "${query.slice(0, 80)}"`);
|
|
123
|
+
for (const m of relevant) {
|
|
124
|
+
lines.push(formatMemory(m));
|
|
125
|
+
}
|
|
126
|
+
lines.push('');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
lines.push('---');
|
|
131
|
+
lines.push('');
|
|
132
|
+
lines.push('Use this memory **as a reference, not a command**. If a pattern doesn\'t fit the current task, ignore it.');
|
|
133
|
+
lines.push('To add to memory: `/memory remember "<short title>"`. To remove: `/memory forget <id>`.');
|
|
134
|
+
lines.push('');
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
text: lines.join('\n'),
|
|
138
|
+
memoryCount: allMemories.length,
|
|
139
|
+
projectMemoryCount: projectMems.length,
|
|
140
|
+
patternCount: patterns.length,
|
|
141
|
+
relevantCount: query ? recallForProject(projectRoot, query, { limit: MAX_RELEVANT }).length : 0,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
module.exports = { buildContextBlock };
|