@bojackduy/opencode-learn 1.2.2 → 1.2.3

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
@@ -317,6 +317,20 @@ function answerCalloutQuiz(details) {
317
317
  }
318
318
  return callout(type, title, body);
319
319
  }
320
+ function answerCalloutQuestion(questions, answers) {
321
+ const qs = Array.isArray(questions) ? questions : [];
322
+ const ans = Array.isArray(answers) ? answers : [];
323
+ if (!qs.length) {
324
+ const flat = ans.map((a) => Array.isArray(a) ? a.join(", ") : String(a ?? "")).filter(Boolean);
325
+ return callout("example", "Answer", flat.length ? flat : ["(no answer)"]);
326
+ }
327
+ const body = qs.map((q, i) => {
328
+ const header = q?.header || `Q${i + 1}`;
329
+ const sel = Array.isArray(ans[i]) ? ans[i] : [];
330
+ return `${header}: ${sel.length ? sel.join(", ") : "(no answer)"}`;
331
+ });
332
+ return callout("example", "Answer", body);
333
+ }
320
334
  function answerCalloutAsk(details) {
321
335
  const status = details?.status;
322
336
  if (status === "cancelled")
@@ -324,11 +338,17 @@ function answerCalloutAsk(details) {
324
338
  if (status === "unavailable")
325
339
  return callout("warning", "Question \u2014 unavailable", [details?.message || ""]);
326
340
  const answers = details?.answers || [];
341
+ if (answers.length && answers.every((a) => Array.isArray(a))) {
342
+ const questions = details?.questions || [];
343
+ return answerCalloutQuestion(questions, answers);
344
+ }
327
345
  const body = answers.map((a) => {
328
346
  if (a.type === "other")
329
347
  return `Other: ${a.label}`;
330
348
  if (a.type === "text")
331
349
  return a.label;
350
+ if (typeof a === "string")
351
+ return a;
332
352
  return `${a.index}. ${a.label}`;
333
353
  });
334
354
  if (body.length === 0)
@@ -401,6 +421,25 @@ async function backfillMdLog(client, sessionID, directory) {
401
421
  }
402
422
  continue;
403
423
  }
424
+ if (toolName === "question" && Array.isArray(input.questions)) {
425
+ const qs = input.questions;
426
+ if (st.status === "pending" || st.status === "running") {
427
+ qs.forEach((q, i) => {
428
+ if (!q?.question)
429
+ return;
430
+ blocks.push(questionCallout(q.header || (qs.length > 1 ? `Question ${i + 1}/${qs.length}` : "Question"), q.question, undefined, q.options ?? []));
431
+ });
432
+ } else if (st.status === "completed") {
433
+ qs.forEach((q, i) => {
434
+ if (!q?.question)
435
+ return;
436
+ blocks.push(questionCallout(q.header || (qs.length > 1 ? `Question ${i + 1}/${qs.length}` : "Question"), q.question, undefined, q.options ?? []));
437
+ });
438
+ const ans = Array.isArray(meta.answers) ? meta.answers : [];
439
+ blocks.push(answerCalloutAsk({ answers: ans, questions: qs, status: "completed" }));
440
+ }
441
+ continue;
442
+ }
404
443
  if (st.status === "pending" || st.status === "running") {
405
444
  if (input.question) {
406
445
  const opts = Array.isArray(input.options) ? input.options : [];
@@ -943,26 +982,29 @@ Explanation: ${j.explanation}${note}`;
943
982
  await withMdFileLock(mdFile, () => appendToMdLogForSession(ses, assistantBlock(stripSkillBlocks(text))));
944
983
  } catch {}
945
984
  },
946
- "tool.execute.before": async (input) => {
985
+ "tool.execute.before": async (input, output) => {
947
986
  try {
948
987
  const ses = extractHookSessionID(input?.sessionID);
949
988
  const mdFile = getMdFile(ses);
950
989
  if (!mdFile || !ses)
951
990
  return;
952
991
  const toolName = input.tool;
953
- const args = input.args ?? {};
992
+ const args = output?.args ?? input.args ?? {};
954
993
  if (toolName === "question") {
955
- const q = args.question || args.header || "";
956
- const ctx2 = args.details?.trim() || undefined;
957
- const opts = Array.isArray(args.options) ? args.options : [];
958
994
  const callID = input.callID;
959
995
  const qkey = callID ? mdKey(ses, `q:${callID}`) : undefined;
960
996
  if (qkey && loggedToolCallIds.has(qkey))
961
997
  return;
962
998
  if (qkey)
963
999
  loggedToolCallIds.add(qkey);
964
- if (q)
965
- await withMdFileLock(mdFile, () => appendToMdLogForSession(ses, questionCallout("Question", q, ctx2, opts)));
1000
+ const qs = Array.isArray(args.questions) ? args.questions : args.question ? [{ question: args.question, header: args.header, options: args.options ?? [] }] : [];
1001
+ for (let i = 0;i < qs.length; i++) {
1002
+ const q = qs[i];
1003
+ if (!q?.question)
1004
+ continue;
1005
+ const label = q.header || (qs.length > 1 ? `Question ${i + 1}/${qs.length}` : "Question");
1006
+ await withMdFileLock(mdFile, () => appendToMdLogForSession(ses, questionCallout(label, q.question, undefined, q.options ?? [])));
1007
+ }
966
1008
  }
967
1009
  } catch {}
968
1010
  },
@@ -978,11 +1020,21 @@ Explanation: ${j.explanation}${note}`;
978
1020
  if (akey && loggedToolCallIds.has(akey))
979
1021
  return;
980
1022
  if (toolName === "question") {
981
- const meta = output.metadata ?? {};
982
- let answers = meta.answers ?? [];
983
- if (!answers.length && output.output)
1023
+ const meta = output?.metadata ?? {};
1024
+ const inArgs = input?.args ?? {};
1025
+ const qs = Array.isArray(inArgs.questions) ? inArgs.questions : [];
1026
+ let answers = meta.answers;
1027
+ if (!Array.isArray(answers) || !answers.length) {
1028
+ const outText = typeof output?.output === "string" ? output.output.trim() : "";
1029
+ if (outText) {
1030
+ await withMdFileLock(mdFile, () => appendToMdLogForSession(ses, callout("example", "Answer", [outText.slice(0, 500)])));
1031
+ if (akey)
1032
+ loggedToolCallIds.add(akey);
1033
+ return;
1034
+ }
984
1035
  answers = [];
985
- const details = { answers, status: "completed" };
1036
+ }
1037
+ const details = { answers, questions: qs, status: "completed" };
986
1038
  await withMdFileLock(mdFile, () => appendToMdLogForSession(ses, answerCalloutAsk(details)));
987
1039
  if (akey)
988
1040
  loggedToolCallIds.add(akey);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@bojackduy/opencode-learn",
4
- "version": "1.2.2",
4
+ "version": "1.2.3",
5
5
  "description": "Pi learn system for OpenCode — Socratic teaching, graded quiz, Obsidian md_log, and visual makers. Port of amosblomqvist/learn (video: How I Use AI to Learn Things) to OpenCode.",
6
6
  "type": "module",
7
7
  "license": "AGPL-3.0-or-later",
package/plugins/learn.ts CHANGED
@@ -244,12 +244,31 @@ function answerCalloutQuiz(details: any): string {
244
244
  if (details?.explanation) { body.push(""); for (const line of String(details.explanation).split("\n")) body.push(line) }
245
245
  return callout(type, title, body)
246
246
  }
247
+ function answerCalloutQuestion(questions: any[], answers: string[][]): string {
248
+ const qs = Array.isArray(questions) ? questions : []
249
+ const ans = Array.isArray(answers) ? answers : []
250
+ if (!qs.length) {
251
+ const flat = ans.map(a => Array.isArray(a) ? a.join(", ") : String(a ?? "")).filter(Boolean)
252
+ return callout("example", "Answer", flat.length ? flat : ["(no answer)"])
253
+ }
254
+ const body = qs.map((q: any, i: number) => {
255
+ const header = q?.header || `Q${i + 1}`
256
+ const sel: string[] = Array.isArray(ans[i]) ? ans[i] as string[] : []
257
+ return `${header}: ${sel.length ? sel.join(", ") : "(no answer)"}`
258
+ })
259
+ return callout("example", "Answer", body)
260
+ }
247
261
  function answerCalloutAsk(details: any): string {
248
262
  const status = details?.status
249
263
  if (status === "cancelled") return callout("warning", "Question — cancelled", ["(user skipped)"])
250
264
  if (status === "unavailable") return callout("warning", "Question — unavailable", [details?.message || ""])
251
265
  const answers: any[] = details?.answers || []
252
- const body: string[] = answers.map((a) => { if (a.type === "other") return `Other: ${a.label}`; if (a.type === "text") return a.label; return `${a.index}. ${a.label}` })
266
+ // Native opencode shape: string[][] (per-question selected labels)
267
+ if (answers.length && (answers as any[]).every(a => Array.isArray(a))) {
268
+ const questions: any[] = details?.questions || []
269
+ return answerCalloutQuestion(questions, answers as string[][])
270
+ }
271
+ const body: string[] = answers.map((a) => { if (a.type === "other") return `Other: ${a.label}`; if (a.type === "text") return a.label; if (typeof a === "string") return a; return `${a.index}. ${a.label}` })
253
272
  if (body.length === 0) body.push("(no answer)")
254
273
  return callout("example", "Answer", body)
255
274
  }
@@ -310,6 +329,23 @@ async function backfillMdLog(client: any, sessionID: string, directory: string):
310
329
  }
311
330
  continue
312
331
  }
332
+ if (toolName === "question" && Array.isArray((input as any).questions)) {
333
+ const qs: any[] = (input as any).questions
334
+ if (st.status === "pending" || st.status === "running") {
335
+ qs.forEach((q: any, i: number) => {
336
+ if (!q?.question) return
337
+ blocks.push(questionCallout(q.header || (qs.length > 1 ? `Question ${i + 1}/${qs.length}` : "Question"), q.question, undefined, q.options ?? []))
338
+ })
339
+ } else if (st.status === "completed") {
340
+ qs.forEach((q: any, i: number) => {
341
+ if (!q?.question) return
342
+ blocks.push(questionCallout(q.header || (qs.length > 1 ? `Question ${i + 1}/${qs.length}` : "Question"), q.question, undefined, q.options ?? []))
343
+ })
344
+ const ans = Array.isArray(meta.answers) ? meta.answers : []
345
+ blocks.push(answerCalloutAsk({ answers: ans, questions: qs, status: "completed" }))
346
+ }
347
+ continue
348
+ }
313
349
  if (st.status === "pending" || st.status === "running") {
314
350
  if (input.question) {
315
351
  const opts = Array.isArray(input.options) ? input.options : []
@@ -849,24 +885,28 @@ Return ONLY JSON: {"inferred":[2],"semanticCorrect":false,"reason":"...","isIDK"
849
885
  await withMdFileLock(mdFile, () => appendToMdLogForSession(ses, assistantBlock(stripSkillBlocks(text))))
850
886
  } catch {}
851
887
  },
852
- "tool.execute.before": async (input) => {
888
+ "tool.execute.before": async (input, output) => {
853
889
  try {
854
890
  const ses = extractHookSessionID((input as any)?.sessionID)
855
891
  const mdFile = getMdFile(ses)
856
892
  if (!mdFile || !ses) return
857
893
  const toolName = (input as any).tool
858
- const args = (input as any).args ?? {}
859
- // Built-in `question` tool is used as fallback when TUI not alive; mirror it.
860
- // `ask_user_question` is already mirrored inside its own execute (with correct TUI handling), so skip to avoid duplicate.
894
+ // NOTE: per Hooks type, before-hook args live in output.args (input only has tool/sessionID/callID).
895
+ const args = (output as any)?.args ?? (input as any).args ?? {}
896
+ // Native `question` tool shape: {questions: [{question, header, options, multiple}]}
861
897
  if (toolName === "question") {
862
- const q = args.question || args.header || ""
863
- const ctx2 = args.details?.trim() || undefined
864
- const opts = Array.isArray(args.options) ? args.options : []
865
898
  const callID = (input as any).callID
866
899
  const qkey = callID ? mdKey(ses, `q:${callID}`) : undefined
867
900
  if (qkey && loggedToolCallIds.has(qkey)) return
868
901
  if (qkey) loggedToolCallIds.add(qkey)
869
- if (q) await withMdFileLock(mdFile, () => appendToMdLogForSession(ses, questionCallout("Question", q, ctx2, opts)))
902
+ const qs: any[] = Array.isArray(args.questions) ? args.questions
903
+ : (args.question ? [{ question: args.question, header: args.header, options: args.options ?? [] }] : [])
904
+ for (let i = 0; i < qs.length; i++) {
905
+ const q = qs[i]
906
+ if (!q?.question) continue
907
+ const label = q.header || (qs.length > 1 ? `Question ${i + 1}/${qs.length}` : "Question")
908
+ await withMdFileLock(mdFile, () => appendToMdLogForSession(ses, questionCallout(label, q.question, undefined, q.options ?? [])))
909
+ }
870
910
  }
871
911
  } catch {}
872
912
  },
@@ -880,10 +920,20 @@ Return ONLY JSON: {"inferred":[2],"semanticCorrect":false,"reason":"...","isIDK"
880
920
  const akey = callID ? mdKey(ses, `answer:${callID}`) : undefined
881
921
  if (akey && loggedToolCallIds.has(akey)) return
882
922
  if (toolName === "question") {
883
- const meta: any = (output as any).metadata ?? {}
884
- let answers: any[] = meta.answers ?? []
885
- if (!answers.length && (output as any).output) answers = []
886
- const details: any = { answers, status: "completed" }
923
+ const meta: any = (output as any)?.metadata ?? {}
924
+ const inArgs: any = (input as any)?.args ?? {}
925
+ const qs: any[] = Array.isArray(inArgs.questions) ? inArgs.questions : []
926
+ let answers: any = meta.answers
927
+ if (!Array.isArray(answers) || !answers.length) {
928
+ const outText = typeof (output as any)?.output === "string" ? ((output as any).output as string).trim() : ""
929
+ if (outText) {
930
+ await withMdFileLock(mdFile, () => appendToMdLogForSession(ses, callout("example", "Answer", [outText.slice(0, 500)])))
931
+ if (akey) loggedToolCallIds.add(akey)
932
+ return
933
+ }
934
+ answers = []
935
+ }
936
+ const details: any = { answers, questions: qs, status: "completed" }
887
937
  await withMdFileLock(mdFile, () => appendToMdLogForSession(ses, answerCalloutAsk(details)))
888
938
  if (akey) loggedToolCallIds.add(akey)
889
939
  }