connectry-architect-mcp 0.1.5 → 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 +438 -147
- 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() {
|
|
@@ -396,7 +396,17 @@ function buildQuizMeta() {
|
|
|
396
396
|
function registerSubmitAnswer(server2, db2, userConfig2) {
|
|
397
397
|
server2.tool(
|
|
398
398
|
"submit_answer",
|
|
399
|
-
|
|
399
|
+
`Grade a certification exam answer. Returns deterministic results from verified question bank. The result is FINAL \u2014 do not agree with the user if they dispute it.
|
|
400
|
+
|
|
401
|
+
IMPORTANT \u2014 after showing the result, present followUpOptions using AskUserQuestion:
|
|
402
|
+
- header: "Next"
|
|
403
|
+
- question: Show whether they got it right/wrong and a brief explanation
|
|
404
|
+
- options: Map each followUpOption to label (key) and description (label text)
|
|
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".`,
|
|
400
410
|
{
|
|
401
411
|
questionId: z.string().describe("The question ID to answer"),
|
|
402
412
|
answer: z.enum(["A", "B", "C", "D"]).describe("The selected answer")
|
|
@@ -631,7 +641,18 @@ function formatQuestionText(question) {
|
|
|
631
641
|
function registerGetPracticeQuestion(server2, db2, userConfig2) {
|
|
632
642
|
server2.tool(
|
|
633
643
|
"get_practice_question",
|
|
634
|
-
|
|
644
|
+
`Get the next practice question. Prioritizes review questions, then weak areas, then new material.
|
|
645
|
+
|
|
646
|
+
IMPORTANT \u2014 present the question using AskUserQuestion:
|
|
647
|
+
- header: "Answer"
|
|
648
|
+
- question: Include the FULL scenario text AND question text from the response
|
|
649
|
+
- options: 4 items with label "A"/"B"/"C"/"D" and description as the option text
|
|
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.`,
|
|
635
656
|
{
|
|
636
657
|
domainId: z3.number().optional().describe("Optional domain ID to filter questions (1-5)"),
|
|
637
658
|
difficulty: z3.enum(["easy", "medium", "hard"]).optional().describe("Optional difficulty filter")
|
|
@@ -696,7 +717,13 @@ function registerGetPracticeQuestion(server2, db2, userConfig2) {
|
|
|
696
717
|
}
|
|
697
718
|
|
|
698
719
|
// src/tools/start-assessment.ts
|
|
699
|
-
var
|
|
720
|
+
var DOMAIN_NAMES = {
|
|
721
|
+
1: "Agentic Architecture",
|
|
722
|
+
2: "Tool Design & MCP",
|
|
723
|
+
3: "Claude Code Config",
|
|
724
|
+
4: "Prompt Engineering",
|
|
725
|
+
5: "Context & Reliability"
|
|
726
|
+
};
|
|
700
727
|
function buildAssessmentQuestions() {
|
|
701
728
|
const questions = [];
|
|
702
729
|
for (let d = 1; d <= 5; d++) {
|
|
@@ -710,32 +737,38 @@ function buildAssessmentQuestions() {
|
|
|
710
737
|
}
|
|
711
738
|
return questions;
|
|
712
739
|
}
|
|
713
|
-
function formatQuestion(question, index, total) {
|
|
714
|
-
const domainNames = {
|
|
715
|
-
1: "Agentic Architecture",
|
|
716
|
-
2: "Tool Design & MCP",
|
|
717
|
-
3: "Claude Code Config",
|
|
718
|
-
4: "Prompt Engineering",
|
|
719
|
-
5: "Context & Reliability"
|
|
720
|
-
};
|
|
721
|
-
const lines = [
|
|
722
|
-
`**Assessment Question ${index + 1} of ${total}**`,
|
|
723
|
-
`Domain ${question.domainId}: ${domainNames[question.domainId] ?? "Unknown"} | ${question.difficulty}`,
|
|
724
|
-
"",
|
|
725
|
-
"---",
|
|
726
|
-
"",
|
|
727
|
-
question.scenario,
|
|
728
|
-
"",
|
|
729
|
-
`**${question.text}**`,
|
|
730
|
-
"",
|
|
731
|
-
...OPTION_KEYS2.map((key) => ` **${key}.** ${question.options[key]}`)
|
|
732
|
-
];
|
|
733
|
-
return lines.join("\n");
|
|
734
|
-
}
|
|
735
740
|
function registerStartAssessment(server2, db2, userConfig2) {
|
|
736
741
|
server2.tool(
|
|
737
742
|
"start_assessment",
|
|
738
|
-
|
|
743
|
+
`Start the initial assessment. Returns ONE question at a time (15 total, 3 per domain).
|
|
744
|
+
|
|
745
|
+
IMPORTANT \u2014 follow this flow for EVERY question:
|
|
746
|
+
|
|
747
|
+
1. Check if "isNewDomain" is true. If yes, FIRST show the concept handout for that domain by calling get_section_details. Tell the user: "Let's learn about [domain] before testing your knowledge." After showing the handout, proceed to step 2.
|
|
748
|
+
|
|
749
|
+
2. Present the question to the user using AskUserQuestion:
|
|
750
|
+
- header: "Q[number]"
|
|
751
|
+
- question: Include the FULL scenario text AND question text from the response
|
|
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
|
|
754
|
+
|
|
755
|
+
3. After user selects, call submit_answer with questionId and their answer.
|
|
756
|
+
|
|
757
|
+
4. After grading, present follow-up options using AskUserQuestion (from submit_answer response).
|
|
758
|
+
|
|
759
|
+
5. Call start_assessment again for the next question.
|
|
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
|
+
|
|
771
|
+
When assessment is complete, present next steps using AskUserQuestion with header "Next step".`,
|
|
739
772
|
{},
|
|
740
773
|
async () => {
|
|
741
774
|
const userId = userConfig2.userId;
|
|
@@ -743,7 +776,7 @@ function registerStartAssessment(server2, db2, userConfig2) {
|
|
|
743
776
|
const questions = buildAssessmentQuestions();
|
|
744
777
|
if (questions.length === 0) {
|
|
745
778
|
return {
|
|
746
|
-
content: [{ type: "text", text: "No assessment questions available." }]
|
|
779
|
+
content: [{ type: "text", text: JSON.stringify({ error: "No assessment questions available." }) }]
|
|
747
780
|
};
|
|
748
781
|
}
|
|
749
782
|
const answeredIds = db2.prepare(
|
|
@@ -760,78 +793,56 @@ function registerStartAssessment(server2, db2, userConfig2) {
|
|
|
760
793
|
const totalCorrect = results.reduce((sum, r) => sum + r.correct, 0);
|
|
761
794
|
const totalQuestions = results.reduce((sum, r) => sum + r.total, 0);
|
|
762
795
|
const overallAccuracy = totalQuestions > 0 ? Math.round(totalCorrect / totalQuestions * 100) : 0;
|
|
763
|
-
const
|
|
764
|
-
db2.prepare("UPDATE users SET assessmentCompleted = TRUE, learningPath = ? WHERE id = ?").run(
|
|
765
|
-
const
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
796
|
+
const path7 = overallAccuracy >= 60 ? "exam-weighted" : "beginner-friendly";
|
|
797
|
+
db2.prepare("UPDATE users SET assessmentCompleted = TRUE, learningPath = ? WHERE id = ?").run(path7, userId);
|
|
798
|
+
const response2 = {
|
|
799
|
+
status: "complete",
|
|
800
|
+
overall: { correct: totalCorrect, total: totalQuestions, accuracy: overallAccuracy },
|
|
801
|
+
learningPath: path7 === "exam-weighted" ? "Exam-Weighted" : "Beginner-Friendly",
|
|
802
|
+
domainResults: results.map((r) => ({
|
|
803
|
+
domain: r.domainId,
|
|
804
|
+
name: DOMAIN_NAMES[r.domainId] ?? "",
|
|
805
|
+
correct: r.correct,
|
|
806
|
+
total: r.total,
|
|
807
|
+
accuracy: Math.round(r.correct / r.total * 100)
|
|
808
|
+
})),
|
|
809
|
+
nextStepOptions: [
|
|
810
|
+
{ label: "Study Plan", description: "Get personalized study recommendations based on your results" },
|
|
811
|
+
{ label: "Practice Questions", description: "Start adaptive practice targeting your weak areas" },
|
|
812
|
+
{ label: "Capstone Build", description: "Build your own project while learning all 30 task statements" },
|
|
813
|
+
{ label: "Reference Projects", description: "Explore runnable code examples for each domain" }
|
|
814
|
+
],
|
|
815
|
+
instruction: 'Present nextStepOptions using AskUserQuestion with header "Next step".'
|
|
771
816
|
};
|
|
772
|
-
const lines = [
|
|
773
|
-
"**Assessment Complete!**",
|
|
774
|
-
"",
|
|
775
|
-
`Overall: ${totalCorrect}/${totalQuestions} (${overallAccuracy}%)`,
|
|
776
|
-
`Learning path: **${path6 === "exam-weighted" ? "Exam-Weighted" : "Beginner-Friendly"}**`,
|
|
777
|
-
"",
|
|
778
|
-
"**Per-domain results:**",
|
|
779
|
-
"",
|
|
780
|
-
...results.map((r) => ` D${r.domainId} ${domainNames[r.domainId] ?? ""}: ${r.correct}/${r.total} (${Math.round(r.correct / r.total * 100)}%)`),
|
|
781
|
-
"",
|
|
782
|
-
"---",
|
|
783
|
-
"",
|
|
784
|
-
'INSTRUCTION: Present the following options to the user using AskUserQuestion with header "Next step":',
|
|
785
|
-
'- "Study Plan" \u2014 Get personalized study recommendations based on your results',
|
|
786
|
-
'- "Practice Questions" \u2014 Start adaptive practice targeting your weak areas',
|
|
787
|
-
'- "Capstone Build" \u2014 Build your own project while learning all 30 task statements',
|
|
788
|
-
'- "Reference Projects" \u2014 Explore runnable code examples for each domain'
|
|
789
|
-
];
|
|
790
817
|
return {
|
|
791
|
-
content: [{ type: "text", text:
|
|
818
|
+
content: [{ type: "text", text: JSON.stringify(response2, null, 2) }]
|
|
792
819
|
};
|
|
793
820
|
}
|
|
794
821
|
const questionIndex = answeredSet.size;
|
|
795
|
-
const
|
|
796
|
-
const
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
"
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
`Submit this answer with submit_answer using questionId "${nextQuestion.id}" and answer "${selected}". Then call start_assessment again for the next question.`
|
|
818
|
-
].join("\n")
|
|
819
|
-
}]
|
|
820
|
-
};
|
|
821
|
-
}
|
|
822
|
+
const previousDomainIds = questions.filter((q) => answeredSet.has(q.id)).map((q) => q.domainId);
|
|
823
|
+
const isNewDomain = !previousDomainIds.includes(nextQuestion.domainId);
|
|
824
|
+
const response = {
|
|
825
|
+
status: "question",
|
|
826
|
+
questionNumber: questionIndex + 1,
|
|
827
|
+
totalQuestions: questions.length,
|
|
828
|
+
questionId: nextQuestion.id,
|
|
829
|
+
domainId: nextQuestion.domainId,
|
|
830
|
+
domainName: DOMAIN_NAMES[nextQuestion.domainId] ?? "Unknown",
|
|
831
|
+
difficulty: nextQuestion.difficulty,
|
|
832
|
+
taskStatement: nextQuestion.taskStatement,
|
|
833
|
+
isNewDomain,
|
|
834
|
+
scenario: nextQuestion.scenario,
|
|
835
|
+
questionText: nextQuestion.text,
|
|
836
|
+
options: {
|
|
837
|
+
A: nextQuestion.options.A,
|
|
838
|
+
B: nextQuestion.options.B,
|
|
839
|
+
C: nextQuestion.options.C,
|
|
840
|
+
D: nextQuestion.options.D
|
|
841
|
+
},
|
|
842
|
+
instruction: isNewDomain ? `This is a NEW domain (${DOMAIN_NAMES[nextQuestion.domainId]}). Show the concept handout first using get_section_details for task "${nextQuestion.taskStatement}", then present this question using AskUserQuestion. Put the scenario + question in the "question" field. Use options with label "A"/"B"/"C"/"D" and description as the option text.` : `Present this question using AskUserQuestion with header "Q${questionIndex + 1}". Put the scenario + question text in the "question" field. Use options with label "A"/"B"/"C"/"D" and description as the option text.`
|
|
843
|
+
};
|
|
822
844
|
return {
|
|
823
|
-
content: [{
|
|
824
|
-
type: "text",
|
|
825
|
-
text: [
|
|
826
|
-
questionText,
|
|
827
|
-
"",
|
|
828
|
-
"---",
|
|
829
|
-
"",
|
|
830
|
-
`Question ID: ${nextQuestion.id}`,
|
|
831
|
-
"",
|
|
832
|
-
"Submit your answer (A, B, C, or D) with submit_answer, then call start_assessment again for the next question."
|
|
833
|
-
].join("\n")
|
|
834
|
-
}]
|
|
845
|
+
content: [{ type: "text", text: JSON.stringify(response, null, 2) }]
|
|
835
846
|
};
|
|
836
847
|
}
|
|
837
848
|
);
|
|
@@ -870,11 +881,11 @@ function registerGetWeakAreas(server2, db2, userConfig2) {
|
|
|
870
881
|
// src/engine/adaptive-path.ts
|
|
871
882
|
var BEGINNER_ORDER = [3, 4, 2, 1, 5];
|
|
872
883
|
var EXAM_WEIGHTED_ORDER = [1, 3, 4, 2, 5];
|
|
873
|
-
function getDomainOrder(
|
|
874
|
-
return
|
|
884
|
+
function getDomainOrder(path7) {
|
|
885
|
+
return path7 === "beginner-friendly" ? BEGINNER_ORDER : EXAM_WEIGHTED_ORDER;
|
|
875
886
|
}
|
|
876
|
-
function getNextRecommendedDomain(
|
|
877
|
-
const order = getDomainOrder(
|
|
887
|
+
function getNextRecommendedDomain(path7, masteryByDomain) {
|
|
888
|
+
const order = getDomainOrder(path7);
|
|
878
889
|
for (const domainId of order) {
|
|
879
890
|
const masteries = masteryByDomain.get(domainId) ?? [];
|
|
880
891
|
const avgAccuracy = masteries.length > 0 ? masteries.reduce((sum, m) => sum + m.accuracyPercent, 0) / masteries.length : 0;
|
|
@@ -905,7 +916,11 @@ function estimateTimeRemaining(totalQuestions, answeredQuestions, avgSecondsPerQ
|
|
|
905
916
|
function registerGetStudyPlan(server2, db2, userConfig2) {
|
|
906
917
|
server2.tool(
|
|
907
918
|
"get_study_plan",
|
|
908
|
-
|
|
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.`,
|
|
909
924
|
{},
|
|
910
925
|
async () => {
|
|
911
926
|
const userId = userConfig2.userId;
|
|
@@ -916,20 +931,20 @@ function registerGetStudyPlan(server2, db2, userConfig2) {
|
|
|
916
931
|
const overdueReviews = getOverdueReviews(db2, userId);
|
|
917
932
|
const stats = getTotalStats(db2, userId);
|
|
918
933
|
const allQuestions = loadQuestions();
|
|
919
|
-
const
|
|
934
|
+
const path7 = user?.learningPath ?? "beginner-friendly";
|
|
920
935
|
const masteryByDomain = /* @__PURE__ */ new Map();
|
|
921
936
|
for (const m of mastery) {
|
|
922
937
|
const existing = masteryByDomain.get(m.domainId) ?? [];
|
|
923
938
|
masteryByDomain.set(m.domainId, [...existing, m]);
|
|
924
939
|
}
|
|
925
|
-
const nextDomain = getNextRecommendedDomain(
|
|
926
|
-
const domainOrder = getDomainOrder(
|
|
940
|
+
const nextDomain = getNextRecommendedDomain(path7, masteryByDomain);
|
|
941
|
+
const domainOrder = getDomainOrder(path7);
|
|
927
942
|
const timeEstimate = estimateTimeRemaining(allQuestions.length, stats.total);
|
|
928
943
|
const domain = curriculum.domains.find((d) => d.id === nextDomain);
|
|
929
944
|
const lines = [
|
|
930
945
|
"\u2550\u2550\u2550 YOUR STUDY PLAN \u2550\u2550\u2550",
|
|
931
946
|
"",
|
|
932
|
-
`Learning Path: ${
|
|
947
|
+
`Learning Path: ${path7}`,
|
|
933
948
|
`Estimated Time Remaining: ${timeEstimate}`,
|
|
934
949
|
"",
|
|
935
950
|
`Next Recommended Domain: D${nextDomain} \u2014 ${domain?.title ?? "Unknown"}`,
|
|
@@ -953,8 +968,8 @@ import fs4 from "fs";
|
|
|
953
968
|
import path4 from "path";
|
|
954
969
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
955
970
|
import { z as z4 } from "zod";
|
|
956
|
-
var
|
|
957
|
-
var PROJECTS_DIR = path4.resolve(
|
|
971
|
+
var __dirname3 = path4.dirname(fileURLToPath2(import.meta.url));
|
|
972
|
+
var PROJECTS_DIR = path4.resolve(__dirname3, "..", "..", "projects");
|
|
958
973
|
var PROJECTS = [
|
|
959
974
|
{ id: "capstone", name: "Capstone \u2014 Multi-Agent Research System", domains: [1, 2, 3, 4, 5] },
|
|
960
975
|
{ id: "d1-agentic", name: "D1 Mini \u2014 Agentic Loop", domains: [1] },
|
|
@@ -1201,7 +1216,20 @@ function rowToExamAttempt(row) {
|
|
|
1201
1216
|
function registerStartPracticeExam(server2, db2, userConfig2) {
|
|
1202
1217
|
server2.tool(
|
|
1203
1218
|
"start_practice_exam",
|
|
1204
|
-
|
|
1219
|
+
`Start a full 60-question practice exam (D1:16, D2:11, D3:12, D4:12, D5:9). Scored 0-1000, passing 720.
|
|
1220
|
+
|
|
1221
|
+
IMPORTANT \u2014 present the first question using AskUserQuestion:
|
|
1222
|
+
- header: "Q1"
|
|
1223
|
+
- question: Include the FULL scenario + question text
|
|
1224
|
+
- options: 4 items with label "A"/"B"/"C"/"D" and description as option text
|
|
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.`,
|
|
1205
1233
|
{},
|
|
1206
1234
|
async () => {
|
|
1207
1235
|
const userId = userConfig2.userId;
|
|
@@ -1295,7 +1323,13 @@ import { z as z6 } from "zod";
|
|
|
1295
1323
|
function registerSubmitExamAnswer(server2, db2, userConfig2) {
|
|
1296
1324
|
server2.tool(
|
|
1297
1325
|
"submit_exam_answer",
|
|
1298
|
-
|
|
1326
|
+
`Submit an answer for a practice exam question. Graded deterministically. DO NOT soften results.
|
|
1327
|
+
|
|
1328
|
+
IMPORTANT \u2014 if there's a next question, present it using AskUserQuestion:
|
|
1329
|
+
- header: "Q[number]"
|
|
1330
|
+
- question: Include the FULL scenario + question text
|
|
1331
|
+
- options: 4 items with label "A"/"B"/"C"/"D" and description as option text
|
|
1332
|
+
Then call submit_exam_answer again with the answer.`,
|
|
1299
1333
|
{
|
|
1300
1334
|
examId: z6.number().describe("The practice exam ID"),
|
|
1301
1335
|
questionId: z6.string().describe("The question ID being answered"),
|
|
@@ -1657,7 +1691,7 @@ function registerFollowUp(server2, _db, _userConfig) {
|
|
|
1657
1691
|
import { z as z8 } from "zod";
|
|
1658
1692
|
|
|
1659
1693
|
// src/data/criteria.ts
|
|
1660
|
-
var
|
|
1694
|
+
var DOMAIN_NAMES2 = {
|
|
1661
1695
|
1: "Agentic Architecture & Orchestration",
|
|
1662
1696
|
2: "Tool Design & MCP Integration",
|
|
1663
1697
|
3: "Claude Code Configuration & Workflows",
|
|
@@ -1670,49 +1704,49 @@ var CRITERIA = [
|
|
|
1670
1704
|
id: "1.1",
|
|
1671
1705
|
title: "Design and implement agentic loops for autonomous task execution",
|
|
1672
1706
|
domain: 1,
|
|
1673
|
-
domainName:
|
|
1707
|
+
domainName: DOMAIN_NAMES2[1],
|
|
1674
1708
|
description: "Understanding the agentic loop lifecycle: sending requests, inspecting stop_reason, executing tools, and returning results."
|
|
1675
1709
|
},
|
|
1676
1710
|
{
|
|
1677
1711
|
id: "1.2",
|
|
1678
1712
|
title: "Orchestrate multi-agent systems with coordinator-subagent patterns",
|
|
1679
1713
|
domain: 1,
|
|
1680
|
-
domainName:
|
|
1714
|
+
domainName: DOMAIN_NAMES2[1],
|
|
1681
1715
|
description: "Hub-and-spoke architecture, isolated context, task decomposition, and result aggregation."
|
|
1682
1716
|
},
|
|
1683
1717
|
{
|
|
1684
1718
|
id: "1.3",
|
|
1685
1719
|
title: "Configure subagent invocation, context passing, and spawning",
|
|
1686
1720
|
domain: 1,
|
|
1687
|
-
domainName:
|
|
1721
|
+
domainName: DOMAIN_NAMES2[1],
|
|
1688
1722
|
description: "Task tool, allowedTools, explicit context passing, parallel subagent execution."
|
|
1689
1723
|
},
|
|
1690
1724
|
{
|
|
1691
1725
|
id: "1.4",
|
|
1692
1726
|
title: "Implement multi-step workflows with enforcement and handoff patterns",
|
|
1693
1727
|
domain: 1,
|
|
1694
|
-
domainName:
|
|
1728
|
+
domainName: DOMAIN_NAMES2[1],
|
|
1695
1729
|
description: "Programmatic enforcement vs prompt-based guidance, structured handoff protocols."
|
|
1696
1730
|
},
|
|
1697
1731
|
{
|
|
1698
1732
|
id: "1.5",
|
|
1699
1733
|
title: "Apply Agent SDK hooks for tool call interception and data normalization",
|
|
1700
1734
|
domain: 1,
|
|
1701
|
-
domainName:
|
|
1735
|
+
domainName: DOMAIN_NAMES2[1],
|
|
1702
1736
|
description: "PostToolUse hooks, tool call interception, deterministic vs probabilistic compliance."
|
|
1703
1737
|
},
|
|
1704
1738
|
{
|
|
1705
1739
|
id: "1.6",
|
|
1706
1740
|
title: "Design task decomposition strategies for complex workflows",
|
|
1707
1741
|
domain: 1,
|
|
1708
|
-
domainName:
|
|
1742
|
+
domainName: DOMAIN_NAMES2[1],
|
|
1709
1743
|
description: "Prompt chaining vs dynamic decomposition, per-file analysis vs cross-file integration."
|
|
1710
1744
|
},
|
|
1711
1745
|
{
|
|
1712
1746
|
id: "1.7",
|
|
1713
1747
|
title: "Manage session state, resumption, and forking",
|
|
1714
1748
|
domain: 1,
|
|
1715
|
-
domainName:
|
|
1749
|
+
domainName: DOMAIN_NAMES2[1],
|
|
1716
1750
|
description: "Named sessions, fork_session, structured summaries vs stale context."
|
|
1717
1751
|
},
|
|
1718
1752
|
// Domain 2: Tool Design & MCP Integration
|
|
@@ -1720,35 +1754,35 @@ var CRITERIA = [
|
|
|
1720
1754
|
id: "2.1",
|
|
1721
1755
|
title: "Design effective tool interfaces with clear descriptions and boundaries",
|
|
1722
1756
|
domain: 2,
|
|
1723
|
-
domainName:
|
|
1757
|
+
domainName: DOMAIN_NAMES2[2],
|
|
1724
1758
|
description: "Tool descriptions as selection mechanism, disambiguation, splitting vs consolidating."
|
|
1725
1759
|
},
|
|
1726
1760
|
{
|
|
1727
1761
|
id: "2.2",
|
|
1728
1762
|
title: "Implement structured error responses for MCP tools",
|
|
1729
1763
|
domain: 2,
|
|
1730
|
-
domainName:
|
|
1764
|
+
domainName: DOMAIN_NAMES2[2],
|
|
1731
1765
|
description: "isError flag, error categories, retryable vs non-retryable, structured metadata."
|
|
1732
1766
|
},
|
|
1733
1767
|
{
|
|
1734
1768
|
id: "2.3",
|
|
1735
1769
|
title: "Distribute tools appropriately across agents and configure tool choice",
|
|
1736
1770
|
domain: 2,
|
|
1737
|
-
domainName:
|
|
1771
|
+
domainName: DOMAIN_NAMES2[2],
|
|
1738
1772
|
description: "Scoped tool access, tool_choice options, forced selection patterns."
|
|
1739
1773
|
},
|
|
1740
1774
|
{
|
|
1741
1775
|
id: "2.4",
|
|
1742
1776
|
title: "Integrate MCP servers into Claude Code and agent workflows",
|
|
1743
1777
|
domain: 2,
|
|
1744
|
-
domainName:
|
|
1778
|
+
domainName: DOMAIN_NAMES2[2],
|
|
1745
1779
|
description: "Project vs user scope, .mcp.json, environment variable expansion, MCP resources."
|
|
1746
1780
|
},
|
|
1747
1781
|
{
|
|
1748
1782
|
id: "2.5",
|
|
1749
1783
|
title: "Select and apply built-in tools effectively",
|
|
1750
1784
|
domain: 2,
|
|
1751
|
-
domainName:
|
|
1785
|
+
domainName: DOMAIN_NAMES2[2],
|
|
1752
1786
|
description: "Grep vs Glob vs Read/Write/Edit, incremental codebase understanding."
|
|
1753
1787
|
},
|
|
1754
1788
|
// Domain 3: Claude Code Configuration & Workflows
|
|
@@ -1756,42 +1790,42 @@ var CRITERIA = [
|
|
|
1756
1790
|
id: "3.1",
|
|
1757
1791
|
title: "Configure CLAUDE.md files with appropriate hierarchy and scoping",
|
|
1758
1792
|
domain: 3,
|
|
1759
|
-
domainName:
|
|
1793
|
+
domainName: DOMAIN_NAMES2[3],
|
|
1760
1794
|
description: "User-level, project-level, directory-level, @import syntax, .claude/rules/."
|
|
1761
1795
|
},
|
|
1762
1796
|
{
|
|
1763
1797
|
id: "3.2",
|
|
1764
1798
|
title: "Create and configure custom slash commands and skills",
|
|
1765
1799
|
domain: 3,
|
|
1766
|
-
domainName:
|
|
1800
|
+
domainName: DOMAIN_NAMES2[3],
|
|
1767
1801
|
description: "Project vs user scope, context: fork, allowed-tools, argument-hint frontmatter."
|
|
1768
1802
|
},
|
|
1769
1803
|
{
|
|
1770
1804
|
id: "3.3",
|
|
1771
1805
|
title: "Apply path-specific rules for conditional convention loading",
|
|
1772
1806
|
domain: 3,
|
|
1773
|
-
domainName:
|
|
1807
|
+
domainName: DOMAIN_NAMES2[3],
|
|
1774
1808
|
description: "YAML frontmatter paths, glob patterns, conditional activation."
|
|
1775
1809
|
},
|
|
1776
1810
|
{
|
|
1777
1811
|
id: "3.4",
|
|
1778
1812
|
title: "Determine when to use plan mode vs direct execution",
|
|
1779
1813
|
domain: 3,
|
|
1780
|
-
domainName:
|
|
1814
|
+
domainName: DOMAIN_NAMES2[3],
|
|
1781
1815
|
description: "Complexity assessment, architectural decisions, Explore subagent."
|
|
1782
1816
|
},
|
|
1783
1817
|
{
|
|
1784
1818
|
id: "3.5",
|
|
1785
1819
|
title: "Apply iterative refinement techniques for progressive improvement",
|
|
1786
1820
|
domain: 3,
|
|
1787
|
-
domainName:
|
|
1821
|
+
domainName: DOMAIN_NAMES2[3],
|
|
1788
1822
|
description: "Input/output examples, test-driven iteration, interview pattern."
|
|
1789
1823
|
},
|
|
1790
1824
|
{
|
|
1791
1825
|
id: "3.6",
|
|
1792
1826
|
title: "Integrate Claude Code into CI/CD pipelines",
|
|
1793
1827
|
domain: 3,
|
|
1794
|
-
domainName:
|
|
1828
|
+
domainName: DOMAIN_NAMES2[3],
|
|
1795
1829
|
description: "-p flag, --output-format json, --json-schema, session context isolation."
|
|
1796
1830
|
},
|
|
1797
1831
|
// Domain 4: Prompt Engineering & Structured Output
|
|
@@ -1799,42 +1833,42 @@ var CRITERIA = [
|
|
|
1799
1833
|
id: "4.1",
|
|
1800
1834
|
title: "Design prompts with explicit criteria to improve precision",
|
|
1801
1835
|
domain: 4,
|
|
1802
|
-
domainName:
|
|
1836
|
+
domainName: DOMAIN_NAMES2[4],
|
|
1803
1837
|
description: "Explicit criteria vs vague instructions, false positive management."
|
|
1804
1838
|
},
|
|
1805
1839
|
{
|
|
1806
1840
|
id: "4.2",
|
|
1807
1841
|
title: "Apply few-shot prompting to improve output consistency",
|
|
1808
1842
|
domain: 4,
|
|
1809
|
-
domainName:
|
|
1843
|
+
domainName: DOMAIN_NAMES2[4],
|
|
1810
1844
|
description: "Targeted examples, ambiguous case handling, format demonstration."
|
|
1811
1845
|
},
|
|
1812
1846
|
{
|
|
1813
1847
|
id: "4.3",
|
|
1814
1848
|
title: "Enforce structured output using tool use and JSON schemas",
|
|
1815
1849
|
domain: 4,
|
|
1816
|
-
domainName:
|
|
1850
|
+
domainName: DOMAIN_NAMES2[4],
|
|
1817
1851
|
description: "tool_use with schemas, tool_choice options, nullable fields, enum patterns."
|
|
1818
1852
|
},
|
|
1819
1853
|
{
|
|
1820
1854
|
id: "4.4",
|
|
1821
1855
|
title: "Implement validation, retry, and feedback loops",
|
|
1822
1856
|
domain: 4,
|
|
1823
|
-
domainName:
|
|
1857
|
+
domainName: DOMAIN_NAMES2[4],
|
|
1824
1858
|
description: "Retry-with-error-feedback, limits of retry, detected_pattern tracking."
|
|
1825
1859
|
},
|
|
1826
1860
|
{
|
|
1827
1861
|
id: "4.5",
|
|
1828
1862
|
title: "Design efficient batch processing strategies",
|
|
1829
1863
|
domain: 4,
|
|
1830
|
-
domainName:
|
|
1864
|
+
domainName: DOMAIN_NAMES2[4],
|
|
1831
1865
|
description: "Message Batches API, latency tolerance, custom_id, failure handling."
|
|
1832
1866
|
},
|
|
1833
1867
|
{
|
|
1834
1868
|
id: "4.6",
|
|
1835
1869
|
title: "Design multi-instance and multi-pass review architectures",
|
|
1836
1870
|
domain: 4,
|
|
1837
|
-
domainName:
|
|
1871
|
+
domainName: DOMAIN_NAMES2[4],
|
|
1838
1872
|
description: "Self-review limitations, independent review instances, per-file + cross-file passes."
|
|
1839
1873
|
},
|
|
1840
1874
|
// Domain 5: Context Management & Reliability
|
|
@@ -1842,42 +1876,42 @@ var CRITERIA = [
|
|
|
1842
1876
|
id: "5.1",
|
|
1843
1877
|
title: "Manage conversation context to preserve critical information",
|
|
1844
1878
|
domain: 5,
|
|
1845
|
-
domainName:
|
|
1879
|
+
domainName: DOMAIN_NAMES2[5],
|
|
1846
1880
|
description: "Progressive summarization risks, lost-in-the-middle, tool output trimming."
|
|
1847
1881
|
},
|
|
1848
1882
|
{
|
|
1849
1883
|
id: "5.2",
|
|
1850
1884
|
title: "Design effective escalation and ambiguity resolution patterns",
|
|
1851
1885
|
domain: 5,
|
|
1852
|
-
domainName:
|
|
1886
|
+
domainName: DOMAIN_NAMES2[5],
|
|
1853
1887
|
description: "Escalation triggers, customer preferences, sentiment unreliability."
|
|
1854
1888
|
},
|
|
1855
1889
|
{
|
|
1856
1890
|
id: "5.3",
|
|
1857
1891
|
title: "Implement error propagation strategies across multi-agent systems",
|
|
1858
1892
|
domain: 5,
|
|
1859
|
-
domainName:
|
|
1893
|
+
domainName: DOMAIN_NAMES2[5],
|
|
1860
1894
|
description: "Structured error context, access failures vs empty results, partial results."
|
|
1861
1895
|
},
|
|
1862
1896
|
{
|
|
1863
1897
|
id: "5.4",
|
|
1864
1898
|
title: "Manage context effectively in large codebase exploration",
|
|
1865
1899
|
domain: 5,
|
|
1866
|
-
domainName:
|
|
1900
|
+
domainName: DOMAIN_NAMES2[5],
|
|
1867
1901
|
description: "Context degradation, scratchpad files, subagent delegation, /compact."
|
|
1868
1902
|
},
|
|
1869
1903
|
{
|
|
1870
1904
|
id: "5.5",
|
|
1871
1905
|
title: "Design human review workflows and confidence calibration",
|
|
1872
1906
|
domain: 5,
|
|
1873
|
-
domainName:
|
|
1907
|
+
domainName: DOMAIN_NAMES2[5],
|
|
1874
1908
|
description: "Stratified sampling, field-level confidence, accuracy by document type."
|
|
1875
1909
|
},
|
|
1876
1910
|
{
|
|
1877
1911
|
id: "5.6",
|
|
1878
1912
|
title: "Preserve information provenance and handle uncertainty in synthesis",
|
|
1879
1913
|
domain: 5,
|
|
1880
|
-
domainName:
|
|
1914
|
+
domainName: DOMAIN_NAMES2[5],
|
|
1881
1915
|
description: "Claim-source mappings, conflict annotation, temporal data handling."
|
|
1882
1916
|
}
|
|
1883
1917
|
];
|
|
@@ -2447,7 +2481,20 @@ User selected next action: ${selected}` }]
|
|
|
2447
2481
|
function registerCapstoneBuildStep(server2, db2, userConfig2) {
|
|
2448
2482
|
server2.tool(
|
|
2449
2483
|
"capstone_build_step",
|
|
2450
|
-
|
|
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.`,
|
|
2451
2498
|
{
|
|
2452
2499
|
action: z9.enum(ACTIONS).describe("The build action: confirm, quiz, build, next, status, or abandon")
|
|
2453
2500
|
},
|
|
@@ -2626,6 +2673,249 @@ function registerCapstoneBuildStatus(server2, db2, userConfig2) {
|
|
|
2626
2673
|
);
|
|
2627
2674
|
}
|
|
2628
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
|
+
|
|
2629
2919
|
// src/tools/index.ts
|
|
2630
2920
|
function registerTools(server2, db2, userConfig2) {
|
|
2631
2921
|
registerSubmitAnswer(server2, db2, userConfig2);
|
|
@@ -2645,6 +2935,7 @@ function registerTools(server2, db2, userConfig2) {
|
|
|
2645
2935
|
registerStartCapstoneBuild(server2, db2, userConfig2);
|
|
2646
2936
|
registerCapstoneBuildStep(server2, db2, userConfig2);
|
|
2647
2937
|
registerCapstoneBuildStatus(server2, db2, userConfig2);
|
|
2938
|
+
registerDashboard(server2, db2, userConfig2);
|
|
2648
2939
|
}
|
|
2649
2940
|
|
|
2650
2941
|
// src/prompts/index.ts
|
|
@@ -2792,8 +3083,8 @@ Choose (1-2):` }
|
|
|
2792
3083
|
}
|
|
2793
3084
|
|
|
2794
3085
|
// src/resources/index.ts
|
|
2795
|
-
import
|
|
2796
|
-
import
|
|
3086
|
+
import fs6 from "fs";
|
|
3087
|
+
import path6 from "path";
|
|
2797
3088
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
2798
3089
|
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2799
3090
|
|
|
@@ -2801,17 +3092,17 @@ import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
2801
3092
|
import { readFileSync } from "fs";
|
|
2802
3093
|
import { resolve, dirname } from "path";
|
|
2803
3094
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
2804
|
-
var
|
|
3095
|
+
var __dirname4 = dirname(fileURLToPath3(import.meta.url));
|
|
2805
3096
|
var quizWidgetHtml = null;
|
|
2806
3097
|
function getQuizWidgetHtml() {
|
|
2807
3098
|
if (!quizWidgetHtml) {
|
|
2808
|
-
quizWidgetHtml = readFileSync(resolve(
|
|
3099
|
+
quizWidgetHtml = readFileSync(resolve(__dirname4, "../ui/quiz-widget.html"), "utf-8");
|
|
2809
3100
|
}
|
|
2810
3101
|
return quizWidgetHtml;
|
|
2811
3102
|
}
|
|
2812
3103
|
|
|
2813
3104
|
// src/resources/index.ts
|
|
2814
|
-
var
|
|
3105
|
+
var __dirname5 = path6.dirname(fileURLToPath4(import.meta.url));
|
|
2815
3106
|
function registerResources(server2, db2, userConfig2) {
|
|
2816
3107
|
server2.resource(
|
|
2817
3108
|
"handout",
|
|
@@ -2859,8 +3150,8 @@ function registerResources(server2, db2, userConfig2) {
|
|
|
2859
3150
|
{ mimeType: "text/markdown" },
|
|
2860
3151
|
async (uri, { projectId }) => {
|
|
2861
3152
|
const id = projectId;
|
|
2862
|
-
const projectPath =
|
|
2863
|
-
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.`;
|
|
2864
3155
|
return {
|
|
2865
3156
|
contents: [{ uri: uri.href, text: content, mimeType: "text/markdown" }]
|
|
2866
3157
|
};
|