@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 +63 -11
- package/package.json +1 -1
- package/plugins/learn.ts +63 -13
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
|
-
|
|
965
|
-
|
|
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
|
|
982
|
-
|
|
983
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
859
|
-
|
|
860
|
-
// `
|
|
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
|
-
|
|
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)
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
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
|
}
|