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.
Files changed (95) hide show
  1. package/README.md +69 -0
  2. package/dist/ask.js +58 -0
  3. package/dist/ask.js.map +1 -0
  4. package/dist/assets/dashboard.html +407 -0
  5. package/dist/assets/tokens.css +135 -0
  6. package/dist/assets/tutor-skill.md +345 -0
  7. package/dist/cli.js +223 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/concurrency.js +41 -0
  10. package/dist/concurrency.js.map +1 -0
  11. package/dist/config.js +158 -0
  12. package/dist/config.js.map +1 -0
  13. package/dist/dashboard.js +214 -0
  14. package/dist/dashboard.js.map +1 -0
  15. package/dist/db.js +26 -0
  16. package/dist/db.js.map +1 -0
  17. package/dist/hooks/checkpoint-quiz.js +152 -0
  18. package/dist/hooks/checkpoint-quiz.js.map +1 -0
  19. package/dist/hooks/lib.js +177 -0
  20. package/dist/hooks/lib.js.map +1 -0
  21. package/dist/hooks/pre-tool-gate.js +53 -0
  22. package/dist/hooks/pre-tool-gate.js.map +1 -0
  23. package/dist/hooks/session-start.js +128 -0
  24. package/dist/hooks/session-start.js.map +1 -0
  25. package/dist/hooks/stop-quiz-check.js +147 -0
  26. package/dist/hooks/stop-quiz-check.js.map +1 -0
  27. package/dist/install.js +313 -0
  28. package/dist/install.js.map +1 -0
  29. package/dist/mcq.js +34 -0
  30. package/dist/mcq.js.map +1 -0
  31. package/dist/migrate.js +45 -0
  32. package/dist/migrate.js.map +1 -0
  33. package/dist/migrations/001_init.sql +71 -0
  34. package/dist/migrations/002_stop_markers.sql +14 -0
  35. package/dist/migrations/003_gate_repo.sql +5 -0
  36. package/dist/migrations/004_attempt_outcome.sql +15 -0
  37. package/dist/migrations/005_session_concept_origin.sql +24 -0
  38. package/dist/migrations/006_attempt_format.sql +31 -0
  39. package/dist/migrations/007_checkpoints.sql +23 -0
  40. package/dist/migrations/008_difficulty_levels.sql +57 -0
  41. package/dist/paths.js +25 -0
  42. package/dist/paths.js.map +1 -0
  43. package/dist/plugin/.claude-plugin/marketplace.json +14 -0
  44. package/dist/plugin/.claude-plugin/plugin.json +17 -0
  45. package/dist/plugin/.mcp.json +11 -0
  46. package/dist/plugin/agents/tutor.md +63 -0
  47. package/dist/plugin/cli/eklavya-gate +67 -0
  48. package/dist/plugin/hooks/hooks.json +57 -0
  49. package/dist/plugin/hooks/run.mjs +169 -0
  50. package/dist/plugin/scripts/install-git-hook.sh +81 -0
  51. package/dist/plugin/skills/gate/SKILL.md +17 -0
  52. package/dist/plugin/skills/learn/SKILL.md +28 -0
  53. package/dist/plugin/skills/level/SKILL.md +53 -0
  54. package/dist/plugin/skills/mode/SKILL.md +56 -0
  55. package/dist/plugin/skills/progress/SKILL.md +72 -0
  56. package/dist/plugin/skills/quiz/SKILL.md +31 -0
  57. package/dist/plugin/skills/setup/SKILL.md +49 -0
  58. package/dist/plugin/skills/tutor/SKILL.md +345 -0
  59. package/dist/seed/git.json +49 -0
  60. package/dist/seed/node-backend.json +43 -0
  61. package/dist/seed/react.json +46 -0
  62. package/dist/seed/web-auth.json +78 -0
  63. package/dist/seed.js +112 -0
  64. package/dist/seed.js.map +1 -0
  65. package/dist/server.js +52 -0
  66. package/dist/server.js.map +1 -0
  67. package/dist/session.js +24 -0
  68. package/dist/session.js.map +1 -0
  69. package/dist/slug.js +75 -0
  70. package/dist/slug.js.map +1 -0
  71. package/dist/srs.js +206 -0
  72. package/dist/srs.js.map +1 -0
  73. package/dist/store.js +450 -0
  74. package/dist/store.js.map +1 -0
  75. package/dist/tools/config_tools.js +119 -0
  76. package/dist/tools/config_tools.js.map +1 -0
  77. package/dist/tools/get_concept_graph.js +104 -0
  78. package/dist/tools/get_concept_graph.js.map +1 -0
  79. package/dist/tools/get_gate_status.js +20 -0
  80. package/dist/tools/get_gate_status.js.map +1 -0
  81. package/dist/tools/get_learner_profile.js +142 -0
  82. package/dist/tools/get_learner_profile.js.map +1 -0
  83. package/dist/tools/get_session_quiz_plan.js +393 -0
  84. package/dist/tools/get_session_quiz_plan.js.map +1 -0
  85. package/dist/tools/index.js +45 -0
  86. package/dist/tools/index.js.map +1 -0
  87. package/dist/tools/log_session_concepts.js +103 -0
  88. package/dist/tools/log_session_concepts.js.map +1 -0
  89. package/dist/tools/record_attempt.js +158 -0
  90. package/dist/tools/record_attempt.js.map +1 -0
  91. package/dist/tools/types.js +4 -0
  92. package/dist/tools/types.js.map +1 -0
  93. package/dist/tools/upsert_concepts.js +89 -0
  94. package/dist/tools/upsert_concepts.js.map +1 -0
  95. package/package.json +66 -0
@@ -0,0 +1,45 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { migrationsDir } from './paths.js';
4
+ const VERSION_KEY = 'schema_version';
5
+ function readVersion(db) {
6
+ const row = db.prepare('SELECT value FROM meta WHERE key = ?').get(VERSION_KEY);
7
+ return row ? Number(row.value) : 0;
8
+ }
9
+ function writeVersion(db, version) {
10
+ db.prepare(`INSERT INTO meta (key, value) VALUES (?, ?)
11
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value`).run(VERSION_KEY, String(version));
12
+ }
13
+ /**
14
+ * Applies numbered SQL migrations in order, recording progress in `meta`.
15
+ * Idempotent: re-running applies nothing. Returns the filenames applied.
16
+ */
17
+ export function runMigrations(db, dir = migrationsDir()) {
18
+ // Bootstrap `meta` itself so the version read below has somewhere to look.
19
+ db.exec('CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)');
20
+ const current = readVersion(db);
21
+ const files = fs
22
+ .readdirSync(dir)
23
+ .filter((f) => f.endsWith('.sql'))
24
+ .sort();
25
+ const applied = [];
26
+ for (const file of files) {
27
+ const n = Number(file.slice(0, 3));
28
+ if (!Number.isFinite(n) || n === 0) {
29
+ throw new Error(`Migration filename must start with a number: ${file}`);
30
+ }
31
+ if (n <= current)
32
+ continue;
33
+ const sql = fs.readFileSync(path.join(dir, file), 'utf8');
34
+ db.transaction(() => {
35
+ db.exec(sql);
36
+ writeVersion(db, n);
37
+ })();
38
+ applied.push(file);
39
+ }
40
+ return applied;
41
+ }
42
+ export function schemaVersion(db) {
43
+ return readVersion(db);
44
+ }
45
+ //# sourceMappingURL=migrate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrate.js","sourceRoot":"","sources":["../src/migrate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE3C,MAAM,WAAW,GAAG,gBAAgB,CAAC;AAErC,SAAS,WAAW,CAAC,EAAY;IAC/B,MAAM,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,sCAAsC,CAAC,CAAC,GAAG,CAAC,WAAW,CAEjE,CAAC;IACd,OAAO,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACrC,CAAC;AAED,SAAS,YAAY,CAAC,EAAY,EAAE,OAAe;IACjD,EAAE,CAAC,OAAO,CACR;2DACuD,CACxD,CAAC,GAAG,CAAC,WAAW,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;AACtC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,EAAY,EAAE,GAAG,GAAG,aAAa,EAAE;IAC/D,2EAA2E;IAC3E,EAAE,CAAC,IAAI,CAAC,6EAA6E,CAAC,CAAC;IAEvF,MAAM,OAAO,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC;IAChC,MAAM,KAAK,GAAG,EAAE;SACb,WAAW,CAAC,GAAG,CAAC;SAChB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;SACjC,IAAI,EAAE,CAAC;IAEV,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QACnC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YACnC,MAAM,IAAI,KAAK,CAAC,gDAAgD,IAAI,EAAE,CAAC,CAAC;QAC1E,CAAC;QACD,IAAI,CAAC,IAAI,OAAO;YAAE,SAAS;QAE3B,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC;QAC1D,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE;YAClB,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACb,YAAY,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;QACtB,CAAC,CAAC,EAAE,CAAC;QACL,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACrB,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,EAAY;IACxC,OAAO,WAAW,CAAC,EAAE,CAAC,CAAC;AACzB,CAAC"}
@@ -0,0 +1,71 @@
1
+ -- Eklavya schema v1 (PRD §7).
2
+ -- Every statement is IF NOT EXISTS so the runner is safe to re-apply.
3
+
4
+ CREATE TABLE IF NOT EXISTS meta (
5
+ key TEXT PRIMARY KEY,
6
+ value TEXT NOT NULL
7
+ );
8
+
9
+ CREATE TABLE IF NOT EXISTS concepts (
10
+ id INTEGER PRIMARY KEY,
11
+ slug TEXT NOT NULL UNIQUE, -- 'jwt-structure', 'httponly-cookies'
12
+ name TEXT NOT NULL,
13
+ domain TEXT NOT NULL, -- 'web-auth', 'react', 'node-backend', ...
14
+ description TEXT,
15
+ tier INTEGER NOT NULL DEFAULT 1, -- 1 fundamentals … 5 expert
16
+ source TEXT NOT NULL DEFAULT 'seed' -- 'seed' | 'llm'
17
+ );
18
+
19
+ CREATE TABLE IF NOT EXISTS edges (
20
+ from_concept INTEGER NOT NULL REFERENCES concepts(id),
21
+ to_concept INTEGER NOT NULL REFERENCES concepts(id),
22
+ relation TEXT NOT NULL CHECK (relation IN ('prerequisite_of','related_to','part_of')),
23
+ PRIMARY KEY (from_concept, to_concept, relation)
24
+ );
25
+
26
+ CREATE TABLE IF NOT EXISTS mastery (
27
+ concept_id INTEGER PRIMARY KEY REFERENCES concepts(id),
28
+ score REAL NOT NULL DEFAULT 0, -- 0..1
29
+ ease REAL NOT NULL DEFAULT 2.5, -- SM-2 ease factor
30
+ interval_d REAL NOT NULL DEFAULT 0, -- current review interval, days
31
+ reps INTEGER NOT NULL DEFAULT 0,
32
+ last_seen TEXT, -- ISO timestamp
33
+ next_review TEXT -- ISO timestamp
34
+ );
35
+
36
+ CREATE TABLE IF NOT EXISTS attempts (
37
+ id INTEGER PRIMARY KEY,
38
+ concept_id INTEGER NOT NULL REFERENCES concepts(id),
39
+ session_id TEXT,
40
+ question TEXT NOT NULL,
41
+ answer TEXT,
42
+ grade INTEGER NOT NULL, -- 0..5 (SM-2 quality)
43
+ difficulty INTEGER NOT NULL, -- tier the question was asked at
44
+ feedback TEXT, -- tutor's explanation
45
+ ts TEXT NOT NULL DEFAULT (datetime('now'))
46
+ );
47
+
48
+ CREATE TABLE IF NOT EXISTS session_concepts (
49
+ session_id TEXT NOT NULL,
50
+ concept_id INTEGER NOT NULL REFERENCES concepts(id),
51
+ context TEXT, -- e.g. 'set httpOnly on refresh cookie in auth.ts'
52
+ ts TEXT NOT NULL DEFAULT (datetime('now')),
53
+ PRIMARY KEY (session_id, concept_id)
54
+ );
55
+
56
+ CREATE TABLE IF NOT EXISTS gates (
57
+ session_id TEXT PRIMARY KEY,
58
+ mode TEXT NOT NULL, -- 'ambient' | 'enforced'
59
+ required INTEGER NOT NULL DEFAULT 0,
60
+ answered INTEGER NOT NULL DEFAULT 0,
61
+ passed INTEGER NOT NULL DEFAULT 0, -- boolean
62
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
63
+ );
64
+
65
+ -- Read paths that must stay fast: the SessionStart hook and the PreToolUse gate
66
+ -- both run on the critical path of a user action (PRD §9).
67
+ CREATE INDEX IF NOT EXISTS idx_concepts_domain ON concepts(domain);
68
+ CREATE INDEX IF NOT EXISTS idx_session_concepts_sid ON session_concepts(session_id);
69
+ CREATE INDEX IF NOT EXISTS idx_attempts_concept_ts ON attempts(concept_id, ts DESC);
70
+ CREATE INDEX IF NOT EXISTS idx_attempts_session ON attempts(session_id);
71
+ CREATE INDEX IF NOT EXISTS idx_mastery_next_review ON mastery(next_review);
@@ -0,0 +1,14 @@
1
+ -- Loop guard for the Stop hook (phase-2, P0).
2
+ --
3
+ -- `stop_hook_active` is no longer a documented hook input (deviation D2), so the
4
+ -- guard cannot lean on the harness at all. The rule is: block at most once per
5
+ -- set of logged concepts. Blocking stamps the count of concepts logged for the
6
+ -- session; the next Stop only blocks again if that count has grown, which means
7
+ -- genuinely new work happened. `block_count` is a hard backstop on top.
8
+
9
+ CREATE TABLE IF NOT EXISTS stop_markers (
10
+ session_id TEXT PRIMARY KEY,
11
+ last_blocked_at TEXT,
12
+ last_logged_count INTEGER NOT NULL DEFAULT 0,
13
+ block_count INTEGER NOT NULL DEFAULT 0
14
+ );
@@ -0,0 +1,5 @@
1
+ -- Phase 3: the git pre-commit hook enforces the gate from outside Claude Code,
2
+ -- so a gate row has to say which repository it belongs to (PRD §9.4).
3
+ ALTER TABLE gates ADD COLUMN repo TEXT;
4
+
5
+ CREATE INDEX IF NOT EXISTS idx_gates_repo ON gates(repo, updated_at DESC);
@@ -0,0 +1,15 @@
1
+ -- Grade 0 was carrying two opposite meanings: "I don't know, teach me" and
2
+ -- "not now, leave me alone". The tutor skill collapsed them into one row of its
3
+ -- grading table, so a learner asking to be taught was dropped exactly like one
4
+ -- asking to be left alone (observed: four blanks in one session, nothing
5
+ -- taught). SM-2 wants both at 0 -- neither is a recall success -- so the
6
+ -- distinction cannot live in `grade`.
7
+ --
8
+ -- It has to be recorded rather than inferred, because the useful question comes
9
+ -- a day later: "did I already teach this, or has it never been explained?"
10
+ -- `feedback` text cannot answer that reliably.
11
+ --
12
+ -- NULL means "recorded before this column existed"; readers must treat it as
13
+ -- unknown, not as any particular outcome.
14
+ ALTER TABLE attempts ADD COLUMN outcome TEXT
15
+ CHECK (outcome IN ('answered','dont_know','declined'));
@@ -0,0 +1,24 @@
1
+ -- `required` is derived from the concepts the session's *work* touched, but
2
+ -- `passed` was satisfiable by any concept in `session_concepts` -- and
3
+ -- `record_attempt` inserts a row there for anything it is asked about, so that
4
+ -- review debt is not free. The two ends never matched: answering concepts the
5
+ -- planner pulled in from elsewhere raised `passedCount` while leaving `required`
6
+ -- frozen, so a gate whose bar was set by today's diff could be cleared without
7
+ -- answering a single question about today's diff.
8
+ --
9
+ -- Focus modes make that the main path rather than a trickle. `concept` focus
10
+ -- widens selection to domain siblings and prerequisites on purpose, and `learn`
11
+ -- focus selects from a topic that may have nothing to do with the session at
12
+ -- all.
13
+ --
14
+ -- So the origin has to be recorded. 'work' is a concept the implementer logged
15
+ -- as part of the task; 'review' is one the tutor pulled in. Both still record
16
+ -- attempts, update mastery and count as `answered` -- review debt stays real --
17
+ -- but only 'work' can satisfy the bar the work itself set.
18
+ --
19
+ -- NULL means "recorded before this column existed". Those rows are read as
20
+ -- 'work', which is what they were in every case that mattered: before focus
21
+ -- modes there was nothing to widen with except same-domain review, and treating
22
+ -- them as 'review' would retroactively unpass gates that have already passed.
23
+ ALTER TABLE session_concepts ADD COLUMN origin TEXT
24
+ CHECK (origin IN ('work','review'));
@@ -0,0 +1,31 @@
1
+ -- A blank prompt is a bad interface for a quiz you did not ask for. Mid-task,
2
+ -- the honest answer to "walk me through what the browser does with this" is
3
+ -- often nothing at all -- not because the learner does not know, but because
4
+ -- typing a paragraph costs more than the question is worth right then.
5
+ -- Recognition is cheap to answer and still teaches, so the default question
6
+ -- shape becomes multiple choice.
7
+ --
8
+ -- Two columns, because the two facts are needed for different reasons:
9
+ --
10
+ -- `format` is what makes the grade readable later. A correct multiple-choice
11
+ -- answer is weaker evidence than a correct free answer -- one in four is a
12
+ -- coin -- so `record_attempt` caps it, and any future rebalancing of mastery
13
+ -- needs to know which attempts were recognition and which were recall. Inferring
14
+ -- it from whether `options` is NULL would work today and break the moment a
15
+ -- format arrives that has no options.
16
+ --
17
+ -- `options` exists so the question text does not have to carry them. Question
18
+ -- fingerprinting (PRD goal 2) hashes `question`, so baking four shuffled options
19
+ -- into that string would make every reshuffle look like a brand-new question and
20
+ -- silently undo "never ask the same question twice". The stem goes in
21
+ -- `question`; the options live here as a JSON array.
22
+ --
23
+ -- NULL `format` means "recorded before this column existed" -- unknown, and in
24
+ -- particular not an assertion that it was free-form.
25
+ --
26
+ -- fill_blank and open are accepted now though nothing writes them yet: the CHECK
27
+ -- is the expensive part to change later, and the next phase adds both.
28
+ ALTER TABLE attempts ADD COLUMN format TEXT
29
+ CHECK (format IN ('mcq','fill_blank','open'));
30
+
31
+ ALTER TABLE attempts ADD COLUMN options TEXT;
@@ -0,0 +1,23 @@
1
+ -- Interleaved quizzing (phase-8).
2
+ --
3
+ -- Until now the only trigger was the Stop hook, so every question landed after
4
+ -- the work was finished -- four of them, in a row, at the exact moment the
5
+ -- developer wanted to be done. That is a tax on shipping, not learning while the
6
+ -- agent works. `PostToolUse` fires mid-turn and the model sees its output
7
+ -- immediately, so a question can now land at the seam where a concept was
8
+ -- logged, while the code that taught it is still on screen.
9
+ --
10
+ -- This table is the pacing state for that. It is deliberately separate from
11
+ -- `stop_markers`: the two guards protect against different failures.
12
+ -- `stop_markers` stops an end-of-turn loop; this stops a mid-turn burst -- ten
13
+ -- concepts logged in one call must not become ten questions in a row.
14
+ --
15
+ -- `count` is the session's spend against `max_questions_per_task`, which becomes
16
+ -- a SESSION budget rather than an end-of-session batch size. The Stop hook reads
17
+ -- attempts, not this column, so a question answered here is one the Stop hook
18
+ -- will not ask again.
19
+ CREATE TABLE IF NOT EXISTS checkpoints (
20
+ session_id TEXT PRIMARY KEY,
21
+ last_checkpoint_at TEXT,
22
+ count INTEGER NOT NULL DEFAULT 0
23
+ );
@@ -0,0 +1,57 @@
1
+ -- Difficulty levels, earned per project (phase-9).
2
+ --
3
+ -- Tier already varies per question: `nextTierToAsk` walks a concept from recall
4
+ -- to design as the learner gets it right. What that cannot express is where the
5
+ -- learner stands *on this codebase*, and the gap shows up at both ends. A cold
6
+ -- start takes the median tier of everything they have ever mastered, so someone
7
+ -- with a solid web-auth history opens an unfamiliar React repo on judgement
8
+ -- questions about code they met four minutes ago -- and the honest answer to
9
+ -- those is "no idea", which is the answer that gets a plugin uninstalled.
10
+ --
11
+ -- So a coarse band sits above the tier ladder: easy (tiers 1-2), medium (2-4),
12
+ -- hard (3-5). Everyone starts at easy, and a level is earned by evidence rather
13
+ -- than chosen.
14
+ --
15
+ -- `repo` on attempts, because the band is per project. "How hard should this be"
16
+ -- is a question about a codebase, not about a person: the same developer is a
17
+ -- different learner on their fourth Node service and their first Rust one, and
18
+ -- one global level would have to be wrong for one of them.
19
+ --
20
+ -- `level` on attempts, because progress has to be attributable to the band it
21
+ -- was earned in. Deriving it from `difficulty` afterwards would be wrong in both
22
+ -- directions: the bands overlap, so tier 2 belongs to easy and medium alike.
23
+ --
24
+ -- Both are NULL for every attempt recorded before this migration, and that is
25
+ -- deliberate rather than a gap to backfill later. `repo` is unrecoverable -- the
26
+ -- session's cwd was never stored -- and reading a level out of an old tier-3
27
+ -- answer would hand out promotions nobody earned, on install day, to everyone.
28
+ -- NULL here means unknown, the same as in migrations 004, 005 and 006.
29
+ ALTER TABLE attempts ADD COLUMN repo TEXT;
30
+
31
+ ALTER TABLE attempts ADD COLUMN level TEXT
32
+ CHECK (level IN ('easy','medium','hard'));
33
+
34
+ -- One row per project, created lazily on the first promotion.
35
+ --
36
+ -- `promoted_at` is when this level was entered, and it is what makes progress
37
+ -- countable: the passing answers that count toward the next promotion are the
38
+ -- ones recorded after it. There is deliberately no `passed_at_level` counter --
39
+ -- see `levelProgress` in store.ts. A counter can drift from the attempt rows
40
+ -- that justify it, and it would freeze `level_up_after` at whatever it was when
41
+ -- each attempt landed, so lowering the threshold would not release the learners
42
+ -- already past it.
43
+ --
44
+ -- `repo` is the git root as `findRepoConfig` resolves it (symlinks resolved, so
45
+ -- the server and the git hook agree on the string). A directory outside any
46
+ -- repository falls into the shared '*' bucket rather than minting a row per cwd.
47
+ CREATE TABLE IF NOT EXISTS project_levels (
48
+ repo TEXT PRIMARY KEY,
49
+ level TEXT NOT NULL DEFAULT 'easy'
50
+ CHECK (level IN ('easy','medium','hard')),
51
+ promoted_at TEXT,
52
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
53
+ );
54
+
55
+ -- The progress count runs on the SessionStart critical path (PRD §9.1), so it
56
+ -- gets its own index rather than scanning a growing attempts table.
57
+ CREATE INDEX IF NOT EXISTS idx_attempts_repo_level ON attempts(repo, level, ts DESC);
package/dist/paths.js ADDED
@@ -0,0 +1,25 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ /**
5
+ * Eklavya keeps all state in one directory so it is trivially inspectable and
6
+ * deletable. `EKLAVYA_HOME` exists so tests never touch the real learner's data.
7
+ */
8
+ export function eklavyaHome() {
9
+ return process.env.EKLAVYA_HOME ?? path.join(os.homedir(), '.eklavya');
10
+ }
11
+ export function dbPath() {
12
+ return process.env.EKLAVYA_DB ?? path.join(eklavyaHome(), 'knowledge.db');
13
+ }
14
+ export function globalConfigPath() {
15
+ return path.join(eklavyaHome(), 'config.json');
16
+ }
17
+ /** Directory of this module — `src/` under tsx, `dist/` after a build. */
18
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url));
19
+ export function migrationsDir() {
20
+ return path.join(moduleDir, 'migrations');
21
+ }
22
+ export function seedDir() {
23
+ return path.join(moduleDir, 'seed');
24
+ }
25
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.js","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC;;;GAGG;AACH,MAAM,UAAU,WAAW;IACzB,OAAO,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,UAAU,CAAC,CAAC;AACzE,CAAC;AAED,MAAM,UAAU,MAAM;IACpB,OAAO,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,cAAc,CAAC,CAAC;AAC5E,CAAC;AAED,MAAM,UAAU,gBAAgB;IAC9B,OAAO,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,aAAa,CAAC,CAAC;AACjD,CAAC;AAED,0EAA0E;AAC1E,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAE/D,MAAM,UAAU,aAAa;IAC3B,OAAO,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;AAC5C,CAAC;AAED,MAAM,UAAU,OAAO;IACrB,OAAO,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;AACtC,CAAC"}
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "eklavya",
3
+ "description": "Local, privacy-preserving learning tools for agent-assisted development.",
4
+ "owner": {
5
+ "name": "Ajay Kumar"
6
+ },
7
+ "plugins": [
8
+ {
9
+ "name": "eklavya",
10
+ "source": "./",
11
+ "description": "Learn while your agent works. Turns coding-agent generation time into adaptive, Socratic learning grounded in the code being written."
12
+ }
13
+ ]
14
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "eklavya",
3
+ "displayName": "Eklavya",
4
+ "version": "1.7.0",
5
+ "description": "Learn while your agent works. Turns coding-agent generation time into adaptive, Socratic learning grounded in the code being written.",
6
+ "author": {
7
+ "name": "Ajay Kumar"
8
+ },
9
+ "license": "MIT",
10
+ "keywords": [
11
+ "learning",
12
+ "spaced-repetition",
13
+ "tutor",
14
+ "knowledge-graph",
15
+ "mentorship"
16
+ ]
17
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "mcpServers": {
3
+ "eklavya": {
4
+ "command": "node",
5
+ "args": [
6
+ "${CLAUDE_PLUGIN_ROOT}/hooks/run.mjs",
7
+ "server"
8
+ ]
9
+ }
10
+ }
11
+ }
@@ -0,0 +1,63 @@
1
+ ---
2
+ name: eklavya-tutor
3
+ description: Teaches the developer the concepts behind work being done, using the Eklavya knowledge graph. Use when you want tutoring to run alongside implementation rather than after it, or when the main thread is busy building and someone should be explaining.
4
+ tools: Read, Grep, Glob, mcp__plugin_eklavya_eklavya__get_learner_profile, mcp__plugin_eklavya_eklavya__get_session_quiz_plan, mcp__plugin_eklavya_eklavya__record_attempt, mcp__plugin_eklavya_eklavya__get_concept_graph, mcp__plugin_eklavya_eklavya__upsert_concepts, mcp__plugin_eklavya_eklavya__get_gate_status, mcp__eklavya__get_learner_profile, mcp__eklavya__get_session_quiz_plan, mcp__eklavya__record_attempt, mcp__eklavya__get_concept_graph, mcp__eklavya__upsert_concepts, mcp__eklavya__get_gate_status
5
+ ---
6
+
7
+ You are Eklavya's tutor, running as a separate agent while implementation happens elsewhere.
8
+
9
+ Follow the `tutor` skill for all pedagogy — profile first, one question at a time, honest grading, and the `framing` the plan returns. This file only covers what is different about running in parallel.
10
+
11
+ Note that "grounded in real code" is `project` focus, not a universal rule: in `concept` focus the plan hands you `context: null` deliberately, and quoting the diff back defeats the point. Read `framing` before writing a question.
12
+
13
+ ## Why the tool list is spelled twice
14
+
15
+ An MCP server provided by a plugin is namespaced: its tools resolve as
16
+ `mcp__plugin_<plugin>_<server>__<tool>`, so Eklavya's are
17
+ `mcp__plugin_eklavya_eklavya__*`. A matcher or allowlist written against the bare
18
+ server key never fires. The bare `mcp__eklavya__*` names are listed as well
19
+ because that *is* the right name when the server is registered from a
20
+ project-level `.mcp.json` — which is how Eklavya's own repo and a Cursor setup
21
+ run it. Whichever install you are in, one set resolves and the other is inert.
22
+
23
+ If none of them resolve, you have no memory of this learner and must say so
24
+ rather than quizzing blind.
25
+
26
+ ## You cannot use AskUserQuestion
27
+
28
+ The main session asks quiz questions as multiple choice through the `AskUserQuestion` tool. You do not have it — your tool list is read-only graph access — so render the four options as text instead:
29
+
30
+ ```
31
+ Which of these does `httpOnly` actually prevent?
32
+
33
+ A) JavaScript reading the cookie via document.cookie
34
+ B) The cookie being sent to a different origin
35
+ C) The cookie surviving a browser restart
36
+ D) The cookie being read over plain HTTP
37
+
38
+ Reply with a letter, or "teach me" if you'd rather I explain it.
39
+ ```
40
+
41
+ Same rules as the skill's *Multiple choice* section: four options, three plausible distractors, the stem alone in `record_attempt`'s `question`, the labels in `options`, `format: "mcq"`, and the grade capped at 4. The only thing that changes is who draws the box.
42
+
43
+ ## You do not write code
44
+
45
+ You have read-only access on purpose. You read what the other agent is building and you teach it. If the developer asks you to change something, tell them to take it back to the main session.
46
+
47
+ ## Reading the work
48
+
49
+ The implementation is happening in files you can read. Use `Read` and `Grep` to look at what was just written, then ground your questions in it. You are at your most useful when you can say "in the middle of `auth.ts` there's a decision you'd have missed" — because you actually looked.
50
+
51
+ Do not guess at code you have not read. A question about a line that does not exist destroys trust faster than no question at all.
52
+
53
+ ## Session
54
+
55
+ You share the knowledge database with the session that spawned you. Omit `session_id` on every call and the server resolves the same session, so what you teach counts toward the same gate and the same mastery history.
56
+
57
+ ## Pacing
58
+
59
+ You are competing for attention with an agent that is producing code. Ask one question, then wait. If the developer does not answer, do not chase them — they are reading a diff. Silence is a legitimate answer and costs nothing.
60
+
61
+ Never record a grade for a question that was not answered. Record a decline (`outcome: "declined"`) only when they actually say so.
62
+
63
+ "I don't know" is not silence and not a decline — it is a request. Teach it: the mechanism, the real lines from the diff, what it generalises to, one takeaway. Then record grade 0 with `outcome: "dont_know"` and move on. See the `tutor` skill for the full shape.
@@ -0,0 +1,67 @@
1
+ #!/bin/sh
2
+ # Exit 0 if committing is allowed, 1 if Eklavya's gate is holding it.
3
+ #
4
+ # This is what makes enforcement editor-agnostic (PRD §9.4): the same check runs
5
+ # from a bare terminal, VS Code, or Cursor via the git pre-commit hook.
6
+ #
7
+ # It fails OPEN. A missing jq, a missing database or an unreadable config lets
8
+ # the commit through with a warning. A learning tool that bricks commits when a
9
+ # dependency is absent gets uninstalled, and an unpassable gate teaches nothing.
10
+
11
+ set -u
12
+
13
+ eklavya_home() { printf '%s' "${EKLAVYA_HOME:-$HOME/.eklavya}"; }
14
+ eklavya_db() { printf '%s' "${EKLAVYA_DB:-$(eklavya_home)/knowledge.db}"; }
15
+
16
+ REPO=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
17
+ [ -n "$REPO" ] || exit 0
18
+
19
+ # Only repos that explicitly opt in are gated.
20
+ CONFIG="$REPO/.eklavya.json"
21
+ [ -f "$CONFIG" ] || exit 0
22
+
23
+ if ! command -v jq >/dev/null 2>&1; then
24
+ printf 'eklavya: jq not found, skipping the commit gate.\n' >&2
25
+ exit 0
26
+ fi
27
+ if ! command -v sqlite3 >/dev/null 2>&1; then
28
+ printf 'eklavya: sqlite3 not found, skipping the commit gate.\n' >&2
29
+ exit 0
30
+ fi
31
+
32
+ MODE=$(jq -r '.mode // "ambient"' "$CONFIG" 2>/dev/null) || exit 0
33
+ [ "$MODE" = "enforced" ] || exit 0
34
+
35
+ DB=$(eklavya_db)
36
+ [ -f "$DB" ] || exit 0
37
+
38
+ ESCAPED=$(printf '%s' "$REPO" | sed "s/'/''/g")
39
+ GATE=$(sqlite3 -noheader -batch -cmd ".timeout 2000" "$DB" "
40
+ SELECT passed || '|' || required || '|' || answered
41
+ FROM gates
42
+ WHERE repo = '$ESCAPED'
43
+ ORDER BY updated_at DESC
44
+ LIMIT 1;" 2>/dev/null) || exit 0
45
+
46
+ # No gate for this repo means Eklavya never taught anything here, so there is
47
+ # nothing to have passed.
48
+ [ -n "$GATE" ] || exit 0
49
+
50
+ PASSED=$(printf '%s' "$GATE" | cut -d'|' -f1)
51
+ REQUIRED=$(printf '%s' "$GATE" | cut -d'|' -f2)
52
+ ANSWERED=$(printf '%s' "$GATE" | cut -d'|' -f3)
53
+
54
+ [ "$PASSED" = "1" ] && exit 0
55
+
56
+ cat >&2 <<EOF
57
+
58
+ Eklavya is holding this commit.
59
+
60
+ You have answered $ANSWERED of $REQUIRED concepts from this session's work.
61
+ Open Claude Code in this repo and run /eklavya:quiz to finish, then commit again.
62
+
63
+ To see what is outstanding: /eklavya:gate
64
+ To turn this off for the repo, set "mode" to "ambient" in .eklavya.json
65
+
66
+ EOF
67
+ exit 1
@@ -0,0 +1,57 @@
1
+ {
2
+ "hooks": {
3
+ "SessionStart": [
4
+ {
5
+ "hooks": [
6
+ {
7
+ "type": "command",
8
+ "command": "node",
9
+ "args": ["${CLAUDE_PLUGIN_ROOT}/hooks/run.mjs", "session-start"],
10
+ "timeout": 10,
11
+ "statusMessage": "Eklavya: loading learner profile"
12
+ }
13
+ ]
14
+ }
15
+ ],
16
+ "PreToolUse": [
17
+ {
18
+ "matcher": "Bash",
19
+ "hooks": [
20
+ {
21
+ "type": "command",
22
+ "command": "node",
23
+ "args": ["${CLAUDE_PLUGIN_ROOT}/hooks/run.mjs", "pre-tool-gate"],
24
+ "timeout": 10
25
+ }
26
+ ]
27
+ }
28
+ ],
29
+ "PostToolUse": [
30
+ {
31
+ "matcher": "mcp__.*log_session_concepts",
32
+ "hooks": [
33
+ {
34
+ "type": "command",
35
+ "command": "node",
36
+ "args": ["${CLAUDE_PLUGIN_ROOT}/hooks/run.mjs", "checkpoint-quiz"],
37
+ "timeout": 10,
38
+ "statusMessage": "Eklavya: checking if it is time to ask"
39
+ }
40
+ ]
41
+ }
42
+ ],
43
+ "Stop": [
44
+ {
45
+ "hooks": [
46
+ {
47
+ "type": "command",
48
+ "command": "node",
49
+ "args": ["${CLAUDE_PLUGIN_ROOT}/hooks/run.mjs", "stop-quiz-check"],
50
+ "timeout": 15,
51
+ "statusMessage": "Eklavya: checking what you learned"
52
+ }
53
+ ]
54
+ }
55
+ ]
56
+ }
57
+ }