@mo7yw4ng/openape 1.0.3 → 1.0.4

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 (79) hide show
  1. package/README.md +30 -5
  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 +13 -14
  5. package/esm/src/commands/assignments.d.ts +3 -0
  6. package/esm/src/commands/assignments.d.ts.map +1 -0
  7. package/esm/src/commands/assignments.js +230 -0
  8. package/esm/src/commands/auth.d.ts.map +1 -1
  9. package/esm/src/commands/auth.js +21 -1
  10. package/esm/src/commands/calendar.d.ts.map +1 -1
  11. package/esm/src/commands/calendar.js +17 -18
  12. package/esm/src/commands/courses.js +3 -3
  13. package/esm/src/commands/forums.d.ts.map +1 -1
  14. package/esm/src/commands/forums.js +125 -33
  15. package/esm/src/commands/materials.d.ts.map +1 -1
  16. package/esm/src/commands/materials.js +6 -38
  17. package/esm/src/commands/quizzes.d.ts.map +1 -1
  18. package/esm/src/commands/quizzes.js +165 -65
  19. package/esm/src/commands/skills.d.ts.map +1 -1
  20. package/esm/src/commands/skills.js +4 -8
  21. package/esm/src/commands/upload.d.ts +3 -0
  22. package/esm/src/commands/upload.d.ts.map +1 -0
  23. package/esm/src/commands/upload.js +58 -0
  24. package/esm/src/commands/videos.d.ts.map +1 -1
  25. package/esm/src/commands/videos.js +5 -4
  26. package/esm/src/index.d.ts.map +1 -1
  27. package/esm/src/index.js +12 -1
  28. package/esm/src/lib/auth.d.ts +23 -1
  29. package/esm/src/lib/auth.d.ts.map +1 -1
  30. package/esm/src/lib/auth.js +36 -3
  31. package/esm/src/lib/moodle.d.ts +179 -1
  32. package/esm/src/lib/moodle.d.ts.map +1 -1
  33. package/esm/src/lib/moodle.js +481 -13
  34. package/esm/src/lib/types.d.ts +81 -164
  35. package/esm/src/lib/types.d.ts.map +1 -1
  36. package/esm/src/lib/types.js +1 -0
  37. package/esm/src/lib/utils.d.ts +20 -0
  38. package/esm/src/lib/utils.d.ts.map +1 -1
  39. package/esm/src/lib/utils.js +48 -1
  40. package/package.json +1 -1
  41. package/script/deno.js +1 -1
  42. package/script/src/commands/announcements.d.ts.map +1 -1
  43. package/script/src/commands/announcements.js +12 -13
  44. package/script/src/commands/assignments.d.ts +3 -0
  45. package/script/src/commands/assignments.d.ts.map +1 -0
  46. package/script/src/commands/assignments.js +269 -0
  47. package/script/src/commands/auth.d.ts.map +1 -1
  48. package/script/src/commands/auth.js +20 -0
  49. package/script/src/commands/calendar.d.ts.map +1 -1
  50. package/script/src/commands/calendar.js +16 -17
  51. package/script/src/commands/courses.js +2 -2
  52. package/script/src/commands/forums.d.ts.map +1 -1
  53. package/script/src/commands/forums.js +125 -33
  54. package/script/src/commands/materials.d.ts.map +1 -1
  55. package/script/src/commands/materials.js +7 -39
  56. package/script/src/commands/quizzes.d.ts.map +1 -1
  57. package/script/src/commands/quizzes.js +163 -63
  58. package/script/src/commands/skills.d.ts.map +1 -1
  59. package/script/src/commands/skills.js +4 -8
  60. package/script/src/commands/upload.d.ts +3 -0
  61. package/script/src/commands/upload.d.ts.map +1 -0
  62. package/script/src/commands/upload.js +64 -0
  63. package/script/src/commands/videos.d.ts.map +1 -1
  64. package/script/src/commands/videos.js +5 -4
  65. package/script/src/index.d.ts.map +1 -1
  66. package/script/src/index.js +12 -1
  67. package/script/src/lib/auth.d.ts +23 -1
  68. package/script/src/lib/auth.d.ts.map +1 -1
  69. package/script/src/lib/auth.js +70 -3
  70. package/script/src/lib/moodle.d.ts +179 -1
  71. package/script/src/lib/moodle.d.ts.map +1 -1
  72. package/script/src/lib/moodle.js +493 -13
  73. package/script/src/lib/types.d.ts +81 -164
  74. package/script/src/lib/types.d.ts.map +1 -1
  75. package/script/src/lib/types.js +1 -0
  76. package/script/src/lib/utils.d.ts +20 -0
  77. package/script/src/lib/utils.d.ts.map +1 -1
  78. package/script/src/lib/utils.js +52 -0
  79. package/skills/openape/SKILL.md +73 -271
package/README.md CHANGED
@@ -8,9 +8,10 @@
8
8
  ## 功能特色
9
9
  - 📚 **課程資訊**:列出修課清單、成績、課程大綱與進度。
10
10
  - 📺 **影片輔助**:列出/下載影片,甚至自動標記為已觀看。
11
- - 📝 **測驗與教材**:快速查看測驗、下載教材。
12
- - 💬 **討論區與公告**:閱讀公告與討論區。
11
+ - 📝 **測驗與教材**:快速進行測驗、下載教材。
12
+ - 💬 **討論區與公告**:閱讀公告、討論區、發表回覆。
13
13
  - 📅 **行事曆**:內建行事曆事件查詢與匯出。
14
+ - ✅ **作業繳交**:查詢作業、檢查繳交狀態、上傳檔案並繳交。
14
15
  - 🤖 **Agent Skills**:提供 Claude Code 等 Skills 工作流支援。
15
16
 
16
17
  ## 安裝
@@ -60,27 +61,51 @@ openape videos download <id> # 下載影片
60
61
  ```bash
61
62
  openape quizzes list <course-id> # 列出特定課程測驗
62
63
  openape quizzes list-all # 列出所有課程測驗
63
- openape quizzes open <id> # 開啟特定測驗
64
+ openape quizzes start <quiz-id> # 開始測驗
65
+ openape quizzes info <attempt-id> # 查看測驗題目
66
+ openape quizzes save <attempt-id> '<answers>' # 儲存測驗答案
64
67
  openape materials list-all # 列出所有可下載教材
65
68
  openape materials download <id> # 下載指定教材
66
69
  openape materials download-all # 批次下載教材
70
+ openape materials complete <id> # 標記教材為已完成
71
+ openape materials complete-all # 批次標記教材為已完成
67
72
  ```
68
73
 
69
74
  ### 成績與其他查詢 (Grades, Forums, Calendar)
70
75
  ```bash
71
76
  openape grades summary # 顯示學期成績總覽
72
77
  openape grades course <id> # 顯示特定課程成績
73
- openape forums list <course-id> # 列出課程論壇
78
+ openape forums list # 列出進行中課程的討論區
79
+ openape forums list-all # 列出所有討論區
80
+ openape forums discussions <forum-id> # 列出討論區中的討論串
81
+ openape forums posts <discussion-id> # 列出討論串中的貼文
82
+ openape forums reply <post-id> <subject> <message> # 回覆貼文
83
+ openape forums post <forum-id> <subject> <message> # 發起新討論
84
+ openape forums delete <post-id> # 刪除討論貼文
74
85
  openape announcements list-all # 列出所有公告
75
86
  openape announcements read <id> # 閱讀特定公告
76
87
  openape calendar events # 查詢行事曆事件
77
88
  openape calendar export # 匯出事件
78
89
  ```
79
90
 
91
+ ### 作業與檔案上傳 (Assignments & Upload)
92
+ ```bash
93
+ # 作業查詢與繳交
94
+ openape assignments list <course-id> # 列出課程作業
95
+ openape assignments list-all # 列出所有作業 (支援 --level all)
96
+ openape assignments status <assignment-id> # 檢查作業繳交狀態
97
+ openape assignments submit <assignment-id> # 繳交作業
98
+ --text "內容" # 線上文字繳交
99
+ --file-id <draft-id> # 使用已上傳的檔案 ID
100
+ --file <path> # 直接上傳檔案並繳交
101
+
102
+ # 檔案上傳至草稿區
103
+ openape upload file <path> # 上傳檔案取得 draft ID
104
+ ```
105
+
80
106
  ### Skills
81
107
  讓你的 AI Agent 也可以控制 OpenApe。只需一個指令即可安裝:
82
108
  ```bash
83
- openape skills list # 查看目前提供的所有 skills
84
109
  openape skills install claude # 為 Claude Code 安裝技能 (支援 claude, codex, opencode)
85
110
  openape skills install --all # 自動偵測環境並安裝給所有支援的 Agent
86
111
  ```
package/esm/deno.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export default {
2
2
  "name": "@openape/openape",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "CLI tool for CYCU i-Learning platform",
5
5
  "license": "MIT",
6
6
  "exports": "./src/index.ts",
@@ -1 +1 @@
1
- {"version":3,"file":"announcements.d.ts","sourceRoot":"","sources":["../../../src/src/commands/announcements.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAqBpC,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAqJnE"}
1
+ {"version":3,"file":"announcements.d.ts","sourceRoot":"","sources":["../../../src/src/commands/announcements.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAqBpC,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAoJnE"}
@@ -1,4 +1,4 @@
1
- import { getBaseDir } from "../lib/utils.js";
1
+ import { getBaseDir, formatTimestamp } from "../lib/utils.js";
2
2
  import { getSiteInfoApi, getMessagesApi, getDiscussionPostsApi } from "../lib/moodle.js";
3
3
  import { createLogger } from "../lib/logger.js";
4
4
  import { loadWsToken } from "../lib/token.js";
@@ -77,27 +77,26 @@ export function registerAnnouncementsCommand(program) {
77
77
  allAnnouncements.sort((a, b) => b.createdAt - a.createdAt);
78
78
  // Apply limit
79
79
  let filteredAnnouncements = allAnnouncements.slice(0, limit);
80
- const output = {
80
+ console.log(JSON.stringify({
81
81
  status: "success",
82
82
  timestamp: new Date().toISOString(),
83
83
  level: options.level,
84
- announcements: filteredAnnouncements.map(a => ({
84
+ total_announcements: allAnnouncements.length,
85
+ shown: filteredAnnouncements.length,
86
+ }));
87
+ for (const a of filteredAnnouncements) {
88
+ console.log(JSON.stringify({
85
89
  course_id: a.course_id,
86
90
  course_name: a.course_name,
87
91
  id: a.id,
88
92
  subject: a.subject,
89
93
  author: a.author,
90
94
  author_id: a.authorId,
91
- created_at: new Date(a.createdAt * 1000).toISOString(),
92
- modified_at: new Date(a.modifiedAt * 1000).toISOString(),
95
+ created_at: formatTimestamp(a.createdAt),
96
+ modified_at: formatTimestamp(a.modifiedAt),
93
97
  unread: a.unread,
94
- })),
95
- summary: {
96
- total_announcements: allAnnouncements.length,
97
- shown: filteredAnnouncements.length,
98
- },
99
- };
100
- console.log(JSON.stringify(output));
98
+ }));
99
+ }
101
100
  });
102
101
  announcementsCmd
103
102
  .command("read")
@@ -125,8 +124,8 @@ export function registerAnnouncementsCommand(program) {
125
124
  subject: firstPost.subject,
126
125
  author: firstPost.author,
127
126
  author_id: firstPost.authorId,
128
- created_at: new Date(firstPost.created * 1000).toISOString(),
129
- modified_at: new Date(firstPost.modified * 1000).toISOString(),
127
+ created_at: formatTimestamp(firstPost.created),
128
+ modified_at: formatTimestamp(firstPost.modified),
130
129
  message: firstPost.message,
131
130
  },
132
131
  };
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ export declare function registerAssignmentsCommand(program: Command): void;
3
+ //# sourceMappingURL=assignments.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assignments.d.ts","sourceRoot":"","sources":["../../../src/src/commands/assignments.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA0PjE"}
@@ -0,0 +1,230 @@
1
+ import { getOutputFormat, formatFileSize, formatMoodleDate } from "../lib/utils.js";
2
+ import { getEnrolledCoursesApi, getAssignmentsByCoursesApi, getSubmissionStatusApi, saveSubmissionApi, uploadFileApi } from "../lib/moodle.js";
3
+ import { createApiContext } from "../lib/auth.js";
4
+ import { formatAndOutput } from "../index.js";
5
+ import path from "node:path";
6
+ import fs from "node:fs/promises";
7
+ export function registerAssignmentsCommand(program) {
8
+ const assignmentsCmd = program.command("assignments");
9
+ assignmentsCmd.description("Assignment operations");
10
+ assignmentsCmd
11
+ .command("list")
12
+ .description("List assignments in a course")
13
+ .argument("<course-id>", "Course ID")
14
+ .option("--output <format>", "Output format: json|csv|table|silent")
15
+ .action(async (courseId, options, command) => {
16
+ const output = getOutputFormat(command);
17
+ const apiContext = await createApiContext(options, command);
18
+ if (!apiContext) {
19
+ process.exitCode = 1;
20
+ return;
21
+ }
22
+ const apiAssignments = await getAssignmentsByCoursesApi(apiContext.session, [parseInt(courseId, 10)]);
23
+ const assignments = apiAssignments.map(a => ({
24
+ id: a.id,
25
+ courseName: courseId,
26
+ name: a.name,
27
+ url: a.url,
28
+ cmid: a.cmid,
29
+ duedate: formatMoodleDate(a.duedate),
30
+ cutoffdate: formatMoodleDate(a.cutoffdate),
31
+ allowSubmissionsFromDate: formatMoodleDate(a.allowSubmissionsFromDate),
32
+ }));
33
+ apiContext.log.info(`\n找到 ${assignments.length} 個作業。`);
34
+ formatAndOutput(assignments, output, apiContext.log);
35
+ });
36
+ assignmentsCmd
37
+ .command("list-all")
38
+ .description("List all assignments across all courses")
39
+ .option("--level <type>", "Course level: in_progress (default) | all", "in_progress")
40
+ .option("--output <format>", "Output format: json|csv|table|silent")
41
+ .action(async (options, command) => {
42
+ const output = getOutputFormat(command);
43
+ const apiContext = await createApiContext(options, command);
44
+ if (!apiContext) {
45
+ process.exitCode = 1;
46
+ return;
47
+ }
48
+ const classification = options.level === "all" ? undefined : "inprogress";
49
+ const courses = await getEnrolledCoursesApi(apiContext.session, {
50
+ classification,
51
+ });
52
+ // Get assignments via WS API (no browser needed!)
53
+ const courseIds = courses.map(c => c.id);
54
+ const apiAssignments = await getAssignmentsByCoursesApi(apiContext.session, courseIds);
55
+ // Build a map of courseId -> course for quick lookup
56
+ const courseMap = new Map(courses.map(c => [c.id, c]));
57
+ const allAssignments = [];
58
+ for (const a of apiAssignments) {
59
+ const course = courseMap.get(a.courseId);
60
+ if (course) {
61
+ allAssignments.push({
62
+ id: a.id,
63
+ courseName: course.fullname,
64
+ name: a.name,
65
+ url: a.url,
66
+ cmid: a.cmid,
67
+ duedate: formatMoodleDate(a.duedate),
68
+ cutoffdate: formatMoodleDate(a.cutoffdate),
69
+ allowSubmissionsFromDate: formatMoodleDate(a.allowSubmissionsFromDate),
70
+ });
71
+ }
72
+ }
73
+ apiContext.log.info(`\n總計發現 ${allAssignments.length} 個作業。`);
74
+ formatAndOutput(allAssignments, output, apiContext.log);
75
+ });
76
+ // ── Submission Status ───────────────────────────────────────────────────────
77
+ assignmentsCmd
78
+ .command("status")
79
+ .description("Check assignment submission status")
80
+ .argument("<assignment-id>", "Assignment instance ID (from list-all)")
81
+ .option("--output <format>", "Output format: json|csv|table|silent")
82
+ .action(async (assignmentId, options, command) => {
83
+ const output = getOutputFormat(command);
84
+ const apiContext = await createApiContext(options, command);
85
+ if (!apiContext) {
86
+ process.exitCode = 1;
87
+ return;
88
+ }
89
+ const id = parseInt(assignmentId, 10);
90
+ apiContext.log.info("檢查繳交狀態...");
91
+ const status = await getSubmissionStatusApi(apiContext.session, id);
92
+ // Build status data object
93
+ const statusData = {
94
+ submitted: status.submitted,
95
+ submitted_text: status.submitted ? "已繳交" : "尚未繳交",
96
+ graded: status.graded,
97
+ graded_text: status.graded ? "已評分" : "尚未評分",
98
+ last_modified: status.lastModified ? new Date(status.lastModified * 1000).toISOString() : null,
99
+ last_modified_text: status.lastModified ? new Date(status.lastModified * 1000).toLocaleString("zh-TW") : null,
100
+ grader: status.grader,
101
+ grade: status.grade,
102
+ feedback: status.feedback,
103
+ files: status.extensions.map(f => ({
104
+ filename: f.filename,
105
+ filesize: f.filesize,
106
+ filesize_kb: formatFileSize(f.filesize),
107
+ })),
108
+ };
109
+ formatAndOutput(statusData, output, apiContext.log);
110
+ });
111
+ // ── Submit Assignment ────────────────────────────────────────────────────────
112
+ assignmentsCmd
113
+ .command("submit")
114
+ .description("Submit an assignment (online text or file)")
115
+ .argument("<assignment-id>", "Assignment instance ID (from list-all)")
116
+ .option("--text <content>", "Online text content to submit")
117
+ .option("--file-id <id>", "Draft file ID from file upload")
118
+ .option("--file <path>", "Upload and submit a file directly")
119
+ .option("--output <format>", "Output format: json|csv|table|silent")
120
+ .action(async (assignmentId, options, command) => {
121
+ const output = getOutputFormat(command);
122
+ const apiContext = await createApiContext(options, command);
123
+ if (!apiContext) {
124
+ process.exitCode = 1;
125
+ return;
126
+ }
127
+ const id = parseInt(assignmentId, 10);
128
+ // Check submission status first
129
+ const status = await getSubmissionStatusApi(apiContext.session, id);
130
+ let fileUploaded;
131
+ let cancelled = false;
132
+ if (status.submitted) {
133
+ const confirm = await promptConfirm("此作業已經繳交!確定要重新繳交嗎?(y/N): ");
134
+ if (!confirm) {
135
+ cancelled = true;
136
+ }
137
+ }
138
+ if (cancelled) {
139
+ const cancelResult = {
140
+ success: false,
141
+ cancelled: true,
142
+ message: "Submission cancelled by user",
143
+ };
144
+ formatAndOutput(cancelResult, output, apiContext.log);
145
+ return;
146
+ }
147
+ // Validate options
148
+ if (!options.text && !options.fileId && !options.file) {
149
+ const errorResult = {
150
+ success: false,
151
+ error: "請提供 --text、--file-id 或 --file 選項。",
152
+ };
153
+ formatAndOutput(errorResult, output, apiContext.log);
154
+ process.exitCode = 1;
155
+ return;
156
+ }
157
+ let fileId = options.fileId ? parseInt(options.fileId, 10) : undefined;
158
+ // Upload file if --file option is provided
159
+ if (options.file) {
160
+ const resolvedPath = path.resolve(options.file);
161
+ // Check if file exists
162
+ try {
163
+ await fs.access(resolvedPath);
164
+ }
165
+ catch {
166
+ const errorResult = {
167
+ success: false,
168
+ error: `檔案不存在: ${options.file}`,
169
+ };
170
+ formatAndOutput(errorResult, output, apiContext.log);
171
+ process.exitCode = 1;
172
+ return;
173
+ }
174
+ const stats = await fs.stat(resolvedPath);
175
+ const fileSizeKB = formatFileSize(stats.size);
176
+ const uploadResult = await uploadFileApi(apiContext.session, resolvedPath);
177
+ if (!uploadResult.success) {
178
+ const errorResult = {
179
+ success: false,
180
+ error: `檔案上傳失敗: ${uploadResult.error}`,
181
+ };
182
+ formatAndOutput(errorResult, output, apiContext.log);
183
+ process.exitCode = 1;
184
+ return;
185
+ }
186
+ fileId = uploadResult.draftId;
187
+ fileUploaded = {
188
+ filename: path.basename(resolvedPath),
189
+ filesize: stats.size,
190
+ filesize_kb: formatFileSize(stats.size),
191
+ draft_id: fileId,
192
+ };
193
+ }
194
+ // Submit
195
+ const result = await saveSubmissionApi(apiContext.session, id, {
196
+ onlineText: options.text ? { text: options.text } : undefined,
197
+ fileId: fileId,
198
+ });
199
+ const submitResult = {
200
+ success: result.success,
201
+ assignment_id: id,
202
+ submitted: !!result.success,
203
+ online_text: !!options.text,
204
+ file_uploaded: fileUploaded,
205
+ file_id: fileId ?? null,
206
+ error: result.success ? undefined : result.error,
207
+ message: result.success ? "Assignment submitted successfully" : result.error,
208
+ };
209
+ formatAndOutput(submitResult, output, apiContext.log);
210
+ if (!result.success) {
211
+ process.exitCode = 1;
212
+ }
213
+ });
214
+ }
215
+ /**
216
+ * Prompt user for yes/no confirmation.
217
+ */
218
+ async function promptConfirm(prompt) {
219
+ const readline = await import("node:readline");
220
+ const rl = readline.createInterface({
221
+ input: process.stdin,
222
+ output: process.stdout,
223
+ });
224
+ return new Promise((resolve) => {
225
+ rl.question(prompt, (answer) => {
226
+ rl.close();
227
+ resolve(/^y/i.test(answer));
228
+ });
229
+ });
230
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../../src/src/commands/auth.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAUpC,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAoQtD"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../../src/src/commands/auth.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAWpC,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAuRtD"}
@@ -2,7 +2,8 @@ import { getBaseDir } from "../lib/utils.js";
2
2
  import { chromium } from "playwright-core";
3
3
  import { createLogger } from "../lib/logger.js";
4
4
  import { findEdgePath } from "../lib/auth.js";
5
- import { saveSesskey, acquireWsToken, saveWsToken } from "../lib/token.js";
5
+ import { saveSesskey, acquireWsToken, saveWsToken, loadWsToken } from "../lib/token.js";
6
+ import { getSiteInfoApi } from "../lib/moodle.js";
6
7
  import path from "node:path";
7
8
  import fs from "node:fs";
8
9
  export function registerCommand(program) {
@@ -211,6 +212,25 @@ export function registerCommand(program) {
211
212
  exists: false
212
213
  }
213
214
  };
215
+ // Try to get user info from WS API
216
+ try {
217
+ const wsToken = loadWsToken(sessionPath);
218
+ if (wsToken) {
219
+ const session = {
220
+ wsToken,
221
+ moodleBaseUrl: "https://ilearning.cycu.edu.tw"
222
+ };
223
+ const siteInfo = await getSiteInfoApi(session);
224
+ result.user = {
225
+ userid: siteInfo.userid,
226
+ username: siteInfo.username,
227
+ fullname: siteInfo.fullname
228
+ };
229
+ }
230
+ }
231
+ catch {
232
+ // WS token might not be available or expired, skip user info
233
+ }
214
234
  console.log(JSON.stringify(result, null, 2));
215
235
  }
216
236
  catch {
@@ -1 +1 @@
1
- {"version":3,"file":"calendar.d.ts","sourceRoot":"","sources":["../../../src/src/commands/calendar.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAoM9D"}
1
+ {"version":3,"file":"calendar.d.ts","sourceRoot":"","sources":["../../../src/src/commands/calendar.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAmM9D"}
@@ -1,4 +1,4 @@
1
- import { getBaseDir } from "../lib/utils.js";
1
+ import { getBaseDir, formatTimestamp } from "../lib/utils.js";
2
2
  import { getEnrolledCoursesApi, getCalendarEventsApi } from "../lib/moodle.js";
3
3
  import { createLogger } from "../lib/logger.js";
4
4
  import { loadWsToken } from "../lib/token.js";
@@ -91,29 +91,28 @@ export function registerCalendarCommand(program) {
91
91
  if (options.upcoming) {
92
92
  filteredEvents = allEvents.filter(e => e.timestart > now);
93
93
  }
94
- const output = {
94
+ console.log(JSON.stringify({
95
95
  status: "success",
96
96
  timestamp: new Date().toISOString(),
97
- events: filteredEvents.map(e => ({
97
+ total_events: allEvents.length,
98
+ upcoming: allEvents.filter(e => e.timestart > now).length,
99
+ by_type: allEvents.reduce((acc, e) => {
100
+ acc[e.eventtype] = (acc[e.eventtype] || 0) + 1;
101
+ return acc;
102
+ }, {}),
103
+ }));
104
+ for (const e of filteredEvents) {
105
+ console.log(JSON.stringify({
98
106
  id: e.id,
99
107
  name: e.name,
100
108
  description: e.description,
101
109
  course_id: e.courseid,
102
110
  event_type: e.eventtype,
103
- start_time: new Date(e.timestart).toISOString(),
104
- end_time: e.timeduration ? new Date(e.timestart + e.timeduration / 1000).toISOString() : null,
111
+ start_time: formatTimestamp(e.timestart),
112
+ end_time: e.timeduration ? formatTimestamp(e.timestart + Math.floor(e.timeduration / 1000)) : null,
105
113
  location: e.location,
106
- })),
107
- summary: {
108
- total_events: allEvents.length,
109
- upcoming: allEvents.filter(e => e.timestart > now).length,
110
- by_type: allEvents.reduce((acc, e) => {
111
- acc[e.eventtype] = (acc[e.eventtype] || 0) + 1;
112
- return acc;
113
- }, {}),
114
- },
115
- };
116
- console.log(JSON.stringify(output));
114
+ }));
115
+ }
117
116
  });
118
117
  calendarCmd
119
118
  .command("export")
@@ -161,8 +160,8 @@ export function registerCalendarCommand(program) {
161
160
  description: e.description,
162
161
  course_id: e.courseid,
163
162
  event_type: e.eventtype,
164
- start_time: new Date(e.timestart).toISOString(),
165
- end_time: e.timeduration ? new Date(e.timestart + e.timeduration / 1000).toISOString() : null,
163
+ start_time: formatTimestamp(e.timestart),
164
+ end_time: e.timeduration ? formatTimestamp(e.timestart + Math.floor(e.timeduration / 1000)) : null,
166
165
  location: e.location,
167
166
  })),
168
167
  summary: {
@@ -1,4 +1,4 @@
1
- import { getBaseDir } from "../lib/utils.js";
1
+ import { getBaseDir, formatTimestamp } from "../lib/utils.js";
2
2
  import { getEnrolledCoursesApi } from "../lib/moodle.js";
3
3
  import { createLogger } from "../lib/logger.js";
4
4
  import { loadWsToken } from "../lib/token.js";
@@ -111,8 +111,8 @@ export function registerCoursesCommand(program) {
111
111
  courseId: course.id,
112
112
  courseName: course.fullname,
113
113
  progress: course.progress ?? 0,
114
- startDate: course.startdate ? new Date(course.startdate * 1000).toISOString() : null,
115
- endDate: course.enddate ? new Date(course.enddate * 1000).toISOString() : null,
114
+ startDate: course.startdate ? formatTimestamp(course.startdate) : null,
115
+ endDate: course.enddate ? formatTimestamp(course.enddate) : null,
116
116
  };
117
117
  formatAndOutput(progressData, output, apiContext.log);
118
118
  });
@@ -1 +1 @@
1
- {"version":3,"file":"forums.d.ts","sourceRoot":"","sources":["../../../src/src/commands/forums.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAkBpC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA6P5D"}
1
+ {"version":3,"file":"forums.d.ts","sourceRoot":"","sources":["../../../src/src/commands/forums.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAmBpC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAkX5D"}