@codio-ai/opencode-authoring-agent 0.1.0 → 0.1.2
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/bin/codio-authoring-register.js +23 -1
- package/dist/agents/orchestrator.d.ts.map +1 -1
- package/dist/agents/page-author.d.ts.map +1 -1
- package/dist/codio-scripts/codio-fetch.js +45931 -0
- package/dist/codio-scripts/codio-publish.js +44984 -0
- package/dist/index.js +225 -6
- package/dist/tools/create-page.d.ts +1 -0
- package/dist/tools/create-page.d.ts.map +1 -1
- package/dist/tools/validate-guide.d.ts +1 -1
- package/dist/tools/validate-guide.d.ts.map +1 -1
- package/package.json +9 -19
- package/src/skills/codio-fetch/SKILL.md +153 -0
- package/src/skills/codio-fetch/scripts/extract-course-export.sh +69 -0
- package/src/skills/codio-fetch/scripts/run-course-export.sh +196 -0
- package/src/skills/codio-publish/SKILL.md +140 -0
package/dist/index.js
CHANGED
|
@@ -72,10 +72,11 @@ var AGENT_DESCRIPTIONS = {
|
|
|
72
72
|
- Role: Writes Codio guide page markdown. Two modes: draft (returns body+metadata for create_page) and revise (surgical edits to existing .md).
|
|
73
73
|
- Permissions: Read + edit (no write, no bash)
|
|
74
74
|
- Stats: Sonnet-class prose quality.
|
|
75
|
-
- Capabilities: Instructional writing, surgical revisions.
|
|
75
|
+
- Capabilities: Instructional writing, surgical revisions, Codio-compliant freeze/try-it/adaptive-instruction generation.
|
|
76
76
|
- **Delegate when:** Drafting a new page, or applying a focused edit to an existing one.
|
|
77
77
|
- **Don't delegate when:** The edit is a single-line typo fix — do it yourself.
|
|
78
|
-
- **Rule of thumb:** New prose or non-trivial edit → @page-author. Trivial fix → yourself
|
|
78
|
+
- **Rule of thumb:** New prose or non-trivial edit → @page-author. Trivial fix → yourself.
|
|
79
|
+
- **layoutConfig:** When delegating, include \`layoutConfig: { openFiles: string[] }\` derived from the page's \`files\` list so @page-author can generate adaptive file instructions.`,
|
|
79
80
|
"assessment-author": `@assessment-author
|
|
80
81
|
- Role: Authors MCQ / code-output-compare / fill-in-the-blanks / parsons-puzzle / llm-based-auto-rubric payloads. Returns structured payloads only.
|
|
81
82
|
- Permissions: Read only
|
|
@@ -130,7 +131,7 @@ ${enabledAgents}
|
|
|
130
131
|
|
|
131
132
|
<Tools>
|
|
132
133
|
|
|
133
|
-
- \`create_page({workspace, stem, title, type, layout, learningObjectives, files?, markdownBody, chapterFolder?})\` — stamp a page JSON with a fresh v4 UUID and write the JSON+MD pair. Use after \`@page-author\` returns a body+metadata in draft mode.
|
|
134
|
+
- \`create_page({workspace, stem, title, type, layout, learningObjectives, files?, markdownBody, chapterFolder?})\` — stamp a page JSON with a fresh v4 UUID and write the JSON+MD pair. Use after \`@page-author\` returns a body+metadata in draft mode. Validates freeze directive syntax before writing — will throw if FREEZE CODE BEGIN/END are unmatched or use wrong comment prefix for the language.
|
|
134
135
|
- \`create_assessment({type, workspace, payload})\` — stamp an assessment JSON with a fresh taskId and write to \`.guides/assessments/\`. Returns \`{ taskId, jsonPath, embedLine }\`. Use after \`@assessment-author\` returns a payload. The returned \`embedLine\` is what you splice into the page markdown.
|
|
135
136
|
- \`validate_guide({workspace})\` — deterministic structural validation (UUIDs, order arrays, embed references, taskId/filename consistency). Run automatically at the end of new-assignment, import-source, and reorder workflows. Run manually on user request (\`/validate-guide\`).
|
|
136
137
|
</Tools>
|
|
@@ -142,7 +143,7 @@ Phases (which apply depends on the user's intent; the workflow-* skills give per
|
|
|
142
143
|
1. **Intake** — confirm topic, audience, LOs, structural constraints, source materials. Do not skip; confirm even if some info was in the user's first message.
|
|
143
144
|
2. **Outline Gate** — for new assignments or substantial restructures, \`@outline-architect\` returns a proposed outline. **Block until the user approves.** No file writes until then.
|
|
144
145
|
3. **Scaffold** — once approved, generate stems (kebab-slug + 4-hex). Build the planning map. Write the root \`.guides/content/index.json\` (and the single chapter folder's index if applicable).
|
|
145
|
-
4. **Drafting** — fan out \`@page-author\` (draft mode) for each page. Use \`create_page\` to stamp each. Run in parallel where pages are independent.
|
|
146
|
+
4. **Drafting** — fan out \`@page-author\` (draft mode) for each page. Pass \`layoutConfig: { openFiles: files }\` derived from that page's \`files\` list. Use \`create_page\` to stamp each. Run in parallel where pages are independent.
|
|
146
147
|
5. **Assessments** — for each page needing one, fan out \`@assessment-author\`. Use \`create_assessment\` for each returned payload. Splice the returned \`embedLine\` into the page markdown using the edit tool. Group multiple embeds under a \`## Check your understanding\` heading.
|
|
147
148
|
6. **Validation** — run \`validate_guide\` tool (always at end of new-assignment / import-source / reorder workflows). Optionally delegate to \`@validator\` for content review when scope warrants.
|
|
148
149
|
7. **Done** — summarize what was built and any flagged issues.
|
|
@@ -295,6 +296,7 @@ The orchestrator gives you:
|
|
|
295
296
|
- \`title\`, \`type\`, \`layout\`, \`learningObjectives\`.
|
|
296
297
|
- \`body_brief\` — bullets describing what the page should cover.
|
|
297
298
|
- \`assignment_context\` — short summary of the assignment and what came before this page.
|
|
299
|
+
- \`layoutConfig\` *(optional)* — \`{ openFiles: string[], panels: [] }\` — which files are open by default in Codio. Use this for adaptive file instructions (see Rule 3 below).
|
|
298
300
|
|
|
299
301
|
What you return:
|
|
300
302
|
- \`markdownBody\` — the full instructional prose.
|
|
@@ -302,9 +304,47 @@ What you return:
|
|
|
302
304
|
|
|
303
305
|
You do **not** insert assessment embed strings. The orchestrator inserts those after \`assessment-author\` returns.
|
|
304
306
|
|
|
307
|
+
## Codio Idioms (Cardinal Rules)
|
|
308
|
+
|
|
309
|
+
These rules apply to **every page** you generate in draft mode.
|
|
310
|
+
|
|
311
|
+
### Rule 1 — Freeze code directives
|
|
312
|
+
|
|
313
|
+
When a code block is scaffold or starter code students must not modify:
|
|
314
|
+
- Wrap with language-correct freeze directives:
|
|
315
|
+
- Python / Bash / Ruby: \`# FREEZE CODE BEGIN\` … \`# FREEZE CODE END\`
|
|
316
|
+
- JavaScript / TypeScript / Java / C / C++: \`// FREEZE CODE BEGIN\` … \`// FREEZE CODE END\`
|
|
317
|
+
- SQL: \`-- FREEZE CODE BEGIN\` … \`-- FREEZE CODE END\`
|
|
318
|
+
- Signal: source comments like \`# DO NOT ALTER THIS CODE\` or context like "starter code" identify freeze-worthy regions.
|
|
319
|
+
- Frozen code shows a padlock icon in Codio; students cannot edit it.
|
|
320
|
+
- Do **not** add a Try It button to frozen blocks.
|
|
321
|
+
|
|
322
|
+
### Rule 2 — Try It buttons
|
|
323
|
+
|
|
324
|
+
For every executable code block that is **not frozen**, append a Try It button immediately after the closing fence:
|
|
325
|
+
- Syntax: \`{Try it}(COMMAND)\` — no space between \`{Try it}\` and the parentheses.
|
|
326
|
+
- Language commands: \`python3 solution.py\` / \`node solution.js\` / \`javac Main.java && java Main\`
|
|
327
|
+
- Skip Try It for pseudocode, non-executable snippets, and frozen blocks.
|
|
328
|
+
|
|
329
|
+
### Rule 3 — Adaptive file instructions
|
|
330
|
+
|
|
331
|
+
Before writing any instruction that references a file:
|
|
332
|
+
- If \`layoutConfig.openFiles\` contains that file → write **"Look at the X file on the left"**.
|
|
333
|
+
- If the file is absent from \`openFiles\`, or no \`layoutConfig\` was provided → write **"Open the X file on the left"**.
|
|
334
|
+
|
|
335
|
+
### Rule 4 — No duplicate page title
|
|
336
|
+
|
|
337
|
+
- The page title (JSON \`title\` field) is the canonical H1.
|
|
338
|
+
- Do **not** start your \`markdownBody\` with a \`# Heading\` that repeats the title.
|
|
339
|
+
- Begin the body with prose or an H2, never a duplicate H1.
|
|
340
|
+
|
|
341
|
+
### Rule 5 — Visualizer file references
|
|
342
|
+
|
|
343
|
+
- Reference visualizer HTML files by local path only: e.g. \`"url": "tree_visualizer.html"\`.
|
|
344
|
+
- Do **not** use full URLs (\`http://…\`) or \`preview:\` schemes.
|
|
345
|
+
|
|
305
346
|
## Style rules
|
|
306
347
|
|
|
307
|
-
- Heading 1 = page title (matches the JSON \`title\`).
|
|
308
348
|
- One concept per page. If you find yourself introducing a second major concept, stop — that's a separate page.
|
|
309
349
|
- Worked examples in fenced code blocks with a language label.
|
|
310
350
|
- "You" not "the student". Active voice. No filler.
|
|
@@ -1087,6 +1127,51 @@ async function createAssessmentHandler(raw) {
|
|
|
1087
1127
|
import { unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1088
1128
|
import { join as join4 } from "node:path";
|
|
1089
1129
|
import { z as z3 } from "zod";
|
|
1130
|
+
var LANG_COMMENT_PREFIX = {
|
|
1131
|
+
python: "#",
|
|
1132
|
+
py: "#",
|
|
1133
|
+
bash: "#",
|
|
1134
|
+
sh: "#",
|
|
1135
|
+
ruby: "#",
|
|
1136
|
+
rb: "#",
|
|
1137
|
+
javascript: "//",
|
|
1138
|
+
js: "//",
|
|
1139
|
+
typescript: "//",
|
|
1140
|
+
ts: "//",
|
|
1141
|
+
java: "//",
|
|
1142
|
+
c: "//",
|
|
1143
|
+
cpp: "//",
|
|
1144
|
+
"c++": "//",
|
|
1145
|
+
sql: "--"
|
|
1146
|
+
};
|
|
1147
|
+
function validateFreezeDirectives(markdownBody) {
|
|
1148
|
+
const errors = [];
|
|
1149
|
+
const fenceRe = /^```(\w*)\n([\s\S]*?)^```/gm;
|
|
1150
|
+
let match;
|
|
1151
|
+
while ((match = fenceRe.exec(markdownBody)) !== null) {
|
|
1152
|
+
const lang = (match[1] ?? "").toLowerCase();
|
|
1153
|
+
const code = match[2] ?? "";
|
|
1154
|
+
const beginCount = (code.match(/FREEZE CODE BEGIN/g) ?? []).length;
|
|
1155
|
+
const endCount = (code.match(/FREEZE CODE END/g) ?? []).length;
|
|
1156
|
+
if (beginCount !== endCount) {
|
|
1157
|
+
errors.push(`\`\`\`${lang || "code"} block has unmatched freeze directives: ${beginCount} FREEZE CODE BEGIN, ${endCount} FREEZE CODE END`);
|
|
1158
|
+
continue;
|
|
1159
|
+
}
|
|
1160
|
+
if (beginCount === 0)
|
|
1161
|
+
continue;
|
|
1162
|
+
const expectedPrefix = LANG_COMMENT_PREFIX[lang];
|
|
1163
|
+
if (!expectedPrefix)
|
|
1164
|
+
continue;
|
|
1165
|
+
for (const m of code.matchAll(/^(.+?)FREEZE CODE BEGIN/gm)) {
|
|
1166
|
+
const actualPrefix = (m[1] ?? "").trim();
|
|
1167
|
+
if (actualPrefix !== expectedPrefix) {
|
|
1168
|
+
errors.push(`Wrong comment prefix for FREEZE CODE in \`\`\`${lang} block: found "${actualPrefix}", expected "${expectedPrefix}"`);
|
|
1169
|
+
break;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
return errors;
|
|
1174
|
+
}
|
|
1090
1175
|
var PAGE_TYPES = [
|
|
1091
1176
|
"intro",
|
|
1092
1177
|
"objectives",
|
|
@@ -1151,6 +1236,12 @@ function writeCreatedPage(workspace, chapterFolder, input) {
|
|
|
1151
1236
|
}
|
|
1152
1237
|
async function createPageHandler(raw) {
|
|
1153
1238
|
const input = createPageInputSchema.parse(raw);
|
|
1239
|
+
const freezeErrors = validateFreezeDirectives(input.markdownBody);
|
|
1240
|
+
if (freezeErrors.length > 0) {
|
|
1241
|
+
throw new Error(`Freeze directive validation failed:
|
|
1242
|
+
${freezeErrors.join(`
|
|
1243
|
+
`)}`);
|
|
1244
|
+
}
|
|
1154
1245
|
return writeCreatedPage(input.workspace, input.chapterFolder, input);
|
|
1155
1246
|
}
|
|
1156
1247
|
|
|
@@ -1175,6 +1266,23 @@ function walkJsonFiles(root) {
|
|
|
1175
1266
|
}
|
|
1176
1267
|
return out;
|
|
1177
1268
|
}
|
|
1269
|
+
var LANG_COMMENT_PREFIX2 = {
|
|
1270
|
+
python: "#",
|
|
1271
|
+
py: "#",
|
|
1272
|
+
bash: "#",
|
|
1273
|
+
sh: "#",
|
|
1274
|
+
ruby: "#",
|
|
1275
|
+
rb: "#",
|
|
1276
|
+
javascript: "//",
|
|
1277
|
+
js: "//",
|
|
1278
|
+
typescript: "//",
|
|
1279
|
+
ts: "//",
|
|
1280
|
+
java: "//",
|
|
1281
|
+
c: "//",
|
|
1282
|
+
cpp: "//",
|
|
1283
|
+
"c++": "//",
|
|
1284
|
+
sql: "--"
|
|
1285
|
+
};
|
|
1178
1286
|
function walkMdFiles(root) {
|
|
1179
1287
|
if (!existsSync3(root))
|
|
1180
1288
|
return [];
|
|
@@ -1205,6 +1313,49 @@ function collectIdFields(value, out) {
|
|
|
1205
1313
|
collectIdFields(v, out);
|
|
1206
1314
|
}
|
|
1207
1315
|
}
|
|
1316
|
+
function findFirstH1(markdown) {
|
|
1317
|
+
const match = markdown.match(/^# (.+)$/m);
|
|
1318
|
+
return match ? (match[1] ?? "").trim() : null;
|
|
1319
|
+
}
|
|
1320
|
+
function checkFreezeDirectives(markdown) {
|
|
1321
|
+
const errors = [];
|
|
1322
|
+
const fenceRe = /^```(\w*)\n([\s\S]*?)^```/gm;
|
|
1323
|
+
let match;
|
|
1324
|
+
while ((match = fenceRe.exec(markdown)) !== null) {
|
|
1325
|
+
const lang = (match[1] ?? "").toLowerCase();
|
|
1326
|
+
const code = match[2] ?? "";
|
|
1327
|
+
const beginCount = (code.match(/FREEZE CODE BEGIN/g) ?? []).length;
|
|
1328
|
+
const endCount = (code.match(/FREEZE CODE END/g) ?? []).length;
|
|
1329
|
+
if (beginCount !== endCount) {
|
|
1330
|
+
errors.push(`\`\`\`${lang || "code"} block: ${beginCount} FREEZE CODE BEGIN vs ${endCount} FREEZE CODE END`);
|
|
1331
|
+
continue;
|
|
1332
|
+
}
|
|
1333
|
+
if (beginCount === 0)
|
|
1334
|
+
continue;
|
|
1335
|
+
const expectedPrefix = LANG_COMMENT_PREFIX2[lang];
|
|
1336
|
+
if (!expectedPrefix)
|
|
1337
|
+
continue;
|
|
1338
|
+
for (const m of code.matchAll(/^(.+?)FREEZE CODE BEGIN/gm)) {
|
|
1339
|
+
const actualPrefix = (m[1] ?? "").trim();
|
|
1340
|
+
if (actualPrefix !== expectedPrefix) {
|
|
1341
|
+
errors.push(`\`\`\`${lang} block uses "${actualPrefix} FREEZE CODE BEGIN", expected "${expectedPrefix} FREEZE CODE BEGIN"`);
|
|
1342
|
+
break;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
return errors;
|
|
1347
|
+
}
|
|
1348
|
+
function findMalformedTryIt(markdown) {
|
|
1349
|
+
const errors = [];
|
|
1350
|
+
const re = /\{Try it\}(?!\()/g;
|
|
1351
|
+
let m;
|
|
1352
|
+
while ((m = re.exec(markdown)) !== null) {
|
|
1353
|
+
const lineNum = markdown.slice(0, m.index).split(`
|
|
1354
|
+
`).length;
|
|
1355
|
+
errors.push(`Line ~${lineNum}: {Try it} missing command parentheses — expected {Try it}(command)`);
|
|
1356
|
+
}
|
|
1357
|
+
return errors;
|
|
1358
|
+
}
|
|
1208
1359
|
async function validateGuideHandler(raw) {
|
|
1209
1360
|
const input = validateGuideInputSchema.parse(raw);
|
|
1210
1361
|
const { guides, content, assessments } = resolveCodioPaths({
|
|
@@ -1295,7 +1446,75 @@ async function validateGuideHandler(raw) {
|
|
|
1295
1446
|
}
|
|
1296
1447
|
}
|
|
1297
1448
|
}
|
|
1298
|
-
|
|
1449
|
+
for (const f of walkJsonFiles(content)) {
|
|
1450
|
+
if (f.endsWith("/index.json"))
|
|
1451
|
+
continue;
|
|
1452
|
+
let pageTitle;
|
|
1453
|
+
try {
|
|
1454
|
+
const parsed = JSON.parse(readFileSync2(f, "utf8"));
|
|
1455
|
+
pageTitle = parsed?.title;
|
|
1456
|
+
} catch {
|
|
1457
|
+
continue;
|
|
1458
|
+
}
|
|
1459
|
+
if (!pageTitle)
|
|
1460
|
+
continue;
|
|
1461
|
+
const mdPath = f.replace(/\.json$/, ".md");
|
|
1462
|
+
if (!existsSync3(mdPath))
|
|
1463
|
+
continue;
|
|
1464
|
+
const body = readFileSync2(mdPath, "utf8");
|
|
1465
|
+
const firstH1 = findFirstH1(body);
|
|
1466
|
+
if (firstH1 && firstH1 === pageTitle) {
|
|
1467
|
+
findings.push({
|
|
1468
|
+
category: "double_header",
|
|
1469
|
+
detail: `${relative(input.workspace, mdPath)}: first H1 "${firstH1}" duplicates page title — remove H1 from body`,
|
|
1470
|
+
severity: "warning"
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
for (const md of walkMdFiles(content)) {
|
|
1475
|
+
const body = readFileSync2(md, "utf8");
|
|
1476
|
+
for (const err of checkFreezeDirectives(body)) {
|
|
1477
|
+
findings.push({
|
|
1478
|
+
category: "freeze_syntax_error",
|
|
1479
|
+
detail: `${relative(input.workspace, md)}: ${err}`,
|
|
1480
|
+
severity: "error"
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
for (const md of walkMdFiles(content)) {
|
|
1485
|
+
const body = readFileSync2(md, "utf8");
|
|
1486
|
+
for (const err of findMalformedTryIt(body)) {
|
|
1487
|
+
findings.push({
|
|
1488
|
+
category: "try_it_syntax_error",
|
|
1489
|
+
detail: `${relative(input.workspace, md)}: ${err}`,
|
|
1490
|
+
severity: "error"
|
|
1491
|
+
});
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
for (const f of walkJsonFiles(content)) {
|
|
1495
|
+
if (f.endsWith("/index.json"))
|
|
1496
|
+
continue;
|
|
1497
|
+
let parsed;
|
|
1498
|
+
try {
|
|
1499
|
+
parsed = JSON.parse(readFileSync2(f, "utf8"));
|
|
1500
|
+
} catch {
|
|
1501
|
+
continue;
|
|
1502
|
+
}
|
|
1503
|
+
for (const entry of parsed?.files ?? []) {
|
|
1504
|
+
const filePath = entry?.path;
|
|
1505
|
+
if (!filePath)
|
|
1506
|
+
continue;
|
|
1507
|
+
if (!existsSync3(join5(input.workspace, filePath))) {
|
|
1508
|
+
findings.push({
|
|
1509
|
+
category: "missing_referenced_file",
|
|
1510
|
+
detail: `${relative(input.workspace, f)}: references file "${filePath}" which does not exist in workspace`,
|
|
1511
|
+
severity: "warning"
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
const errorCount = findings.filter((f) => f.severity === "error").length;
|
|
1517
|
+
return { findings, passed: errorCount === 0 };
|
|
1299
1518
|
}
|
|
1300
1519
|
|
|
1301
1520
|
// src/tools/index.ts
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
export declare function validateFreezeDirectives(markdownBody: string): string[];
|
|
2
3
|
export declare const PAGE_TYPES: readonly ["intro", "objectives", "instructional", "assessment", "conclusion"];
|
|
3
4
|
export declare const PAGE_LAYOUTS: readonly ["1-panel", "2-panels-tree"];
|
|
4
5
|
export declare const createPageInputSchema: z.ZodObject<{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-page.d.ts","sourceRoot":"","sources":["../../src/tools/create-page.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"create-page.d.ts","sourceRoot":"","sources":["../../src/tools/create-page.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAWxB,wBAAgB,wBAAwB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,EAAE,CA6BvE;AAED,eAAO,MAAM,UAAU,+EAMb,CAAC;AAEX,eAAO,MAAM,YAAY,uCAAwC,CAAC;AAElE,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;iBAuB/B,CAAC;AAEJ,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,UAAU,CAAC;IACxB,MAAM,EAAE,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC;IACtC,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,WAAW,EAAE,OAAO,CAAC;IACrB,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,eAAe,GAAG,QAAQ,CAgB9D;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,KAAK,EAAE,eAAe,GACrB,gBAAgB,CA6BlB;AAED,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,OAAO,GACX,OAAO,CAAC,gBAAgB,CAAC,CAO3B"}
|
|
@@ -3,7 +3,7 @@ export declare const validateGuideInputSchema: z.ZodObject<{
|
|
|
3
3
|
workspace: z.ZodString;
|
|
4
4
|
}, z.core.$strip>;
|
|
5
5
|
export type ValidateGuideInput = z.infer<typeof validateGuideInputSchema>;
|
|
6
|
-
export type FindingCategory = 'missing_content_dir' | 'invalid_uuid' | 'orphan_order_stem' | 'broken_assessment_ref' | 'taskid_filename_mismatch';
|
|
6
|
+
export type FindingCategory = 'missing_content_dir' | 'invalid_uuid' | 'orphan_order_stem' | 'broken_assessment_ref' | 'taskid_filename_mismatch' | 'double_header' | 'freeze_syntax_error' | 'try_it_syntax_error' | 'missing_referenced_file';
|
|
7
7
|
export interface Finding {
|
|
8
8
|
category: FindingCategory;
|
|
9
9
|
detail: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-guide.d.ts","sourceRoot":"","sources":["../../src/tools/validate-guide.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB,eAAO,MAAM,wBAAwB;;iBAEnC,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAE1E,MAAM,MAAM,eAAe,GACvB,qBAAqB,GACrB,cAAc,GACd,mBAAmB,GACnB,uBAAuB,GACvB,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"validate-guide.d.ts","sourceRoot":"","sources":["../../src/tools/validate-guide.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB,eAAO,MAAM,wBAAwB;;iBAEnC,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAE1E,MAAM,MAAM,eAAe,GACvB,qBAAqB,GACrB,cAAc,GACd,mBAAmB,GACnB,uBAAuB,GACvB,0BAA0B,GAC1B,eAAe,GACf,qBAAqB,GACrB,qBAAqB,GACrB,yBAAyB,CAAC;AAE9B,MAAM,WAAW,OAAO;IACtB,QAAQ,EAAE,eAAe,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;CAC/B;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,MAAM,EAAE,OAAO,CAAC;CACjB;AAgGD,wBAAsB,oBAAoB,CACxC,GAAG,EAAE,OAAO,GACX,OAAO,CAAC,mBAAmB,CAAC,CA+K9B"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codio-ai/opencode-authoring-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Opencode plugin for authoring Codio guide assignments — orchestrator, subagents, tools, and skills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -11,19 +11,13 @@
|
|
|
11
11
|
"types": "./dist/index.d.ts"
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
|
-
"files": [
|
|
15
|
-
"dist",
|
|
16
|
-
"src/skills",
|
|
17
|
-
"bin",
|
|
18
|
-
"README.md",
|
|
19
|
-
"LICENSE"
|
|
20
|
-
],
|
|
14
|
+
"files": ["dist", "src/skills", "bin", "README.md", "LICENSE"],
|
|
21
15
|
"bin": {
|
|
22
|
-
"codio-authoring-register": "bin/codio-authoring-register.js",
|
|
23
|
-
"codio-authoring-unregister": "bin/codio-authoring-unregister.js"
|
|
16
|
+
"codio-authoring-register": "./bin/codio-authoring-register.js",
|
|
17
|
+
"codio-authoring-unregister": "./bin/codio-authoring-unregister.js"
|
|
24
18
|
},
|
|
25
19
|
"scripts": {
|
|
26
|
-
"build": "bun build src/index.ts --outdir dist --target node --format esm --external @opencode-ai/plugin --external @opencode-ai/sdk --external zod && tsc --emitDeclarationOnly",
|
|
20
|
+
"build": "bun build src/index.ts --outdir dist --target node --format esm --external @opencode-ai/plugin --external @opencode-ai/sdk --external zod && bun build src/codio-scripts/codio-fetch.js src/codio-scripts/codio-publish.js --outdir dist/codio-scripts --target node --format esm && tsc --emitDeclarationOnly",
|
|
27
21
|
"postinstall": "node ./bin/codio-authoring-register.js || true",
|
|
28
22
|
"preuninstall": "node ./bin/codio-authoring-unregister.js || true",
|
|
29
23
|
"test": "bun test",
|
|
@@ -33,17 +27,11 @@
|
|
|
33
27
|
"check": "biome check --write .",
|
|
34
28
|
"check:ci": "biome check ."
|
|
35
29
|
},
|
|
36
|
-
"keywords": [
|
|
37
|
-
"opencode",
|
|
38
|
-
"opencode-plugin",
|
|
39
|
-
"codio",
|
|
40
|
-
"assignment-authoring",
|
|
41
|
-
"education"
|
|
42
|
-
],
|
|
30
|
+
"keywords": ["opencode", "opencode-plugin", "codio", "assignment-authoring", "education"],
|
|
43
31
|
"license": "MIT",
|
|
44
32
|
"repository": {
|
|
45
33
|
"type": "git",
|
|
46
|
-
"url": "
|
|
34
|
+
"url": "https://github.com/codio-extensions/codio-authoring-opencode-plugin.git"
|
|
47
35
|
},
|
|
48
36
|
"homepage": "https://github.com/codio-extensions/codio-authoring-opencode-plugin#readme",
|
|
49
37
|
"bugs": "https://github.com/codio-extensions/codio-authoring-opencode-plugin/issues",
|
|
@@ -61,6 +49,8 @@
|
|
|
61
49
|
"@biomejs/biome": "2.4.11",
|
|
62
50
|
"@types/node": "^24.6.1",
|
|
63
51
|
"bun-types": "1.3.12",
|
|
52
|
+
"codio-api-js": "^0.18.0",
|
|
53
|
+
"json-2-csv": "^5.5.10",
|
|
64
54
|
"typescript": "^5.9.3",
|
|
65
55
|
"zod": "~4.1.0"
|
|
66
56
|
},
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: codio-fetch
|
|
3
|
+
description: Fetch and export an existing Codio course into an AI-ready local project. Use when the user asks to fetch, export, download, pull, or import a course from Codio, or to create a local AI-ready project from an existing Codio course.
|
|
4
|
+
argument-hint: [--course-id <id>] [--output-dir <path>] [--domain <domain>]
|
|
5
|
+
allowed-tools: [Bash, Read, Write, Edit]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Codio Fetch
|
|
9
|
+
|
|
10
|
+
Export an existing Codio course into a clean, AI-ready local project that can be edited and re-published with `codio-publish`.
|
|
11
|
+
|
|
12
|
+
## What this skill does
|
|
13
|
+
|
|
14
|
+
1. Loads credentials from `.env` at the workspace root (the Codio assignment directory).
|
|
15
|
+
2. Authenticates with the Codio API using `CODIO_CLIENT_ID` and `CODIO_CLIENT_SECRET`.
|
|
16
|
+
3. Fetches course metadata and builds a `course-manifest.json` with all module and assignment IDs.
|
|
17
|
+
4. Downloads the course source export ZIP from Codio.
|
|
18
|
+
5. Extracts the ZIP into `course-content/`, expanding nested `.tar.zst` assignment archives.
|
|
19
|
+
6. Writes `course-assignments.csv` as a flat index for easy review.
|
|
20
|
+
7. Cleans up temporary download files.
|
|
21
|
+
|
|
22
|
+
The output is a self-contained project folder ready for `codio-publish` to read back.
|
|
23
|
+
|
|
24
|
+
## Output structure
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
<output-dir>/ ← default: ./course-project
|
|
28
|
+
├── course-manifest.json ← machine-readable index with all IDs and paths
|
|
29
|
+
├── course-assignments.csv ← flat CSV listing all modules and assignments
|
|
30
|
+
└── course-content/
|
|
31
|
+
└── <module-folder>/
|
|
32
|
+
└── <assignment-folder>/
|
|
33
|
+
└── .guides/ ← guide pages, assessments, images
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The `<output-dir>` is always a named subfolder, never at the workspace root itself.
|
|
37
|
+
|
|
38
|
+
## Prerequisites
|
|
39
|
+
|
|
40
|
+
Before running, verify:
|
|
41
|
+
|
|
42
|
+
1. **`.env` file** — must exist at the workspace root:
|
|
43
|
+
```
|
|
44
|
+
CODIO_CLIENT_ID=your-client-id
|
|
45
|
+
CODIO_CLIENT_SECRET=your-client-secret
|
|
46
|
+
CODIO_COURSE_ID=your-course-id # optional if passing --course-id
|
|
47
|
+
CODIO_DOMAIN=codio.com # optional, defaults to codio.com
|
|
48
|
+
```
|
|
49
|
+
The script finds `.env` by walking up from the current directory, so it works from anywhere inside the workspace.
|
|
50
|
+
|
|
51
|
+
2. **Check `.env` is in place:**
|
|
52
|
+
```bash
|
|
53
|
+
[ -f .env ] && echo "found" || echo "missing — create .env before running"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
3. **Required system tools:** `node`, `unzip`, `tar`, `find`
|
|
57
|
+
|
|
58
|
+
## Running the export
|
|
59
|
+
|
|
60
|
+
Run from the workspace root (the Codio assignment directory):
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
bash ~/.config/opencode/skills/codio-fetch/scripts/run-course-export.sh --course-id <id>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Options
|
|
67
|
+
|
|
68
|
+
| Option | Description |
|
|
69
|
+
|---|---|
|
|
70
|
+
| `--course-id <id>` | Codio course ID (overrides `CODIO_COURSE_ID` from `.env`) |
|
|
71
|
+
| `--output-dir <path>` | Output project folder (default: `./course-project`) |
|
|
72
|
+
| `--domain <domain>` | Codio domain (default: `codio.com` or `CODIO_DOMAIN` from `.env`) |
|
|
73
|
+
|
|
74
|
+
### Examples
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# Use course ID from .env, output to ./course-project
|
|
78
|
+
bash ~/.config/opencode/skills/codio-fetch/scripts/run-course-export.sh
|
|
79
|
+
|
|
80
|
+
# Specify course ID and custom output folder
|
|
81
|
+
bash ~/.config/opencode/skills/codio-fetch/scripts/run-course-export.sh \
|
|
82
|
+
--course-id c87b4540de108faad55c8cefe7847492 \
|
|
83
|
+
--output-dir ./my-java-course
|
|
84
|
+
|
|
85
|
+
# Use a staging Codio domain
|
|
86
|
+
bash ~/.config/opencode/skills/codio-fetch/scripts/run-course-export.sh \
|
|
87
|
+
--course-id c87b4540de108faad55c8cefe7847492 \
|
|
88
|
+
--domain codio.co.uk
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## How credentials are loaded
|
|
92
|
+
|
|
93
|
+
The shell script walks up from the current directory looking for `.env`, then sources it with `set -a; . .env; set +a`. Values in `.env` **override** anything already in the environment.
|
|
94
|
+
|
|
95
|
+
The `.env` is shared across all Codio skills (`codio-fetch`, `codio-publish`). There is one `.env` at the workspace root, not one per skill.
|
|
96
|
+
|
|
97
|
+
## Environment variables
|
|
98
|
+
|
|
99
|
+
| Variable | Required | Description |
|
|
100
|
+
|---|---|---|
|
|
101
|
+
| `CODIO_CLIENT_ID` | Yes | Codio API client ID |
|
|
102
|
+
| `CODIO_CLIENT_SECRET` | Yes | Codio API client secret |
|
|
103
|
+
| `CODIO_COURSE_ID` | No | Course ID (can be passed as `--course-id` instead) |
|
|
104
|
+
| `CODIO_DOMAIN` | No | Codio domain, default `codio.com` |
|
|
105
|
+
|
|
106
|
+
## After the export
|
|
107
|
+
|
|
108
|
+
- Open `<output-dir>/course-manifest.json` to see the full module/assignment hierarchy with IDs.
|
|
109
|
+
- Open `<output-dir>/course-assignments.csv` for a flat spreadsheet view.
|
|
110
|
+
- Assignment content is in `<output-dir>/course-content/<module>/<assignment>/`.
|
|
111
|
+
- Use `codio-publish` to push edits back to Codio — the manifest with existing IDs is already in place.
|
|
112
|
+
|
|
113
|
+
## Troubleshooting
|
|
114
|
+
|
|
115
|
+
| Error | Fix |
|
|
116
|
+
|---|---|
|
|
117
|
+
| `Missing required environment variable: CODIO_CLIENT_ID` | Create or fix `.env` at workspace root |
|
|
118
|
+
| `Missing course id` | Pass `--course-id <id>` or set `CODIO_COURSE_ID` in `.env` |
|
|
119
|
+
| `Required command not found: unzip` | Install `unzip` via your package manager |
|
|
120
|
+
| API 401 / auth error | Credentials in `.env` are invalid or expired |
|
|
121
|
+
|
|
122
|
+
## Manifest format
|
|
123
|
+
|
|
124
|
+
`course-manifest.json` produced by this skill is directly compatible with `codio-publish`. All assignment IDs are pre-populated, so a subsequent publish run will update existing assignments rather than create new ones.
|
|
125
|
+
|
|
126
|
+
```jsonc
|
|
127
|
+
{
|
|
128
|
+
"schemaVersion": 2,
|
|
129
|
+
"courseId": "...",
|
|
130
|
+
"exportedAt": "...",
|
|
131
|
+
"counts": { "modules": 2, "assignments": 6 },
|
|
132
|
+
"paths": {
|
|
133
|
+
"assignmentsCsv": "course-assignments.csv",
|
|
134
|
+
"courseContent": "course-content"
|
|
135
|
+
},
|
|
136
|
+
"modules": [
|
|
137
|
+
{
|
|
138
|
+
"name": "Module Name",
|
|
139
|
+
"folder": "module-name",
|
|
140
|
+
"path": "course-content/module-name",
|
|
141
|
+
"assignmentCount": 3,
|
|
142
|
+
"assignments": [
|
|
143
|
+
{
|
|
144
|
+
"id": "...",
|
|
145
|
+
"name": "Assignment Name",
|
|
146
|
+
"folder": "assignment-name",
|
|
147
|
+
"path": "course-content/module-name/assignment-name"
|
|
148
|
+
}
|
|
149
|
+
]
|
|
150
|
+
}
|
|
151
|
+
]
|
|
152
|
+
}
|
|
153
|
+
```
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -Eeuo pipefail
|
|
3
|
+
shopt -s nullglob
|
|
4
|
+
|
|
5
|
+
log() {
|
|
6
|
+
printf '[%s] %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*"
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
fail() {
|
|
10
|
+
log "ERROR: $*"
|
|
11
|
+
exit 1
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
require_command() {
|
|
15
|
+
command -v "$1" >/dev/null 2>&1 || fail "Required command not found: $1"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
extract_assignment_archives() {
|
|
19
|
+
local root_dir="$1"
|
|
20
|
+
local archive_count=0
|
|
21
|
+
|
|
22
|
+
while IFS= read -r -d '' archive_path; do
|
|
23
|
+
archive_count=$((archive_count + 1))
|
|
24
|
+
|
|
25
|
+
local archive_dir archive_name destination_dir
|
|
26
|
+
archive_dir="$(dirname "$archive_path")"
|
|
27
|
+
archive_name="$(basename "$archive_path")"
|
|
28
|
+
destination_dir="$archive_dir/${archive_name%.tar.zst}"
|
|
29
|
+
|
|
30
|
+
rm -rf "$destination_dir"
|
|
31
|
+
mkdir -p "$destination_dir"
|
|
32
|
+
|
|
33
|
+
log "Extracting nested archive ${archive_name}"
|
|
34
|
+
tar --zstd -xf "$archive_path" -C "$destination_dir"
|
|
35
|
+
rm -f "$archive_path"
|
|
36
|
+
done < <(find "$root_dir" -type f -name '*.tar.zst' -print0)
|
|
37
|
+
|
|
38
|
+
log "Expanded ${archive_count} nested assignment archive(s)"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
main() {
|
|
42
|
+
require_command unzip
|
|
43
|
+
require_command tar
|
|
44
|
+
require_command find
|
|
45
|
+
require_command cp
|
|
46
|
+
|
|
47
|
+
local export_zip_path="${1:-}"
|
|
48
|
+
local staging_dir="${2:-}"
|
|
49
|
+
local visible_output_dir="${3:-}"
|
|
50
|
+
|
|
51
|
+
[[ -n "$export_zip_path" ]] || fail 'Usage: extract-course-export.sh <export-zip-path> <staging-dir> <visible-output-dir>'
|
|
52
|
+
[[ -n "$staging_dir" ]] || fail 'Missing staging directory'
|
|
53
|
+
[[ -n "$visible_output_dir" ]] || fail 'Missing visible output directory'
|
|
54
|
+
[[ -f "$export_zip_path" ]] || fail "Export ZIP not found: $export_zip_path"
|
|
55
|
+
|
|
56
|
+
rm -rf "$staging_dir" "$visible_output_dir"
|
|
57
|
+
mkdir -p "$staging_dir" "$visible_output_dir"
|
|
58
|
+
|
|
59
|
+
log "Unzipping $(basename "$export_zip_path") into hidden staging"
|
|
60
|
+
unzip -q "$export_zip_path" -d "$staging_dir"
|
|
61
|
+
|
|
62
|
+
extract_assignment_archives "$staging_dir"
|
|
63
|
+
|
|
64
|
+
cp -R "$staging_dir"/. "$visible_output_dir"
|
|
65
|
+
|
|
66
|
+
log "Published extracted course content to $visible_output_dir"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
main "$@"
|