@mo7yw4ng/openape 1.0.2 → 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 (81) hide show
  1. package/README.md +37 -9
  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 +1 -1
  9. package/esm/src/commands/auth.d.ts.map +1 -1
  10. package/esm/src/commands/auth.js +24 -8
  11. package/esm/src/commands/calendar.d.ts.map +1 -1
  12. package/esm/src/commands/calendar.js +17 -18
  13. package/esm/src/commands/courses.js +3 -3
  14. package/esm/src/commands/forums.d.ts.map +1 -1
  15. package/esm/src/commands/forums.js +132 -45
  16. package/esm/src/commands/materials.d.ts.map +1 -1
  17. package/esm/src/commands/materials.js +176 -48
  18. package/esm/src/commands/quizzes.d.ts.map +1 -1
  19. package/esm/src/commands/quizzes.js +165 -65
  20. package/esm/src/commands/skills.d.ts.map +1 -1
  21. package/esm/src/commands/skills.js +4 -8
  22. package/esm/src/commands/upload.d.ts +3 -0
  23. package/esm/src/commands/upload.d.ts.map +1 -0
  24. package/esm/src/commands/upload.js +58 -0
  25. package/esm/src/commands/videos.d.ts.map +1 -1
  26. package/esm/src/commands/videos.js +113 -79
  27. package/esm/src/index.d.ts.map +1 -1
  28. package/esm/src/index.js +14 -3
  29. package/esm/src/lib/auth.d.ts +23 -1
  30. package/esm/src/lib/auth.d.ts.map +1 -1
  31. package/esm/src/lib/auth.js +36 -3
  32. package/esm/src/lib/moodle.d.ts +226 -2
  33. package/esm/src/lib/moodle.d.ts.map +1 -1
  34. package/esm/src/lib/moodle.js +648 -37
  35. package/esm/src/lib/types.d.ts +82 -164
  36. package/esm/src/lib/types.d.ts.map +1 -1
  37. package/esm/src/lib/types.js +1 -0
  38. package/esm/src/lib/utils.d.ts +40 -0
  39. package/esm/src/lib/utils.d.ts.map +1 -1
  40. package/esm/src/lib/utils.js +82 -4
  41. package/package.json +1 -2
  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 +12 -13
  45. package/script/src/commands/assignments.d.ts +3 -0
  46. package/script/src/commands/assignments.d.ts.map +1 -0
  47. package/script/src/commands/assignments.js +269 -0
  48. package/script/src/commands/auth.d.ts +1 -1
  49. package/script/src/commands/auth.d.ts.map +1 -1
  50. package/script/src/commands/auth.js +24 -8
  51. package/script/src/commands/calendar.d.ts.map +1 -1
  52. package/script/src/commands/calendar.js +16 -17
  53. package/script/src/commands/courses.js +2 -2
  54. package/script/src/commands/forums.d.ts.map +1 -1
  55. package/script/src/commands/forums.js +132 -45
  56. package/script/src/commands/materials.d.ts.map +1 -1
  57. package/script/src/commands/materials.js +177 -49
  58. package/script/src/commands/quizzes.d.ts.map +1 -1
  59. package/script/src/commands/quizzes.js +163 -63
  60. package/script/src/commands/skills.d.ts.map +1 -1
  61. package/script/src/commands/skills.js +4 -8
  62. package/script/src/commands/upload.d.ts +3 -0
  63. package/script/src/commands/upload.d.ts.map +1 -0
  64. package/script/src/commands/upload.js +64 -0
  65. package/script/src/commands/videos.d.ts.map +1 -1
  66. package/script/src/commands/videos.js +114 -80
  67. package/script/src/index.d.ts.map +1 -1
  68. package/script/src/index.js +13 -2
  69. package/script/src/lib/auth.d.ts +23 -1
  70. package/script/src/lib/auth.d.ts.map +1 -1
  71. package/script/src/lib/auth.js +70 -3
  72. package/script/src/lib/moodle.d.ts +226 -2
  73. package/script/src/lib/moodle.d.ts.map +1 -1
  74. package/script/src/lib/moodle.js +663 -37
  75. package/script/src/lib/types.d.ts +82 -164
  76. package/script/src/lib/types.d.ts.map +1 -1
  77. package/script/src/lib/types.js +1 -0
  78. package/script/src/lib/utils.d.ts +40 -0
  79. package/script/src/lib/utils.d.ts.map +1 -1
  80. package/script/src/lib/utils.js +89 -3
  81. package/skills/openape/SKILL.md +73 -291
package/README.md CHANGED
@@ -1,13 +1,17 @@
1
- # OpenApe CLI (Unofficial)
1
+ <h1 align="center">
2
+ <img src="assets/logo.svg" width="150" /><br/>
3
+ OpenApe CLI (Unofficial)
4
+ </h1>
2
5
 
3
6
  中原大學 [i-Learning](https://ilearning.cycu.edu.tw/) (Moodle) 平台自動化命令列工具 (CLI),幫助你快速查詢課程、觀看影片、下載教材。
4
7
 
5
8
  ## 功能特色
6
9
  - 📚 **課程資訊**:列出修課清單、成績、課程大綱與進度。
7
10
  - 📺 **影片輔助**:列出/下載影片,甚至自動標記為已觀看。
8
- - 📝 **測驗與教材**:快速查看測驗、下載教材。
9
- - 💬 **討論區與公告**:在終端機直接閱讀公告與討論區。
11
+ - 📝 **測驗與教材**:快速進行測驗、下載教材。
12
+ - 💬 **討論區與公告**:閱讀公告、討論區、發表回覆。
10
13
  - 📅 **行事曆**:內建行事曆事件查詢與匯出。
14
+ - ✅ **作業繳交**:查詢作業、檢查繳交狀態、上傳檔案並繳交。
11
15
  - 🤖 **Agent Skills**:提供 Claude Code 等 Skills 工作流支援。
12
16
 
13
17
  ## 安裝
@@ -33,8 +37,8 @@ deno install -A -g -n openape jsr:@openape/openape
33
37
  第一次使用需要登入,會開啟瀏覽器讓你手動完成登入,隨後會快取 Session 供未來使用。
34
38
  ```bash
35
39
  openape login # 登入並儲存 session (開啟瀏覽器)
36
- openape auth status # 檢查當前登入狀態
37
- openape auth logout # 登出並清除 session
40
+ openape status # 檢查當前登入狀態
41
+ openape logout # 登出並清除 session
38
42
  ```
39
43
 
40
44
  ### 課程 (Courses)
@@ -42,7 +46,7 @@ openape auth logout # 登出並清除 session
42
46
  openape courses list # 列出所有課程 (支援 --incomplete-only, --level)
43
47
  openape courses info <id> # 顯示特定課程的詳細資訊
44
48
  openape courses progress <id> # 顯示特定課程的進度
45
- openape courses syllabus <id> # 顯示課程大綱 (從 CMAP 取得)
49
+ openape courses syllabus <id> # 顯示課程大綱
46
50
  ```
47
51
 
48
52
  ### 影片 (Videos)
@@ -57,27 +61,51 @@ openape videos download <id> # 下載影片
57
61
  ```bash
58
62
  openape quizzes list <course-id> # 列出特定課程測驗
59
63
  openape quizzes list-all # 列出所有課程測驗
60
- openape quizzes open <id> # 開啟特定測驗
64
+ openape quizzes start <quiz-id> # 開始測驗
65
+ openape quizzes info <attempt-id> # 查看測驗題目
66
+ openape quizzes save <attempt-id> '<answers>' # 儲存測驗答案
61
67
  openape materials list-all # 列出所有可下載教材
62
68
  openape materials download <id> # 下載指定教材
63
69
  openape materials download-all # 批次下載教材
70
+ openape materials complete <id> # 標記教材為已完成
71
+ openape materials complete-all # 批次標記教材為已完成
64
72
  ```
65
73
 
66
74
  ### 成績與其他查詢 (Grades, Forums, Calendar)
67
75
  ```bash
68
76
  openape grades summary # 顯示學期成績總覽
69
77
  openape grades course <id> # 顯示特定課程成績
70
- 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> # 刪除討論貼文
71
85
  openape announcements list-all # 列出所有公告
72
86
  openape announcements read <id> # 閱讀特定公告
73
87
  openape calendar events # 查詢行事曆事件
74
88
  openape calendar export # 匯出事件
75
89
  ```
76
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
+
77
106
  ### Skills
78
107
  讓你的 AI Agent 也可以控制 OpenApe。只需一個指令即可安裝:
79
108
  ```bash
80
- openape skills list # 查看目前提供的所有 skills
81
109
  openape skills install claude # 為 Claude Code 安裝技能 (支援 claude, codex, opencode)
82
110
  openape skills install --all # 自動偵測環境並安裝給所有支援的 Agent
83
111
  ```
package/esm/deno.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export default {
2
2
  "name": "@openape/openape",
3
- "version": "1.0.2",
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,3 +1,3 @@
1
1
  import { Command } from "commander";
2
- export declare function registerAuthCommand(program: Command): void;
2
+ export declare function registerCommand(program: Command): void;
3
3
  //# sourceMappingURL=auth.d.ts.map
@@ -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,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAyQ1D"}
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,13 +2,11 @@ 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
- export function registerAuthCommand(program) {
9
- const authCmd = program.command("auth");
10
- authCmd.description("Authentication commands");
11
- // Register login directly on program (not under auth subcommand)
9
+ export function registerCommand(program) {
12
10
  program
13
11
  .command("login")
14
12
  .description("Login to iLearning manually and save session")
@@ -186,8 +184,7 @@ export function registerAuthCommand(program) {
186
184
  await new Promise(resolve => setTimeout(resolve, 500));
187
185
  }
188
186
  });
189
- // auth subcommand group (for status and other auth-related commands)
190
- authCmd
187
+ program
191
188
  .command("status")
192
189
  .description("Check session status")
193
190
  .option("--session <path>", "Session file path", ".auth/storage-state.json")
@@ -215,6 +212,25 @@ export function registerAuthCommand(program) {
215
212
  exists: false
216
213
  }
217
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
+ }
218
234
  console.log(JSON.stringify(result, null, 2));
219
235
  }
220
236
  catch {
@@ -236,7 +252,7 @@ export function registerAuthCommand(program) {
236
252
  console.log(JSON.stringify(result, null, 2));
237
253
  }
238
254
  });
239
- authCmd
255
+ program
240
256
  .command("logout")
241
257
  .description("Remove saved session")
242
258
  .option("--session <path>", "Session file path", ".auth/storage-state.json")
@@ -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;AAiBpC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA0Q5D"}
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"}