eklavya 1.20.0 → 1.21.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/assets/tutor/SKILL.md +1 -1
- package/dist/assets/tutor/references/focus-and-level.md +4 -3
- package/dist/claude-mem.js +228 -0
- package/dist/claude-mem.js.map +1 -0
- package/dist/cli.js +52 -46
- package/dist/cli.js.map +1 -1
- package/dist/install.js +162 -53
- package/dist/install.js.map +1 -1
- package/dist/plugin/.claude-plugin/plugin.json +1 -1
- package/dist/plugin/skills/quiz/SKILL.md +2 -0
- package/dist/plugin/skills/tutor/SKILL.md +1 -1
- package/dist/plugin/skills/tutor/references/focus-and-level.md +4 -3
- package/dist/store.js +62 -0
- package/dist/store.js.map +1 -1
- package/dist/theme.js +80 -0
- package/dist/theme.js.map +1 -0
- package/dist/tools/get_session_quiz_plan.js +29 -13
- package/dist/tools/get_session_quiz_plan.js.map +1 -1
- package/dist/user-skill/eklavya/SKILL.md +1 -1
- package/package.json +1 -1
|
@@ -80,7 +80,7 @@ checkpoint answered is one the sweep no longer asks, so a session that
|
|
|
80
80
|
checkpointed through the budget ends in silence. That is intended — do not top it
|
|
81
81
|
up because the ending felt quiet. What the budget never reached stays unmastered
|
|
82
82
|
and comes back in a later plan with `reason: "backlog"`, once that session's own
|
|
83
|
-
concepts and review debt are covered, from the domains
|
|
83
|
+
concepts and review debt are covered, from that project and the domains it touches, and
|
|
84
84
|
never when `quiz.enforced` is set. It is not spaced repetition that brings it
|
|
85
85
|
back — that only schedules concepts already answered once.
|
|
86
86
|
|
|
@@ -72,9 +72,10 @@ you reach for the idea instead. Ask about them on their own terms.
|
|
|
72
72
|
|
|
73
73
|
Items with `reason: "backlog"` are older work — a concept an earlier session
|
|
74
74
|
logged that no question ever reached, offered once this session's own concepts
|
|
75
|
-
and review debt are covered, and only from the domains this
|
|
76
|
-
there is one database for every project, so the scope is what
|
|
77
|
-
session off last month's Terraform.
|
|
75
|
+
and review debt are covered, and only from this project and the domains this
|
|
76
|
+
session touched -- there is one database for every project, so the scope is what
|
|
77
|
+
keeps a Flutter session off last month's Terraform. Review debt is scoped to this
|
|
78
|
+
project the same way. Never when `quiz.enforced` is set: a backlog answer
|
|
78
79
|
cannot open a commit gate, so offering one there would only spend the budget the
|
|
79
80
|
gate needs. They also arrive with `context: null`, and for a
|
|
80
81
|
blunter reason: the code that taught them is not on screen and may not even be
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Mem, seen from `eklavya install`.
|
|
3
|
+
*
|
|
4
|
+
* Both tools record every tool call and recall it at session start. Installed
|
|
5
|
+
* together, every session is captured twice and recalled twice, into two
|
|
6
|
+
* databases that never agree. So install asks once which one records, and acts
|
|
7
|
+
* on the answer:
|
|
8
|
+
*
|
|
9
|
+
* eklavya uninstall the Claude Mem plugin, import its history with every
|
|
10
|
+
* project it can place filed under its checkout, and retire
|
|
11
|
+
* `~/.claude-mem` (moved, never deleted — it is the rollback).
|
|
12
|
+
* claude-mem set `memory.enabled: false`. The quiz half is unaffected.
|
|
13
|
+
*
|
|
14
|
+
* The hard part is the project map. Claude Mem names a project with a bare
|
|
15
|
+
* string; Eklavya keys it by checkout path. Every Claude Mem session carries the
|
|
16
|
+
* Claude Code session id, and Claude Code's transcript for that id records the
|
|
17
|
+
* cwd — so the map is read off the transcripts rather than guessed from names.
|
|
18
|
+
*/
|
|
19
|
+
import fs from 'node:fs';
|
|
20
|
+
import os from 'node:os';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { spawnSync } from 'node:child_process';
|
|
23
|
+
import Database from 'better-sqlite3';
|
|
24
|
+
import { findRepoConfig } from './config.js';
|
|
25
|
+
import { projectKey } from './store.js';
|
|
26
|
+
/** `CLAUDE_MEM_DATA_DIR` is Claude Mem's own override, so it is ours too. */
|
|
27
|
+
export function claudeMemDir() {
|
|
28
|
+
return process.env.CLAUDE_MEM_DATA_DIR ?? path.join(os.homedir(), '.claude-mem');
|
|
29
|
+
}
|
|
30
|
+
export function claudeMemDb() {
|
|
31
|
+
return path.join(claudeMemDir(), 'claude-mem.db');
|
|
32
|
+
}
|
|
33
|
+
function readJson(file) {
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return {};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Every registered plugin id that is Claude Mem, whatever marketplace it came from. */
|
|
42
|
+
export function claudeMemPluginIds(claudeHome) {
|
|
43
|
+
const installed = readJson(path.join(claudeHome, 'plugins', 'installed_plugins.json'));
|
|
44
|
+
const settings = readJson(path.join(claudeHome, 'settings.json'));
|
|
45
|
+
const ids = [
|
|
46
|
+
...Object.keys((installed.plugins ?? {})),
|
|
47
|
+
...Object.keys((settings.enabledPlugins ?? {})),
|
|
48
|
+
];
|
|
49
|
+
return [...new Set(ids.filter((id) => id.startsWith('claude-mem@')))];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Removes the plugin through Claude Code's own CLI, and falls back to switching
|
|
53
|
+
* it off in settings.json when `claude` is not on PATH. Off is enough to stop
|
|
54
|
+
* the double recording; the fallback just cannot delete the files.
|
|
55
|
+
*/
|
|
56
|
+
export function removeClaudeMemPlugin(claudeHome, ids) {
|
|
57
|
+
let viaCli = true;
|
|
58
|
+
for (const id of ids) {
|
|
59
|
+
const res = spawnSync('claude', ['plugin', 'uninstall', id], {
|
|
60
|
+
stdio: 'ignore',
|
|
61
|
+
shell: process.platform === 'win32',
|
|
62
|
+
env: { ...process.env, CLAUDE_CONFIG_DIR: claudeHome },
|
|
63
|
+
});
|
|
64
|
+
if (res.status !== 0)
|
|
65
|
+
viaCli = false;
|
|
66
|
+
}
|
|
67
|
+
if (viaCli)
|
|
68
|
+
return 'uninstalled';
|
|
69
|
+
const file = path.join(claudeHome, 'settings.json');
|
|
70
|
+
const settings = readJson(file);
|
|
71
|
+
const enabled = (settings.enabledPlugins ?? {});
|
|
72
|
+
for (const id of ids)
|
|
73
|
+
enabled[id] = false;
|
|
74
|
+
settings.enabledPlugins = enabled;
|
|
75
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
76
|
+
fs.writeFileSync(tmp, `${JSON.stringify(settings, null, 2)}\n`, 'utf8');
|
|
77
|
+
fs.renameSync(tmp, file);
|
|
78
|
+
return 'disabled';
|
|
79
|
+
}
|
|
80
|
+
/** The first `cwd` a transcript records, or null. Only the head is read. */
|
|
81
|
+
function transcriptCwd(file) {
|
|
82
|
+
try {
|
|
83
|
+
const fd = fs.openSync(file, 'r');
|
|
84
|
+
const buf = Buffer.alloc(64 * 1024);
|
|
85
|
+
const n = fs.readSync(fd, buf, 0, buf.length, 0);
|
|
86
|
+
fs.closeSync(fd);
|
|
87
|
+
const m = /"cwd":"((?:[^"\\]|\\.)*)"/.exec(buf.toString('utf8', 0, n));
|
|
88
|
+
return m ? JSON.parse(`"${m[1]}"`) : null;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function safeMtime(file) {
|
|
95
|
+
try {
|
|
96
|
+
return fs.statSync(file).mtimeMs;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return -1;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** The checkout a path belongs to, folded through worktrees — or null if it is gone. */
|
|
103
|
+
function checkoutOf(dir) {
|
|
104
|
+
if (!dir || !fs.existsSync(dir))
|
|
105
|
+
return null;
|
|
106
|
+
const root = findRepoConfig(dir).repoRoot;
|
|
107
|
+
return root ? projectKey(root) : null;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Where a transcript directory's name points today. Claude Code names it after
|
|
111
|
+
* the project path with every non-alphanumeric turned into `-`, and renames it
|
|
112
|
+
* when the checkout moves — while the transcripts inside keep the old `cwd`. The
|
|
113
|
+
* encoding is lossy (`a_b`, `a-b` and `a/b` all read `a-b`), so it is decoded by
|
|
114
|
+
* walking the filesystem, keeping only segments that exist. Case-insensitive,
|
|
115
|
+
* because macOS is. Returns null on anything it cannot walk, Windows included.
|
|
116
|
+
*/
|
|
117
|
+
function decodeTranscriptDir(name) {
|
|
118
|
+
const enc = (s) => `-${s.replace(/[^A-Za-z0-9]/g, '-')}`.toLowerCase();
|
|
119
|
+
const walk = (dir, rest) => {
|
|
120
|
+
if (!rest)
|
|
121
|
+
return dir;
|
|
122
|
+
let entries = [];
|
|
123
|
+
try {
|
|
124
|
+
entries = fs.readdirSync(dir);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
for (const e of entries) {
|
|
130
|
+
const seg = enc(e);
|
|
131
|
+
if (rest !== seg && !rest.startsWith(`${seg}-`))
|
|
132
|
+
continue;
|
|
133
|
+
const hit = walk(path.join(dir, e), rest.slice(seg.length));
|
|
134
|
+
if (hit)
|
|
135
|
+
return hit;
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
};
|
|
139
|
+
return walk('/', name.toLowerCase());
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Claude Mem project name → Eklavya project key, for every project that can be
|
|
143
|
+
* placed. A project is placed by majority vote over its sessions' transcripts —
|
|
144
|
+
* each session counts for the checkout its `cwd` is in, or, where that path is
|
|
145
|
+
* gone, the checkout its transcript directory now points at. Failing that, by a
|
|
146
|
+
* unique live checkout whose folder carries the same name. Anything else is left
|
|
147
|
+
* out, and the importer keeps it as-is — searchable with `--all-projects`,
|
|
148
|
+
* mappable later with `--map`.
|
|
149
|
+
*
|
|
150
|
+
* ponytail: reads at most 20 transcripts per project, which is plenty to vote.
|
|
151
|
+
*/
|
|
152
|
+
export function guessProjectMap(sourceDb, claudeHome) {
|
|
153
|
+
const projectsDir = path.join(claudeHome, 'projects');
|
|
154
|
+
let dirs = [];
|
|
155
|
+
try {
|
|
156
|
+
dirs = fs.readdirSync(projectsDir);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return {};
|
|
160
|
+
}
|
|
161
|
+
const byId = new Map();
|
|
162
|
+
const liveByName = new Map();
|
|
163
|
+
for (const d of dirs) {
|
|
164
|
+
let files = [];
|
|
165
|
+
try {
|
|
166
|
+
files = fs.readdirSync(path.join(projectsDir, d)).filter((f) => f.endsWith('.jsonl'));
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const paths = files.map((f) => path.join(projectsDir, d, f));
|
|
172
|
+
// The newest transcript is likeliest to name where the project lives now.
|
|
173
|
+
let newest;
|
|
174
|
+
let newestAt = -1;
|
|
175
|
+
for (const file of paths) {
|
|
176
|
+
const at = safeMtime(file);
|
|
177
|
+
if (at > newestAt)
|
|
178
|
+
[newest, newestAt] = [file, at];
|
|
179
|
+
}
|
|
180
|
+
const dirRoot = checkoutOf(newest ? transcriptCwd(newest) : null) ?? checkoutOf(decodeTranscriptDir(d));
|
|
181
|
+
for (const file of paths)
|
|
182
|
+
byId.set(path.basename(file, '.jsonl'), { file, dirRoot });
|
|
183
|
+
if (dirRoot) {
|
|
184
|
+
const name = path.basename(dirRoot);
|
|
185
|
+
liveByName.set(name, (liveByName.get(name) ?? new Set()).add(dirRoot));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const src = new Database(sourceDb, { readonly: true, fileMustExist: true });
|
|
189
|
+
let rows = [];
|
|
190
|
+
try {
|
|
191
|
+
rows = src
|
|
192
|
+
.prepare('SELECT project, group_concat(content_session_id) AS ids FROM sdk_sessions GROUP BY project')
|
|
193
|
+
.all();
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
/* no sdk_sessions: nothing to vote with */
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
src.close();
|
|
200
|
+
}
|
|
201
|
+
const map = {};
|
|
202
|
+
for (const { project, ids } of rows) {
|
|
203
|
+
const votes = new Map();
|
|
204
|
+
for (const id of (ids ?? '').split(',').slice(0, 20)) {
|
|
205
|
+
const t = byId.get(id);
|
|
206
|
+
const root = t ? checkoutOf(transcriptCwd(t.file)) ?? t.dirRoot : null;
|
|
207
|
+
if (root)
|
|
208
|
+
votes.set(root, (votes.get(root) ?? 0) + 1);
|
|
209
|
+
}
|
|
210
|
+
const best = [...votes].sort((a, b) => b[1] - a[1])[0];
|
|
211
|
+
const byName = liveByName.get(path.basename(project));
|
|
212
|
+
if (best)
|
|
213
|
+
map[project] = best[0];
|
|
214
|
+
else if (byName?.size === 1)
|
|
215
|
+
map[project] = [...byName][0];
|
|
216
|
+
}
|
|
217
|
+
return map;
|
|
218
|
+
}
|
|
219
|
+
/** `~/.claude-mem` → `~/.claude-mem.retired` (or `.retired-2`, …). Never a delete. */
|
|
220
|
+
export function retireClaudeMemDir() {
|
|
221
|
+
const from = claudeMemDir();
|
|
222
|
+
let to = `${from}.retired`;
|
|
223
|
+
for (let i = 2; fs.existsSync(to); i++)
|
|
224
|
+
to = `${from}.retired-${i}`;
|
|
225
|
+
fs.renameSync(from, to);
|
|
226
|
+
return to;
|
|
227
|
+
}
|
|
228
|
+
//# sourceMappingURL=claude-mem.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claude-mem.js","sourceRoot":"","sources":["../src/claude-mem.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.js
CHANGED
|
@@ -20,6 +20,7 @@ import { START_LEVEL } from './srs.js';
|
|
|
20
20
|
import Database from 'better-sqlite3';
|
|
21
21
|
import { startDashboard, openInBrowser } from './dashboard.js';
|
|
22
22
|
import { install, uninstall, health } from './install.js';
|
|
23
|
+
import { check, dim, heading, verdict } from './theme.js';
|
|
23
24
|
import { identityFor } from './memory/identity.js';
|
|
24
25
|
import { countEntries, entryById, entryEvents, entryTags, pendingEventCount, receiptTotals, resumePaused, timeline, } from './memory/store.js';
|
|
25
26
|
import { search } from './memory/search.js';
|
|
@@ -317,13 +318,14 @@ function doctor() {
|
|
|
317
318
|
if (repoHere)
|
|
318
319
|
migrateLegacyRepoConfig(repoHere, mainRepoRoot(repoHere));
|
|
319
320
|
const resolved = loadConfig();
|
|
320
|
-
const
|
|
321
|
+
const rows = [];
|
|
322
|
+
const add = (mark, label, detail) => rows.push([mark, label, detail]);
|
|
321
323
|
let ok = true;
|
|
322
324
|
// Kept apart from `ok` so the blanket remedy below stays true: `eklavya
|
|
323
325
|
// install` repairs a broken install and cannot do a thing about a paused
|
|
324
326
|
// queue. A memory failure still exits non-zero; it just names its own fix.
|
|
325
327
|
let memoryOk = true;
|
|
326
|
-
|
|
328
|
+
add('ok', 'home', eklavyaHome());
|
|
327
329
|
// The install checks come first because they are what someone is looking for
|
|
328
330
|
// when Eklavya has gone quiet. Everything below reads fine on an install that
|
|
329
331
|
// Claude Code can no longer load at all.
|
|
@@ -331,7 +333,7 @@ function doctor() {
|
|
|
331
333
|
for (const check of checks) {
|
|
332
334
|
if (!check.ok)
|
|
333
335
|
ok = false;
|
|
334
|
-
|
|
336
|
+
add(check.ok ? 'ok' : 'fail', check.name, `${check.ok ? '' : 'FAILED — '}${check.detail}`);
|
|
335
337
|
}
|
|
336
338
|
// Which surfaces the checks above actually cover. The `plugin` check reads
|
|
337
339
|
// Claude Code's registry under `~/.claude`, which the Code tab in Claude
|
|
@@ -339,10 +341,10 @@ function doctor() {
|
|
|
339
341
|
// inside the app's data directory and is installed from its own UI, so a
|
|
340
342
|
// green doctor says nothing about it. Someone whose Cowork sessions are
|
|
341
343
|
// silent needs to be told that here, not left reading a clean report.
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
344
|
+
add('ok', 'surfaces', 'Claude Code CLI and the Code tab in Claude Desktop (checked above)');
|
|
345
|
+
add('skip', 'cowork', dim('keeps a separate plugin list — install it there from Customize → Plugins;'));
|
|
346
|
+
add('skip', 'cowork', dim('this command cannot see it. Your learning history is shared either way.'));
|
|
347
|
+
add(fs.existsSync(file) ? 'ok' : 'warn', 'database', `${file}${fs.existsSync(file) ? '' : dim(' (not created yet)')}`);
|
|
346
348
|
let edgesDropped = 0;
|
|
347
349
|
// Held open past the try so the memory section below can read it, and can
|
|
348
350
|
// still report when it is null -- the spool drop count is exactly the number
|
|
@@ -359,20 +361,20 @@ function doctor() {
|
|
|
359
361
|
const concepts = db.prepare('SELECT count(*) n FROM concepts').get().n;
|
|
360
362
|
const attempts = db.prepare('SELECT count(*) n FROM attempts').get().n;
|
|
361
363
|
const known = db.prepare('SELECT count(*) n FROM mastery WHERE score >= 0.7 AND reps >= 2').get().n;
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
364
|
+
add('ok', 'concepts', String(concepts));
|
|
365
|
+
add('ok', 'attempts', String(attempts));
|
|
366
|
+
add('ok', 'mastered', String(known));
|
|
367
|
+
add('ok', 'journal', String(db.pragma('journal_mode', { simple: true })));
|
|
366
368
|
// The project's band, and the runway left in it. Read here because `doctor`
|
|
367
369
|
// is where someone looks when the questions feel wrong for them.
|
|
368
370
|
const standing = levelStanding(db, resolved.config, resolved.repoRoot);
|
|
369
|
-
|
|
370
|
-
? '
|
|
371
|
-
: `
|
|
371
|
+
add('ok', 'level', `${standing.level} ${dim(standing.pinned
|
|
372
|
+
? '(pinned by config — no progression)'
|
|
373
|
+
: `(${standing.counts.passed}/${standing.needed.answers} passing answers in ${standing.repo})`)}`);
|
|
372
374
|
}
|
|
373
375
|
catch (err) {
|
|
374
376
|
ok = false;
|
|
375
|
-
|
|
377
|
+
add('fail', 'database', `FAILED — ${err instanceof Error ? err.message : String(err)}`);
|
|
376
378
|
}
|
|
377
379
|
// The memory half. `eklavya memory status` says more, but it is scoped to one
|
|
378
380
|
// project and nobody runs it when the question is "is anything broken" — so
|
|
@@ -388,20 +390,20 @@ function doctor() {
|
|
|
388
390
|
const rawQuiz = resolved.raw.quiz;
|
|
389
391
|
if (rawQuiz?.enabled === false && rawQuiz?.enforced === true) {
|
|
390
392
|
ok = false;
|
|
391
|
-
|
|
393
|
+
add('fail', 'conflict', 'FAILED — quiz.enabled is false and quiz.enforced is true. A gate needs ' +
|
|
392
394
|
'passed questions and nothing will ask any, so the enforcement is being ignored ' +
|
|
393
395
|
'rather than blocking every commit. Set quiz.enabled true to gate commits, or drop ' +
|
|
394
396
|
'quiz.enforced to accept the silence.');
|
|
395
397
|
}
|
|
396
398
|
const memory = resolved.config.memory;
|
|
397
|
-
|
|
399
|
+
add(memory.enabled ? 'ok' : 'skip', 'memory', memory.enabled ? `on · capture ${memory.capture}` : 'off (memory.enabled is false)');
|
|
398
400
|
if (db) {
|
|
399
401
|
const entries = safely(() => countEntries(db), 0);
|
|
400
402
|
const evidence = safely(() => db.prepare('SELECT count(*) n FROM evidence_events').get().n, 0);
|
|
401
403
|
const waiting = safely(() => pendingEventCount(db), 0);
|
|
402
|
-
|
|
404
|
+
add('ok', 'memory', `${entries} entries, ${evidence} evidence events ${dim(`(${waiting} not yet summarised)`)}`);
|
|
403
405
|
const queue = safely(() => queueDepth(db), { pending: 0, paused: 0, failed: 0, oldest: null });
|
|
404
|
-
|
|
406
|
+
add('ok', 'memory', `queue ${queue.pending} pending · ${queue.paused} paused · ${queue.failed} failed`);
|
|
405
407
|
// The class, never the message. `last_error` is the provider's own prose
|
|
406
408
|
// and has carried a URL with a token in it; the class is what tells someone
|
|
407
409
|
// whether to fix a key or a quota, and it is a fixed vocabulary.
|
|
@@ -412,20 +414,20 @@ function doctor() {
|
|
|
412
414
|
.join(', '), '') || 'unclassified';
|
|
413
415
|
if (queue.paused > 0) {
|
|
414
416
|
memoryOk = false;
|
|
415
|
-
|
|
416
|
-
|
|
417
|
+
add('fail', 'memory', `FAILED — ${queue.paused} job(s) paused (${classes('paused')}); nothing is being summarised`);
|
|
418
|
+
add('fail', 'memory', dim('fix the credentials or quota behind providers.observer, then: eklavya memory process'));
|
|
417
419
|
}
|
|
418
420
|
if (queue.failed > 0) {
|
|
419
421
|
// Not a failure: a permanently failed job is a batch that will never
|
|
420
422
|
// summarise, and no command repairs it. Saying so beats a clean report.
|
|
421
|
-
|
|
423
|
+
add('warn', 'memory', `${queue.failed} job(s) failed permanently (${classes('failed')})`);
|
|
422
424
|
}
|
|
423
425
|
// The capture heartbeat: memory that is "on" with nothing arriving is the
|
|
424
426
|
// failure the entry count alone cannot show.
|
|
425
427
|
const newest = safely(() => db.prepare('SELECT MAX(occurred_at) AS at FROM evidence_events').get().at, null);
|
|
426
|
-
|
|
428
|
+
add('ok', 'memory', `last evidence ${newest ?? '— none captured yet'}`);
|
|
427
429
|
const sync = safely(() => syncStatus(db, resolved.config), null);
|
|
428
|
-
|
|
430
|
+
add('ok', 'memory', `sync ${sync?.enabled ? `on -> ${sync.target ?? '(no target set — set sync.target)'}` : 'off'}`);
|
|
429
431
|
}
|
|
430
432
|
const dropped = safely(() => droppedCount(), 0);
|
|
431
433
|
if (dropped > 0) {
|
|
@@ -433,10 +435,10 @@ function doctor() {
|
|
|
433
435
|
// spool file, so nothing replays them on its own. The transcripts are the
|
|
434
436
|
// only remaining copy.
|
|
435
437
|
memoryOk = false;
|
|
436
|
-
|
|
437
|
-
|
|
438
|
+
add('fail', 'memory', `FAILED — ${dropped} event(s) dropped before they reached the database`);
|
|
439
|
+
add('fail', 'memory', dim('recover them from this checkout’s transcripts with: eklavya memory replay'));
|
|
438
440
|
}
|
|
439
|
-
|
|
441
|
+
add('ok', 'memory', `provider ${resolved.config.providers.observer ? 'configured — batches leave this machine' : 'none — nothing leaves this machine'}`);
|
|
440
442
|
db?.close();
|
|
441
443
|
// Per key, not per file. A project config that sets only `quiz` must not make
|
|
442
444
|
// `focus` and `cadence` claim they came from it -- they came from the
|
|
@@ -444,18 +446,18 @@ function doctor() {
|
|
|
444
446
|
// as the dial this release renamed. `projectPath` is a path Eklavya *would*
|
|
445
447
|
// write to, so the file is read rather than assumed to exist.
|
|
446
448
|
const projectKeys = new Set(resolved.projectPath ? Object.keys(readConfigFile(resolved.projectPath)) : []);
|
|
447
|
-
const from = (key) => (projectKeys.has(key) ? ' (set for this project)' : '');
|
|
449
|
+
const from = (key) => (projectKeys.has(key) ? dim(' (set for this project)') : '');
|
|
448
450
|
const q = resolved.config.quiz;
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
? `
|
|
453
|
-
: '
|
|
451
|
+
add(q.enabled ? 'ok' : 'skip', 'quiz', `${q.enabled ? 'on' : 'off — memory is unaffected'}${q.enforced ? ' · enforced (commits gated)' : ''}${from('quiz')}`);
|
|
452
|
+
add('ok', 'focus', `${resolved.config.focus}${resolved.config.focus === 'learn' ? ` (${resolved.config.focus_topic ?? 'no topic set'})` : ''}${from('focus')}`);
|
|
453
|
+
add('ok', 'cadence', `${resolved.config.cadence} ${dim(resolved.config.cadence === 'interleaved'
|
|
454
|
+
? `(one question mid-task, min ${resolved.config.min_minutes_between_checkpoints}m apart)`
|
|
455
|
+
: '(all questions at the end of the task)')}${from('cadence')}`);
|
|
454
456
|
if (resolved.overrides.length > 0) {
|
|
455
|
-
|
|
457
|
+
add('warn', 'overrides', `for this project: ${resolved.overrides.join(', ')}`);
|
|
456
458
|
}
|
|
457
459
|
if (resolved.projectPath)
|
|
458
|
-
|
|
460
|
+
add('ok', 'project', resolved.projectPath);
|
|
459
461
|
// Packs, and the one place a broken one is visible. `loadPacks` never throws
|
|
460
462
|
// -- a malformed file in ~/.eklavya/packs/ makes one pack unavailable, not
|
|
461
463
|
// Eklavya -- so without this line the failure is a domain that quietly never
|
|
@@ -463,41 +465,45 @@ function doctor() {
|
|
|
463
465
|
const packs = loadPacks();
|
|
464
466
|
if (packs.length > 0) {
|
|
465
467
|
const good = packs.filter((p) => p.pack);
|
|
466
|
-
|
|
468
|
+
add('ok', 'packs', `${good.length} loaded${good.length > 0
|
|
467
469
|
? ` — ${good.map((p) => `${p.pack.pack}${p.pack.version ? `@${p.pack.version}` : ''} (${p.scope})`).join(', ')}`
|
|
468
470
|
: ''}`);
|
|
469
471
|
if (edgesDropped > 0) {
|
|
470
472
|
// An edge endpoint naming nothing is almost always a typo, and it is
|
|
471
473
|
// silent everywhere else: the pack loads, the concept appears, and the
|
|
472
474
|
// prerequisite it was meant to hang off simply is not there.
|
|
473
|
-
|
|
475
|
+
add('warn', 'packs', `${edgesDropped} edge(s) dropped — an endpoint named a slug that does not exist`);
|
|
474
476
|
}
|
|
475
477
|
// The one remaining way an Eklavya file ends up in a checkout, and it only
|
|
476
478
|
// ever got there before the move. Named rather than fixed: a committed pack
|
|
477
479
|
// is authored content somebody reviewed, so relocating it is their call.
|
|
478
480
|
const inRepo = good.filter((p) => p.scope === 'repo');
|
|
479
481
|
if (inRepo.length > 0) {
|
|
480
|
-
|
|
482
|
+
add('warn', 'packs', `${inRepo.length} still inside the checkout (${inRepo
|
|
481
483
|
.map((p) => p.pack.pack)
|
|
482
484
|
.join(', ')}) — they load, but Eklavya no longer writes there;`);
|
|
483
|
-
|
|
485
|
+
add('warn', 'packs', dim('move them to ~/.eklavya/projects/<checkout>/packs/ to keep the repo clean'));
|
|
484
486
|
}
|
|
485
487
|
for (const bad of packs.filter((p) => !p.pack)) {
|
|
486
488
|
// Deliberately does NOT set `ok`. A bad pack costs that pack and nothing
|
|
487
489
|
// else, and the blanket remedy below is `eklavya install`, which never
|
|
488
490
|
// touches ~/.eklavya/packs/ and could not repair this if it wanted to.
|
|
489
|
-
|
|
490
|
-
|
|
491
|
+
add('fail', 'packs', `FAILED — ${bad.file}: ${bad.error}`);
|
|
492
|
+
add('fail', 'packs', dim('fix or delete that file; everything else is unaffected'));
|
|
491
493
|
}
|
|
492
494
|
}
|
|
493
495
|
// One fix for all of them: `install` is idempotent, so re-running it is the
|
|
494
496
|
// repair. Naming it here is the whole point of the checks above — a report
|
|
495
497
|
// nobody can act on is worse than no report.
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
498
|
+
heading('eklavya doctor');
|
|
499
|
+
// A label is said once, and its glyph again only when the state changes: a run
|
|
500
|
+
// of rows under one label reads as a block, and a failure inside it still shows.
|
|
501
|
+
rows.forEach(([mark, label, detail], i) => {
|
|
502
|
+
const prev = rows[i - 1];
|
|
503
|
+
const same = prev?.[1] === label;
|
|
504
|
+
check(same && prev[0] === mark ? null : mark, same ? '' : label, detail);
|
|
505
|
+
});
|
|
506
|
+
verdict(!ok ? 'Something is broken. Run: eklavya install' : !memoryOk ? 'Memory needs attention — see above' : null, 'ALL CLEAR · Eklavya is wired up');
|
|
501
507
|
if (!ok || !memoryOk)
|
|
502
508
|
process.exit(1);
|
|
503
509
|
}
|