eklavya-mcp 1.0.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 +42 -0
- package/bin/eklavya-mcp.sh +25 -0
- package/dist/assets/tutor-skill.md +106 -0
- package/dist/cli.js +145 -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 +116 -0
- package/dist/config.js.map +1 -0
- package/dist/db.js +26 -0
- package/dist/db.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/paths.js +25 -0
- package/dist/paths.js.map +1 -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 +39 -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 +130 -0
- package/dist/srs.js.map +1 -0
- package/dist/store.js +122 -0
- package/dist/store.js.map +1 -0
- package/dist/tools/config_tools.js +73 -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 +78 -0
- package/dist/tools/get_learner_profile.js.map +1 -0
- package/dist/tools/get_session_quiz_plan.js +107 -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 +83 -0
- package/dist/tools/log_session_concepts.js.map +1 -0
- package/dist/tools/record_attempt.js +69 -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 +68 -0
|
@@ -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);
|
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,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"domain": "git",
|
|
3
|
+
"concepts": [
|
|
4
|
+
{ "slug": "git-repository", "name": "Repository and object model", "tier": 1, "description": "Commits, trees and blobs in a content-addressed store — everything else in git is a view over this." },
|
|
5
|
+
{ "slug": "git-commit", "name": "Commits", "tier": 1, "description": "An immutable snapshot plus parent pointers; history is a DAG, not a list." },
|
|
6
|
+
{ "slug": "git-staging-area", "name": "Staging area", "tier": 1, "description": "The index — the explicit space between working tree and commit that makes partial commits possible." },
|
|
7
|
+
{ "slug": "git-branch", "name": "Branches", "tier": 1, "description": "A movable pointer to a commit; branching is cheap because nothing is copied." },
|
|
8
|
+
{ "slug": "git-remote", "name": "Remotes and tracking branches", "tier": 1, "description": "Local mirrors of another repository's refs, updated only by fetch." },
|
|
9
|
+
{ "slug": "gitignore", "name": ".gitignore", "tier": 1, "description": "Patterns excluding files from tracking — and why it does nothing for already-tracked files." },
|
|
10
|
+
|
|
11
|
+
{ "slug": "git-merge", "name": "Merging", "tier": 2, "description": "Joining histories with a merge commit; fast-forward versus true merge." },
|
|
12
|
+
{ "slug": "git-rebase", "name": "Rebasing", "tier": 2, "description": "Replaying commits onto a new base, producing new commit objects with new hashes." },
|
|
13
|
+
{ "slug": "git-conflict-resolution", "name": "Conflict resolution", "tier": 2, "description": "What a conflict actually is, and why resolving it is a content decision git cannot make." },
|
|
14
|
+
{ "slug": "git-stash", "name": "Stashing", "tier": 2, "description": "Parking uncommitted work on a hidden ref to get a clean tree." },
|
|
15
|
+
{ "slug": "git-detached-head", "name": "Detached HEAD", "tier": 2, "description": "HEAD pointing at a commit rather than a branch, and how commits made there get orphaned." },
|
|
16
|
+
{ "slug": "git-hooks", "name": "Git hooks", "tier": 2, "description": "Local scripts fired at lifecycle points such as pre-commit — the mechanism Eklavya's own gate uses." },
|
|
17
|
+
|
|
18
|
+
{ "slug": "git-reset-modes", "name": "reset --soft/--mixed/--hard", "tier": 3, "description": "Which of HEAD, index and working tree each mode moves — the difference between recoverable and lost work." },
|
|
19
|
+
{ "slug": "git-reflog", "name": "Reflog", "tier": 3, "description": "The local log of where refs have pointed; the recovery tool for almost every 'I lost it' situation." },
|
|
20
|
+
{ "slug": "git-cherry-pick", "name": "Cherry-picking", "tier": 3, "description": "Applying one commit's change onto another branch, and the duplicate-commit problem it creates." },
|
|
21
|
+
{ "slug": "git-merge-vs-rebase", "name": "Merge vs rebase: tradeoffs", "tier": 3, "description": "Truthful history versus readable history, and why the choice is a team policy, not a technical fact." },
|
|
22
|
+
|
|
23
|
+
{ "slug": "git-interactive-rebase", "name": "Interactive rebase", "tier": 4, "description": "Reordering, squashing and editing history — and what it does to anyone who already pulled it." },
|
|
24
|
+
{ "slug": "git-force-push-safety", "name": "Force-push safety", "tier": 4, "description": "Why --force-with-lease exists and what plain --force silently destroys." },
|
|
25
|
+
{ "slug": "git-bisect", "name": "Bisect", "tier": 4, "description": "Binary search over history to find the commit that introduced a regression." }
|
|
26
|
+
],
|
|
27
|
+
"edges": [
|
|
28
|
+
{ "from": "git-repository", "to": "git-commit", "relation": "prerequisite_of" },
|
|
29
|
+
{ "from": "git-commit", "to": "git-branch", "relation": "prerequisite_of" },
|
|
30
|
+
{ "from": "git-commit", "to": "git-staging-area", "relation": "prerequisite_of" },
|
|
31
|
+
{ "from": "git-branch", "to": "git-merge", "relation": "prerequisite_of" },
|
|
32
|
+
{ "from": "git-branch", "to": "git-rebase", "relation": "prerequisite_of" },
|
|
33
|
+
{ "from": "git-branch", "to": "git-remote", "relation": "prerequisite_of" },
|
|
34
|
+
{ "from": "git-branch", "to": "git-detached-head", "relation": "prerequisite_of" },
|
|
35
|
+
{ "from": "git-merge", "to": "git-conflict-resolution", "relation": "prerequisite_of" },
|
|
36
|
+
{ "from": "git-merge", "to": "git-merge-vs-rebase", "relation": "prerequisite_of" },
|
|
37
|
+
{ "from": "git-rebase", "to": "git-merge-vs-rebase", "relation": "prerequisite_of" },
|
|
38
|
+
{ "from": "git-rebase", "to": "git-interactive-rebase", "relation": "prerequisite_of" },
|
|
39
|
+
{ "from": "git-interactive-rebase", "to": "git-force-push-safety", "relation": "prerequisite_of" },
|
|
40
|
+
{ "from": "git-remote", "to": "git-force-push-safety", "relation": "prerequisite_of" },
|
|
41
|
+
{ "from": "git-staging-area", "to": "git-reset-modes", "relation": "prerequisite_of" },
|
|
42
|
+
{ "from": "git-detached-head", "to": "git-reflog", "relation": "prerequisite_of" },
|
|
43
|
+
{ "from": "git-reset-modes", "to": "git-reflog", "relation": "prerequisite_of" },
|
|
44
|
+
{ "from": "git-commit", "to": "git-cherry-pick", "relation": "prerequisite_of" },
|
|
45
|
+
{ "from": "git-commit", "to": "git-bisect", "relation": "prerequisite_of" },
|
|
46
|
+
{ "from": "git-staging-area", "to": "git-stash", "relation": "related_to" },
|
|
47
|
+
{ "from": "gitignore", "to": "git-staging-area", "relation": "related_to" }
|
|
48
|
+
]
|
|
49
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"domain": "node-backend",
|
|
3
|
+
"concepts": [
|
|
4
|
+
{ "slug": "node-event-loop", "name": "The event loop", "tier": 1, "description": "One thread, a queue of callbacks; concurrency comes from not waiting, not from parallelism." },
|
|
5
|
+
{ "slug": "node-modules", "name": "CommonJS vs ESM", "tier": 1, "description": "require versus import, and the resolution and interop rules that differ between them." },
|
|
6
|
+
{ "slug": "node-http-server", "name": "HTTP server basics", "tier": 1, "description": "Request and response streams underneath every framework abstraction." },
|
|
7
|
+
{ "slug": "node-env-vars", "name": "Environment variables and secrets", "tier": 1, "description": "Configuration outside the code, and why secrets never belong in the repository." },
|
|
8
|
+
|
|
9
|
+
{ "slug": "express-routing", "name": "Routing", "tier": 2, "description": "Matching method and path to a handler, including how route order decides ambiguity." },
|
|
10
|
+
{ "slug": "express-middleware", "name": "Middleware", "tier": 2, "description": "Functions in a chain that may inspect, mutate, short-circuit or pass along a request." },
|
|
11
|
+
{ "slug": "node-async-await", "name": "async/await and promises", "tier": 2, "description": "Sequencing asynchronous work without blocking the loop, and where errors surface." },
|
|
12
|
+
{ "slug": "express-body-parsing", "name": "Request body parsing", "tier": 2, "description": "Turning a request stream into a usable value, and why size limits matter." },
|
|
13
|
+
{ "slug": "node-input-validation", "name": "Input validation", "tier": 2, "description": "Rejecting malformed input at the boundary rather than deep inside a handler." },
|
|
14
|
+
{ "slug": "express-error-handling", "name": "Error-handling middleware", "tier": 2, "description": "The four-argument handler, and why an unawaited rejection never reaches it." },
|
|
15
|
+
|
|
16
|
+
{ "slug": "express-middleware-order", "name": "Middleware ordering", "tier": 3, "description": "The chain is sequential; a misplaced auth or parser leaves a route unprotected or a body undefined." },
|
|
17
|
+
{ "slug": "node-connection-pooling", "name": "Connection pooling", "tier": 3, "description": "Reusing database connections, and what pool exhaustion looks like from the outside." },
|
|
18
|
+
{ "slug": "node-streams-backpressure", "name": "Streams and backpressure", "tier": 3, "description": "Letting a slow consumer throttle a fast producer instead of buffering into memory." },
|
|
19
|
+
{ "slug": "node-logging-observability", "name": "Structured logging", "tier": 3, "description": "Machine-readable logs with request correlation, so production failures are diagnosable." },
|
|
20
|
+
|
|
21
|
+
{ "slug": "node-event-loop-blocking", "name": "Blocking the event loop", "tier": 4, "description": "Synchronous CPU work stalling every other request on the process." },
|
|
22
|
+
{ "slug": "node-graceful-shutdown", "name": "Graceful shutdown", "tier": 4, "description": "Draining in-flight requests and closing resources on SIGTERM instead of dropping them." },
|
|
23
|
+
{ "slug": "node-clustering-scaling", "name": "Clustering and horizontal scale", "tier": 4, "description": "Multiple processes behind a balancer, and what that breaks for in-process state." }
|
|
24
|
+
],
|
|
25
|
+
"edges": [
|
|
26
|
+
{ "from": "node-event-loop", "to": "node-async-await", "relation": "prerequisite_of" },
|
|
27
|
+
{ "from": "node-event-loop", "to": "node-event-loop-blocking", "relation": "prerequisite_of" },
|
|
28
|
+
{ "from": "node-http-server", "to": "express-routing", "relation": "prerequisite_of" },
|
|
29
|
+
{ "from": "node-http-server", "to": "node-streams-backpressure", "relation": "prerequisite_of" },
|
|
30
|
+
{ "from": "express-routing", "to": "express-middleware", "relation": "prerequisite_of" },
|
|
31
|
+
{ "from": "express-middleware", "to": "express-middleware-order", "relation": "prerequisite_of" },
|
|
32
|
+
{ "from": "express-middleware", "to": "express-body-parsing", "relation": "prerequisite_of" },
|
|
33
|
+
{ "from": "express-middleware", "to": "express-error-handling", "relation": "prerequisite_of" },
|
|
34
|
+
{ "from": "node-async-await", "to": "express-error-handling", "relation": "prerequisite_of" },
|
|
35
|
+
{ "from": "express-body-parsing", "to": "node-input-validation", "relation": "prerequisite_of" },
|
|
36
|
+
{ "from": "node-async-await", "to": "node-connection-pooling", "relation": "prerequisite_of" },
|
|
37
|
+
{ "from": "node-http-server", "to": "node-graceful-shutdown", "relation": "prerequisite_of" },
|
|
38
|
+
{ "from": "node-env-vars", "to": "node-clustering-scaling", "relation": "related_to" },
|
|
39
|
+
{ "from": "node-connection-pooling", "to": "node-clustering-scaling", "relation": "prerequisite_of" },
|
|
40
|
+
{ "from": "node-modules", "to": "node-http-server", "relation": "related_to" },
|
|
41
|
+
{ "from": "node-logging-observability", "to": "node-graceful-shutdown", "relation": "related_to" }
|
|
42
|
+
]
|
|
43
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"domain": "react",
|
|
3
|
+
"concepts": [
|
|
4
|
+
{ "slug": "react-component", "name": "Components", "tier": 1, "description": "A function taking props and returning a description of UI — not a template and not an object." },
|
|
5
|
+
{ "slug": "react-jsx", "name": "JSX", "tier": 1, "description": "Syntax sugar over createElement calls; it produces data, not DOM." },
|
|
6
|
+
{ "slug": "react-props", "name": "Props", "tier": 1, "description": "Read-only inputs flowing down; mutating them breaks the model." },
|
|
7
|
+
{ "slug": "react-state", "name": "State", "tier": 1, "description": "Values a component owns, whose change triggers a re-render." },
|
|
8
|
+
{ "slug": "react-lists-keys", "name": "Lists and keys", "tier": 1, "description": "Keys give list children a stable identity across renders; index keys quietly break on reorder." },
|
|
9
|
+
|
|
10
|
+
{ "slug": "react-hooks-rules", "name": "Rules of hooks", "tier": 2, "description": "Hooks are matched to state by call order, which is why they cannot be conditional." },
|
|
11
|
+
{ "slug": "react-usestate", "name": "useState", "tier": 2, "description": "Declaring state, and why updates are queued rather than applied immediately." },
|
|
12
|
+
{ "slug": "react-useeffect", "name": "useEffect", "tier": 2, "description": "Synchronizing with something outside React — not a lifecycle callback." },
|
|
13
|
+
{ "slug": "react-controlled-inputs", "name": "Controlled inputs", "tier": 2, "description": "Form values driven by state, with React as the single source of truth." },
|
|
14
|
+
{ "slug": "react-lifting-state", "name": "Lifting state up", "tier": 2, "description": "Moving shared state to the nearest common ancestor instead of syncing two copies." },
|
|
15
|
+
|
|
16
|
+
{ "slug": "react-render-model", "name": "Render and commit model", "tier": 3, "description": "Render is a pure computation; commit applies the diff. Confusing the two explains most surprising bugs." },
|
|
17
|
+
{ "slug": "react-useeffect-deps", "name": "useEffect dependencies", "tier": 3, "description": "The dependency array is a correctness contract, not a performance knob." },
|
|
18
|
+
{ "slug": "react-useeffect-cleanup", "name": "useEffect cleanup", "tier": 3, "description": "Returning a teardown to cancel subscriptions and in-flight work before the next effect run." },
|
|
19
|
+
{ "slug": "react-context", "name": "Context", "tier": 3, "description": "Passing values past intermediate components — and the re-render cost of doing it carelessly." },
|
|
20
|
+
{ "slug": "react-memoization", "name": "useMemo and useCallback", "tier": 3, "description": "Stabilizing values and identities; usually a fix for referential equality, not for slow code." },
|
|
21
|
+
|
|
22
|
+
{ "slug": "react-stale-closure", "name": "Stale closures", "tier": 4, "description": "A callback capturing an old render's variables — the classic source of 'my state is one step behind'." },
|
|
23
|
+
{ "slug": "react-reconciliation", "name": "Reconciliation", "tier": 4, "description": "How React decides to update, move or remount a node, and how component identity is determined." },
|
|
24
|
+
{ "slug": "react-race-conditions", "name": "Async race conditions", "tier": 4, "description": "Out-of-order responses overwriting newer data, and why cleanup or an abort signal is the fix." }
|
|
25
|
+
],
|
|
26
|
+
"edges": [
|
|
27
|
+
{ "from": "react-component", "to": "react-jsx", "relation": "prerequisite_of" },
|
|
28
|
+
{ "from": "react-component", "to": "react-props", "relation": "prerequisite_of" },
|
|
29
|
+
{ "from": "react-props", "to": "react-state", "relation": "prerequisite_of" },
|
|
30
|
+
{ "from": "react-state", "to": "react-usestate", "relation": "prerequisite_of" },
|
|
31
|
+
{ "from": "react-hooks-rules", "to": "react-usestate", "relation": "prerequisite_of" },
|
|
32
|
+
{ "from": "react-hooks-rules", "to": "react-useeffect", "relation": "prerequisite_of" },
|
|
33
|
+
{ "from": "react-usestate", "to": "react-controlled-inputs", "relation": "prerequisite_of" },
|
|
34
|
+
{ "from": "react-state", "to": "react-lifting-state", "relation": "prerequisite_of" },
|
|
35
|
+
{ "from": "react-lifting-state", "to": "react-context", "relation": "prerequisite_of" },
|
|
36
|
+
{ "from": "react-useeffect", "to": "react-useeffect-deps", "relation": "prerequisite_of" },
|
|
37
|
+
{ "from": "react-useeffect", "to": "react-useeffect-cleanup", "relation": "prerequisite_of" },
|
|
38
|
+
{ "from": "react-useeffect-cleanup", "to": "react-race-conditions", "relation": "prerequisite_of" },
|
|
39
|
+
{ "from": "react-state", "to": "react-render-model", "relation": "prerequisite_of" },
|
|
40
|
+
{ "from": "react-render-model", "to": "react-memoization", "relation": "prerequisite_of" },
|
|
41
|
+
{ "from": "react-render-model", "to": "react-reconciliation", "relation": "prerequisite_of" },
|
|
42
|
+
{ "from": "react-lists-keys", "to": "react-reconciliation", "relation": "prerequisite_of" },
|
|
43
|
+
{ "from": "react-useeffect-deps", "to": "react-stale-closure", "relation": "prerequisite_of" },
|
|
44
|
+
{ "from": "react-memoization", "to": "react-stale-closure", "relation": "related_to" }
|
|
45
|
+
]
|
|
46
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"domain": "web-auth",
|
|
3
|
+
"concepts": [
|
|
4
|
+
{ "slug": "http-statelessness", "name": "HTTP statelessness", "tier": 1, "description": "HTTP carries no memory between requests; every auth mechanism exists to re-establish identity on each one." },
|
|
5
|
+
{ "slug": "http-cookies", "name": "Cookies", "tier": 1, "description": "Key-value pairs the browser stores per origin and replays automatically on matching requests." },
|
|
6
|
+
{ "slug": "cookie-attributes", "name": "Cookie attributes", "tier": 1, "description": "Domain, Path, Expires/Max-Age, Secure, HttpOnly and SameSite — each narrows when the browser will send the cookie." },
|
|
7
|
+
{ "slug": "password-hashing", "name": "Password hashing", "tier": 1, "description": "Storing a slow salted hash (bcrypt/argon2) rather than the password, so a database leak is not an account leak." },
|
|
8
|
+
{ "slug": "https-tls", "name": "HTTPS and TLS", "tier": 1, "description": "Transport encryption; without it every credential and token is readable in transit." },
|
|
9
|
+
{ "slug": "base64url-encoding", "name": "base64url encoding", "tier": 1, "description": "URL-safe encoding used by JWT segments — encoding, not encryption, and trivially reversible." },
|
|
10
|
+
{ "slug": "auth-vs-authz", "name": "Authentication vs authorization", "tier": 1, "description": "Who you are versus what you may do; conflating them is a common source of privilege bugs." },
|
|
11
|
+
|
|
12
|
+
{ "slug": "session-auth", "name": "Server-side sessions", "tier": 2, "description": "Server stores session state, the client holds only an opaque session id in a cookie." },
|
|
13
|
+
{ "slug": "token-auth", "name": "Token-based auth", "tier": 2, "description": "The client holds a self-describing credential the server validates without a session store lookup." },
|
|
14
|
+
{ "slug": "jwt-structure", "name": "JWT structure", "tier": 2, "description": "header.payload.signature — three base64url segments, the first two readable by anyone." },
|
|
15
|
+
{ "slug": "jwt-signing", "name": "JWT signing", "tier": 2, "description": "HMAC or asymmetric signature proving the token was issued by someone holding the key." },
|
|
16
|
+
{ "slug": "bearer-scheme", "name": "Bearer token scheme", "tier": 2, "description": "Authorization: Bearer <token> — possession alone is authority, which is exactly the risk." },
|
|
17
|
+
{ "slug": "httponly-cookies", "name": "HttpOnly cookies", "tier": 2, "description": "Marks a cookie unreadable from JavaScript, removing the easiest XSS token-theft path." },
|
|
18
|
+
{ "slug": "secure-cookie-flag", "name": "Secure cookie flag", "tier": 2, "description": "Restricts a cookie to HTTPS connections so it never crosses the wire in cleartext." },
|
|
19
|
+
{ "slug": "samesite-cookies", "name": "SameSite cookies", "tier": 2, "description": "Lax/Strict/None controls whether the browser attaches a cookie to cross-site requests — the main structural CSRF defence." },
|
|
20
|
+
{ "slug": "access-token", "name": "Access tokens", "tier": 2, "description": "Short-lived credential presented on each API call; short life is what limits the blast radius of theft." },
|
|
21
|
+
{ "slug": "refresh-token", "name": "Refresh tokens", "tier": 2, "description": "Long-lived credential exchanged for new access tokens, so the powerful credential is used rarely." },
|
|
22
|
+
{ "slug": "cors-basics", "name": "CORS basics", "tier": 2, "description": "Browser-enforced rules for cross-origin requests, including whether credentials may be attached." },
|
|
23
|
+
{ "slug": "auth-middleware", "name": "Auth middleware", "tier": 2, "description": "The request-pipeline stage that verifies the credential and attaches the identity to the request." },
|
|
24
|
+
|
|
25
|
+
{ "slug": "session-vs-token-tradeoffs", "name": "Sessions vs tokens: tradeoffs", "tier": 3, "description": "Revocability and server state versus statelessness and horizontal scale — there is no free option." },
|
|
26
|
+
{ "slug": "jwt-expiry-claims", "name": "JWT expiry and claims", "tier": 3, "description": "exp, iat, nbf, aud, iss — validating all of them, not just the signature." },
|
|
27
|
+
{ "slug": "token-storage-tradeoffs", "name": "Token storage tradeoffs", "tier": 3, "description": "localStorage, memory, or httpOnly cookie — each trades an XSS exposure against a CSRF exposure." },
|
|
28
|
+
{ "slug": "csrf", "name": "CSRF", "tier": 3, "description": "Forcing a browser to make an authenticated request it did not intend, exploiting automatic cookie attachment." },
|
|
29
|
+
{ "slug": "xss-token-theft", "name": "XSS token theft", "tier": 3, "description": "Injected script reading any credential the JavaScript context can reach." },
|
|
30
|
+
{ "slug": "middleware-order-auth", "name": "Auth middleware ordering", "tier": 3, "description": "Body parsing, CORS, auth and route handlers must run in an order that never leaves a route unprotected." },
|
|
31
|
+
{ "slug": "refresh-token-rotation", "name": "Refresh token rotation", "tier": 3, "description": "Issuing a new refresh token on every use so a stolen one becomes detectable and short-lived." },
|
|
32
|
+
{ "slug": "rbac", "name": "Role-based access control", "tier": 3, "description": "Authorization decided by role assignments rather than per-user checks scattered through handlers." },
|
|
33
|
+
{ "slug": "oauth2-authorization-code", "name": "OAuth2 authorization code flow", "tier": 3, "description": "Delegated authorization via a redirect and a back-channel code exchange." },
|
|
34
|
+
|
|
35
|
+
{ "slug": "pkce", "name": "PKCE", "tier": 4, "description": "Proof Key for Code Exchange — binds the code exchange to the client that started the flow, defeating code interception." },
|
|
36
|
+
{ "slug": "jwt-revocation", "name": "JWT revocation", "tier": 4, "description": "The core weakness of stateless tokens: you cannot un-issue one without reintroducing server state." },
|
|
37
|
+
{ "slug": "session-fixation", "name": "Session fixation", "tier": 4, "description": "Attacker pre-sets a session id and waits for the victim to authenticate into it; regenerate on login." },
|
|
38
|
+
{ "slug": "token-replay-detection", "name": "Token replay detection", "tier": 4, "description": "Detecting reuse of a rotated refresh token as a theft signal, and killing the whole token family." },
|
|
39
|
+
|
|
40
|
+
{ "slug": "key-rotation-jwks", "name": "Signing key rotation and JWKS", "tier": 5, "description": "Publishing and rotating signing keys with `kid` so verification survives a key change without downtime." }
|
|
41
|
+
],
|
|
42
|
+
"edges": [
|
|
43
|
+
{ "from": "http-statelessness", "to": "session-auth", "relation": "prerequisite_of" },
|
|
44
|
+
{ "from": "http-statelessness", "to": "token-auth", "relation": "prerequisite_of" },
|
|
45
|
+
{ "from": "http-cookies", "to": "cookie-attributes", "relation": "prerequisite_of" },
|
|
46
|
+
{ "from": "http-cookies", "to": "session-auth", "relation": "prerequisite_of" },
|
|
47
|
+
{ "from": "cookie-attributes", "to": "httponly-cookies", "relation": "prerequisite_of" },
|
|
48
|
+
{ "from": "cookie-attributes", "to": "secure-cookie-flag", "relation": "prerequisite_of" },
|
|
49
|
+
{ "from": "cookie-attributes", "to": "samesite-cookies", "relation": "prerequisite_of" },
|
|
50
|
+
{ "from": "https-tls", "to": "secure-cookie-flag", "relation": "prerequisite_of" },
|
|
51
|
+
{ "from": "base64url-encoding", "to": "jwt-structure", "relation": "prerequisite_of" },
|
|
52
|
+
{ "from": "jwt-structure", "to": "jwt-signing", "relation": "prerequisite_of" },
|
|
53
|
+
{ "from": "jwt-structure", "to": "jwt-expiry-claims", "relation": "prerequisite_of" },
|
|
54
|
+
{ "from": "token-auth", "to": "jwt-structure", "relation": "prerequisite_of" },
|
|
55
|
+
{ "from": "token-auth", "to": "access-token", "relation": "prerequisite_of" },
|
|
56
|
+
{ "from": "access-token", "to": "refresh-token", "relation": "prerequisite_of" },
|
|
57
|
+
{ "from": "token-auth", "to": "bearer-scheme", "relation": "prerequisite_of" },
|
|
58
|
+
{ "from": "auth-vs-authz", "to": "rbac", "relation": "prerequisite_of" },
|
|
59
|
+
{ "from": "session-auth", "to": "session-vs-token-tradeoffs", "relation": "prerequisite_of" },
|
|
60
|
+
{ "from": "token-auth", "to": "session-vs-token-tradeoffs", "relation": "prerequisite_of" },
|
|
61
|
+
{ "from": "samesite-cookies", "to": "csrf", "relation": "prerequisite_of" },
|
|
62
|
+
{ "from": "httponly-cookies", "to": "xss-token-theft", "relation": "prerequisite_of" },
|
|
63
|
+
{ "from": "xss-token-theft", "to": "token-storage-tradeoffs", "relation": "prerequisite_of" },
|
|
64
|
+
{ "from": "csrf", "to": "token-storage-tradeoffs", "relation": "prerequisite_of" },
|
|
65
|
+
{ "from": "auth-middleware", "to": "middleware-order-auth", "relation": "prerequisite_of" },
|
|
66
|
+
{ "from": "refresh-token", "to": "refresh-token-rotation", "relation": "prerequisite_of" },
|
|
67
|
+
{ "from": "refresh-token-rotation", "to": "token-replay-detection", "relation": "prerequisite_of" },
|
|
68
|
+
{ "from": "jwt-expiry-claims", "to": "jwt-revocation", "relation": "prerequisite_of" },
|
|
69
|
+
{ "from": "session-vs-token-tradeoffs", "to": "jwt-revocation", "relation": "prerequisite_of" },
|
|
70
|
+
{ "from": "session-auth", "to": "session-fixation", "relation": "prerequisite_of" },
|
|
71
|
+
{ "from": "oauth2-authorization-code", "to": "pkce", "relation": "prerequisite_of" },
|
|
72
|
+
{ "from": "token-auth", "to": "oauth2-authorization-code", "relation": "prerequisite_of" },
|
|
73
|
+
{ "from": "jwt-signing", "to": "key-rotation-jwks", "relation": "prerequisite_of" },
|
|
74
|
+
{ "from": "password-hashing", "to": "session-auth", "relation": "related_to" },
|
|
75
|
+
{ "from": "cors-basics", "to": "csrf", "relation": "related_to" },
|
|
76
|
+
{ "from": "auth-middleware", "to": "rbac", "relation": "related_to" }
|
|
77
|
+
]
|
|
78
|
+
}
|
package/dist/seed.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { seedDir } from './paths.js';
|
|
4
|
+
import { isValidSlug } from './slug.js';
|
|
5
|
+
/**
|
|
6
|
+
* Bump when seed content changes so existing installs pick up the new graphs on
|
|
7
|
+
* next start. Seeding is idempotent, so re-running is always safe.
|
|
8
|
+
*/
|
|
9
|
+
export const SEED_VERSION = 1;
|
|
10
|
+
const SEED_VERSION_KEY = 'seed_version';
|
|
11
|
+
export const RELATIONS = ['prerequisite_of', 'related_to', 'part_of'];
|
|
12
|
+
function validateGraph(graph, file) {
|
|
13
|
+
if (!graph.domain)
|
|
14
|
+
throw new Error(`${file}: missing "domain"`);
|
|
15
|
+
if (!Array.isArray(graph.concepts) || graph.concepts.length === 0) {
|
|
16
|
+
throw new Error(`${file}: "concepts" must be a non-empty array`);
|
|
17
|
+
}
|
|
18
|
+
const slugs = new Set();
|
|
19
|
+
for (const c of graph.concepts) {
|
|
20
|
+
if (!isValidSlug(c.slug))
|
|
21
|
+
throw new Error(`${file}: invalid slug "${c.slug}"`);
|
|
22
|
+
if (slugs.has(c.slug))
|
|
23
|
+
throw new Error(`${file}: duplicate slug "${c.slug}"`);
|
|
24
|
+
if (!c.name)
|
|
25
|
+
throw new Error(`${file}: concept "${c.slug}" missing name`);
|
|
26
|
+
if (!Number.isInteger(c.tier) || c.tier < 1 || c.tier > 5) {
|
|
27
|
+
throw new Error(`${file}: concept "${c.slug}" has tier ${c.tier}, expected 1..5`);
|
|
28
|
+
}
|
|
29
|
+
slugs.add(c.slug);
|
|
30
|
+
}
|
|
31
|
+
for (const e of graph.edges ?? []) {
|
|
32
|
+
if (!RELATIONS.includes(e.relation)) {
|
|
33
|
+
throw new Error(`${file}: unknown relation "${e.relation}"`);
|
|
34
|
+
}
|
|
35
|
+
// Edges may only point within the same seed file: cross-domain links are the
|
|
36
|
+
// LLM's job via upsert_concepts, and this keeps each file independently valid.
|
|
37
|
+
if (!slugs.has(e.from))
|
|
38
|
+
throw new Error(`${file}: edge from unknown slug "${e.from}"`);
|
|
39
|
+
if (!slugs.has(e.to))
|
|
40
|
+
throw new Error(`${file}: edge to unknown slug "${e.to}"`);
|
|
41
|
+
if (e.from === e.to)
|
|
42
|
+
throw new Error(`${file}: self-edge on "${e.from}"`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function loadSeedGraphs(dir = seedDir()) {
|
|
46
|
+
return fs
|
|
47
|
+
.readdirSync(dir)
|
|
48
|
+
.filter((f) => f.endsWith('.json'))
|
|
49
|
+
.sort()
|
|
50
|
+
.map((file) => {
|
|
51
|
+
const graph = JSON.parse(fs.readFileSync(path.join(dir, file), 'utf8'));
|
|
52
|
+
validateGraph(graph, file);
|
|
53
|
+
return graph;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Upserts a graph by slug. Mastery rows are never touched — a learner's history
|
|
58
|
+
* survives any number of seed updates.
|
|
59
|
+
*/
|
|
60
|
+
export function applySeedGraph(db, graph) {
|
|
61
|
+
const upsertConcept = db.prepare(`INSERT INTO concepts (slug, name, domain, description, tier, source)
|
|
62
|
+
VALUES (@slug, @name, @domain, @description, @tier, 'seed')
|
|
63
|
+
ON CONFLICT(slug) DO UPDATE SET
|
|
64
|
+
name = excluded.name,
|
|
65
|
+
domain = excluded.domain,
|
|
66
|
+
description = excluded.description,
|
|
67
|
+
tier = excluded.tier,
|
|
68
|
+
source = 'seed'`);
|
|
69
|
+
const idOf = db.prepare('SELECT id FROM concepts WHERE slug = ?');
|
|
70
|
+
const insertEdge = db.prepare(`INSERT OR IGNORE INTO edges (from_concept, to_concept, relation) VALUES (?, ?, ?)`);
|
|
71
|
+
let edges = 0;
|
|
72
|
+
db.transaction(() => {
|
|
73
|
+
for (const c of graph.concepts) {
|
|
74
|
+
upsertConcept.run({
|
|
75
|
+
slug: c.slug,
|
|
76
|
+
name: c.name,
|
|
77
|
+
domain: c.domain ?? graph.domain,
|
|
78
|
+
description: c.description ?? null,
|
|
79
|
+
tier: c.tier,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
for (const e of graph.edges ?? []) {
|
|
83
|
+
const from = idOf.get(e.from);
|
|
84
|
+
const to = idOf.get(e.to);
|
|
85
|
+
if (!from || !to)
|
|
86
|
+
continue;
|
|
87
|
+
insertEdge.run(from.id, to.id, e.relation);
|
|
88
|
+
edges += 1;
|
|
89
|
+
}
|
|
90
|
+
})();
|
|
91
|
+
return { concepts: graph.concepts.length, edges, domains: [graph.domain] };
|
|
92
|
+
}
|
|
93
|
+
export function seedAll(db, dir = seedDir()) {
|
|
94
|
+
const summary = { concepts: 0, edges: 0, domains: [] };
|
|
95
|
+
for (const graph of loadSeedGraphs(dir)) {
|
|
96
|
+
const s = applySeedGraph(db, graph);
|
|
97
|
+
summary.concepts += s.concepts;
|
|
98
|
+
summary.edges += s.edges;
|
|
99
|
+
summary.domains.push(graph.domain);
|
|
100
|
+
}
|
|
101
|
+
db.prepare(`INSERT INTO meta (key, value) VALUES (?, ?)
|
|
102
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`).run(SEED_VERSION_KEY, String(SEED_VERSION));
|
|
103
|
+
return summary;
|
|
104
|
+
}
|
|
105
|
+
/** Seeds on first run, and again whenever SEED_VERSION moves. */
|
|
106
|
+
export function seedIfNeeded(db, dir = seedDir()) {
|
|
107
|
+
const row = db.prepare('SELECT value FROM meta WHERE key = ?').get(SEED_VERSION_KEY);
|
|
108
|
+
if (row && Number(row.value) === SEED_VERSION)
|
|
109
|
+
return null;
|
|
110
|
+
return seedAll(db, dir);
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=seed.js.map
|
package/dist/seed.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seed.js","sourceRoot":"","sources":["../src/seed.ts"],"names":[],"mappings":"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"}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { openDb } from './db.js';
|
|
5
|
+
import { dbPath } from './paths.js';
|
|
6
|
+
import { registerTools } from './tools/index.js';
|
|
7
|
+
// stdout is the MCP transport. Anything diagnostic goes to stderr or it corrupts
|
|
8
|
+
// the protocol stream.
|
|
9
|
+
function log(message) {
|
|
10
|
+
process.stderr.write(`[eklavya-mcp] ${message}\n`);
|
|
11
|
+
}
|
|
12
|
+
async function main() {
|
|
13
|
+
const db = openDb();
|
|
14
|
+
log(`db ready at ${dbPath()}`);
|
|
15
|
+
const server = new McpServer({ name: 'eklavya', version: '0.1.0' }, {
|
|
16
|
+
capabilities: { tools: {} },
|
|
17
|
+
instructions: 'Eklavya tracks what this developer has actually learned. Log the concepts your work touches, ' +
|
|
18
|
+
'and quiz from the learner profile rather than from scratch — never ask about a concept already mastered.',
|
|
19
|
+
});
|
|
20
|
+
registerTools(server, db);
|
|
21
|
+
const shutdown = () => {
|
|
22
|
+
try {
|
|
23
|
+
db.close();
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// closing a already-closed handle on shutdown is not worth reporting
|
|
27
|
+
}
|
|
28
|
+
process.exit(0);
|
|
29
|
+
};
|
|
30
|
+
process.on('SIGINT', shutdown);
|
|
31
|
+
process.on('SIGTERM', shutdown);
|
|
32
|
+
await server.connect(new StdioServerTransport());
|
|
33
|
+
log('server connected over stdio');
|
|
34
|
+
}
|
|
35
|
+
main().catch((err) => {
|
|
36
|
+
log(`fatal: ${err instanceof Error ? err.stack ?? err.message : String(err)}`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
});
|
|
39
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEjD,iFAAiF;AACjF,uBAAuB;AACvB,SAAS,GAAG,CAAC,OAAe;IAC1B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,iBAAiB,OAAO,IAAI,CAAC,CAAC;AACrD,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC;IACpB,GAAG,CAAC,eAAe,MAAM,EAAE,EAAE,CAAC,CAAC;IAE/B,MAAM,MAAM,GAAG,IAAI,SAAS,CAC1B,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,EACrC;QACE,YAAY,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE;QAC3B,YAAY,EACV,+FAA+F;YAC/F,0GAA0G;KAC7G,CACF,CAAC;IAEF,aAAa,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAE1B,MAAM,QAAQ,GAAG,GAAG,EAAE;QACpB,IAAI,CAAC;YACH,EAAE,CAAC,KAAK,EAAE,CAAC;QACb,CAAC;QAAC,MAAM,CAAC;YACP,qEAAqE;QACvE,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC;IACF,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAC/B,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IAEhC,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,oBAAoB,EAAE,CAAC,CAAC;IACjD,GAAG,CAAC,6BAA6B,CAAC,CAAC;AACrC,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;IAC5B,GAAG,CAAC,UAAU,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAC/E,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
package/dist/session.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const CURRENT_SESSION_KEY = 'current_session';
|
|
2
|
+
export const FALLBACK_SESSION_ID = 'default';
|
|
3
|
+
export function getCurrentSession(db) {
|
|
4
|
+
const row = db.prepare('SELECT value FROM meta WHERE key = ?').get(CURRENT_SESSION_KEY);
|
|
5
|
+
return row?.value ?? null;
|
|
6
|
+
}
|
|
7
|
+
export function setCurrentSession(db, sessionId) {
|
|
8
|
+
db.prepare(`INSERT INTO meta (key, value) VALUES (?, ?)
|
|
9
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`).run(CURRENT_SESSION_KEY, sessionId);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The model cannot see its own Claude Code session id, but the Phase 2 hooks
|
|
13
|
+
* receive the real one on stdin — so both sides have to agree on a resolution
|
|
14
|
+
* order or the hooks query rows that were written under a different key
|
|
15
|
+
* (phase-1 decision G1).
|
|
16
|
+
*/
|
|
17
|
+
export function resolveSessionId(db, explicit) {
|
|
18
|
+
const candidate = (explicit && explicit.trim()) ||
|
|
19
|
+
(process.env.EKLAVYA_SESSION_ID && process.env.EKLAVYA_SESSION_ID.trim()) ||
|
|
20
|
+
getCurrentSession(db) ||
|
|
21
|
+
FALLBACK_SESSION_ID;
|
|
22
|
+
return candidate;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=session.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAEA,MAAM,mBAAmB,GAAG,iBAAiB,CAAC;AAC9C,MAAM,CAAC,MAAM,mBAAmB,GAAG,SAAS,CAAC;AAE7C,MAAM,UAAU,iBAAiB,CAAC,EAAM;IACtC,MAAM,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,sCAAsC,CAAC,CAAC,GAAG,CAAC,mBAAmB,CAEzE,CAAC;IACd,OAAO,GAAG,EAAE,KAAK,IAAI,IAAI,CAAC;AAC5B,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,EAAM,EAAE,SAAiB;IACzD,EAAE,CAAC,OAAO,CACR;2DACuD,CACxD,CAAC,GAAG,CAAC,mBAAmB,EAAE,SAAS,CAAC,CAAC;AACxC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,EAAM,EAAE,QAAwB;IAC/D,MAAM,SAAS,GACb,CAAC,QAAQ,IAAI,QAAQ,CAAC,IAAI,EAAE,CAAC;QAC7B,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,IAAI,EAAE,CAAC;QACzE,iBAAiB,CAAC,EAAE,CAAC;QACrB,mBAAmB,CAAC;IAEtB,OAAO,SAAS,CAAC;AACnB,CAAC"}
|
package/dist/slug.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Concept slugs are the join key across the whole system, and the LLM is allowed
|
|
3
|
+
* to mint new ones (PRD §8 tool 6). Normalizing hard here is what keeps slug
|
|
4
|
+
* sprawl from turning the graph into mush (PRD §15).
|
|
5
|
+
*/
|
|
6
|
+
export function normalizeSlug(input) {
|
|
7
|
+
return input
|
|
8
|
+
.normalize('NFKD')
|
|
9
|
+
.toLowerCase()
|
|
10
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
11
|
+
.replace(/^-+|-+$/g, '')
|
|
12
|
+
.replace(/-{2,}/g, '-')
|
|
13
|
+
.slice(0, 80);
|
|
14
|
+
}
|
|
15
|
+
export function isValidSlug(slug) {
|
|
16
|
+
return /^[a-z0-9]+(-[a-z0-9]+)*$/.test(slug) && slug.length <= 80;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Token-set similarity between two slugs.
|
|
20
|
+
*/
|
|
21
|
+
export function tokenJaccard(a, b) {
|
|
22
|
+
const ta = new Set(a.split('-').filter(Boolean));
|
|
23
|
+
const tb = new Set(b.split('-').filter(Boolean));
|
|
24
|
+
if (ta.size === 0 || tb.size === 0)
|
|
25
|
+
return 0;
|
|
26
|
+
let intersection = 0;
|
|
27
|
+
for (const t of ta)
|
|
28
|
+
if (tb.has(t))
|
|
29
|
+
intersection += 1;
|
|
30
|
+
const union = ta.size + tb.size - intersection;
|
|
31
|
+
return union === 0 ? 0 : intersection / union;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Words that add no meaning to a concept name. Stripping these is what lets
|
|
35
|
+
* `jwt-structure-basics` find `jwt-structure` without also merging
|
|
36
|
+
* `refresh-token-rotation` into `refresh-token` — both pairs look identical to a
|
|
37
|
+
* similarity score, but only one of them is the same idea twice.
|
|
38
|
+
*/
|
|
39
|
+
const QUALIFIER_TOKENS = new Set([
|
|
40
|
+
'basic', 'basics', 'fundamental', 'fundamentals', 'intro', 'introduction',
|
|
41
|
+
'overview', 'explained', 'explainer', 'concept', 'concepts', 'general',
|
|
42
|
+
'generic', 'guide', 'tutorial', 'usage', 'primer', '101',
|
|
43
|
+
'strategy', 'strategies', 'approach', 'approaches',
|
|
44
|
+
]);
|
|
45
|
+
export function stripQualifiers(slug) {
|
|
46
|
+
const tokens = slug.split('-').filter(Boolean);
|
|
47
|
+
while (tokens.length > 1 && QUALIFIER_TOKENS.has(tokens[tokens.length - 1]))
|
|
48
|
+
tokens.pop();
|
|
49
|
+
while (tokens.length > 1 && QUALIFIER_TOKENS.has(tokens[0]))
|
|
50
|
+
tokens.shift();
|
|
51
|
+
return tokens.join('-');
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Deliberately strict: 1.0 only catches a pure token reordering
|
|
55
|
+
* (`structure-jwt` ~ `jwt-structure`). Everything else has to survive the
|
|
56
|
+
* qualifier strip above. A test asserts no two shipped seed concepts match.
|
|
57
|
+
*/
|
|
58
|
+
export const FUZZY_MATCH_THRESHOLD = 0.8;
|
|
59
|
+
export function findFuzzyMatch(slug, candidates, threshold = FUZZY_MATCH_THRESHOLD) {
|
|
60
|
+
const stripped = stripQualifiers(slug);
|
|
61
|
+
const sameOnceQualifiersGo = candidates.find((c) => stripQualifiers(c.slug) === stripped);
|
|
62
|
+
if (sameOnceQualifiersGo)
|
|
63
|
+
return sameOnceQualifiersGo;
|
|
64
|
+
let best;
|
|
65
|
+
let bestScore = 0;
|
|
66
|
+
for (const candidate of candidates) {
|
|
67
|
+
const score = tokenJaccard(stripped, stripQualifiers(candidate.slug));
|
|
68
|
+
if (score > bestScore) {
|
|
69
|
+
bestScore = score;
|
|
70
|
+
best = candidate;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return bestScore >= threshold ? best : undefined;
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=slug.js.map
|
package/dist/slug.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slug.js","sourceRoot":"","sources":["../src/slug.ts"],"names":[],"mappings":"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"}
|