@mo7yw4ng/openape 1.0.3 → 1.0.5

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 (87) 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 +16 -17
  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 +45 -15
  10. package/esm/src/commands/calendar.d.ts.map +1 -1
  11. package/esm/src/commands/calendar.js +20 -21
  12. package/esm/src/commands/courses.js +6 -6
  13. package/esm/src/commands/forums.d.ts.map +1 -1
  14. package/esm/src/commands/forums.js +128 -36
  15. package/esm/src/commands/grades.js +3 -3
  16. package/esm/src/commands/materials.d.ts.map +1 -1
  17. package/esm/src/commands/materials.js +115 -224
  18. package/esm/src/commands/quizzes.d.ts.map +1 -1
  19. package/esm/src/commands/quizzes.js +179 -68
  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 +10 -9
  27. package/esm/src/index.d.ts.map +1 -1
  28. package/esm/src/index.js +12 -1
  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/logger.d.ts +1 -1
  33. package/esm/src/lib/logger.d.ts.map +1 -1
  34. package/esm/src/lib/logger.js +7 -4
  35. package/esm/src/lib/moodle.d.ts +183 -1
  36. package/esm/src/lib/moodle.d.ts.map +1 -1
  37. package/esm/src/lib/moodle.js +498 -13
  38. package/esm/src/lib/types.d.ts +81 -164
  39. package/esm/src/lib/types.d.ts.map +1 -1
  40. package/esm/src/lib/types.js +1 -0
  41. package/esm/src/lib/utils.d.ts +20 -0
  42. package/esm/src/lib/utils.d.ts.map +1 -1
  43. package/esm/src/lib/utils.js +48 -1
  44. package/package.json +1 -1
  45. package/script/deno.js +1 -1
  46. package/script/src/commands/announcements.d.ts.map +1 -1
  47. package/script/src/commands/announcements.js +15 -16
  48. package/script/src/commands/assignments.d.ts +3 -0
  49. package/script/src/commands/assignments.d.ts.map +1 -0
  50. package/script/src/commands/assignments.js +269 -0
  51. package/script/src/commands/auth.d.ts.map +1 -1
  52. package/script/src/commands/auth.js +44 -14
  53. package/script/src/commands/calendar.d.ts.map +1 -1
  54. package/script/src/commands/calendar.js +19 -20
  55. package/script/src/commands/courses.js +5 -5
  56. package/script/src/commands/forums.d.ts.map +1 -1
  57. package/script/src/commands/forums.js +128 -36
  58. package/script/src/commands/grades.js +3 -3
  59. package/script/src/commands/materials.d.ts.map +1 -1
  60. package/script/src/commands/materials.js +115 -224
  61. package/script/src/commands/quizzes.d.ts.map +1 -1
  62. package/script/src/commands/quizzes.js +177 -66
  63. package/script/src/commands/skills.d.ts.map +1 -1
  64. package/script/src/commands/skills.js +4 -8
  65. package/script/src/commands/upload.d.ts +3 -0
  66. package/script/src/commands/upload.d.ts.map +1 -0
  67. package/script/src/commands/upload.js +64 -0
  68. package/script/src/commands/videos.d.ts.map +1 -1
  69. package/script/src/commands/videos.js +10 -9
  70. package/script/src/index.d.ts.map +1 -1
  71. package/script/src/index.js +12 -1
  72. package/script/src/lib/auth.d.ts +23 -1
  73. package/script/src/lib/auth.d.ts.map +1 -1
  74. package/script/src/lib/auth.js +70 -3
  75. package/script/src/lib/logger.d.ts +1 -1
  76. package/script/src/lib/logger.d.ts.map +1 -1
  77. package/script/src/lib/logger.js +7 -4
  78. package/script/src/lib/moodle.d.ts +183 -1
  79. package/script/src/lib/moodle.d.ts.map +1 -1
  80. package/script/src/lib/moodle.js +511 -13
  81. package/script/src/lib/types.d.ts +81 -164
  82. package/script/src/lib/types.d.ts.map +1 -1
  83. package/script/src/lib/types.js +1 -0
  84. package/script/src/lib/utils.d.ts +20 -0
  85. package/script/src/lib/utils.d.ts.map +1 -1
  86. package/script/src/lib/utils.js +52 -0
  87. package/skills/openape/SKILL.md +74 -270
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.5",
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";
@@ -17,19 +17,19 @@ export function registerAnnouncementsCommand(program) {
17
17
  const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
18
18
  const outputFormat = getOutputFormat(command || { optsWithGlobals: () => ({ output: "json" }) });
19
19
  const silent = outputFormat === "json" && !opts.verbose;
20
- const log = createLogger(opts.verbose, silent);
20
+ const log = createLogger(opts.verbose, silent, outputFormat);
21
21
  const baseDir = getBaseDir();
22
22
  const sessionPath = path.resolve(baseDir, ".auth", "storage-state.json");
23
23
  // Check if session exists
24
24
  if (!fs.existsSync(sessionPath)) {
25
- log.error("未找到登入 session。請先執行 'openape auth login' 進行登入。");
25
+ console.error("未找到登入 session。請先執行 'openape login' 進行登入。");
26
26
  log.info(`Session 預期位置: ${sessionPath}`);
27
27
  return null;
28
28
  }
29
29
  // Try to load WS token
30
30
  const wsToken = loadWsToken(sessionPath);
31
31
  if (!wsToken) {
32
- log.error("未找到 WS token。請先執行 'openape auth login' 進行登入。");
32
+ console.error("未找到 WS token。請先執行 'openape login' 進行登入。");
33
33
  return null;
34
34
  }
35
35
  return {
@@ -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,CAkStD"}
@@ -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) {
@@ -128,31 +129,41 @@ export function registerCommand(program) {
128
129
  catch {
129
130
  // Ignore sesskey extraction errors
130
131
  }
131
- // Try to acquire WS token
132
+ // Acquire WS token
133
+ let wsToken;
132
134
  try {
133
- const config = {
134
- username: "",
135
- password: "",
136
- courseUrl: "",
137
- moodleBaseUrl: "https://ilearning.cycu.edu.tw",
138
- headless: false,
139
- slowMo: 0,
140
- authStatePath: sessionPath,
141
- ollamaBaseUrl: "",
142
- };
143
- const wsToken = await acquireWsToken(page, config, log);
135
+ wsToken = await acquireWsToken(page, { moodleBaseUrl: "https://ilearning.cycu.edu.tw" }, log);
144
136
  saveWsToken(sessionPath, wsToken);
145
137
  }
146
138
  catch {
147
139
  // WS token is optional, ignore errors
148
140
  }
149
141
  const stats = fs.statSync(sessionPath);
142
+ // Get user info via WS API
143
+ let user;
144
+ try {
145
+ if (wsToken) {
146
+ const siteInfo = await getSiteInfoApi({
147
+ wsToken,
148
+ moodleBaseUrl: "https://ilearning.cycu.edu.tw",
149
+ });
150
+ user = {
151
+ userid: siteInfo.userid,
152
+ username: siteInfo.username,
153
+ fullname: siteInfo.fullname,
154
+ };
155
+ }
156
+ }
157
+ catch {
158
+ // Ignore
159
+ }
150
160
  const result = {
151
161
  status: "success",
152
162
  message: "Login successful",
153
163
  session_path: sessionPath,
154
164
  session_size: stats.size,
155
- updated: true
165
+ updated: true,
166
+ ...(user ? { user } : {}),
156
167
  };
157
168
  console.log(JSON.stringify(result, null, 2));
158
169
  }
@@ -211,6 +222,25 @@ export function registerCommand(program) {
211
222
  exists: false
212
223
  }
213
224
  };
225
+ // Try to get user info from WS API
226
+ try {
227
+ const wsToken = loadWsToken(sessionPath);
228
+ if (wsToken) {
229
+ const session = {
230
+ wsToken,
231
+ moodleBaseUrl: "https://ilearning.cycu.edu.tw"
232
+ };
233
+ const siteInfo = await getSiteInfoApi(session);
234
+ result.user = {
235
+ userid: siteInfo.userid,
236
+ username: siteInfo.username,
237
+ fullname: siteInfo.fullname
238
+ };
239
+ }
240
+ }
241
+ catch {
242
+ // WS token might not be available or expired, skip user info
243
+ }
214
244
  console.log(JSON.stringify(result, null, 2));
215
245
  }
216
246
  catch {
@@ -227,7 +257,7 @@ export function registerCommand(program) {
227
257
  status: "error",
228
258
  error: "Session not found",
229
259
  session_path: sessionPath,
230
- hint: "Run 'openape auth login' first"
260
+ hint: "Run 'openape login' first"
231
261
  };
232
262
  console.log(JSON.stringify(result, null, 2));
233
263
  }
@@ -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";
@@ -17,19 +17,19 @@ export function registerCalendarCommand(program) {
17
17
  const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
18
18
  const outputFormat = getOutputFormat(command || { optsWithGlobals: () => ({ output: "json" }) });
19
19
  const silent = outputFormat === "json" && !opts.verbose;
20
- const log = createLogger(opts.verbose, silent);
20
+ const log = createLogger(opts.verbose, silent, outputFormat);
21
21
  const baseDir = getBaseDir();
22
22
  const sessionPath = path.resolve(baseDir, ".auth", "storage-state.json");
23
23
  // Check if session exists
24
24
  if (!fs.existsSync(sessionPath)) {
25
- log.error("未找到登入 session。請先執行 'openape auth login' 進行登入。");
25
+ console.error("未找到登入 session。請先執行 'openape login' 進行登入。");
26
26
  log.info(`Session 預期位置: ${sessionPath}`);
27
27
  return null;
28
28
  }
29
29
  // Try to load WS token
30
30
  const wsToken = loadWsToken(sessionPath);
31
31
  if (!wsToken) {
32
- log.error("未找到 WS token。請先執行 'openape auth login' 進行登入。");
32
+ console.error("未找到 WS token。請先執行 'openape login' 進行登入。");
33
33
  return null;
34
34
  }
35
35
  return {
@@ -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";
@@ -18,19 +18,19 @@ export function registerCoursesCommand(program) {
18
18
  const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
19
19
  const outputFormat = getOutputFormat(command || { optsWithGlobals: () => ({ output: "json" }) });
20
20
  const silent = outputFormat === "json" && !opts.verbose;
21
- const log = createLogger(opts.verbose, silent);
21
+ const log = createLogger(opts.verbose, silent, outputFormat);
22
22
  const baseDir = getBaseDir();
23
23
  const sessionPath = path.resolve(baseDir, ".auth", "storage-state.json");
24
24
  // Check if session exists
25
25
  if (!fs.existsSync(sessionPath)) {
26
- log.error("未找到登入 session。請先執行 'openape auth login' 進行登入。");
26
+ console.error("未找到登入 session。請先執行 'openape login' 進行登入。");
27
27
  log.info(`Session 預期位置: ${sessionPath}`);
28
28
  return null;
29
29
  }
30
30
  // Try to load WS token
31
31
  const wsToken = loadWsToken(sessionPath);
32
32
  if (!wsToken) {
33
- log.error("未找到 WS token。請先執行 'openape auth login' 進行登入。");
33
+ console.error("未找到 WS token。請先執行 'openape login' 進行登入。");
34
34
  return null;
35
35
  }
36
36
  return {
@@ -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"}