eklavya 1.7.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/README.md +69 -0
- package/dist/ask.js +58 -0
- package/dist/ask.js.map +1 -0
- package/dist/assets/dashboard.html +407 -0
- package/dist/assets/tokens.css +135 -0
- package/dist/assets/tutor-skill.md +345 -0
- package/dist/cli.js +223 -0
- package/dist/cli.js.map +1 -0
- package/dist/concurrency.js +41 -0
- package/dist/concurrency.js.map +1 -0
- package/dist/config.js +158 -0
- package/dist/config.js.map +1 -0
- package/dist/dashboard.js +214 -0
- package/dist/dashboard.js.map +1 -0
- package/dist/db.js +26 -0
- package/dist/db.js.map +1 -0
- package/dist/hooks/checkpoint-quiz.js +152 -0
- package/dist/hooks/checkpoint-quiz.js.map +1 -0
- package/dist/hooks/lib.js +177 -0
- package/dist/hooks/lib.js.map +1 -0
- package/dist/hooks/pre-tool-gate.js +53 -0
- package/dist/hooks/pre-tool-gate.js.map +1 -0
- package/dist/hooks/session-start.js +128 -0
- package/dist/hooks/session-start.js.map +1 -0
- package/dist/hooks/stop-quiz-check.js +147 -0
- package/dist/hooks/stop-quiz-check.js.map +1 -0
- package/dist/install.js +313 -0
- package/dist/install.js.map +1 -0
- package/dist/mcq.js +34 -0
- package/dist/mcq.js.map +1 -0
- package/dist/migrate.js +45 -0
- package/dist/migrate.js.map +1 -0
- package/dist/migrations/001_init.sql +71 -0
- package/dist/migrations/002_stop_markers.sql +14 -0
- package/dist/migrations/003_gate_repo.sql +5 -0
- package/dist/migrations/004_attempt_outcome.sql +15 -0
- package/dist/migrations/005_session_concept_origin.sql +24 -0
- package/dist/migrations/006_attempt_format.sql +31 -0
- package/dist/migrations/007_checkpoints.sql +23 -0
- package/dist/migrations/008_difficulty_levels.sql +57 -0
- package/dist/paths.js +25 -0
- package/dist/paths.js.map +1 -0
- package/dist/plugin/.claude-plugin/marketplace.json +14 -0
- package/dist/plugin/.claude-plugin/plugin.json +17 -0
- package/dist/plugin/.mcp.json +11 -0
- package/dist/plugin/agents/tutor.md +63 -0
- package/dist/plugin/cli/eklavya-gate +67 -0
- package/dist/plugin/hooks/hooks.json +57 -0
- package/dist/plugin/hooks/run.mjs +169 -0
- package/dist/plugin/scripts/install-git-hook.sh +81 -0
- package/dist/plugin/skills/gate/SKILL.md +17 -0
- package/dist/plugin/skills/learn/SKILL.md +28 -0
- package/dist/plugin/skills/level/SKILL.md +53 -0
- package/dist/plugin/skills/mode/SKILL.md +56 -0
- package/dist/plugin/skills/progress/SKILL.md +72 -0
- package/dist/plugin/skills/quiz/SKILL.md +31 -0
- package/dist/plugin/skills/setup/SKILL.md +49 -0
- package/dist/plugin/skills/tutor/SKILL.md +345 -0
- package/dist/seed/git.json +49 -0
- package/dist/seed/node-backend.json +43 -0
- package/dist/seed/react.json +46 -0
- package/dist/seed/web-auth.json +78 -0
- package/dist/seed.js +112 -0
- package/dist/seed.js.map +1 -0
- package/dist/server.js +52 -0
- package/dist/server.js.map +1 -0
- package/dist/session.js +24 -0
- package/dist/session.js.map +1 -0
- package/dist/slug.js +75 -0
- package/dist/slug.js.map +1 -0
- package/dist/srs.js +206 -0
- package/dist/srs.js.map +1 -0
- package/dist/store.js +450 -0
- package/dist/store.js.map +1 -0
- package/dist/tools/config_tools.js +119 -0
- package/dist/tools/config_tools.js.map +1 -0
- package/dist/tools/get_concept_graph.js +104 -0
- package/dist/tools/get_concept_graph.js.map +1 -0
- package/dist/tools/get_gate_status.js +20 -0
- package/dist/tools/get_gate_status.js.map +1 -0
- package/dist/tools/get_learner_profile.js +142 -0
- package/dist/tools/get_learner_profile.js.map +1 -0
- package/dist/tools/get_session_quiz_plan.js +393 -0
- package/dist/tools/get_session_quiz_plan.js.map +1 -0
- package/dist/tools/index.js +45 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/log_session_concepts.js +103 -0
- package/dist/tools/log_session_concepts.js.map +1 -0
- package/dist/tools/record_attempt.js +158 -0
- package/dist/tools/record_attempt.js.map +1 -0
- package/dist/tools/types.js +4 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/tools/upsert_concepts.js +89 -0
- package/dist/tools/upsert_concepts.js.map +1 -0
- package/package.json +66 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionStart: stamp the session id and print a one-line learner profile.
|
|
3
|
+
*
|
|
4
|
+
* Hard rule (PRD §9.1): this must never break a session. Every failure path
|
|
5
|
+
* exits 0 with no output — `run()` enforces it.
|
|
6
|
+
*/
|
|
7
|
+
import { levelStanding } from '../store.js';
|
|
8
|
+
import { run, openExisting, config, cwdOf, sessionId } from './lib.js';
|
|
9
|
+
/**
|
|
10
|
+
* The whole tutoring loop starts at log_session_concepts: it is the only writer
|
|
11
|
+
* of session_concepts, and the Stop hook quizzes from nothing else. An unlogged
|
|
12
|
+
* session is a silent no-op no matter how much was taught in it, and the Stop
|
|
13
|
+
* hook exits 0 without a word, so the failure looks like "Eklavya is broken".
|
|
14
|
+
*
|
|
15
|
+
* The tutor skill already says to log, but it is model-invoked — it competes
|
|
16
|
+
* with every other skill on the machine and may never load. So the instruction
|
|
17
|
+
* is stated here, where every session sees it before any skill is chosen.
|
|
18
|
+
*/
|
|
19
|
+
const DIRECTIVE = `[Eklavya] Standing instruction for this session, on every task — not only when asked to teach:
|
|
20
|
+
- Once you know what a task involves, call log_session_concepts with the 3-8 concepts it
|
|
21
|
+
genuinely exercises. Omit session_id. Log while implementing, not at the end.
|
|
22
|
+
- Log what the code is *about* (jwt-verification, react-effect-cleanup, sql-index-selectivity),
|
|
23
|
+
not what you did. Give each one a context line naming the real code you wrote.
|
|
24
|
+
- Guessed slugs are fine: unknown ones are fuzzy-matched or created.
|
|
25
|
+
- Skip this and there is nothing to quiz on, so nothing is ever learned. Load the tutor skill
|
|
26
|
+
for how to ask and grade.
|
|
27
|
+
- Logging may come straight back with an [Eklavya checkpoint]: ONE multiple-choice question to
|
|
28
|
+
ask right then, before the next line of code, then back to the task in the same turn. That
|
|
29
|
+
interruption is the product -- learning while the work happens, not a pile of questions after
|
|
30
|
+
it. One question, no summary, no re-plan, no second question.`;
|
|
31
|
+
/**
|
|
32
|
+
* "seen" is a mastery row, which only exists once a concept has been attempted.
|
|
33
|
+
* Gated on that rather than on anything being mastered yet, or a learner three
|
|
34
|
+
* attempts in is told they have no history.
|
|
35
|
+
*/
|
|
36
|
+
function domainSummary(db) {
|
|
37
|
+
const rows = db
|
|
38
|
+
.prepare(`SELECT c.domain AS domain,
|
|
39
|
+
sum(CASE WHEN m.score >= 0.7 AND m.reps >= 2 THEN 1 ELSE 0 END) AS known,
|
|
40
|
+
count(*) AS total,
|
|
41
|
+
sum(CASE WHEN m.concept_id IS NOT NULL THEN 1 ELSE 0 END) AS seen
|
|
42
|
+
FROM concepts c LEFT JOIN mastery m ON m.concept_id = c.id
|
|
43
|
+
GROUP BY c.domain
|
|
44
|
+
HAVING seen > 0
|
|
45
|
+
ORDER BY known DESC, seen DESC
|
|
46
|
+
LIMIT 3`)
|
|
47
|
+
.all();
|
|
48
|
+
return rows.map((r) => `${r.domain} ${r.known}/${r.total} known`).join(', ');
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Not "reps > 0": a failing grade resets reps to 0, so that filter would hide
|
|
52
|
+
* precisely the concepts the learner is struggling with. A mastery row at all
|
|
53
|
+
* means it has been attempted.
|
|
54
|
+
*/
|
|
55
|
+
function weakest(db) {
|
|
56
|
+
const rows = db
|
|
57
|
+
.prepare(`SELECT c.slug AS slug FROM concepts c
|
|
58
|
+
JOIN mastery m ON m.concept_id = c.id
|
|
59
|
+
WHERE m.score < 0.5
|
|
60
|
+
ORDER BY m.score ASC
|
|
61
|
+
LIMIT 3`)
|
|
62
|
+
.all();
|
|
63
|
+
return rows.map((r) => r.slug).join(', ');
|
|
64
|
+
}
|
|
65
|
+
function dueCount(db) {
|
|
66
|
+
const row = db
|
|
67
|
+
.prepare(`SELECT count(*) AS n FROM mastery
|
|
68
|
+
WHERE next_review IS NOT NULL
|
|
69
|
+
AND next_review <= strftime('%Y-%m-%dT%H:%M:%fZ','now')`)
|
|
70
|
+
.get();
|
|
71
|
+
return row?.n ?? 0;
|
|
72
|
+
}
|
|
73
|
+
await run(async (input) => {
|
|
74
|
+
const db = openExisting();
|
|
75
|
+
if (!db)
|
|
76
|
+
return 0;
|
|
77
|
+
const cwd = cwdOf(input);
|
|
78
|
+
const sid = sessionId(input, db);
|
|
79
|
+
// Stamp the session so MCP tools resolve the same id the hooks will use later
|
|
80
|
+
// (decision G1). This happens even when the banner is suppressed.
|
|
81
|
+
if (sid) {
|
|
82
|
+
db.prepare(`INSERT INTO meta (key, value) VALUES ('current_session', ?)
|
|
83
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`).run(sid);
|
|
84
|
+
}
|
|
85
|
+
const resolved = config(cwd);
|
|
86
|
+
const { mode, focus, focus_topic, cadence, difficulty, quiet } = resolved.config;
|
|
87
|
+
if (mode === 'off')
|
|
88
|
+
return 0;
|
|
89
|
+
const focusLabel = focus === 'learn' && focus_topic ? `learn (${focus_topic})` : focus;
|
|
90
|
+
// The level, and how far into it they are. This is the one number that makes
|
|
91
|
+
// the runway visible: without it, week one and week ten look identical from
|
|
92
|
+
// the outside, and a learner on `easy` reads tier-2 questions as Eklavya being
|
|
93
|
+
// shallow rather than as a band they are working through.
|
|
94
|
+
//
|
|
95
|
+
// `levelStanding` is the server's own function, so the banner cannot drift
|
|
96
|
+
// from what the quiz planner actually does — the shell version reimplemented
|
|
97
|
+
// this query and had to keep it in step by hand.
|
|
98
|
+
let levelLabel = `${difficulty} (pinned)`;
|
|
99
|
+
if (difficulty === 'auto') {
|
|
100
|
+
const standing = levelStanding(db, resolved.config, resolved.repoRoot);
|
|
101
|
+
levelLabel = `${standing.level} (${standing.counts.passed}/${standing.needed.answers} on this project)`;
|
|
102
|
+
}
|
|
103
|
+
if (quiet)
|
|
104
|
+
return 0;
|
|
105
|
+
const domains = domainSummary(db);
|
|
106
|
+
const out = [];
|
|
107
|
+
if (!domains) {
|
|
108
|
+
// Nothing learned yet — say the useful thing instead of an empty scoreboard.
|
|
109
|
+
out.push(`[Eklavya] No learning history yet. Mode: ${mode}. Focus: ${focusLabel}. Cadence: ${cadence}. Level: ${levelLabel}.`);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
let line = `[Eklavya] Learner profile: ${domains}.`;
|
|
113
|
+
const weak = weakest(db);
|
|
114
|
+
if (weak)
|
|
115
|
+
line += ` Weak: ${weak}.`;
|
|
116
|
+
const due = dueCount(db);
|
|
117
|
+
if (due > 0)
|
|
118
|
+
line += ` ${due} concept(s) due for review.`;
|
|
119
|
+
out.push(`${line} Mode: ${mode}. Focus: ${focusLabel}. Cadence: ${cadence}. Level: ${levelLabel}.`);
|
|
120
|
+
}
|
|
121
|
+
if (resolved.overrides.length > 0) {
|
|
122
|
+
out.push(`[Eklavya] This repo overrides your global setting for: ${resolved.overrides.join(' ')} (.eklavya.json wins).`);
|
|
123
|
+
}
|
|
124
|
+
out.push(DIRECTIVE);
|
|
125
|
+
process.stdout.write(`${out.join('\n')}\n`);
|
|
126
|
+
return 0;
|
|
127
|
+
});
|
|
128
|
+
//# sourceMappingURL=session-start.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-start.js","sourceRoot":"","sources":["../../src/hooks/session-start.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,GAAG,EAAE,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAW,MAAM,UAAU,CAAC;AAEhF;;;;;;;;;GASG;AACH,MAAM,SAAS,GAAG;;;;;;;;;;;kEAWgD,CAAC;AAEnE;;;;GAIG;AACH,SAAS,aAAa,CAAC,EAAM;IAC3B,MAAM,IAAI,GAAG,EAAE;SACZ,OAAO,CACN;;;;;;;;gBAQU,CACX;SACA,GAAG,EAA6D,CAAC;IAEpE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC/E,CAAC;AAED;;;;GAIG;AACH,SAAS,OAAO,CAAC,EAAM;IACrB,MAAM,IAAI,GAAG,EAAE;SACZ,OAAO,CACN;;;;gBAIU,CACX;SACA,GAAG,EAA6B,CAAC;IAEpC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC5C,CAAC;AAED,SAAS,QAAQ,CAAC,EAAM;IACtB,MAAM,GAAG,GAAG,EAAE;SACX,OAAO,CACN;;kEAE4D,CAC7D;SACA,GAAG,EAA+B,CAAC;IACtC,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;AACrB,CAAC;AAED,MAAM,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;IACxB,MAAM,EAAE,GAAG,YAAY,EAAE,CAAC;IAC1B,IAAI,CAAC,EAAE;QAAE,OAAO,CAAC,CAAC;IAElB,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC;IACzB,MAAM,GAAG,GAAG,SAAS,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAEjC,8EAA8E;IAC9E,kEAAkE;IAClE,IAAI,GAAG,EAAE,CAAC;QACR,EAAE,CAAC,OAAO,CACR;+DACyD,CAC1D,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACb,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;IAC7B,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,GAAG,QAAQ,CAAC,MAAM,CAAC;IACjF,IAAI,IAAI,KAAK,KAAK;QAAE,OAAO,CAAC,CAAC;IAE7B,MAAM,UAAU,GAAG,KAAK,KAAK,OAAO,IAAI,WAAW,CAAC,CAAC,CAAC,UAAU,WAAW,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC;IAEvF,6EAA6E;IAC7E,4EAA4E;IAC5E,+EAA+E;IAC/E,0DAA0D;IAC1D,EAAE;IACF,2EAA2E;IAC3E,6EAA6E;IAC7E,iDAAiD;IACjD,IAAI,UAAU,GAAG,GAAG,UAAU,WAAW,CAAC;IAC1C,IAAI,UAAU,KAAK,MAAM,EAAE,CAAC;QAC1B,MAAM,QAAQ,GAAG,aAAa,CAAC,EAAE,EAAE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACvE,UAAU,GAAG,GAAG,QAAQ,CAAC,KAAK,KAAK,QAAQ,CAAC,MAAM,CAAC,MAAM,IAAI,QAAQ,CAAC,MAAM,CAAC,OAAO,mBAAmB,CAAC;IAC1G,CAAC;IAED,IAAI,KAAK;QAAE,OAAO,CAAC,CAAC;IAEpB,MAAM,OAAO,GAAG,aAAa,CAAC,EAAE,CAAC,CAAC;IAClC,MAAM,GAAG,GAAa,EAAE,CAAC;IAEzB,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,6EAA6E;QAC7E,GAAG,CAAC,IAAI,CACN,4CAA4C,IAAI,YAAY,UAAU,cAAc,OAAO,YAAY,UAAU,GAAG,CACrH,CAAC;IACJ,CAAC;SAAM,CAAC;QACN,IAAI,IAAI,GAAG,8BAA8B,OAAO,GAAG,CAAC;QACpD,MAAM,IAAI,GAAG,OAAO,CAAC,EAAE,CAAC,CAAC;QACzB,IAAI,IAAI;YAAE,IAAI,IAAI,UAAU,IAAI,GAAG,CAAC;QACpC,MAAM,GAAG,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAC;QACzB,IAAI,GAAG,GAAG,CAAC;YAAE,IAAI,IAAI,IAAI,GAAG,6BAA6B,CAAC;QAC1D,GAAG,CAAC,IAAI,CACN,GAAG,IAAI,UAAU,IAAI,YAAY,UAAU,cAAc,OAAO,YAAY,UAAU,GAAG,CAC1F,CAAC;IACJ,CAAC;IAED,IAAI,QAAQ,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAClC,GAAG,CAAC,IAAI,CACN,0DAA0D,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,wBAAwB,CAC/G,CAAC;IACJ,CAAC;IAED,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACpB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5C,OAAO,CAAC,CAAC;AACX,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stop: turn a finished task into a quiz, at most once per batch of work.
|
|
3
|
+
*
|
|
4
|
+
* Blocking is exit 2 with the reason on stderr (deviation D1) — the current hooks
|
|
5
|
+
* reference documents no Stop-specific JSON envelope, but does document that
|
|
6
|
+
* exit 2 prevents stopping and uses stderr as the message.
|
|
7
|
+
*
|
|
8
|
+
* THE LOOP GUARD IS THE POINT OF THIS FILE.
|
|
9
|
+
* `stop_hook_active` is no longer a documented input (deviation D2), so nothing
|
|
10
|
+
* here may depend on the harness noticing we already blocked. The rule that keeps
|
|
11
|
+
* this finite:
|
|
12
|
+
*
|
|
13
|
+
* block only when the number of concepts logged for this session has GROWN
|
|
14
|
+
* since the last block.
|
|
15
|
+
*
|
|
16
|
+
* Blocking stamps the current count. A Stop that follows with the same count does
|
|
17
|
+
* not block, whatever happened in between — quiz answered, quiz skipped, model
|
|
18
|
+
* ignored us entirely. Only genuinely new logged work re-arms it, and
|
|
19
|
+
* `block_count` caps even that.
|
|
20
|
+
*/
|
|
21
|
+
import { run, openExisting, config, cwdOf, sessionId, minutesSince, framingFor } from './lib.js';
|
|
22
|
+
await run(async (input) => {
|
|
23
|
+
const db = openExisting();
|
|
24
|
+
if (!db)
|
|
25
|
+
return 0;
|
|
26
|
+
// Belt and braces: honor it if the harness still sends it, depend on it never.
|
|
27
|
+
if (input.stop_hook_active === true)
|
|
28
|
+
return 0;
|
|
29
|
+
const cwd = cwdOf(input);
|
|
30
|
+
const sid = sessionId(input, db);
|
|
31
|
+
if (!sid)
|
|
32
|
+
return 0;
|
|
33
|
+
const { mode, focus, focus_topic, max_questions_per_task, max_stop_blocks_per_session, min_minutes_between_quizzes, } = config(cwd).config;
|
|
34
|
+
if (mode === 'off')
|
|
35
|
+
return 0;
|
|
36
|
+
const stats = db
|
|
37
|
+
.prepare(`SELECT
|
|
38
|
+
(SELECT count(*) FROM session_concepts sc
|
|
39
|
+
JOIN concepts c ON c.id = sc.concept_id
|
|
40
|
+
LEFT JOIN mastery m ON m.concept_id = c.id
|
|
41
|
+
WHERE sc.session_id = @sid
|
|
42
|
+
AND COALESCE(sc.origin,'work') = 'work'
|
|
43
|
+
AND NOT (COALESCE(m.score,0) >= 0.7 AND COALESCE(m.reps,0) >= 2)
|
|
44
|
+
AND sc.concept_id NOT IN
|
|
45
|
+
(SELECT concept_id FROM attempts WHERE session_id = @sid)) AS unmastered,
|
|
46
|
+
-- 'work' only, and the loop guard depends on it: record_attempt inserts a
|
|
47
|
+
-- session_concepts row for anything it is quizzed on, so counting review
|
|
48
|
+
-- rows here would let answering a question re-arm the block that asked it.
|
|
49
|
+
(SELECT count(*) FROM session_concepts
|
|
50
|
+
WHERE session_id = @sid AND COALESCE(origin,'work') = 'work') AS logged,
|
|
51
|
+
COALESCE((SELECT last_logged_count FROM stop_markers WHERE session_id = @sid), -1) AS last_logged,
|
|
52
|
+
COALESCE((SELECT block_count FROM stop_markers WHERE session_id = @sid), 0) AS blocks,
|
|
53
|
+
(SELECT last_blocked_at FROM stop_markers WHERE session_id = @sid) AS last_blocked,
|
|
54
|
+
(SELECT ts FROM attempts WHERE session_id = @sid ORDER BY id DESC LIMIT 1) AS last_answer,
|
|
55
|
+
-- Budget already spent, wherever it was spent. Since 1.4 the mid-work
|
|
56
|
+
-- checkpoint hook asks from the same allowance, so this is what turns the
|
|
57
|
+
-- Stop quiz from a fixed batch of four into a sweep of whatever is left.
|
|
58
|
+
(SELECT count(*) FROM attempts WHERE session_id = @sid) AS spent`)
|
|
59
|
+
.get({ sid });
|
|
60
|
+
if (!stats)
|
|
61
|
+
return 0;
|
|
62
|
+
// Nothing worth asking about. Concepts already answered in this session are
|
|
63
|
+
// excluded above: they have had their turn, and re-offering them is exactly the
|
|
64
|
+
// "asked me the same thing twice" failure this tool exists to avoid.
|
|
65
|
+
if (stats.unmastered <= 0)
|
|
66
|
+
return 0;
|
|
67
|
+
// --- the loop guard --------------------------------------------------------
|
|
68
|
+
if (!(stats.logged > stats.last_logged))
|
|
69
|
+
return 0;
|
|
70
|
+
if (stats.blocks >= max_stop_blocks_per_session)
|
|
71
|
+
return 0;
|
|
72
|
+
// Ambient mode respects the quiz cadence. Enforced mode must not, or a cooldown
|
|
73
|
+
// could make a commit gate unpassable (decision G5).
|
|
74
|
+
//
|
|
75
|
+
// Both clocks matter, and for the same reason. get_session_quiz_plan applies its
|
|
76
|
+
// cooldown from the last *answer*; this hook stamps the last *block*. Checking
|
|
77
|
+
// only the block would let us block a turn that the quiz plan then refuses as
|
|
78
|
+
// too soon, which reads to the model as being told to teach and given nothing to
|
|
79
|
+
// teach. Whichever happened more recently wins.
|
|
80
|
+
if (mode === 'ambient') {
|
|
81
|
+
if (minutesSince(stats.last_blocked) < min_minutes_between_quizzes)
|
|
82
|
+
return 0;
|
|
83
|
+
if (minutesSince(stats.last_answer) < min_minutes_between_quizzes)
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// --- what is left of the session budget ------------------------------------
|
|
88
|
+
// `max_questions_per_task` is a SESSION allowance, not an end-of-session batch
|
|
89
|
+
// size. Every question answered while the agent was working is one this hook
|
|
90
|
+
// must not ask again -- that is the whole trade the interleaved cadence makes,
|
|
91
|
+
// and without this subtraction it would be a lie: four questions mid-task and
|
|
92
|
+
// then four more at the end is worse than what it replaced.
|
|
93
|
+
//
|
|
94
|
+
// Answer the whole budget during the work and the Stop hook says nothing at all.
|
|
95
|
+
const remaining = max_questions_per_task - stats.spent;
|
|
96
|
+
if (remaining <= 0)
|
|
97
|
+
return 0;
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
const rows = db
|
|
100
|
+
.prepare(`SELECT c.slug || COALESCE(' (' || sc.context || ')', '') AS line
|
|
101
|
+
FROM session_concepts sc
|
|
102
|
+
JOIN concepts c ON c.id = sc.concept_id
|
|
103
|
+
LEFT JOIN mastery m ON m.concept_id = c.id
|
|
104
|
+
WHERE sc.session_id = @sid
|
|
105
|
+
AND COALESCE(sc.origin,'work') = 'work'
|
|
106
|
+
AND NOT (COALESCE(m.score,0) >= 0.7 AND COALESCE(m.reps,0) >= 2)
|
|
107
|
+
AND sc.concept_id NOT IN
|
|
108
|
+
(SELECT concept_id FROM attempts WHERE session_id = @sid)
|
|
109
|
+
ORDER BY sc.ts ASC
|
|
110
|
+
LIMIT @remaining`)
|
|
111
|
+
.all({ sid, remaining });
|
|
112
|
+
const concepts = rows.map((r) => r.line).join('; ');
|
|
113
|
+
// Stamp the guard BEFORE blocking. If anything below fails, the worst case is a
|
|
114
|
+
// missed quiz — never a loop.
|
|
115
|
+
db.prepare(`INSERT INTO stop_markers (session_id, last_blocked_at, last_logged_count, block_count)
|
|
116
|
+
VALUES (@sid, strftime('%Y-%m-%dT%H:%M:%fZ','now'), @logged, 1)
|
|
117
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
118
|
+
last_blocked_at = excluded.last_blocked_at,
|
|
119
|
+
last_logged_count = excluded.last_logged_count,
|
|
120
|
+
block_count = stop_markers.block_count + 1`).run({ sid, logged: stats.logged });
|
|
121
|
+
const framing = framingFor(focus, focus_topic, 'stop');
|
|
122
|
+
const tone = mode === 'enforced'
|
|
123
|
+
? 'This session is in enforced mode: the commit gate needs this quiz.'
|
|
124
|
+
: 'If they say skip, record it as grade 0 and let them go — do not ask twice.';
|
|
125
|
+
process.stderr.write(`Eklavya: before finishing, quiz the developer on what this task just taught.
|
|
126
|
+
|
|
127
|
+
Concepts: ${concepts}
|
|
128
|
+
|
|
129
|
+
Use the eklavya MCP server and the tutor skill: call get_session_quiz_plan, ask
|
|
130
|
+
ONE question at a time at each concept's tier_to_ask, and grade each answer with
|
|
131
|
+
record_attempt. The plan returns a "framing" field. Follow it.
|
|
132
|
+
|
|
133
|
+
Ask each question as MULTIPLE CHOICE via the AskUserQuestion tool: four options,
|
|
134
|
+
one correct and three plausible, header "Eklavya" so it is clear who is asking.
|
|
135
|
+
The tool's own "Other" choice is the escape hatch for "I don't know". They are
|
|
136
|
+
mid-task -- a blank prompt gets skipped whether or not they knew the answer.
|
|
137
|
+
End each question's text with a blank line and then that plan item's ask_footer,
|
|
138
|
+
verbatim: the mode, focus, level, tier and how many questions are coming. It is
|
|
139
|
+
the only thing on screen saying why a question is pitched where it is.
|
|
140
|
+
Record with format "mcq", the labels in "options", and only the stem in
|
|
141
|
+
"question" -- never the footer.
|
|
142
|
+
${framing}
|
|
143
|
+
${tone}
|
|
144
|
+
`);
|
|
145
|
+
return 2;
|
|
146
|
+
});
|
|
147
|
+
//# sourceMappingURL=stop-quiz-check.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stop-quiz-check.js","sourceRoot":"","sources":["../../src/hooks/stop-quiz-check.ts"],"names":[],"mappings":"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"}
|
package/dist/install.js
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `eklavya install` — the whole setup, from a terminal, on macOS or Windows.
|
|
3
|
+
*
|
|
4
|
+
* Claude Code's own route is `/plugin marketplace add` + `/plugin install`, and
|
|
5
|
+
* that still works. This is the other door: it needs no Claude Code session, and
|
|
6
|
+
* it is the one that can install what Eklavya depends on, because a slash
|
|
7
|
+
* command cannot run npm.
|
|
8
|
+
*
|
|
9
|
+
* What it has to get right, in order:
|
|
10
|
+
*
|
|
11
|
+
* 1. Node 22+. Below that, better-sqlite3 has no prebuilt binary and npm falls
|
|
12
|
+
* through to node-gyp, which needs a C++ toolchain nobody has on Windows.
|
|
13
|
+
* This is a hard refusal with per-platform instructions, not a warning:
|
|
14
|
+
* continuing produces a half-install that fails later and mystifies.
|
|
15
|
+
* 2. The runtime. `npm install eklavya@<version> --prefix ~/.eklavya/runtime`
|
|
16
|
+
* puts the compiled server, the hooks and that one native dependency
|
|
17
|
+
* somewhere both install routes can find them.
|
|
18
|
+
* 3. The plugin payload, copied into the Claude Code marketplace directory.
|
|
19
|
+
* 4. Claude Code's three registry files, written directly. There is no public
|
|
20
|
+
* API for "install this plugin" from outside a session, so this reproduces
|
|
21
|
+
* what `/plugin install` does. See the comment on `register()`.
|
|
22
|
+
* 5. The database, created and seeded.
|
|
23
|
+
*
|
|
24
|
+
* Every step is idempotent: running it twice is how you upgrade.
|
|
25
|
+
*/
|
|
26
|
+
import fs from 'node:fs';
|
|
27
|
+
import os from 'node:os';
|
|
28
|
+
import path from 'node:path';
|
|
29
|
+
import { spawnSync } from 'node:child_process';
|
|
30
|
+
import { fileURLToPath } from 'node:url';
|
|
31
|
+
import { openDb } from './db.js';
|
|
32
|
+
import { dbPath, eklavyaHome } from './paths.js';
|
|
33
|
+
const MIN_NODE_MAJOR = 22;
|
|
34
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
35
|
+
/** The npm package root — `dist/` under a build, so one level up. */
|
|
36
|
+
function packageRoot() {
|
|
37
|
+
return path.dirname(moduleDir);
|
|
38
|
+
}
|
|
39
|
+
/** The plugin payload the build copies into the package (see copy-assets.mjs). */
|
|
40
|
+
function payloadDir() {
|
|
41
|
+
return path.join(moduleDir, 'plugin');
|
|
42
|
+
}
|
|
43
|
+
function packageVersion() {
|
|
44
|
+
const manifest = path.join(packageRoot(), 'package.json');
|
|
45
|
+
return JSON.parse(fs.readFileSync(manifest, 'utf8')).version;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Claude Code's config root. `CLAUDE_CONFIG_DIR` is the documented override and
|
|
49
|
+
* people with several profiles do set it — writing to `~/.claude` regardless
|
|
50
|
+
* would install into a directory their Claude Code never reads.
|
|
51
|
+
*/
|
|
52
|
+
export function claudeHome() {
|
|
53
|
+
return process.env.CLAUDE_CONFIG_DIR ?? path.join(os.homedir(), '.claude');
|
|
54
|
+
}
|
|
55
|
+
export function marketplaceDir() {
|
|
56
|
+
return path.join(claudeHome(), 'plugins', 'marketplaces', 'eklavya');
|
|
57
|
+
}
|
|
58
|
+
export function runtimeHome() {
|
|
59
|
+
return process.env.EKLAVYA_RUNTIME ?? path.join(eklavyaHome(), 'runtime');
|
|
60
|
+
}
|
|
61
|
+
const say = (line) => process.stdout.write(`${line}\n`);
|
|
62
|
+
// --- 1. prerequisites -------------------------------------------------------
|
|
63
|
+
/** How to get a modern Node, on the platform actually in front of them. */
|
|
64
|
+
function nodeAdvice() {
|
|
65
|
+
if (process.platform === 'win32') {
|
|
66
|
+
return [
|
|
67
|
+
' winget install OpenJS.NodeJS.LTS',
|
|
68
|
+
' …or download the installer from https://nodejs.org',
|
|
69
|
+
].join('\n');
|
|
70
|
+
}
|
|
71
|
+
if (process.platform === 'darwin') {
|
|
72
|
+
return [' brew install node', ' …or download the installer from https://nodejs.org'].join('\n');
|
|
73
|
+
}
|
|
74
|
+
return [
|
|
75
|
+
' Use your distribution package, nvm, or https://nodejs.org',
|
|
76
|
+
' e.g. nvm install 22 && nvm use 22',
|
|
77
|
+
].join('\n');
|
|
78
|
+
}
|
|
79
|
+
function checkNode() {
|
|
80
|
+
const major = Number(process.versions.node.split('.')[0]);
|
|
81
|
+
if (major >= MIN_NODE_MAJOR)
|
|
82
|
+
return;
|
|
83
|
+
process.stderr.write(`Eklavya needs Node ${MIN_NODE_MAJOR} or newer — this is Node ${process.versions.node}.\n\n` +
|
|
84
|
+
'Below Node 22, the SQLite driver has no prebuilt binary for your platform and npm\n' +
|
|
85
|
+
'tries to compile it, which needs a full C++ toolchain. Upgrading Node is the fix:\n\n' +
|
|
86
|
+
`${nodeAdvice()}\n\nThen run \`npx eklavya install\` again.\n`);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* git is optional, and saying so matters. Eklavya keys a project's difficulty
|
|
91
|
+
* level on the repository root, and the commit gate is a git hook — without git
|
|
92
|
+
* both degrade to the shared bucket rather than failing.
|
|
93
|
+
*/
|
|
94
|
+
function checkGit() {
|
|
95
|
+
const probe = spawnSync('git', ['--version'], { stdio: 'ignore', shell: process.platform === 'win32' });
|
|
96
|
+
return probe.status === 0;
|
|
97
|
+
}
|
|
98
|
+
// --- 2. the runtime ---------------------------------------------------------
|
|
99
|
+
function installRuntime(version) {
|
|
100
|
+
const home = runtimeHome();
|
|
101
|
+
fs.mkdirSync(home, { recursive: true });
|
|
102
|
+
// A stamp file so the plugin's background self-heal does not race a manual
|
|
103
|
+
// install. Removed in the finally, whatever happens.
|
|
104
|
+
const stamp = path.join(home, '.installing');
|
|
105
|
+
fs.writeFileSync(stamp, String(process.pid), 'utf8');
|
|
106
|
+
try {
|
|
107
|
+
const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
108
|
+
const result = spawnSync(npm, [
|
|
109
|
+
'install',
|
|
110
|
+
`eklavya@${version}`,
|
|
111
|
+
'--prefix',
|
|
112
|
+
home,
|
|
113
|
+
'--omit=dev',
|
|
114
|
+
'--no-audit',
|
|
115
|
+
'--no-fund',
|
|
116
|
+
'--loglevel=error',
|
|
117
|
+
], { stdio: ['ignore', 'inherit', 'inherit'], shell: process.platform === 'win32' });
|
|
118
|
+
if (result.status !== 0) {
|
|
119
|
+
process.stderr.write('\nInstalling the Eklavya runtime failed. The output above says why — the usual\n' +
|
|
120
|
+
'causes are no network, or a registry proxy that blocks the download.\n' +
|
|
121
|
+
`You can retry with: npm install eklavya@${version} --prefix ${home}\n`);
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
try {
|
|
127
|
+
fs.rmSync(stamp, { force: true });
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
/* nothing depends on the stamp being gone */
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Proves the native dependency actually loads, rather than trusting that npm
|
|
136
|
+
* exiting 0 means a working binary. A prebuilt binary for the wrong ABI installs
|
|
137
|
+
* cleanly and throws on first require — which would otherwise surface as a dead
|
|
138
|
+
* MCP server three commands later.
|
|
139
|
+
*/
|
|
140
|
+
function verifyRuntime() {
|
|
141
|
+
const entry = path.join(runtimeHome(), 'node_modules', 'eklavya', 'dist', 'server.js');
|
|
142
|
+
if (!fs.existsSync(entry)) {
|
|
143
|
+
process.stderr.write(`The runtime installed but ${entry} is missing.\n`);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
const probe = spawnSync(process.execPath, ['-e', 'require(process.argv[1]); console.log("ok")', path.join(runtimeHome(), 'node_modules', 'better-sqlite3')], { encoding: 'utf8' });
|
|
147
|
+
if (probe.status !== 0) {
|
|
148
|
+
process.stderr.write('\nThe SQLite driver installed but will not load:\n' +
|
|
149
|
+
`${(probe.stderr ?? '').trim()}\n\n` +
|
|
150
|
+
'This usually means the prebuilt binary does not match this Node version.\n' +
|
|
151
|
+
`Try removing ${runtimeHome()} and running \`npx eklavya install\` again.\n`);
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// --- 3. the plugin payload --------------------------------------------------
|
|
156
|
+
function copyPayload() {
|
|
157
|
+
const from = payloadDir();
|
|
158
|
+
if (!fs.existsSync(from)) {
|
|
159
|
+
process.stderr.write(`The plugin payload is missing from this package (expected ${from}).\n` +
|
|
160
|
+
'This is a packaging bug — please report it.\n');
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
const to = marketplaceDir();
|
|
164
|
+
// Replace rather than merge: a stale hook or skill left behind by an older
|
|
165
|
+
// version is worse than a slow copy, and this directory is ours entirely.
|
|
166
|
+
fs.rmSync(to, { recursive: true, force: true });
|
|
167
|
+
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
168
|
+
fs.cpSync(from, to, { recursive: true });
|
|
169
|
+
}
|
|
170
|
+
// --- 4. Claude Code's registries --------------------------------------------
|
|
171
|
+
function readJson(file) {
|
|
172
|
+
try {
|
|
173
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return {};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/** Write via temp file + rename: Claude Code may be reading this mid-write. */
|
|
180
|
+
function writeJson(file, data) {
|
|
181
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
182
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
183
|
+
fs.writeFileSync(tmp, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
|
|
184
|
+
fs.renameSync(tmp, file);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Registers the plugin the way `/plugin install` would.
|
|
188
|
+
*
|
|
189
|
+
* There is no supported CLI for this, so these three files are written directly.
|
|
190
|
+
* That is a real cost and worth naming: their shape is Claude Code's private
|
|
191
|
+
* business and can change — `installed_plugins.json` already carries a
|
|
192
|
+
* `"version": 2`. The mitigation is that every write is additive and keyed, so a
|
|
193
|
+
* shape change loses the registration rather than corrupting anyone's config,
|
|
194
|
+
* and `eklavya doctor` reports it.
|
|
195
|
+
*
|
|
196
|
+
* The marketplace is registered with its GitHub source even though the files
|
|
197
|
+
* came from npm, and deliberately: that is what lets Claude Code's own
|
|
198
|
+
* `/plugin update` take over afterwards. npm is the on-ramp, not the channel.
|
|
199
|
+
*
|
|
200
|
+
* ponytail: three private files, no public API. If Claude Code ever ships
|
|
201
|
+
* `claude plugin install --local`, delete this and shell out to it.
|
|
202
|
+
*/
|
|
203
|
+
function register(version) {
|
|
204
|
+
const pluginsDir = path.join(claudeHome(), 'plugins');
|
|
205
|
+
const marketplaces = path.join(pluginsDir, 'known_marketplaces.json');
|
|
206
|
+
const known = readJson(marketplaces);
|
|
207
|
+
known.eklavya = {
|
|
208
|
+
source: { source: 'github', repo: 'ProjectAJ14/eklavya' },
|
|
209
|
+
installLocation: marketplaceDir(),
|
|
210
|
+
lastUpdated: new Date().toISOString(),
|
|
211
|
+
autoUpdate: true,
|
|
212
|
+
};
|
|
213
|
+
writeJson(marketplaces, known);
|
|
214
|
+
const installedPath = path.join(pluginsDir, 'installed_plugins.json');
|
|
215
|
+
const installed = readJson(installedPath);
|
|
216
|
+
if (typeof installed.version !== 'number')
|
|
217
|
+
installed.version = 2;
|
|
218
|
+
const plugins = (installed.plugins ?? {});
|
|
219
|
+
plugins['eklavya@eklavya'] = [
|
|
220
|
+
{
|
|
221
|
+
scope: 'user',
|
|
222
|
+
installPath: marketplaceDir(),
|
|
223
|
+
version,
|
|
224
|
+
installedAt: new Date().toISOString(),
|
|
225
|
+
lastUpdated: new Date().toISOString(),
|
|
226
|
+
},
|
|
227
|
+
];
|
|
228
|
+
installed.plugins = plugins;
|
|
229
|
+
writeJson(installedPath, installed);
|
|
230
|
+
const settingsPath = path.join(claudeHome(), 'settings.json');
|
|
231
|
+
const settings = readJson(settingsPath);
|
|
232
|
+
const enabled = (settings.enabledPlugins ?? {});
|
|
233
|
+
enabled['eklavya@eklavya'] = true;
|
|
234
|
+
settings.enabledPlugins = enabled;
|
|
235
|
+
writeJson(settingsPath, settings);
|
|
236
|
+
}
|
|
237
|
+
function deregister() {
|
|
238
|
+
const pluginsDir = path.join(claudeHome(), 'plugins');
|
|
239
|
+
const marketplaces = path.join(pluginsDir, 'known_marketplaces.json');
|
|
240
|
+
const known = readJson(marketplaces);
|
|
241
|
+
if ('eklavya' in known) {
|
|
242
|
+
delete known.eklavya;
|
|
243
|
+
writeJson(marketplaces, known);
|
|
244
|
+
}
|
|
245
|
+
const installedPath = path.join(pluginsDir, 'installed_plugins.json');
|
|
246
|
+
const installed = readJson(installedPath);
|
|
247
|
+
const plugins = (installed.plugins ?? {});
|
|
248
|
+
if ('eklavya@eklavya' in plugins) {
|
|
249
|
+
delete plugins['eklavya@eklavya'];
|
|
250
|
+
installed.plugins = plugins;
|
|
251
|
+
writeJson(installedPath, installed);
|
|
252
|
+
}
|
|
253
|
+
const settingsPath = path.join(claudeHome(), 'settings.json');
|
|
254
|
+
const settings = readJson(settingsPath);
|
|
255
|
+
const enabled = (settings.enabledPlugins ?? {});
|
|
256
|
+
if ('eklavya@eklavya' in enabled) {
|
|
257
|
+
delete enabled['eklavya@eklavya'];
|
|
258
|
+
settings.enabledPlugins = enabled;
|
|
259
|
+
writeJson(settingsPath, settings);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// --- commands ---------------------------------------------------------------
|
|
263
|
+
export function install(args) {
|
|
264
|
+
const version = packageVersion();
|
|
265
|
+
say(`Installing Eklavya ${version}`);
|
|
266
|
+
checkNode();
|
|
267
|
+
say(` node ${process.versions.node}`);
|
|
268
|
+
if (!args.includes('--skip-runtime')) {
|
|
269
|
+
say(' runtime installing (first run downloads the SQLite driver)…');
|
|
270
|
+
installRuntime(version);
|
|
271
|
+
verifyRuntime();
|
|
272
|
+
say(` runtime ${runtimeHome()}`);
|
|
273
|
+
}
|
|
274
|
+
copyPayload();
|
|
275
|
+
say(` plugin ${marketplaceDir()}`);
|
|
276
|
+
register(version);
|
|
277
|
+
say(' registered eklavya@eklavya, enabled for Claude Code');
|
|
278
|
+
// Creating the DB here rather than on first server start means `eklavya
|
|
279
|
+
// doctor` and the dashboard work before Claude Code has ever been opened.
|
|
280
|
+
const db = openDb();
|
|
281
|
+
db.close();
|
|
282
|
+
say(` database ${dbPath()}`);
|
|
283
|
+
if (!checkGit()) {
|
|
284
|
+
say('');
|
|
285
|
+
say(' note: git was not found. Eklavya still works — the per-project difficulty');
|
|
286
|
+
say(' level falls back to a shared bucket, and the commit gate needs git.');
|
|
287
|
+
}
|
|
288
|
+
say('');
|
|
289
|
+
say('Done. Restart Claude Code (or start a session) and Eklavya loads with it.');
|
|
290
|
+
say('Next: run /eklavya:setup in Claude Code to choose a mode, or `eklavya doctor` here.');
|
|
291
|
+
}
|
|
292
|
+
export function uninstall(args) {
|
|
293
|
+
const purge = args.includes('--purge');
|
|
294
|
+
deregister();
|
|
295
|
+
say(' registered removed from Claude Code');
|
|
296
|
+
fs.rmSync(marketplaceDir(), { recursive: true, force: true });
|
|
297
|
+
say(' plugin removed');
|
|
298
|
+
fs.rmSync(runtimeHome(), { recursive: true, force: true });
|
|
299
|
+
say(' runtime removed');
|
|
300
|
+
if (purge) {
|
|
301
|
+
// Only ever on an explicit flag. This is everything the learner has done —
|
|
302
|
+
// months of spaced repetition — and an uninstall that silently deletes it is
|
|
303
|
+
// not an uninstall, it is data loss.
|
|
304
|
+
fs.rmSync(eklavyaHome(), { recursive: true, force: true });
|
|
305
|
+
say(` data removed (${eklavyaHome()})`);
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
say(` data kept (${dbPath()}) — pass --purge to delete your learning history`);
|
|
309
|
+
}
|
|
310
|
+
say('');
|
|
311
|
+
say('Eklavya is uninstalled. Restart Claude Code to unload it.');
|
|
312
|
+
}
|
|
313
|
+
//# sourceMappingURL=install.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install.js","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"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"}
|
package/dist/mcq.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the correct option goes in a multiple-choice question.
|
|
3
|
+
*
|
|
4
|
+
* This is decided server-side, for the same reason `tier_to_ask` and
|
|
5
|
+
* `format_to_use` are: left to the model, it is not decided at all. An LLM asked
|
|
6
|
+
* to "vary the position" reliably puts the right answer first — it writes the
|
|
7
|
+
* answer it is thinking of, then invents distractors after it — so learners
|
|
8
|
+
* discover within a handful of questions that A is always correct and stop
|
|
9
|
+
* reading the options. The quiz keeps working and stops measuring anything.
|
|
10
|
+
*
|
|
11
|
+
* Deterministic rather than random: the same question asked twice in a session
|
|
12
|
+
* must not move under the learner, tests need a fixed answer, and a plan is
|
|
13
|
+
* rebuilt on every call. Varying by attempt count is what makes a re-ask of the
|
|
14
|
+
* same concept land somewhere new.
|
|
15
|
+
*/
|
|
16
|
+
/** FNV-1a, 32-bit. Small, dependency-free, and well spread over short strings. */
|
|
17
|
+
function hash(seed) {
|
|
18
|
+
let h = 0x811c9dc5;
|
|
19
|
+
for (let i = 0; i < seed.length; i += 1) {
|
|
20
|
+
h ^= seed.charCodeAt(i);
|
|
21
|
+
h = Math.imul(h, 0x01000193) >>> 0;
|
|
22
|
+
}
|
|
23
|
+
return h >>> 0;
|
|
24
|
+
}
|
|
25
|
+
export const MCQ_OPTION_COUNT = 4;
|
|
26
|
+
/**
|
|
27
|
+
* 1-based position for the correct option. `askedCount` is how many questions
|
|
28
|
+
* this learner has already been asked about the concept, so a concept that comes
|
|
29
|
+
* back for review does not come back in the same shape.
|
|
30
|
+
*/
|
|
31
|
+
export function answerPosition(slug, askedCount = 0, optionCount = MCQ_OPTION_COUNT) {
|
|
32
|
+
return (hash(`${slug}:${askedCount}`) % optionCount) + 1;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=mcq.js.map
|
package/dist/mcq.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcq.js","sourceRoot":"","sources":["../src/mcq.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,kFAAkF;AAClF,SAAS,IAAI,CAAC,IAAY;IACxB,IAAI,CAAC,GAAG,UAAU,CAAC;IACnB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QACxC,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;QACxB,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC;IACrC,CAAC;IACD,OAAO,CAAC,KAAK,CAAC,CAAC;AACjB,CAAC;AAED,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC;AAElC;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,IAAY,EAAE,UAAU,GAAG,CAAC,EAAE,WAAW,GAAG,gBAAgB;IACzF,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,IAAI,UAAU,EAAE,CAAC,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;AAC3D,CAAC"}
|