eklavya 1.18.2 → 1.19.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.
Files changed (98) hide show
  1. package/dist/assets/dashboard.html +559 -14
  2. package/dist/assets/tutor/references/focus-and-level.md +29 -0
  3. package/dist/cli.js +648 -20
  4. package/dist/cli.js.map +1 -1
  5. package/dist/config-path.js +156 -0
  6. package/dist/config-path.js.map +1 -0
  7. package/dist/config.js +231 -2
  8. package/dist/config.js.map +1 -1
  9. package/dist/dashboard.js +355 -0
  10. package/dist/dashboard.js.map +1 -1
  11. package/dist/eval/retrieval-score.js +77 -0
  12. package/dist/eval/retrieval-score.js.map +1 -0
  13. package/dist/hooks/capture-tool.js +107 -0
  14. package/dist/hooks/capture-tool.js.map +1 -0
  15. package/dist/hooks/checkpoint-quiz.js +5 -6
  16. package/dist/hooks/checkpoint-quiz.js.map +1 -1
  17. package/dist/hooks/lib.js +7 -33
  18. package/dist/hooks/lib.js.map +1 -1
  19. package/dist/hooks/memory-lib.js +181 -0
  20. package/dist/hooks/memory-lib.js.map +1 -0
  21. package/dist/hooks/prompt-submit-nudge.js +56 -20
  22. package/dist/hooks/prompt-submit-nudge.js.map +1 -1
  23. package/dist/hooks/session-start.js +73 -74
  24. package/dist/hooks/session-start.js.map +1 -1
  25. package/dist/hooks/stop-quiz-check.js +44 -7
  26. package/dist/hooks/stop-quiz-check.js.map +1 -1
  27. package/dist/install.js +37 -0
  28. package/dist/install.js.map +1 -1
  29. package/dist/memory/capture.js +123 -0
  30. package/dist/memory/capture.js.map +1 -0
  31. package/dist/memory/code.js +186 -0
  32. package/dist/memory/code.js.map +1 -0
  33. package/dist/memory/collections.js +87 -0
  34. package/dist/memory/collections.js.map +1 -0
  35. package/dist/memory/embed.js +81 -0
  36. package/dist/memory/embed.js.map +1 -0
  37. package/dist/memory/hosts.js +71 -0
  38. package/dist/memory/hosts.js.map +1 -0
  39. package/dist/memory/identity.js +74 -0
  40. package/dist/memory/identity.js.map +1 -0
  41. package/dist/memory/import.js +916 -0
  42. package/dist/memory/import.js.map +1 -0
  43. package/dist/memory/learning.js +148 -0
  44. package/dist/memory/learning.js.map +1 -0
  45. package/dist/memory/notify.js +220 -0
  46. package/dist/memory/notify.js.map +1 -0
  47. package/dist/memory/privacy.js +117 -0
  48. package/dist/memory/privacy.js.map +1 -0
  49. package/dist/memory/provider.js +155 -0
  50. package/dist/memory/provider.js.map +1 -0
  51. package/dist/memory/recall.js +327 -0
  52. package/dist/memory/recall.js.map +1 -0
  53. package/dist/memory/replay.js +174 -0
  54. package/dist/memory/replay.js.map +1 -0
  55. package/dist/memory/search.js +143 -0
  56. package/dist/memory/search.js.map +1 -0
  57. package/dist/memory/spool.js +93 -0
  58. package/dist/memory/spool.js.map +1 -0
  59. package/dist/memory/store.js +399 -0
  60. package/dist/memory/store.js.map +1 -0
  61. package/dist/memory/summarize.js +178 -0
  62. package/dist/memory/summarize.js.map +1 -0
  63. package/dist/memory/sync.js +595 -0
  64. package/dist/memory/sync.js.map +1 -0
  65. package/dist/memory/tokens.js +55 -0
  66. package/dist/memory/tokens.js.map +1 -0
  67. package/dist/memory/worker.js +207 -0
  68. package/dist/memory/worker.js.map +1 -0
  69. package/dist/migrations/009_memory.sql +226 -0
  70. package/dist/migrations/010_import.sql +26 -0
  71. package/dist/migrations/011_sync.sql +76 -0
  72. package/dist/migrations/012_job_backoff.sql +13 -0
  73. package/dist/migrations/013_batch_provenance.sql +18 -0
  74. package/dist/migrations/014_batch_events_index.sql +18 -0
  75. package/dist/plugin/.claude-plugin/plugin.json +1 -1
  76. package/dist/plugin/agents/tutor.md +3 -1
  77. package/dist/plugin/hooks/CLAUDE.md +30 -6
  78. package/dist/plugin/hooks/hooks.json +10 -0
  79. package/dist/plugin/skills/CLAUDE.md +17 -7
  80. package/dist/plugin/skills/memory/SKILL.md +88 -0
  81. package/dist/plugin/skills/setup/SKILL.md +16 -2
  82. package/dist/plugin/skills/tutor/references/focus-and-level.md +29 -0
  83. package/dist/time.js +41 -0
  84. package/dist/time.js.map +1 -0
  85. package/dist/tools/code_tools.js +73 -0
  86. package/dist/tools/code_tools.js.map +1 -0
  87. package/dist/tools/collection_tools.js +92 -0
  88. package/dist/tools/collection_tools.js.map +1 -0
  89. package/dist/tools/config_tools.js +104 -1
  90. package/dist/tools/config_tools.js.map +1 -1
  91. package/dist/tools/index.js +15 -0
  92. package/dist/tools/index.js.map +1 -1
  93. package/dist/tools/memory_read_tools.js +277 -0
  94. package/dist/tools/memory_read_tools.js.map +1 -0
  95. package/dist/tools/memory_write_tools.js +115 -0
  96. package/dist/tools/memory_write_tools.js.map +1 -0
  97. package/dist/user-skill/eklavya/SKILL.md +64 -8
  98. package/package.json +2 -1
@@ -0,0 +1,174 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { identityFor } from './identity.js';
5
+ import { prepare } from './capture.js';
6
+ import { appendEvent } from './store.js';
7
+ /**
8
+ * Claude Code stores transcripts under a directory named after the project
9
+ * path with every separator replaced by a dash.
10
+ */
11
+ export function transcriptDirFor(cwd) {
12
+ const encoded = path.resolve(cwd).replace(/[/\\]/g, '-');
13
+ return path.join(os.homedir(), '.claude', 'projects', encoded);
14
+ }
15
+ export function transcriptsFor(cwd) {
16
+ const dir = transcriptDirFor(cwd);
17
+ try {
18
+ return fs
19
+ .readdirSync(dir)
20
+ .filter((f) => f.endsWith('.jsonl'))
21
+ .map((f) => path.join(dir, f))
22
+ .sort();
23
+ }
24
+ catch {
25
+ return [];
26
+ }
27
+ }
28
+ function textOf(content) {
29
+ if (typeof content === 'string')
30
+ return content;
31
+ if (!Array.isArray(content))
32
+ return '';
33
+ return content
34
+ .map((block) => {
35
+ if (!block || typeof block !== 'object')
36
+ return '';
37
+ const b = block;
38
+ return b.type === 'text' && typeof b.text === 'string' ? b.text : '';
39
+ })
40
+ .filter(Boolean)
41
+ .join('\n');
42
+ }
43
+ function toolUsesOf(content) {
44
+ if (!Array.isArray(content))
45
+ return [];
46
+ const uses = [];
47
+ for (const block of content) {
48
+ if (!block || typeof block !== 'object')
49
+ continue;
50
+ const b = block;
51
+ if (b.type === 'tool_use' && typeof b.name === 'string') {
52
+ uses.push({ name: b.name, input: b.input ?? {} });
53
+ }
54
+ }
55
+ return uses;
56
+ }
57
+ const EDIT_TOOLS = new Set(['Edit', 'Write', 'NotebookEdit', 'MultiEdit']);
58
+ const READ_TOOLS = new Set(['Read', 'NotebookRead', 'Glob', 'Grep']);
59
+ function kindFor(tool) {
60
+ if (EDIT_TOOLS.has(tool))
61
+ return 'file_edit';
62
+ if (READ_TOOLS.has(tool))
63
+ return 'file_read';
64
+ return 'tool_use';
65
+ }
66
+ function bodyFor(input) {
67
+ const parts = [];
68
+ for (const key of ['command', 'description', 'prompt', 'pattern', 'query']) {
69
+ if (typeof input[key] === 'string')
70
+ parts.push(input[key]);
71
+ }
72
+ if (typeof input.old_string === 'string' && typeof input.new_string === 'string') {
73
+ parts.push(`- ${input.old_string.slice(0, 600)}`, `+ ${input.new_string.slice(0, 600)}`);
74
+ }
75
+ if (!parts.length) {
76
+ const keys = Object.keys(input).filter((k) => k !== 'content');
77
+ if (keys.length)
78
+ parts.push(keys.map((k) => `${k}=${String(input[k]).slice(0, 200)}`).join(' '));
79
+ }
80
+ return parts.join('\n');
81
+ }
82
+ function filesFrom(input) {
83
+ return [input.file_path, input.notebook_path, input.path].filter((v) => typeof v === 'string' && v.length > 0);
84
+ }
85
+ function alreadyHave(db, sessionId, kind, tool, body) {
86
+ const row = db
87
+ .prepare(`SELECT 1 AS hit FROM evidence_events
88
+ WHERE session_id = ? AND kind = ? AND IFNULL(tool, '') = ? AND body = ? LIMIT 1`)
89
+ .get(sessionId, kind, tool ?? '', body);
90
+ return Boolean(row);
91
+ }
92
+ /** Replays one transcript file. Idempotent: running it twice captures nothing new. */
93
+ export function replayTranscript(db, config, file, opts = {}) {
94
+ const result = { file, read: 0, captured: 0, duplicates: 0, excluded: 0 };
95
+ let text;
96
+ try {
97
+ text = fs.readFileSync(file, 'utf8');
98
+ }
99
+ catch {
100
+ return result;
101
+ }
102
+ const lines = text.split('\n').filter(Boolean).slice(0, opts.maxLines ?? 20_000);
103
+ for (const raw of lines) {
104
+ let line;
105
+ try {
106
+ line = JSON.parse(raw);
107
+ }
108
+ catch {
109
+ continue;
110
+ }
111
+ // A sidechain is a subagent's own transcript; the parent's line already
112
+ // records that the delegation happened, and replaying both files records
113
+ // the same work twice under two identities.
114
+ if (line.isSidechain)
115
+ continue;
116
+ if (line.type !== 'user' && line.type !== 'assistant')
117
+ continue;
118
+ result.read++;
119
+ const identity = identityFor({
120
+ cwd: line.cwd ?? opts.cwd ?? process.cwd(),
121
+ sessionId: line.sessionId ?? 'replay',
122
+ host: 'claude-code',
123
+ });
124
+ const events = [];
125
+ if (line.type === 'user') {
126
+ const prompt = textOf(line.message?.content);
127
+ // A user line carrying a tool result is the host echoing the agent's own
128
+ // work back, not the developer typing. Only a real prompt is one.
129
+ if (prompt.trim() && !line.toolUseResult) {
130
+ events.push({ kind: 'prompt', title: 'prompt', body: prompt, occurredAt: line.timestamp, source: 'replay' });
131
+ }
132
+ }
133
+ else {
134
+ for (const use of toolUsesOf(line.message?.content)) {
135
+ const body = bodyFor(use.input);
136
+ if (!body.trim())
137
+ continue;
138
+ events.push({
139
+ kind: kindFor(use.name),
140
+ tool: use.name,
141
+ title: use.name,
142
+ body,
143
+ files: filesFrom(use.input),
144
+ occurredAt: line.timestamp,
145
+ source: 'replay',
146
+ });
147
+ }
148
+ }
149
+ for (const event of events) {
150
+ const input = prepare(config, identity, event);
151
+ if (!input) {
152
+ result.excluded++;
153
+ continue;
154
+ }
155
+ if (alreadyHave(db, input.sessionId, input.kind, input.tool ?? null, input.body)) {
156
+ result.duplicates++;
157
+ continue;
158
+ }
159
+ const { inserted } = appendEvent(db, input);
160
+ if (inserted)
161
+ result.captured++;
162
+ else
163
+ result.duplicates++;
164
+ }
165
+ }
166
+ return result;
167
+ }
168
+ /** Replays every transcript Claude Code has for this checkout. */
169
+ export function replayProject(db, config, cwd, opts = {}) {
170
+ return transcriptsFor(cwd)
171
+ .slice(-(opts.limit ?? 20))
172
+ .map((file) => replayTranscript(db, config, file, { cwd }));
173
+ }
174
+ //# sourceMappingURL=replay.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"replay.js","sourceRoot":"","sources":["../../src/memory/replay.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,143 @@
1
+ import { cosine, embedLocal, fromBlob, LOCAL_EMBEDDER } from './embed.js';
2
+ import { entriesByIds } from './store.js';
3
+ /**
4
+ * FTS5 has a query language, and a developer's search box does not. Feeding
5
+ * `fix: auth (retry?)` straight to MATCH is a syntax error, and quoting the
6
+ * whole string turns a two-word search into a phrase search that matches almost
7
+ * nothing. So: split into terms, quote each one, and combine them.
8
+ */
9
+ function ftsQuery(raw, join) {
10
+ const terms = raw
11
+ .toLowerCase()
12
+ .split(/[^\p{L}\p{N}_]+/u)
13
+ .filter((t) => t.length > 1)
14
+ .map((t) => `"${t.replace(/"/g, '')}"`);
15
+ if (!terms.length)
16
+ return null;
17
+ return terms.join(` ${join} `);
18
+ }
19
+ function scopeClause(filter, alias = 'e') {
20
+ const where = [`${alias}.deleted_at IS NULL`, `${alias}.superseded_by IS NULL`];
21
+ const args = [];
22
+ if (!filter.allProjects && filter.project) {
23
+ where.push(`${alias}.project = ?`);
24
+ args.push(filter.project);
25
+ }
26
+ if (filter.type) {
27
+ where.push(`${alias}.type = ?`);
28
+ args.push(filter.type);
29
+ }
30
+ if (filter.since) {
31
+ where.push(`${alias}.occurred_at >= ?`);
32
+ args.push(filter.since);
33
+ }
34
+ if (filter.until) {
35
+ where.push(`${alias}.occurred_at <= ?`);
36
+ args.push(filter.until);
37
+ }
38
+ if (filter.tag) {
39
+ where.push(`EXISTS (SELECT 1 FROM memory_entry_tags t WHERE t.entry_id = ${alias}.id AND t.tag = ?)`);
40
+ args.push(filter.tag.toLowerCase());
41
+ }
42
+ return { sql: where.join(' AND '), args };
43
+ }
44
+ export function keywordSearch(db, query, filter = {}) {
45
+ const limit = filter.limit ?? 20;
46
+ const scope = scopeClause(filter);
47
+ const run = (match) => {
48
+ const rows = db
49
+ .prepare(`SELECT e.*, bm25(memory_fts) AS rank
50
+ FROM memory_fts
51
+ JOIN memory_entries e ON e.id = memory_fts.rowid
52
+ WHERE memory_fts MATCH ? AND ${scope.sql}
53
+ ORDER BY rank LIMIT ?`)
54
+ .all(match, ...scope.args, limit);
55
+ // bm25 returns a negative score where more negative is better; flip it so
56
+ // every mode in this module speaks "higher is better".
57
+ return rows.map(({ rank, ...entry }) => ({ entry: entry, score: -rank, via: 'keyword' }));
58
+ };
59
+ const andQuery = ftsQuery(query, 'AND');
60
+ if (!andQuery)
61
+ return [];
62
+ const exact = run(andQuery);
63
+ if (exact.length || filter.strict)
64
+ return exact;
65
+ // Nothing matched every term. Widen rather than return nothing: a developer
66
+ // searching "refresh cookie rotation" wants the rotation note even if the
67
+ // wording differs.
68
+ const orQuery = ftsQuery(query, 'OR');
69
+ return orQuery ? run(orQuery) : [];
70
+ }
71
+ /** Bounded scan: cosine over the project's most recent vectors (ADR-03). */
72
+ export function semanticSearch(db, query, filter = {}, scanLimit = 5000) {
73
+ const scope = scopeClause(filter);
74
+ const rows = db
75
+ .prepare(`SELECT e.id, v.vec FROM memory_entries e
76
+ JOIN memory_vectors v ON v.entry_id = e.id AND v.embedder_id = ?
77
+ WHERE ${scope.sql}
78
+ ORDER BY e.occurred_at DESC LIMIT ?`)
79
+ .all(LOCAL_EMBEDDER, ...scope.args, scanLimit);
80
+ if (!rows.length)
81
+ return [];
82
+ const q = embedLocal(query);
83
+ const scored = rows
84
+ .map((r) => ({ id: r.id, score: cosine(q, fromBlob(r.vec)) }))
85
+ .filter((r) => r.score > 0.05)
86
+ .sort((a, b) => b.score - a.score)
87
+ .slice(0, filter.limit ?? 20);
88
+ const entries = new Map(entriesByIds(db, scored.map((s) => s.id)).map((e) => [e.id, e]));
89
+ return scored
90
+ .map((s) => ({ entry: entries.get(s.id), score: s.score, via: 'semantic' }))
91
+ .filter((h) => h.entry);
92
+ }
93
+ /**
94
+ * Reciprocal rank fusion of the two lists.
95
+ *
96
+ * RRF rather than a weighted sum of scores because bm25 and cosine are not on a
97
+ * common scale, and normalising them per query invents a calibration the data
98
+ * does not support. Rank is the only thing the two agree on.
99
+ */
100
+ export function hybridSearch(db, query, filter = {}) {
101
+ const limit = filter.limit ?? 20;
102
+ const wide = { ...filter, limit: Math.max(limit * 3, 30) };
103
+ const keyword = keywordSearch(db, query, wide);
104
+ const semantic = semanticSearch(db, query, wide);
105
+ const K = 60;
106
+ const fused = new Map();
107
+ const add = (hits) => {
108
+ hits.forEach((hit, index) => {
109
+ const prev = fused.get(hit.entry.id);
110
+ const contribution = 1 / (K + index + 1);
111
+ if (prev)
112
+ prev.score += contribution;
113
+ else
114
+ fused.set(hit.entry.id, { entry: hit.entry, score: contribution });
115
+ });
116
+ };
117
+ add(keyword);
118
+ add(semantic);
119
+ return [...fused.values()]
120
+ .sort((a, b) => b.score - a.score)
121
+ .slice(0, limit)
122
+ .map((f) => ({ entry: f.entry, score: f.score, via: 'hybrid' }));
123
+ }
124
+ export function search(db, query, mode, filter = {}) {
125
+ if (!query.trim())
126
+ return [];
127
+ if (mode === 'keyword')
128
+ return keywordSearch(db, query, filter);
129
+ if (mode === 'semantic')
130
+ return semanticSearch(db, query, filter);
131
+ return hybridSearch(db, query, filter);
132
+ }
133
+ /** Every entry that touched a file, newest first (PRD RET-01 file history). */
134
+ export function fileHistory(db, file, filter = {}) {
135
+ const scope = scopeClause(filter);
136
+ return db
137
+ .prepare(`SELECT e.* FROM memory_entries e
138
+ WHERE ${scope.sql} AND e.files IS NOT NULL
139
+ AND EXISTS (SELECT 1 FROM json_each(e.files) WHERE json_each.value LIKE ?)
140
+ ORDER BY e.occurred_at DESC LIMIT ?`)
141
+ .all(...scope.args, `%${file}%`, filter.limit ?? 20);
142
+ }
143
+ //# sourceMappingURL=search.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search.js","sourceRoot":"","sources":["../../src/memory/search.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,93 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { eklavyaHome } from '../paths.js';
4
+ import { nowIso } from '../time.js';
5
+ /**
6
+ * The bounded local spool (PRD CAP-03).
7
+ *
8
+ * The database is normally available — the hooks open it directly — but "the
9
+ * disk is full", "another writer holds the lock past the busy timeout" and "the
10
+ * file is mid-upgrade" all happen, and a session must keep working through
11
+ * them. Events that cannot be committed are appended here as JSON lines and
12
+ * replayed on the next healthy open.
13
+ *
14
+ * Bounded, and honest about the bound: past `MAX_BYTES` new events are dropped
15
+ * and counted rather than allowed to fill the disk that is already full. A
16
+ * dropped event is reported by `eklavya doctor`, never silently promised.
17
+ */
18
+ const MAX_BYTES = 4 * 1024 * 1024;
19
+ export function spoolPath() {
20
+ return path.join(eklavyaHome(), 'spool', 'events.jsonl');
21
+ }
22
+ function droppedPath() {
23
+ return path.join(eklavyaHome(), 'spool', 'dropped.json');
24
+ }
25
+ export function spoolEvent(record) {
26
+ const file = spoolPath();
27
+ try {
28
+ fs.mkdirSync(path.dirname(file), { recursive: true });
29
+ const size = fs.existsSync(file) ? fs.statSync(file).size : 0;
30
+ if (size > MAX_BYTES) {
31
+ noteDropped();
32
+ return 'dropped';
33
+ }
34
+ // One `appendFileSync` of a single line under the pipe buffer is atomic
35
+ // enough for concurrent hook processes: neither interleaves a partial line.
36
+ fs.appendFileSync(file, `${JSON.stringify(record)}\n`, 'utf8');
37
+ return 'spooled';
38
+ }
39
+ catch {
40
+ return 'dropped';
41
+ }
42
+ }
43
+ function noteDropped() {
44
+ try {
45
+ const file = droppedPath();
46
+ const prev = fs.existsSync(file)
47
+ ? JSON.parse(fs.readFileSync(file, 'utf8'))
48
+ : {};
49
+ fs.writeFileSync(file, JSON.stringify({ count: (prev.count ?? 0) + 1, last: nowIso() }), 'utf8');
50
+ }
51
+ catch {
52
+ // The spool is already the degraded path; a failure to count a drop is not
53
+ // worth taking a session down for.
54
+ }
55
+ }
56
+ export function droppedCount() {
57
+ try {
58
+ const parsed = JSON.parse(fs.readFileSync(droppedPath(), 'utf8'));
59
+ return parsed.count ?? 0;
60
+ }
61
+ catch {
62
+ return 0;
63
+ }
64
+ }
65
+ /**
66
+ * Reads and clears the spool. The rename-then-read order matters: a hook
67
+ * writing concurrently appends to a fresh file rather than to the one being
68
+ * replayed, so nothing is read twice and nothing written during the replay is
69
+ * lost. Idempotent appends downstream make a crash mid-replay harmless.
70
+ */
71
+ export function takeSpooled() {
72
+ const file = spoolPath();
73
+ try {
74
+ if (!fs.existsSync(file))
75
+ return [];
76
+ const taken = `${file}.replay-${process.pid}`;
77
+ fs.renameSync(file, taken);
78
+ const lines = fs.readFileSync(taken, 'utf8').split('\n').filter(Boolean);
79
+ fs.rmSync(taken, { force: true });
80
+ return lines.flatMap((line) => {
81
+ try {
82
+ return [JSON.parse(line)];
83
+ }
84
+ catch {
85
+ return [];
86
+ }
87
+ });
88
+ }
89
+ catch {
90
+ return [];
91
+ }
92
+ }
93
+ //# sourceMappingURL=spool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spool.js","sourceRoot":"","sources":["../../src/memory/spool.ts"],"names":[],"mappings":"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"}