connectry-architect-mcp 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +315 -27
- package/dist/index.js.map +1 -1
- package/dist/ui/dashboard.html +1003 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -254,8 +254,8 @@ import fs3 from "fs";
|
|
|
254
254
|
import path3 from "path";
|
|
255
255
|
import { fileURLToPath } from "url";
|
|
256
256
|
var __filename = fileURLToPath(import.meta.url);
|
|
257
|
-
var
|
|
258
|
-
var DATA_DIR = fs3.existsSync(path3.join(
|
|
257
|
+
var __dirname2 = path3.dirname(__filename);
|
|
258
|
+
var DATA_DIR = fs3.existsSync(path3.join(__dirname2, "data", "curriculum.json")) ? path3.join(__dirname2, "data") : __dirname2;
|
|
259
259
|
var cachedCurriculum = null;
|
|
260
260
|
var cachedQuestions = null;
|
|
261
261
|
function loadCurriculum() {
|
|
@@ -402,7 +402,11 @@ IMPORTANT \u2014 after showing the result, present followUpOptions using AskUser
|
|
|
402
402
|
- header: "Next"
|
|
403
403
|
- question: Show whether they got it right/wrong and a brief explanation
|
|
404
404
|
- options: Map each followUpOption to label (key) and description (label text)
|
|
405
|
-
Then call follow_up with questionId and the selected action key
|
|
405
|
+
Then call follow_up with questionId and the selected action key.
|
|
406
|
+
|
|
407
|
+
EDGE CASES:
|
|
408
|
+
- "Other": Answer the user's question about this answer, then re-present the SAME follow-up options via AskUserQuestion.
|
|
409
|
+
- "Skip": Treat as "next question" \u2014 call follow_up with action "next".`,
|
|
406
410
|
{
|
|
407
411
|
questionId: z.string().describe("The question ID to answer"),
|
|
408
412
|
answer: z.enum(["A", "B", "C", "D"]).describe("The selected answer")
|
|
@@ -643,7 +647,12 @@ IMPORTANT \u2014 present the question using AskUserQuestion:
|
|
|
643
647
|
- header: "Answer"
|
|
644
648
|
- question: Include the FULL scenario text AND question text from the response
|
|
645
649
|
- options: 4 items with label "A"/"B"/"C"/"D" and description as the option text
|
|
646
|
-
|
|
650
|
+
- If the scenario contains code, add a "preview" field on each option showing the code snippet
|
|
651
|
+
Then call submit_answer with the questionId and selected answer.
|
|
652
|
+
|
|
653
|
+
EDGE CASES:
|
|
654
|
+
- "Other": Answer the user's question, then re-present the SAME question via AskUserQuestion.
|
|
655
|
+
- "Skip": Call get_practice_question again for a new question. Never break the flow.`,
|
|
647
656
|
{
|
|
648
657
|
domainId: z3.number().optional().describe("Optional domain ID to filter questions (1-5)"),
|
|
649
658
|
difficulty: z3.enum(["easy", "medium", "hard"]).optional().describe("Optional difficulty filter")
|
|
@@ -741,6 +750,7 @@ IMPORTANT \u2014 follow this flow for EVERY question:
|
|
|
741
750
|
- header: "Q[number]"
|
|
742
751
|
- question: Include the FULL scenario text AND question text from the response
|
|
743
752
|
- options: Use the 4 answer options (A/B/C/D) with label as the letter and description as the option text
|
|
753
|
+
- If the scenario contains code, add a "preview" field on each option showing the relevant code snippet so the user can reference it while choosing
|
|
744
754
|
|
|
745
755
|
3. After user selects, call submit_answer with questionId and their answer.
|
|
746
756
|
|
|
@@ -748,6 +758,16 @@ IMPORTANT \u2014 follow this flow for EVERY question:
|
|
|
748
758
|
|
|
749
759
|
5. Call start_assessment again for the next question.
|
|
750
760
|
|
|
761
|
+
EDGE CASES:
|
|
762
|
+
- If user selects "Other" and types a question/comment: Answer their question helpfully, then re-present the SAME quiz question using AskUserQuestion again. Never lose the current question.
|
|
763
|
+
- If user clicks "Skip": Treat it as moving to the next question. Call start_assessment again immediately. The skipped question remains unanswered and will appear again later.
|
|
764
|
+
- NEVER let Other or Skip break the assessment flow. Always continue to the next question or re-ask the current one.
|
|
765
|
+
|
|
766
|
+
PROGRESS TRACKING:
|
|
767
|
+
- At the START of the assessment, create a TodoWrite checklist with all 15 questions (Q1-Q15) grouped by domain, all set to "pending".
|
|
768
|
+
- After each answer, update the corresponding todo item to "completed" (with correct/incorrect note).
|
|
769
|
+
- This gives the user a visual progress tracker throughout the assessment.
|
|
770
|
+
|
|
751
771
|
When assessment is complete, present next steps using AskUserQuestion with header "Next step".`,
|
|
752
772
|
{},
|
|
753
773
|
async () => {
|
|
@@ -773,12 +793,12 @@ When assessment is complete, present next steps using AskUserQuestion with heade
|
|
|
773
793
|
const totalCorrect = results.reduce((sum, r) => sum + r.correct, 0);
|
|
774
794
|
const totalQuestions = results.reduce((sum, r) => sum + r.total, 0);
|
|
775
795
|
const overallAccuracy = totalQuestions > 0 ? Math.round(totalCorrect / totalQuestions * 100) : 0;
|
|
776
|
-
const
|
|
777
|
-
db2.prepare("UPDATE users SET assessmentCompleted = TRUE, learningPath = ? WHERE id = ?").run(
|
|
796
|
+
const path7 = overallAccuracy >= 60 ? "exam-weighted" : "beginner-friendly";
|
|
797
|
+
db2.prepare("UPDATE users SET assessmentCompleted = TRUE, learningPath = ? WHERE id = ?").run(path7, userId);
|
|
778
798
|
const response2 = {
|
|
779
799
|
status: "complete",
|
|
780
800
|
overall: { correct: totalCorrect, total: totalQuestions, accuracy: overallAccuracy },
|
|
781
|
-
learningPath:
|
|
801
|
+
learningPath: path7 === "exam-weighted" ? "Exam-Weighted" : "Beginner-Friendly",
|
|
782
802
|
domainResults: results.map((r) => ({
|
|
783
803
|
domain: r.domainId,
|
|
784
804
|
name: DOMAIN_NAMES[r.domainId] ?? "",
|
|
@@ -861,11 +881,11 @@ function registerGetWeakAreas(server2, db2, userConfig2) {
|
|
|
861
881
|
// src/engine/adaptive-path.ts
|
|
862
882
|
var BEGINNER_ORDER = [3, 4, 2, 1, 5];
|
|
863
883
|
var EXAM_WEIGHTED_ORDER = [1, 3, 4, 2, 5];
|
|
864
|
-
function getDomainOrder(
|
|
865
|
-
return
|
|
884
|
+
function getDomainOrder(path7) {
|
|
885
|
+
return path7 === "beginner-friendly" ? BEGINNER_ORDER : EXAM_WEIGHTED_ORDER;
|
|
866
886
|
}
|
|
867
|
-
function getNextRecommendedDomain(
|
|
868
|
-
const order = getDomainOrder(
|
|
887
|
+
function getNextRecommendedDomain(path7, masteryByDomain) {
|
|
888
|
+
const order = getDomainOrder(path7);
|
|
869
889
|
for (const domainId of order) {
|
|
870
890
|
const masteries = masteryByDomain.get(domainId) ?? [];
|
|
871
891
|
const avgAccuracy = masteries.length > 0 ? masteries.reduce((sum, m) => sum + m.accuracyPercent, 0) / masteries.length : 0;
|
|
@@ -896,7 +916,11 @@ function estimateTimeRemaining(totalQuestions, answeredQuestions, avgSecondsPerQ
|
|
|
896
916
|
function registerGetStudyPlan(server2, db2, userConfig2) {
|
|
897
917
|
server2.tool(
|
|
898
918
|
"get_study_plan",
|
|
899
|
-
|
|
919
|
+
`Get a personalized study plan based on your assessment results, weak areas, and learning path.
|
|
920
|
+
|
|
921
|
+
IMPORTANT \u2014 after showing the study plan, use AskUserQuestion with header "Focus" and multiSelect: true to let the user pick which domains they want to focus on. Options should be the 5 domains with their current mastery as descriptions. Then use their selection to filter get_practice_question calls.
|
|
922
|
+
|
|
923
|
+
Also use TodoWrite to create a study checklist showing each recommended topic with status (pending/in_progress/completed) so the user can track progress visually.`,
|
|
900
924
|
{},
|
|
901
925
|
async () => {
|
|
902
926
|
const userId = userConfig2.userId;
|
|
@@ -907,20 +931,20 @@ function registerGetStudyPlan(server2, db2, userConfig2) {
|
|
|
907
931
|
const overdueReviews = getOverdueReviews(db2, userId);
|
|
908
932
|
const stats = getTotalStats(db2, userId);
|
|
909
933
|
const allQuestions = loadQuestions();
|
|
910
|
-
const
|
|
934
|
+
const path7 = user?.learningPath ?? "beginner-friendly";
|
|
911
935
|
const masteryByDomain = /* @__PURE__ */ new Map();
|
|
912
936
|
for (const m of mastery) {
|
|
913
937
|
const existing = masteryByDomain.get(m.domainId) ?? [];
|
|
914
938
|
masteryByDomain.set(m.domainId, [...existing, m]);
|
|
915
939
|
}
|
|
916
|
-
const nextDomain = getNextRecommendedDomain(
|
|
917
|
-
const domainOrder = getDomainOrder(
|
|
940
|
+
const nextDomain = getNextRecommendedDomain(path7, masteryByDomain);
|
|
941
|
+
const domainOrder = getDomainOrder(path7);
|
|
918
942
|
const timeEstimate = estimateTimeRemaining(allQuestions.length, stats.total);
|
|
919
943
|
const domain = curriculum.domains.find((d) => d.id === nextDomain);
|
|
920
944
|
const lines = [
|
|
921
945
|
"\u2550\u2550\u2550 YOUR STUDY PLAN \u2550\u2550\u2550",
|
|
922
946
|
"",
|
|
923
|
-
`Learning Path: ${
|
|
947
|
+
`Learning Path: ${path7}`,
|
|
924
948
|
`Estimated Time Remaining: ${timeEstimate}`,
|
|
925
949
|
"",
|
|
926
950
|
`Next Recommended Domain: D${nextDomain} \u2014 ${domain?.title ?? "Unknown"}`,
|
|
@@ -944,8 +968,8 @@ import fs4 from "fs";
|
|
|
944
968
|
import path4 from "path";
|
|
945
969
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
946
970
|
import { z as z4 } from "zod";
|
|
947
|
-
var
|
|
948
|
-
var PROJECTS_DIR = path4.resolve(
|
|
971
|
+
var __dirname3 = path4.dirname(fileURLToPath2(import.meta.url));
|
|
972
|
+
var PROJECTS_DIR = path4.resolve(__dirname3, "..", "..", "projects");
|
|
949
973
|
var PROJECTS = [
|
|
950
974
|
{ id: "capstone", name: "Capstone \u2014 Multi-Agent Research System", domains: [1, 2, 3, 4, 5] },
|
|
951
975
|
{ id: "d1-agentic", name: "D1 Mini \u2014 Agentic Loop", domains: [1] },
|
|
@@ -1198,7 +1222,14 @@ IMPORTANT \u2014 present the first question using AskUserQuestion:
|
|
|
1198
1222
|
- header: "Q1"
|
|
1199
1223
|
- question: Include the FULL scenario + question text
|
|
1200
1224
|
- options: 4 items with label "A"/"B"/"C"/"D" and description as option text
|
|
1201
|
-
|
|
1225
|
+
- If code in scenario, add preview field on options
|
|
1226
|
+
Then call submit_exam_answer with the answer.
|
|
1227
|
+
|
|
1228
|
+
PROGRESS TRACKING: Create a TodoWrite checklist "Practice Exam Q1-Q60" grouped by domain, all "pending". Update each to "completed" after grading.
|
|
1229
|
+
|
|
1230
|
+
EDGE CASES:
|
|
1231
|
+
- "Other": Answer the question, re-present the SAME exam question via AskUserQuestion.
|
|
1232
|
+
- "Skip": Move to next exam question without grading. Never break the flow.`,
|
|
1202
1233
|
{},
|
|
1203
1234
|
async () => {
|
|
1204
1235
|
const userId = userConfig2.userId;
|
|
@@ -2450,7 +2481,20 @@ User selected next action: ${selected}` }]
|
|
|
2450
2481
|
function registerCapstoneBuildStep(server2, db2, userConfig2) {
|
|
2451
2482
|
server2.tool(
|
|
2452
2483
|
"capstone_build_step",
|
|
2453
|
-
|
|
2484
|
+
`Drive your guided capstone build \u2014 quiz, build, and advance through 18 progressive steps.
|
|
2485
|
+
|
|
2486
|
+
IMPORTANT:
|
|
2487
|
+
- When presenting quiz questions, use AskUserQuestion with header "Answer" for A/B/C/D selection. If code is in the scenario, add preview fields.
|
|
2488
|
+
- When presenting action choices (quiz/build/next), use AskUserQuestion with header "Action".
|
|
2489
|
+
|
|
2490
|
+
PROGRESS TRACKING:
|
|
2491
|
+
- On "confirm": Create a TodoWrite checklist with all 18 build steps, all set to "pending".
|
|
2492
|
+
- On "next": Update the completed step to "completed" and the new current step to "in_progress".
|
|
2493
|
+
- This gives the user a visual build progress tracker.
|
|
2494
|
+
|
|
2495
|
+
EDGE CASES:
|
|
2496
|
+
- "Other": Answer the question, then re-present the current options via AskUserQuestion.
|
|
2497
|
+
- "Skip": During quiz, treat as moving to the build phase. During build, treat as advancing to next step.`,
|
|
2454
2498
|
{
|
|
2455
2499
|
action: z9.enum(ACTIONS).describe("The build action: confirm, quiz, build, next, status, or abandon")
|
|
2456
2500
|
},
|
|
@@ -2629,6 +2673,249 @@ function registerCapstoneBuildStatus(server2, db2, userConfig2) {
|
|
|
2629
2673
|
);
|
|
2630
2674
|
}
|
|
2631
2675
|
|
|
2676
|
+
// src/ui/server.ts
|
|
2677
|
+
import http from "http";
|
|
2678
|
+
import fs5 from "fs";
|
|
2679
|
+
import path5 from "path";
|
|
2680
|
+
var DOMAIN_NAMES3 = {
|
|
2681
|
+
1: "Agentic Architecture",
|
|
2682
|
+
2: "Tool Design & MCP",
|
|
2683
|
+
3: "Claude Code Config",
|
|
2684
|
+
4: "Prompt Engineering",
|
|
2685
|
+
5: "Context & Reliability"
|
|
2686
|
+
};
|
|
2687
|
+
var ALL_DOMAIN_IDS = [1, 2, 3, 4, 5];
|
|
2688
|
+
function buildDashboardData(db2, userId) {
|
|
2689
|
+
const domainRows = db2.prepare(`
|
|
2690
|
+
SELECT domainId,
|
|
2691
|
+
SUM(totalAttempts) as totalAttempts,
|
|
2692
|
+
AVG(accuracyPercent) as avgAccuracy,
|
|
2693
|
+
MIN(masteryLevel) as masteryLevel,
|
|
2694
|
+
COUNT(*) as taskCount
|
|
2695
|
+
FROM domain_mastery
|
|
2696
|
+
WHERE userId = ?
|
|
2697
|
+
GROUP BY domainId
|
|
2698
|
+
ORDER BY domainId ASC
|
|
2699
|
+
`).all(userId);
|
|
2700
|
+
const domainMap = new Map(domainRows.map((r) => [r.domainId, r]));
|
|
2701
|
+
const domains = ALL_DOMAIN_IDS.map((id) => {
|
|
2702
|
+
const row = domainMap.get(id);
|
|
2703
|
+
return {
|
|
2704
|
+
id,
|
|
2705
|
+
name: DOMAIN_NAMES3[id] ?? `Domain ${id}`,
|
|
2706
|
+
mastery: row ? Math.round(row.avgAccuracy) : 0,
|
|
2707
|
+
level: row ? deriveDomainLevel(row.avgAccuracy) : "unassessed",
|
|
2708
|
+
answered: row ? row.totalAttempts : 0,
|
|
2709
|
+
total: row ? row.taskCount : 0
|
|
2710
|
+
};
|
|
2711
|
+
});
|
|
2712
|
+
const overallReadiness = domains.length > 0 ? Math.round(domains.reduce((sum, d) => sum + d.mastery, 0) / domains.length) : 0;
|
|
2713
|
+
const examRows = db2.prepare(`
|
|
2714
|
+
SELECT completedAt, score, passed
|
|
2715
|
+
FROM exam_attempts
|
|
2716
|
+
WHERE userId = ? AND completedAt IS NOT NULL
|
|
2717
|
+
ORDER BY completedAt DESC
|
|
2718
|
+
`).all(userId);
|
|
2719
|
+
const examHistory = examRows.map((r) => ({
|
|
2720
|
+
date: r.completedAt,
|
|
2721
|
+
score: r.score,
|
|
2722
|
+
passed: Boolean(r.passed)
|
|
2723
|
+
}));
|
|
2724
|
+
const answerRows = db2.prepare(`
|
|
2725
|
+
SELECT questionId, domainId, isCorrect, answeredAt
|
|
2726
|
+
FROM answers
|
|
2727
|
+
WHERE userId = ?
|
|
2728
|
+
ORDER BY answeredAt DESC
|
|
2729
|
+
LIMIT 10
|
|
2730
|
+
`).all(userId);
|
|
2731
|
+
const recentActivity = answerRows.map((r) => ({
|
|
2732
|
+
questionId: r.questionId,
|
|
2733
|
+
domain: DOMAIN_NAMES3[r.domainId] ?? `Domain ${r.domainId}`,
|
|
2734
|
+
correct: Boolean(r.isCorrect),
|
|
2735
|
+
timestamp: r.answeredAt
|
|
2736
|
+
}));
|
|
2737
|
+
const capstoneRow = db2.prepare(`
|
|
2738
|
+
SELECT id, theme, currentStep
|
|
2739
|
+
FROM capstone_builds
|
|
2740
|
+
WHERE userId = ? AND status IN ('shaping', 'building')
|
|
2741
|
+
ORDER BY createdAt DESC
|
|
2742
|
+
LIMIT 1
|
|
2743
|
+
`).get(userId);
|
|
2744
|
+
let capstoneBuild = null;
|
|
2745
|
+
if (capstoneRow) {
|
|
2746
|
+
const steps = db2.prepare(`
|
|
2747
|
+
SELECT buildCompleted, taskStatements
|
|
2748
|
+
FROM capstone_build_steps
|
|
2749
|
+
WHERE buildId = ?
|
|
2750
|
+
ORDER BY stepIndex ASC
|
|
2751
|
+
`).all(capstoneRow.id);
|
|
2752
|
+
const coveredCriteria = /* @__PURE__ */ new Set();
|
|
2753
|
+
for (const step of steps) {
|
|
2754
|
+
if (!step.buildCompleted) continue;
|
|
2755
|
+
const taskStatements = JSON.parse(step.taskStatements);
|
|
2756
|
+
for (const ts of taskStatements) {
|
|
2757
|
+
coveredCriteria.add(ts);
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
capstoneBuild = {
|
|
2761
|
+
theme: capstoneRow.theme,
|
|
2762
|
+
currentStep: capstoneRow.currentStep,
|
|
2763
|
+
totalSteps: BUILD_STEPS.length,
|
|
2764
|
+
criteriaCompleted: coveredCriteria.size,
|
|
2765
|
+
totalCriteria: BUILD_STEPS.reduce((sum, s) => sum + s.taskStatements.length, 0)
|
|
2766
|
+
};
|
|
2767
|
+
}
|
|
2768
|
+
return { overallReadiness, domains, examHistory, recentActivity, capstoneBuild };
|
|
2769
|
+
}
|
|
2770
|
+
function deriveDomainLevel(avgAccuracy) {
|
|
2771
|
+
if (avgAccuracy >= 90) return "mastered";
|
|
2772
|
+
if (avgAccuracy >= 70) return "strong";
|
|
2773
|
+
if (avgAccuracy >= 50) return "developing";
|
|
2774
|
+
if (avgAccuracy > 0) return "weak";
|
|
2775
|
+
return "unassessed";
|
|
2776
|
+
}
|
|
2777
|
+
var MIME_TYPES = {
|
|
2778
|
+
".html": "text/html; charset=utf-8",
|
|
2779
|
+
".css": "text/css; charset=utf-8",
|
|
2780
|
+
".js": "application/javascript; charset=utf-8",
|
|
2781
|
+
".json": "application/json; charset=utf-8",
|
|
2782
|
+
".png": "image/png",
|
|
2783
|
+
".svg": "image/svg+xml"
|
|
2784
|
+
};
|
|
2785
|
+
function getMimeType(filePath) {
|
|
2786
|
+
const ext = path5.extname(filePath).toLowerCase();
|
|
2787
|
+
return MIME_TYPES[ext] ?? "application/octet-stream";
|
|
2788
|
+
}
|
|
2789
|
+
function startDashboardServer(db2, userConfig2) {
|
|
2790
|
+
const distUiDir = path5.resolve(__dirname, "../../dist/ui");
|
|
2791
|
+
const userId = userConfig2.userId;
|
|
2792
|
+
const server2 = http.createServer((req, res) => {
|
|
2793
|
+
const url = new URL(req.url ?? "/", `http://localhost`);
|
|
2794
|
+
const pathname = url.pathname;
|
|
2795
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
2796
|
+
if (pathname === "/api/data") {
|
|
2797
|
+
const data = buildDashboardData(db2, userId);
|
|
2798
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
2799
|
+
res.end(JSON.stringify(data));
|
|
2800
|
+
return;
|
|
2801
|
+
}
|
|
2802
|
+
if (pathname === "/dashboard" || pathname === "/") {
|
|
2803
|
+
const htmlPath = path5.join(distUiDir, "dashboard.html");
|
|
2804
|
+
let html;
|
|
2805
|
+
try {
|
|
2806
|
+
html = fs5.readFileSync(htmlPath, "utf-8");
|
|
2807
|
+
} catch {
|
|
2808
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
2809
|
+
res.end("dashboard.html not found");
|
|
2810
|
+
return;
|
|
2811
|
+
}
|
|
2812
|
+
const data = buildDashboardData(db2, userId);
|
|
2813
|
+
const injection = `<script>window.__DASHBOARD_DATA__ = ${JSON.stringify(data)};</script>`;
|
|
2814
|
+
const injectedHtml = html.replace("</head>", `${injection}
|
|
2815
|
+
</head>`);
|
|
2816
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
2817
|
+
res.end(injectedHtml);
|
|
2818
|
+
return;
|
|
2819
|
+
}
|
|
2820
|
+
const safePath = path5.normalize(pathname).replace(/^(\.\.[/\\])+/, "");
|
|
2821
|
+
const filePath = path5.join(distUiDir, safePath);
|
|
2822
|
+
if (!filePath.startsWith(distUiDir)) {
|
|
2823
|
+
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
2824
|
+
res.end("Forbidden");
|
|
2825
|
+
return;
|
|
2826
|
+
}
|
|
2827
|
+
try {
|
|
2828
|
+
const content = fs5.readFileSync(filePath);
|
|
2829
|
+
res.writeHead(200, { "Content-Type": getMimeType(filePath) });
|
|
2830
|
+
res.end(content);
|
|
2831
|
+
} catch {
|
|
2832
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
2833
|
+
res.end("Not found");
|
|
2834
|
+
}
|
|
2835
|
+
});
|
|
2836
|
+
return new Promise((resolve2, reject) => {
|
|
2837
|
+
server2.on("error", reject);
|
|
2838
|
+
server2.listen(0, "127.0.0.1", () => {
|
|
2839
|
+
const addr = server2.address();
|
|
2840
|
+
if (!addr || typeof addr === "string") {
|
|
2841
|
+
reject(new Error("Failed to get server address"));
|
|
2842
|
+
return;
|
|
2843
|
+
}
|
|
2844
|
+
resolve2({
|
|
2845
|
+
port: addr.port,
|
|
2846
|
+
close: () => server2.close()
|
|
2847
|
+
});
|
|
2848
|
+
});
|
|
2849
|
+
});
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
// src/tools/dashboard.ts
|
|
2853
|
+
var cachedServer = null;
|
|
2854
|
+
function registerDashboard(server2, db2, userConfig2) {
|
|
2855
|
+
server2.tool(
|
|
2856
|
+
"get_dashboard",
|
|
2857
|
+
'Open the study progress dashboard in Claude Preview. Shows mastery levels, exam history, activity timeline, and capstone progress.\n\nIMPORTANT: After getting the URL, use the preview_start tool to open it in Claude Preview. If the user says "show dashboard" or "open dashboard", call this tool.',
|
|
2858
|
+
{},
|
|
2859
|
+
async () => {
|
|
2860
|
+
if (!cachedServer) {
|
|
2861
|
+
cachedServer = await startDashboardServer(db2, userConfig2);
|
|
2862
|
+
}
|
|
2863
|
+
const url = `http://127.0.0.1:${cachedServer.port}/dashboard`;
|
|
2864
|
+
const summary = buildTextSummary(db2, userConfig2.userId);
|
|
2865
|
+
return {
|
|
2866
|
+
content: [
|
|
2867
|
+
{
|
|
2868
|
+
type: "text",
|
|
2869
|
+
text: `Dashboard ready at: ${url}
|
|
2870
|
+
|
|
2871
|
+
Use preview_start to open this URL in Claude Preview.
|
|
2872
|
+
|
|
2873
|
+
${summary}`
|
|
2874
|
+
}
|
|
2875
|
+
]
|
|
2876
|
+
};
|
|
2877
|
+
}
|
|
2878
|
+
);
|
|
2879
|
+
}
|
|
2880
|
+
function buildTextSummary(db2, userId) {
|
|
2881
|
+
const DOMAIN_NAMES4 = {
|
|
2882
|
+
1: "Agentic Architecture",
|
|
2883
|
+
2: "Tool Design & MCP",
|
|
2884
|
+
3: "Claude Code Config",
|
|
2885
|
+
4: "Prompt Engineering",
|
|
2886
|
+
5: "Context & Reliability"
|
|
2887
|
+
};
|
|
2888
|
+
const domainRows = db2.prepare(`
|
|
2889
|
+
SELECT domainId, AVG(accuracyPercent) as avgAccuracy, SUM(totalAttempts) as totalAttempts
|
|
2890
|
+
FROM domain_mastery
|
|
2891
|
+
WHERE userId = ?
|
|
2892
|
+
GROUP BY domainId
|
|
2893
|
+
ORDER BY domainId ASC
|
|
2894
|
+
`).all(userId);
|
|
2895
|
+
const domainMap = new Map(domainRows.map((r) => [r.domainId, r]));
|
|
2896
|
+
const domainLines = [1, 2, 3, 4, 5].map((id) => {
|
|
2897
|
+
const row = domainMap.get(id);
|
|
2898
|
+
const mastery = row ? Math.round(row.avgAccuracy) : 0;
|
|
2899
|
+
const answered = row ? row.totalAttempts : 0;
|
|
2900
|
+
return ` D${id}: ${DOMAIN_NAMES4[id]} \u2014 ${mastery}% mastery, ${answered} answered`;
|
|
2901
|
+
});
|
|
2902
|
+
const overallMastery = domainRows.length > 0 ? Math.round(domainRows.reduce((sum, r) => sum + r.avgAccuracy, 0) / 5) : 0;
|
|
2903
|
+
const examStats = db2.prepare(`
|
|
2904
|
+
SELECT COUNT(*) as total, SUM(CASE WHEN passed THEN 1 ELSE 0 END) as passed
|
|
2905
|
+
FROM exam_attempts
|
|
2906
|
+
WHERE userId = ? AND completedAt IS NOT NULL
|
|
2907
|
+
`).get(userId);
|
|
2908
|
+
return [
|
|
2909
|
+
"--- TEXT SUMMARY ---",
|
|
2910
|
+
`Overall Readiness: ${overallMastery}%`,
|
|
2911
|
+
"",
|
|
2912
|
+
"Domain Progress:",
|
|
2913
|
+
...domainLines,
|
|
2914
|
+
"",
|
|
2915
|
+
`Practice Exams: ${examStats.total} taken, ${examStats.passed ?? 0} passed`
|
|
2916
|
+
].join("\n");
|
|
2917
|
+
}
|
|
2918
|
+
|
|
2632
2919
|
// src/tools/index.ts
|
|
2633
2920
|
function registerTools(server2, db2, userConfig2) {
|
|
2634
2921
|
registerSubmitAnswer(server2, db2, userConfig2);
|
|
@@ -2648,6 +2935,7 @@ function registerTools(server2, db2, userConfig2) {
|
|
|
2648
2935
|
registerStartCapstoneBuild(server2, db2, userConfig2);
|
|
2649
2936
|
registerCapstoneBuildStep(server2, db2, userConfig2);
|
|
2650
2937
|
registerCapstoneBuildStatus(server2, db2, userConfig2);
|
|
2938
|
+
registerDashboard(server2, db2, userConfig2);
|
|
2651
2939
|
}
|
|
2652
2940
|
|
|
2653
2941
|
// src/prompts/index.ts
|
|
@@ -2795,8 +3083,8 @@ Choose (1-2):` }
|
|
|
2795
3083
|
}
|
|
2796
3084
|
|
|
2797
3085
|
// src/resources/index.ts
|
|
2798
|
-
import
|
|
2799
|
-
import
|
|
3086
|
+
import fs6 from "fs";
|
|
3087
|
+
import path6 from "path";
|
|
2800
3088
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
2801
3089
|
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2802
3090
|
|
|
@@ -2804,17 +3092,17 @@ import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
2804
3092
|
import { readFileSync } from "fs";
|
|
2805
3093
|
import { resolve, dirname } from "path";
|
|
2806
3094
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
2807
|
-
var
|
|
3095
|
+
var __dirname4 = dirname(fileURLToPath3(import.meta.url));
|
|
2808
3096
|
var quizWidgetHtml = null;
|
|
2809
3097
|
function getQuizWidgetHtml() {
|
|
2810
3098
|
if (!quizWidgetHtml) {
|
|
2811
|
-
quizWidgetHtml = readFileSync(resolve(
|
|
3099
|
+
quizWidgetHtml = readFileSync(resolve(__dirname4, "../ui/quiz-widget.html"), "utf-8");
|
|
2812
3100
|
}
|
|
2813
3101
|
return quizWidgetHtml;
|
|
2814
3102
|
}
|
|
2815
3103
|
|
|
2816
3104
|
// src/resources/index.ts
|
|
2817
|
-
var
|
|
3105
|
+
var __dirname5 = path6.dirname(fileURLToPath4(import.meta.url));
|
|
2818
3106
|
function registerResources(server2, db2, userConfig2) {
|
|
2819
3107
|
server2.resource(
|
|
2820
3108
|
"handout",
|
|
@@ -2862,8 +3150,8 @@ function registerResources(server2, db2, userConfig2) {
|
|
|
2862
3150
|
{ mimeType: "text/markdown" },
|
|
2863
3151
|
async (uri, { projectId }) => {
|
|
2864
3152
|
const id = projectId;
|
|
2865
|
-
const projectPath =
|
|
2866
|
-
const content =
|
|
3153
|
+
const projectPath = path6.join(__dirname5, "..", "..", "projects", id, "README.md");
|
|
3154
|
+
const content = fs6.existsSync(projectPath) ? fs6.readFileSync(projectPath, "utf-8") : `Reference project "${id}" is not yet available. It will be added in the content creation phase.`;
|
|
2867
3155
|
return {
|
|
2868
3156
|
contents: [{ uri: uri.href, text: content, mimeType: "text/markdown" }]
|
|
2869
3157
|
};
|