@bojackduy/opencode-learn 0.1.6 → 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/dist/server.js CHANGED
@@ -147,6 +147,18 @@ function resolveCorrect(correctAnswer, options) {
147
147
  }
148
148
  return { indices: Array.from(new Set(indices)).sort((a, b) => a - b) };
149
149
  }
150
+ function decodeQuizText(s) {
151
+ if (!s || typeof s !== "string")
152
+ return s;
153
+ if (!s.includes("\\"))
154
+ return s;
155
+ let out = s.replace(/\\r\\n/g, `
156
+ `).replace(/\\n/g, `
157
+ `).replace(/\\r/g, "\r").replace(/\\t/g, "\t");
158
+ out = out.replace(/\\"/g, '"').replace(/\\'/g, "'");
159
+ out = out.replace(/\\\\/g, "\\");
160
+ return out;
161
+ }
150
162
  var mdLogFile = null;
151
163
  var mdLogWriteLock = Promise.resolve();
152
164
  function withMdLock(fn) {
@@ -891,9 +903,13 @@ Explanation: ${j.explanation}${note}`;
891
903
  shuffle: tool.schema.boolean().optional().describe("Default true: shuffle before display. False only if order matters.")
892
904
  },
893
905
  async execute(args, ctx) {
906
+ const qFixed = decodeQuizText(args.question) ?? args.question;
907
+ const dFixed = decodeQuizText(args.details);
908
+ const eFixed = decodeQuizText(args.explanation) ?? args.explanation;
909
+ const optsDecoded = args.options?.map((o) => ({ ...o, label: decodeQuizText(o.label) ?? o.label, description: o.description ? decodeQuizText(o.description) : o.description }));
894
910
  let options;
895
911
  try {
896
- options = normalizeQuizOptions(args.options);
912
+ options = normalizeQuizOptions(optsDecoded);
897
913
  } catch (e) {
898
914
  return `quiz error: ${e.message}`;
899
915
  }
@@ -917,11 +933,11 @@ Explanation: ${j.explanation}${note}`;
917
933
  const payload = {
918
934
  id,
919
935
  type: "quiz",
920
- question: args.question,
921
- details: args.details,
936
+ question: qFixed,
937
+ details: dFixed,
922
938
  options: options.map((o, i) => ({ label: o.label, value: o.value, description: o.description, index: i + 1 })),
923
939
  correctIndices,
924
- explanation: args.explanation,
940
+ explanation: eFixed,
925
941
  multiSelect: !!args.multiSelect,
926
942
  sessionID: ctx.sessionID,
927
943
  timestamp: Date.now()
@@ -933,7 +949,7 @@ Explanation: ${j.explanation}${note}`;
933
949
  slog("quiz write failed", String(e));
934
950
  }
935
951
  try {
936
- await ctx.metadata?.({ title: `Quiz: ${args.question.slice(0, 40)}`, metadata: { pendingId: id } });
952
+ await ctx.metadata?.({ title: `Quiz: ${qFixed.slice(0, 40)}`, metadata: { pendingId: id } });
937
953
  } catch {}
938
954
  watchAndInject(client, directory, id, ctx.sessionID, (r) => {
939
955
  const dk = !!r?.dontKnow;
@@ -949,21 +965,21 @@ Note: ${r.note}` : "";
949
965
  answers: r?.answers || [],
950
966
  correct: ok,
951
967
  correctIndices,
952
- explanation: args.explanation,
968
+ explanation: eFixed,
953
969
  dontKnow: dk,
954
970
  note: r?.note
955
971
  };
956
972
  withMdLock(() => appendToMdLog(answerCalloutQuiz(details)));
957
973
  }
958
- return dk ? `[quiz answered] "${args.question}" -> I don't know (genuine gap).
974
+ return dk ? `[quiz answered] "${qFixed}" -> I don't know (genuine gap).
959
975
  Correct: ${correctStr}
960
- Explanation: ${args.explanation}${note}` : `[quiz answered] "${args.question}" -> ${sel} = ${ok ? "CORRECT" : "INCORRECT"}.
976
+ Explanation: ${eFixed}${note}` : `[quiz answered] "${qFixed}" -> ${sel} = ${ok ? "CORRECT" : "INCORRECT"}.
961
977
  Correct: ${correctStr}
962
- Explanation: ${args.explanation}${note}`;
978
+ Explanation: ${eFixed}${note}`;
963
979
  });
964
980
  if (mdLogFile) {
965
981
  try {
966
- await withMdLock(() => appendToMdLog(questionCallout("Quiz", args.question, args.details?.trim() || undefined, options.map((o) => ({ label: o.label })))));
982
+ await withMdLock(() => appendToMdLog(questionCallout("Quiz", qFixed, dFixed?.trim() || undefined, options.map((o) => ({ label: o.label })))));
967
983
  } catch {}
968
984
  }
969
985
  if (tuiAlive) {
@@ -998,9 +1014,9 @@ ${args.multiSelect ? "Select all correct (comma-separated numbers, e.g. 1,3) or
998
1014
  if (trimmed === "0" || trimmed.toLowerCase() === "i don't know") {
999
1015
  const msg = `User selected "I don't know" \u2014 genuine gap, not a guess.
1000
1016
  Correct: ${correctStr}
1001
- Explanation: ${args.explanation}`;
1017
+ Explanation: ${eFixed}`;
1002
1018
  if (mdLogFile)
1003
- await withMdLock(() => appendToMdLog(callout("question", "Quiz \u2014 I don't know", [args.question, trimmed, `Correct: ${correctStr}`, args.explanation])));
1019
+ await withMdLock(() => appendToMdLog(callout("question", "Quiz \u2014 I don't know", [qFixed, trimmed, `Correct: ${correctStr}`, eFixed])));
1004
1020
  return msg;
1005
1021
  }
1006
1022
  const nums = trimmed.split(/[,\s]+/).map((s) => parseInt(s, 10)).filter((n) => !isNaN(n) && n >= 1 && n <= options.length);
@@ -1012,30 +1028,30 @@ Explanation: ${args.explanation}`;
1012
1028
  const result = `User answered ${verdict}.
1013
1029
  Selected: ${selectedStr}
1014
1030
  Correct: ${correctStr}
1015
- Explanation: ${args.explanation}`;
1016
- ctx.metadata?.({ title: correct ? "Quiz \u2014 correct \u2713" : "Quiz \u2014 incorrect \u2717", metadata: { correct, correctIndices, explanation: args.explanation } });
1031
+ Explanation: ${eFixed}`;
1032
+ ctx.metadata?.({ title: correct ? "Quiz \u2014 correct \u2713" : "Quiz \u2014 incorrect \u2717", metadata: { correct, correctIndices, explanation: eFixed } });
1017
1033
  if (mdLogFile)
1018
- await withMdLock(() => appendToMdLog(callout(correct ? "success" : "failure", correct ? "Quiz \u2014 correct \u2713" : "Quiz \u2014 incorrect \u2717", [`Q: ${args.question}`, `Selected: ${selectedStr}`, `Correct: ${correctStr}`, args.explanation])));
1034
+ await withMdLock(() => appendToMdLog(callout(correct ? "success" : "failure", correct ? "Quiz \u2014 correct \u2713" : "Quiz \u2014 incorrect \u2717", [`Q: ${qFixed}`, `Selected: ${selectedStr}`, `Correct: ${correctStr}`, eFixed])));
1019
1035
  return result;
1020
1036
  }
1021
1037
  const instruction = [
1022
1038
  `[quiz ready \u2014 awaiting user answer via \`question\` tool]`,
1023
- `Question: ${args.question}`,
1024
- args.details ? `Details: ${args.details}` : null,
1039
+ `Question: ${qFixed}`,
1040
+ dFixed ? `Details: ${dFixed}` : null,
1025
1041
  `Options (display order, already shuffled):`,
1026
1042
  ...options.map((o, i) => `${i + 1}. ${o.label}${o.description ? ` \u2014 ${o.description}` : ""} (value="${o.value}")`),
1027
1043
  `Correct indices: ${correctIndices.join(", ")} (Correct values: ${correctStr})`,
1028
- `Explanation (reveal AFTER answer): ${args.explanation}`,
1044
+ `Explanation (reveal AFTER answer): ${eFixed}`,
1029
1045
  `Mode: ${args.multiSelect ? "multi-select (exact set)" : "single-select"}`,
1030
1046
  ``,
1031
1047
  `INSTRUCTION FOR LLM: Call the built-in \`question\` tool with:`,
1032
1048
  ` header: "Quiz"`,
1033
- ` question: "${args.question.replace(/"/g, "\\\"")}"`,
1049
+ ` question: "${qFixed.replace(/"/g, "\\\"")}"`,
1034
1050
  ` options: [${options.map((o) => `{label:"${o.label.replace(/"/g, "\\\"")}", description:"${(o.description ?? "").replace(/"/g, "\\\"")}"}`).join(", ")}]`,
1035
1051
  `Then compare the user's selected labels to correct indices [${correctIndices.join(", ")}]. Grade as ${args.multiSelect ? "exact-set match" : "single match"}, show \u2713/\u2717, reveal Correct: ${correctStr}, and Explanation. An 'I don't know' maps to dontKnow (genuine gap).`
1036
1052
  ].filter(Boolean).join(`
1037
1053
  `);
1038
- ctx.metadata?.({ title: `Quiz: ${args.question.slice(0, 40)}`, metadata: { correctIndices, explanation: args.explanation, options: options.map((o, i) => ({ index: i + 1, label: o.label })) } });
1054
+ ctx.metadata?.({ title: `Quiz: ${qFixed.slice(0, 40)}`, metadata: { correctIndices, explanation: eFixed, options: options.map((o, i) => ({ index: i + 1, label: o.label })) } });
1039
1055
  return instruction;
1040
1056
  }
1041
1057
  }),
@@ -1063,23 +1079,27 @@ Explanation: ${args.explanation}`;
1063
1079
  slog("quiz_batch isAlive", isAlive);
1064
1080
  const normalized = [];
1065
1081
  for (const q of args.quizzes) {
1082
+ const qFixed = decodeQuizText(q.question) ?? q.question;
1083
+ const dFixed = decodeQuizText(q.details);
1084
+ const eFixed = decodeQuizText(q.explanation) ?? q.explanation;
1085
+ const optsDecoded = q.options?.map((o) => ({ ...o, label: decodeQuizText(o.label) ?? o.label, description: o.description ? decodeQuizText(o.description) : o.description }));
1066
1086
  let opts;
1067
1087
  try {
1068
- opts = normalizeQuizOptions(q.options);
1088
+ opts = normalizeQuizOptions(optsDecoded);
1069
1089
  } catch (e) {
1070
1090
  slog("quiz_batch normalize error", e.message);
1071
- return `quiz_batch error: ${e.message} in "${q.question}"`;
1091
+ return `quiz_batch error: ${e.message} in "${qFixed}"`;
1072
1092
  }
1073
1093
  if (q.shuffle !== false)
1074
1094
  opts = shuffleOptions(opts);
1075
1095
  const { indices, error } = resolveCorrect(q.correctAnswer, opts);
1076
1096
  if (error) {
1077
1097
  slog("quiz_batch resolveCorrect error", error);
1078
- return `quiz_batch error: ${error} in "${q.question}"`;
1098
+ return `quiz_batch error: ${error} in "${qFixed}"`;
1079
1099
  }
1080
1100
  if (opts.length < 2)
1081
- return `quiz_batch error: need 2+ options in "${q.question}"`;
1082
- normalized.push({ question: q.question, details: q.details, options: opts, correctIndices: indices, explanation: q.explanation, multiSelect: !!q.multiSelect });
1101
+ return `quiz_batch error: need 2+ options in "${qFixed}"`;
1102
+ normalized.push({ question: qFixed, details: dFixed, options: opts, correctIndices: indices, explanation: eFixed, multiSelect: !!q.multiSelect });
1083
1103
  }
1084
1104
  slog("quiz_batch normalized", normalized.length);
1085
1105
  try {