@mo7yw4ng/openape 1.0.4 → 1.0.6

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.
Files changed (81) hide show
  1. package/README.md +11 -8
  2. package/esm/deno.js +1 -1
  3. package/esm/src/commands/announcements.d.ts.map +1 -1
  4. package/esm/src/commands/announcements.js +22 -85
  5. package/esm/src/commands/assignments.d.ts.map +1 -1
  6. package/esm/src/commands/assignments.js +2 -3
  7. package/esm/src/commands/auth.d.ts.map +1 -1
  8. package/esm/src/commands/auth.js +24 -14
  9. package/esm/src/commands/calendar.d.ts.map +1 -1
  10. package/esm/src/commands/calendar.js +32 -84
  11. package/esm/src/commands/courses.d.ts.map +1 -1
  12. package/esm/src/commands/courses.js +2 -38
  13. package/esm/src/commands/forums.d.ts.map +1 -1
  14. package/esm/src/commands/forums.js +47 -175
  15. package/esm/src/commands/grades.d.ts.map +1 -1
  16. package/esm/src/commands/grades.js +10 -47
  17. package/esm/src/commands/materials.d.ts.map +1 -1
  18. package/esm/src/commands/materials.js +135 -223
  19. package/esm/src/commands/quizzes.d.ts.map +1 -1
  20. package/esm/src/commands/quizzes.js +32 -56
  21. package/esm/src/commands/skills.js +3 -3
  22. package/esm/src/commands/upload.d.ts.map +1 -1
  23. package/esm/src/commands/upload.js +2 -5
  24. package/esm/src/commands/videos.d.ts.map +1 -1
  25. package/esm/src/commands/videos.js +6 -76
  26. package/esm/src/index.d.ts +2 -1
  27. package/esm/src/index.d.ts.map +1 -1
  28. package/esm/src/index.js +5 -1
  29. package/esm/src/lib/auth.d.ts +21 -2
  30. package/esm/src/lib/auth.d.ts.map +1 -1
  31. package/esm/src/lib/auth.js +79 -20
  32. package/esm/src/lib/logger.d.ts +2 -2
  33. package/esm/src/lib/logger.d.ts.map +1 -1
  34. package/esm/src/lib/logger.js +6 -4
  35. package/esm/src/lib/moodle.d.ts +18 -0
  36. package/esm/src/lib/moodle.d.ts.map +1 -1
  37. package/esm/src/lib/moodle.js +54 -2
  38. package/esm/src/lib/utils.d.ts +3 -8
  39. package/esm/src/lib/utils.d.ts.map +1 -1
  40. package/esm/src/lib/utils.js +3 -10
  41. package/package.json +1 -1
  42. package/script/deno.js +1 -1
  43. package/script/src/commands/announcements.d.ts.map +1 -1
  44. package/script/src/commands/announcements.js +23 -89
  45. package/script/src/commands/assignments.d.ts.map +1 -1
  46. package/script/src/commands/assignments.js +2 -3
  47. package/script/src/commands/auth.d.ts.map +1 -1
  48. package/script/src/commands/auth.js +24 -14
  49. package/script/src/commands/calendar.d.ts.map +1 -1
  50. package/script/src/commands/calendar.js +33 -85
  51. package/script/src/commands/courses.d.ts.map +1 -1
  52. package/script/src/commands/courses.js +9 -48
  53. package/script/src/commands/forums.d.ts.map +1 -1
  54. package/script/src/commands/forums.js +50 -181
  55. package/script/src/commands/grades.d.ts.map +1 -1
  56. package/script/src/commands/grades.js +14 -54
  57. package/script/src/commands/materials.d.ts.map +1 -1
  58. package/script/src/commands/materials.js +132 -220
  59. package/script/src/commands/quizzes.d.ts.map +1 -1
  60. package/script/src/commands/quizzes.js +40 -67
  61. package/script/src/commands/skills.js +3 -3
  62. package/script/src/commands/upload.d.ts.map +1 -1
  63. package/script/src/commands/upload.js +2 -5
  64. package/script/src/commands/videos.d.ts.map +1 -1
  65. package/script/src/commands/videos.js +11 -81
  66. package/script/src/index.d.ts +2 -1
  67. package/script/src/index.d.ts.map +1 -1
  68. package/script/src/index.js +5 -1
  69. package/script/src/lib/auth.d.ts +21 -2
  70. package/script/src/lib/auth.d.ts.map +1 -1
  71. package/script/src/lib/auth.js +83 -56
  72. package/script/src/lib/logger.d.ts +2 -2
  73. package/script/src/lib/logger.d.ts.map +1 -1
  74. package/script/src/lib/logger.js +6 -4
  75. package/script/src/lib/moodle.d.ts +18 -0
  76. package/script/src/lib/moodle.d.ts.map +1 -1
  77. package/script/src/lib/moodle.js +56 -2
  78. package/script/src/lib/utils.d.ts +3 -8
  79. package/script/src/lib/utils.d.ts.map +1 -1
  80. package/script/src/lib/utils.js +3 -11
  81. package/skills/openape/SKILL.md +10 -8
@@ -1,62 +1,25 @@
1
- import { getBaseDir, stripHtmlTags, getOutputFormat, formatTimestamp } from "../lib/utils.js";
2
- import { getEnrolledCoursesApi, getForumsApi, getForumDiscussionsApi, getDiscussionPostsApi, addForumDiscussionApi, addForumPostApi, deleteForumPostApi } from "../lib/moodle.js";
3
- import { createLogger } from "../lib/logger.js";
4
- import { loadWsToken, loadSesskey } from "../lib/token.js";
5
- import path from "node:path";
6
- import fs from "node:fs";
1
+ import { stripHtmlTags, getOutputFormat, formatTimestamp } from "../lib/utils.js";
2
+ import { getEnrolledCoursesApi, getForumsApi, getForumDiscussionsApi, getDiscussionPostsApi, addForumDiscussionApi, addForumPostApi, deleteForumPostApi, resolveForumId } from "../lib/moodle.js";
3
+ import { createApiContext } from "../lib/auth.js";
4
+ import { formatAndOutput } from "../index.js";
7
5
  export function registerForumsCommand(program) {
8
6
  const forumsCmd = program.command("forums");
9
7
  forumsCmd.description("Forum operations");
10
- // Pure API context - no browser required (fast!)
11
- async function createApiContext(options, command) {
12
- const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
13
- const outputFormat = getOutputFormat(command || { optsWithGlobals: () => ({ output: "json" }) });
14
- const silent = outputFormat === "json" && !opts.verbose;
15
- const log = createLogger(opts.verbose, silent);
16
- const baseDir = getBaseDir();
17
- const sessionPath = path.resolve(baseDir, ".auth", "storage-state.json");
18
- // Check if session exists
19
- if (!fs.existsSync(sessionPath)) {
20
- log.error("未找到登入 session。請先執行 'openape auth login' 進行登入。");
21
- log.info(`Session 預期位置: ${sessionPath}`);
22
- return null;
23
- }
24
- // Try to load WS token
25
- const wsToken = loadWsToken(sessionPath);
26
- if (!wsToken) {
27
- log.error("未找到 WS token。請先執行 'openape auth login' 進行登入。");
28
- return null;
29
- }
30
- // Try to load sesskey from cache
31
- const sesskey = loadSesskey(sessionPath) || undefined;
32
- return {
33
- log,
34
- session: {
35
- wsToken,
36
- moodleBaseUrl: "https://ilearning.cycu.edu.tw",
37
- sesskey,
38
- },
39
- };
40
- }
41
- forumsCmd
42
- .command("list")
43
- .description("List forums from in-progress courses")
44
- .option("--output <format>", "Output format: json|csv|table|silent")
45
- .action(async (options, command) => {
46
- const apiContext = await createApiContext(options, command);
8
+ async function listForums(classification) {
9
+ const apiContext = await createApiContext({});
47
10
  if (!apiContext) {
48
11
  process.exitCode = 1;
49
12
  return;
50
13
  }
51
14
  const courses = await getEnrolledCoursesApi(apiContext.session, {
52
- classification: "inprogress",
15
+ classification,
53
16
  });
54
- // Get forums via WS API (no browser needed!)
55
17
  const courseIds = courses.map(c => c.id);
56
18
  const wsForums = await getForumsApi(apiContext.session, courseIds);
19
+ const courseMap = new Map(courses.map(c => [c.id, c]));
57
20
  const allForums = [];
58
21
  for (const wsForum of wsForums) {
59
- const course = courses.find(c => c.id === wsForum.courseid);
22
+ const course = courseMap.get(wsForum.courseid);
60
23
  if (course) {
61
24
  allForums.push({
62
25
  course_id: wsForum.courseid,
@@ -66,62 +29,22 @@ export function registerForumsCommand(program) {
66
29
  forum_id: wsForum.id,
67
30
  name: wsForum.name,
68
31
  timemodified: wsForum.timemodified,
69
- // url: `https://ilearning.cycu.edu.tw/mod/forum/view.php?id=${wsForum.cmid}`,
70
32
  });
71
33
  }
72
34
  }
73
- console.log(JSON.stringify({
74
- status: "success",
75
- timestamp: new Date().toISOString(),
76
- total_courses: courses.length,
77
- total_forums: allForums.length,
78
- }));
79
- for (const forum of allForums) {
80
- console.log(JSON.stringify(forum));
81
- }
82
- });
35
+ formatAndOutput(allForums, "json", apiContext.log, { status: "success", timestamp: new Date().toISOString(), total_courses: courses.length, total_forums: allForums.length });
36
+ }
37
+ forumsCmd
38
+ .command("list")
39
+ .description("List forums from in-progress courses")
40
+ .action(() => listForums("inprogress"));
83
41
  forumsCmd
84
42
  .command("list-all")
85
43
  .description("List all forums across all courses")
86
44
  .option("--level <type>", "Course level: in_progress (default) | all", "in_progress")
87
- .option("--output <format>", "Output format: json|csv|table|silent")
88
- .action(async (options, command) => {
89
- const apiContext = await createApiContext(options, command);
90
- if (!apiContext) {
91
- process.exitCode = 1;
92
- return;
93
- }
45
+ .action(async (options) => {
94
46
  const classification = options.level === "all" ? undefined : "inprogress";
95
- const courses = await getEnrolledCoursesApi(apiContext.session, {
96
- classification,
97
- });
98
- // Get forums via WS API (no browser needed!)
99
- const courseIds = courses.map(c => c.id);
100
- const wsForums = await getForumsApi(apiContext.session, courseIds);
101
- const allForums = [];
102
- for (const wsForum of wsForums) {
103
- const course = courses.find(c => c.id === wsForum.courseid);
104
- if (course) {
105
- allForums.push({
106
- course_id: wsForum.courseid,
107
- course_name: course.fullname,
108
- intro: wsForum.intro,
109
- cmid: wsForum.cmid.toString(),
110
- forum_id: wsForum.id,
111
- name: wsForum.name,
112
- timemodified: wsForum.timemodified,
113
- });
114
- }
115
- }
116
- console.log(JSON.stringify({
117
- status: "success",
118
- timestamp: new Date().toISOString(),
119
- total_courses: courses.length,
120
- total_forums: allForums.length,
121
- }));
122
- for (const forum of allForums) {
123
- console.log(JSON.stringify(forum));
124
- }
47
+ await listForums(classification);
125
48
  });
126
49
  forumsCmd
127
50
  .command("discussions")
@@ -129,51 +52,29 @@ export function registerForumsCommand(program) {
129
52
  .argument("<forum-id>", "Forum ID")
130
53
  .option("--output <format>", "Output format: json|csv|table|silent")
131
54
  .action(async (forumId, options, command) => {
55
+ const output = getOutputFormat(command);
132
56
  const apiContext = await createApiContext(options, command);
133
57
  if (!apiContext) {
134
58
  process.exitCode = 1;
135
59
  return;
136
60
  }
137
- // Get courses via WS API
138
- const courses = await getEnrolledCoursesApi(apiContext.session, {
139
- classification: "inprogress",
140
- });
141
- // Get forums via WS API
142
- const courseIds = courses.map(c => c.id);
143
- const wsForums = await getForumsApi(apiContext.session, courseIds);
144
- // Find forum by cmid or instance ID
145
- const targetForum = wsForums.find(f => f.cmid.toString() === forumId || f.id === parseInt(forumId, 10));
146
- if (!targetForum) {
147
- console.log(JSON.stringify({ status: "error", error: "Forum not found" }));
61
+ const resolved = await resolveForumId(apiContext.session, forumId);
62
+ if (!resolved) {
63
+ apiContext.log.error("Forum not found");
148
64
  process.exitCode = 1;
149
65
  return;
150
66
  }
151
- const course = courses.find(c => c.id === targetForum.courseid);
152
- // Get discussions via WS API
153
- const discussions = await getForumDiscussionsApi(apiContext.session, targetForum.id);
154
- // Output NDJSON: one line per discussion entry for stream-friendly parsing
155
- const meta = {
156
- status: "success",
157
- timestamp: new Date().toISOString(),
158
- forum_id: targetForum.id,
159
- forum_name: targetForum.name,
160
- forum_intro: targetForum.intro,
161
- course_id: course?.id,
162
- course_name: course?.fullname,
163
- total_discussions: discussions.length,
164
- };
165
- console.log(JSON.stringify(meta));
166
- for (const d of discussions) {
167
- console.log(JSON.stringify({
168
- id: d.id,
169
- name: d.name,
170
- user_id: d.userId,
171
- time_modified: d.timeModified,
172
- post_count: d.postCount,
173
- unread: d.unread,
174
- message: stripHtmlTags(d.message || ""),
175
- }));
176
- }
67
+ const discussions = await getForumDiscussionsApi(apiContext.session, resolved.forumId);
68
+ const items = discussions.map(d => ({
69
+ id: d.id,
70
+ name: d.name,
71
+ user_id: d.userId,
72
+ time_modified: d.timeModified,
73
+ post_count: d.postCount,
74
+ unread: d.unread,
75
+ message: stripHtmlTags(d.message || ""),
76
+ }));
77
+ formatAndOutput(items, output, apiContext.log, { status: "success", timestamp: new Date().toISOString(), forum_id: resolved.forumId, forum_name: resolved.name ?? null, course_id: resolved.courseid ?? null, total_discussions: discussions.length });
177
78
  });
178
79
  forumsCmd
179
80
  .command("posts")
@@ -188,38 +89,17 @@ export function registerForumsCommand(program) {
188
89
  return;
189
90
  }
190
91
  const posts = await getDiscussionPostsApi(apiContext.session, parseInt(discussionId, 10));
191
- if (output === "json") {
192
- const result = {
193
- status: "success",
194
- timestamp: new Date().toISOString(),
195
- discussion_id: discussionId,
196
- posts: posts.map(p => ({
197
- id: p.id,
198
- subject: p.subject,
199
- author: p.author,
200
- author_id: p.authorId,
201
- created: formatTimestamp(p.created),
202
- modified: formatTimestamp(p.modified),
203
- message: p.message,
204
- unread: p.unread,
205
- })),
206
- summary: {
207
- total_posts: posts.length,
208
- },
209
- };
210
- console.log(JSON.stringify(result));
211
- }
212
- else if (output === "table") {
213
- console.log(`Discussion ${discussionId} - ${posts.length} posts`);
214
- console.log("Use --output json to see full post content");
215
- const tablePosts = posts.map(p => ({
216
- id: p.id,
217
- subject: p.subject.substring(0, 50) + (p.subject.length > 50 ? "..." : ""),
218
- author: p.author,
219
- created: new Date(p.created * 1000).toLocaleString(),
220
- }));
221
- console.table(tablePosts);
222
- }
92
+ const items = posts.map(p => ({
93
+ id: p.id,
94
+ subject: p.subject,
95
+ author: p.author,
96
+ author_id: p.authorId,
97
+ created: formatTimestamp(p.created),
98
+ modified: formatTimestamp(p.modified),
99
+ message: p.message,
100
+ unread: p.unread,
101
+ }));
102
+ formatAndOutput(items, output, apiContext.log, { status: "success", timestamp: new Date().toISOString(), discussion_id: discussionId, total_posts: posts.length });
223
103
  });
224
104
  forumsCmd
225
105
  .command("post")
@@ -236,22 +116,14 @@ export function registerForumsCommand(program) {
236
116
  return;
237
117
  }
238
118
  const { log, session } = apiContext;
239
- // Get courses to find the forum
240
- const courses = await getEnrolledCoursesApi(session, {
241
- classification: "inprogress",
242
- });
243
- const courseIds = courses.map(c => c.id);
244
- const wsForums = await getForumsApi(session, courseIds);
245
- // Find forum by cmid or instance ID
246
- const targetForum = wsForums.find(f => f.cmid.toString() === forumId || f.id === parseInt(forumId, 10));
247
- if (!targetForum) {
119
+ const resolved = await resolveForumId(session, forumId);
120
+ if (!resolved) {
248
121
  log.error(`Forum not found: ${forumId}`);
249
122
  process.exitCode = 1;
250
123
  return;
251
124
  }
252
- const course = courses.find(c => c.id === targetForum.courseid);
253
- log.info(`Posting to forum: ${targetForum.name} (${course?.fullname})`);
254
- const result = await addForumDiscussionApi(session, targetForum.id, subject, message);
125
+ log.info(`Posting to forum: ${resolved.name ?? forumId}`);
126
+ const result = await addForumDiscussionApi(session, resolved.forumId, subject, message);
255
127
  if (result.success) {
256
128
  log.success(`✓ Discussion posted successfully!`);
257
129
  log.info(` Discussion ID: ${result.discussionId}`);
@@ -1 +1 @@
1
- {"version":3,"file":"grades.d.ts","sourceRoot":"","sources":["../../../src/src/commands/grades.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAkBpC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAsI5D"}
1
+ {"version":3,"file":"grades.d.ts","sourceRoot":"","sources":["../../../src/src/commands/grades.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAgBpC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA+F5D"}
@@ -1,45 +1,10 @@
1
- import { getBaseDir } from "../lib/utils.js";
2
1
  import { getEnrolledCoursesApi, getCourseGradesApi } from "../lib/moodle.js";
3
- import { createLogger } from "../lib/logger.js";
4
- import { loadWsToken } from "../lib/token.js";
2
+ import { createApiContext } from "../lib/auth.js";
5
3
  import { formatAndOutput } from "../index.js";
6
- import path from "node:path";
7
- import fs from "node:fs";
4
+ import { getOutputFormat } from "../lib/utils.js";
8
5
  export function registerGradesCommand(program) {
9
6
  const gradesCmd = program.command("grades");
10
7
  gradesCmd.description("Grade operations");
11
- function getOutputFormat(command) {
12
- const opts = command.optsWithGlobals();
13
- return opts.output || "json";
14
- }
15
- // Pure API context - no browser required (fast!)
16
- async function createApiContext(options, command) {
17
- const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
18
- const outputFormat = getOutputFormat(command || { optsWithGlobals: () => ({ output: "json" }) });
19
- const silent = outputFormat === "json" && !opts.verbose;
20
- const log = createLogger(opts.verbose, silent);
21
- const baseDir = getBaseDir();
22
- const sessionPath = path.resolve(baseDir, ".auth", "storage-state.json");
23
- // Check if session exists
24
- if (!fs.existsSync(sessionPath)) {
25
- log.error("未找到登入 session。請先執行 'openape auth login' 進行登入。");
26
- log.info(`Session 預期位置: ${sessionPath}`);
27
- return null;
28
- }
29
- // Try to load WS token
30
- const wsToken = loadWsToken(sessionPath);
31
- if (!wsToken) {
32
- log.error("未找到 WS token。請先執行 'openape auth login' 進行登入。");
33
- return null;
34
- }
35
- return {
36
- log,
37
- session: {
38
- wsToken,
39
- moodleBaseUrl: "https://ilearning.cycu.edu.tw",
40
- },
41
- };
42
- }
43
8
  gradesCmd
44
9
  .command("summary")
45
10
  .description("Show grade summary across all courses")
@@ -52,9 +17,13 @@ export function registerGradesCommand(program) {
52
17
  return;
53
18
  }
54
19
  const courses = await getEnrolledCoursesApi(apiContext.session);
20
+ const gradeResults = await Promise.allSettled(courses.map(course => getCourseGradesApi(apiContext.session, course.id)
21
+ .then(grades => ({ course, grades }))));
55
22
  const gradeSummaries = [];
56
- for (const course of courses) {
57
- const grades = await getCourseGradesApi(apiContext.session, course.id);
23
+ for (const result of gradeResults) {
24
+ if (result.status !== "fulfilled")
25
+ continue;
26
+ const { course, grades } = result.value;
58
27
  gradeSummaries.push({
59
28
  courseId: course.id,
60
29
  courseName: course.fullname,
@@ -64,19 +33,13 @@ export function registerGradesCommand(program) {
64
33
  totalUsers: grades.totalUsers,
65
34
  });
66
35
  }
67
- // Calculate overall statistics
68
36
  const gradedCourses = gradeSummaries.filter(g => g.grade !== undefined && g.grade !== null && g.grade !== "-");
69
37
  const averageRank = gradeSummaries
70
38
  .filter(g => g.rank !== undefined && g.rank !== null)
71
39
  .reduce((sum, g) => sum + (g.rank || 0), 0) /
72
40
  (gradeSummaries.filter(g => g.rank !== undefined && g.rank !== null).length || 1);
73
- const summaryData = {
74
- total_courses: courses.length,
75
- graded_courses: gradedCourses.length,
76
- average_rank: averageRank.toFixed(1),
77
- grades: gradeSummaries,
78
- };
79
- formatAndOutput(summaryData, output, apiContext.log);
41
+ apiContext.log.info(`Total: ${courses.length} courses, ${gradedCourses.length} graded, avg rank: ${averageRank.toFixed(1)}`);
42
+ formatAndOutput(gradeSummaries, output, apiContext.log);
80
43
  });
81
44
  gradesCmd
82
45
  .command("course")
@@ -1 +1 @@
1
- {"version":3,"file":"materials.d.ts","sourceRoot":"","sources":["../../../src/src/commands/materials.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAgCpC,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA8iB/D"}
1
+ {"version":3,"file":"materials.d.ts","sourceRoot":"","sources":["../../../src/src/commands/materials.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA4BpC,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAwd/D"}