@mo7yw4ng/openape 1.0.5 → 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 (77) 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/calendar.d.ts.map +1 -1
  8. package/esm/src/commands/calendar.js +32 -84
  9. package/esm/src/commands/courses.d.ts.map +1 -1
  10. package/esm/src/commands/courses.js +2 -38
  11. package/esm/src/commands/forums.d.ts.map +1 -1
  12. package/esm/src/commands/forums.js +47 -175
  13. package/esm/src/commands/grades.d.ts.map +1 -1
  14. package/esm/src/commands/grades.js +10 -47
  15. package/esm/src/commands/materials.d.ts.map +1 -1
  16. package/esm/src/commands/materials.js +47 -58
  17. package/esm/src/commands/quizzes.d.ts.map +1 -1
  18. package/esm/src/commands/quizzes.js +2 -37
  19. package/esm/src/commands/skills.js +3 -3
  20. package/esm/src/commands/upload.d.ts.map +1 -1
  21. package/esm/src/commands/upload.js +2 -5
  22. package/esm/src/commands/videos.d.ts.map +1 -1
  23. package/esm/src/commands/videos.js +6 -76
  24. package/esm/src/index.d.ts +2 -1
  25. package/esm/src/index.d.ts.map +1 -1
  26. package/esm/src/index.js +5 -1
  27. package/esm/src/lib/auth.d.ts +21 -2
  28. package/esm/src/lib/auth.d.ts.map +1 -1
  29. package/esm/src/lib/auth.js +78 -19
  30. package/esm/src/lib/logger.d.ts +2 -2
  31. package/esm/src/lib/logger.d.ts.map +1 -1
  32. package/esm/src/lib/logger.js +1 -2
  33. package/esm/src/lib/moodle.d.ts +14 -0
  34. package/esm/src/lib/moodle.d.ts.map +1 -1
  35. package/esm/src/lib/moodle.js +35 -0
  36. package/esm/src/lib/utils.d.ts +3 -8
  37. package/esm/src/lib/utils.d.ts.map +1 -1
  38. package/esm/src/lib/utils.js +3 -10
  39. package/package.json +1 -1
  40. package/script/deno.js +1 -1
  41. package/script/src/commands/announcements.d.ts.map +1 -1
  42. package/script/src/commands/announcements.js +23 -89
  43. package/script/src/commands/assignments.d.ts.map +1 -1
  44. package/script/src/commands/assignments.js +2 -3
  45. package/script/src/commands/calendar.d.ts.map +1 -1
  46. package/script/src/commands/calendar.js +33 -85
  47. package/script/src/commands/courses.d.ts.map +1 -1
  48. package/script/src/commands/courses.js +9 -48
  49. package/script/src/commands/forums.d.ts.map +1 -1
  50. package/script/src/commands/forums.js +50 -181
  51. package/script/src/commands/grades.d.ts.map +1 -1
  52. package/script/src/commands/grades.js +14 -54
  53. package/script/src/commands/materials.d.ts.map +1 -1
  54. package/script/src/commands/materials.js +47 -58
  55. package/script/src/commands/quizzes.d.ts.map +1 -1
  56. package/script/src/commands/quizzes.js +11 -49
  57. package/script/src/commands/skills.js +3 -3
  58. package/script/src/commands/upload.d.ts.map +1 -1
  59. package/script/src/commands/upload.js +2 -5
  60. package/script/src/commands/videos.d.ts.map +1 -1
  61. package/script/src/commands/videos.js +11 -81
  62. package/script/src/index.d.ts +2 -1
  63. package/script/src/index.d.ts.map +1 -1
  64. package/script/src/index.js +5 -1
  65. package/script/src/lib/auth.d.ts +21 -2
  66. package/script/src/lib/auth.d.ts.map +1 -1
  67. package/script/src/lib/auth.js +83 -56
  68. package/script/src/lib/logger.d.ts +2 -2
  69. package/script/src/lib/logger.d.ts.map +1 -1
  70. package/script/src/lib/logger.js +1 -2
  71. package/script/src/lib/moodle.d.ts +14 -0
  72. package/script/src/lib/moodle.d.ts.map +1 -1
  73. package/script/src/lib/moodle.js +36 -0
  74. package/script/src/lib/utils.d.ts +3 -8
  75. package/script/src/lib/utils.d.ts.map +1 -1
  76. package/script/src/lib/utils.js +3 -11
  77. package/skills/openape/SKILL.md +6 -6
@@ -1,16 +1,10 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  exports.registerQuizzesCommand = registerQuizzesCommand;
7
4
  const utils_js_1 = require("../lib/utils.js");
8
5
  const moodle_js_1 = require("../lib/moodle.js");
9
- const logger_js_1 = require("../lib/logger.js");
6
+ const auth_js_1 = require("../lib/auth.js");
10
7
  const index_js_1 = require("../index.js");
11
- const token_js_1 = require("../lib/token.js");
12
- const node_path_1 = __importDefault(require("node:path"));
13
- const node_fs_1 = __importDefault(require("node:fs"));
14
8
  function stripHtmlKeepLines(html) {
15
9
  return html
16
10
  .replace(/<br\s*\/?>/gi, "\n")
@@ -65,38 +59,6 @@ function parseQuizQuestions(questions) {
65
59
  function registerQuizzesCommand(program) {
66
60
  const quizzesCmd = program.command("quizzes");
67
61
  quizzesCmd.description("Quiz operations");
68
- function getOutputFormat(command) {
69
- const opts = command.optsWithGlobals();
70
- return opts.output || "json";
71
- }
72
- // Pure API context - no browser required (fast!)
73
- async function createApiContext(options, command) {
74
- const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
75
- const outputFormat = getOutputFormat(command || { optsWithGlobals: () => ({ output: "json" }) });
76
- const silent = outputFormat === "json" && !opts.verbose;
77
- const log = (0, logger_js_1.createLogger)(opts.verbose, silent, outputFormat);
78
- const baseDir = (0, utils_js_1.getBaseDir)();
79
- const sessionPath = node_path_1.default.resolve(baseDir, ".auth", "storage-state.json");
80
- // Check if session exists
81
- if (!node_fs_1.default.existsSync(sessionPath)) {
82
- log.error("未找到登入 session。請先執行 'openape login' 進行登入。");
83
- log.info(`Session 預期位置: ${sessionPath}`);
84
- return null;
85
- }
86
- // Try to load WS token
87
- const wsToken = (0, token_js_1.loadWsToken)(sessionPath);
88
- if (!wsToken) {
89
- log.error("未找到 WS token。請先執行 'openape login' 進行登入。");
90
- return null;
91
- }
92
- return {
93
- log,
94
- session: {
95
- wsToken,
96
- moodleBaseUrl: "https://ilearning.cycu.edu.tw",
97
- },
98
- };
99
- }
100
62
  quizzesCmd
101
63
  .command("list")
102
64
  .description("List incomplete quizzes in a course")
@@ -104,8 +66,8 @@ function registerQuizzesCommand(program) {
104
66
  .option("--all", "Include completed quizzes")
105
67
  .option("--output <format>", "Output format: json|csv|table|silent")
106
68
  .action(async (courseId, options, command) => {
107
- const output = getOutputFormat(command);
108
- const apiContext = await createApiContext(options, command);
69
+ const output = (0, utils_js_1.getOutputFormat)(command);
70
+ const apiContext = await (0, auth_js_1.createApiContext)(options, command);
109
71
  if (!apiContext) {
110
72
  process.exitCode = 1;
111
73
  return;
@@ -126,8 +88,8 @@ function registerQuizzesCommand(program) {
126
88
  .option("--all", "Include completed quizzes")
127
89
  .option("--output <format>", "Output format: json|csv|table|silent")
128
90
  .action(async (options, command) => {
129
- const output = getOutputFormat(command);
130
- const apiContext = await createApiContext(options, command);
91
+ const output = (0, utils_js_1.getOutputFormat)(command);
92
+ const apiContext = await (0, auth_js_1.createApiContext)(options, command);
131
93
  if (!apiContext) {
132
94
  process.exitCode = 1;
133
95
  return;
@@ -167,8 +129,8 @@ function registerQuizzesCommand(program) {
167
129
  .argument("<quiz-id>", "Quiz ID")
168
130
  .option("--output <format>", "Output format: json|csv|table|silent")
169
131
  .action(async (quizCmid, options, command) => {
170
- const output = getOutputFormat(command);
171
- const apiContext = await createApiContext(options, command);
132
+ const output = (0, utils_js_1.getOutputFormat)(command);
133
+ const apiContext = await (0, auth_js_1.createApiContext)(options, command);
172
134
  if (!apiContext) {
173
135
  process.exitCode = 1;
174
136
  return;
@@ -205,8 +167,8 @@ function registerQuizzesCommand(program) {
205
167
  .option("--page <number>", "Page number (-1 for all pages)", "-1")
206
168
  .option("--output <format>", "Output format: json|csv|table|silent")
207
169
  .action(async (attemptId, options, command) => {
208
- const output = getOutputFormat(command);
209
- const apiContext = await createApiContext(options, command);
170
+ const output = (0, utils_js_1.getOutputFormat)(command);
171
+ const apiContext = await (0, auth_js_1.createApiContext)(options, command);
210
172
  if (!apiContext) {
211
173
  process.exitCode = 1;
212
174
  return;
@@ -240,8 +202,8 @@ function registerQuizzesCommand(program) {
240
202
  .option("--submit", "Submit the attempt after saving")
241
203
  .option("--output <format>", "Output format: json|csv|table|silent")
242
204
  .action(async (attemptId, answersJson, options, command) => {
243
- const output = getOutputFormat(command);
244
- const apiContext = await createApiContext(options, command);
205
+ const output = (0, utils_js_1.getOutputFormat)(command);
206
+ const apiContext = await (0, auth_js_1.createApiContext)(options, command);
245
207
  if (!apiContext) {
246
208
  process.exitCode = 1;
247
209
  return;
@@ -7,6 +7,7 @@ exports.registerSkillsCommand = registerSkillsCommand;
7
7
  const node_fs_1 = __importDefault(require("node:fs"));
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
9
  const node_os_1 = __importDefault(require("node:os"));
10
+ const node_url_1 = require("node:url");
10
11
  const SKILL_NAME = "openape";
11
12
  const GITHUB_RAW_URL = `https://raw.githubusercontent.com/mo7yw4ng/openape/refs/heads/main/skills/${SKILL_NAME}/SKILL.md`;
12
13
  /**
@@ -24,11 +25,10 @@ const PLATFORMS = {
24
25
  async function readSkillContent() {
25
26
  // Try local path first (relative to this file's location)
26
27
  try {
27
- const base = node_path_1.default.dirname(new URL(globalThis[Symbol.for("import-meta-ponyfill-commonjs")](require, module).url).pathname);
28
- const normalized = process.platform === "win32" ? base.replace(/^\//, "") : base;
28
+ const base = node_path_1.default.dirname((0, node_url_1.fileURLToPath)(globalThis[Symbol.for("import-meta-ponyfill-commonjs")](require, module).url));
29
29
  // When running from source: src/commands/ → ../../skills/openape/SKILL.md
30
30
  // When bundled by dnt into build/: esm/commands/ or script/ → ../../skills/openape/SKILL.md
31
- const localPath = node_path_1.default.resolve(normalized, "..", "..", "skills", SKILL_NAME, "SKILL.md");
31
+ const localPath = node_path_1.default.resolve(base, "..", "..", "skills", SKILL_NAME, "SKILL.md");
32
32
  return await node_fs_1.default.promises.readFile(localPath, "utf-8");
33
33
  }
34
34
  catch {
@@ -1 +1 @@
1
- {"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../../../src/src/commands/upload.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA2D5D"}
1
+ {"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../../../src/src/commands/upload.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAwD5D"}
@@ -26,19 +26,16 @@ function registerUploadCommand(program) {
26
26
  process.exitCode = 1;
27
27
  return;
28
28
  }
29
- // Resolve file path
30
29
  const resolvedPath = node_path_1.default.resolve(filePath);
31
- // Check if file exists
30
+ let stats;
32
31
  try {
33
- await promises_1.default.access(resolvedPath);
32
+ stats = await promises_1.default.stat(resolvedPath);
34
33
  }
35
34
  catch {
36
35
  apiContext.log.error(`檔案不存在: ${filePath}`);
37
36
  process.exitCode = 1;
38
37
  return;
39
38
  }
40
- // Get file size
41
- const stats = await promises_1.default.stat(resolvedPath);
42
39
  const fileSizeKB = (0, utils_js_1.formatFileSize)(stats.size);
43
40
  apiContext.log.info(`上傳檔案: ${node_path_1.default.basename(resolvedPath)} (${fileSizeKB} KB)`);
44
41
  // Upload file
@@ -1 +1 @@
1
- {"version":3,"file":"videos.d.ts","sourceRoot":"","sources":["../../../src/src/commands/videos.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAYpC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAgY5D"}
1
+ {"version":3,"file":"videos.d.ts","sourceRoot":"","sources":["../../../src/src/commands/videos.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAgT5D"}
@@ -6,80 +6,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.registerVideosCommand = registerVideosCommand;
7
7
  const utils_js_1 = require("../lib/utils.js");
8
8
  const moodle_js_1 = require("../lib/moodle.js");
9
- const logger_js_1 = require("../lib/logger.js");
10
9
  const auth_js_1 = require("../lib/auth.js");
11
- const session_js_1 = require("../lib/session.js");
12
- const auth_js_2 = require("../lib/auth.js");
13
10
  const index_js_1 = require("../index.js");
14
- const token_js_1 = require("../lib/token.js");
15
11
  const node_path_1 = __importDefault(require("node:path"));
16
12
  const node_fs_1 = __importDefault(require("node:fs"));
17
13
  function registerVideosCommand(program) {
18
14
  const videosCmd = program.command("videos");
19
15
  videosCmd.description("Video progress operations");
20
- // Pure API context - no browser required (fast!)
21
- async function createApiContext(options, command) {
22
- const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
23
- // Don't silence logs for commands that don't have explicit output format control
24
- const outputFormat = command && command.optsWithGlobals ? (0, utils_js_1.getOutputFormat)(command) : "table";
25
- const silent = outputFormat === "json" && !opts.verbose;
26
- const log = (0, logger_js_1.createLogger)(opts.verbose, silent, outputFormat);
27
- const baseDir = (0, utils_js_1.getBaseDir)();
28
- const sessionPath = node_path_1.default.resolve(baseDir, ".auth", "storage-state.json");
29
- // Check if session exists
30
- if (!node_fs_1.default.existsSync(sessionPath)) {
31
- console.error("未找到登入 session。請先執行 'openape login' 進行登入。");
32
- log.info(`Session 預期位置: ${sessionPath}`);
33
- return null;
34
- }
35
- // Try to load WS token
36
- const wsToken = (0, token_js_1.loadWsToken)(sessionPath);
37
- if (!wsToken) {
38
- console.error("未找到 WS token。請先執行 'openape login' 進行登入。");
39
- return null;
40
- }
41
- return {
42
- log,
43
- session: {
44
- wsToken,
45
- moodleBaseUrl: "https://ilearning.cycu.edu.tw",
46
- },
47
- };
48
- }
49
- // Helper function to create session context (for browser-only commands)
50
- async function createSessionContext(options, command) {
51
- const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
52
- const outputFormat = (0, utils_js_1.getOutputFormat)(command || { optsWithGlobals: () => ({ output: "json" }) });
53
- const silent = outputFormat === "json" && !opts.verbose;
54
- const log = (0, logger_js_1.createLogger)(opts.verbose, silent, outputFormat);
55
- const baseDir = (0, utils_js_1.getBaseDir)();
56
- const sessionPath = node_path_1.default.resolve(baseDir, ".auth", "storage-state.json");
57
- if (!node_fs_1.default.existsSync(sessionPath)) {
58
- console.error("未找到登入 session。請先執行 'openape login' 進行登入。");
59
- return null;
60
- }
61
- const config = {
62
- username: "",
63
- password: "",
64
- courseUrl: "",
65
- moodleBaseUrl: "https://ilearning.cycu.edu.tw",
66
- headless: !options.headed,
67
- slowMo: 0,
68
- authStatePath: sessionPath,
69
- ollamaBaseUrl: "",
70
- };
71
- log.info("啟動瀏覽器...");
72
- const { browser, context, page } = await (0, auth_js_1.launchAuthenticated)(config, log);
73
- try {
74
- const session = await (0, session_js_1.extractSessionInfo)(page, config, log);
75
- return { log, page, session, browser, context };
76
- }
77
- catch (err) {
78
- await context.close();
79
- await browser.close();
80
- throw err;
81
- }
82
- }
83
16
  videosCmd
84
17
  .command("list")
85
18
  .description("List videos in a course")
@@ -88,7 +21,7 @@ function registerVideosCommand(program) {
88
21
  .option("--output <format>", "Output format: json|csv|table|silent")
89
22
  .action(async (courseId, options, command) => {
90
23
  const output = (0, utils_js_1.getOutputFormat)(command);
91
- const apiContext = await createApiContext(options, command);
24
+ const apiContext = await (0, auth_js_1.createApiContext)(options, command);
92
25
  if (!apiContext) {
93
26
  process.exitCode = 1;
94
27
  return;
@@ -109,7 +42,7 @@ function registerVideosCommand(program) {
109
42
  .action(async (courseId, options, command) => {
110
43
  const output = (0, utils_js_1.getOutputFormat)(command);
111
44
  // Get API context for getting incomplete videos and completion
112
- const apiContext = await createApiContext(options, command);
45
+ const apiContext = await (0, auth_js_1.createApiContext)(options, command);
113
46
  if (!apiContext) {
114
47
  process.exitCode = 1;
115
48
  return;
@@ -134,7 +67,7 @@ function registerVideosCommand(program) {
134
67
  return;
135
68
  }
136
69
  // Need browser only for getting viewId and duration (not needed for dry-run)
137
- const context = await createSessionContext(options, command);
70
+ const context = await (0, auth_js_1.createBrowserContext)(options, command);
138
71
  if (!context) {
139
72
  process.exitCode = 1;
140
73
  return;
@@ -171,7 +104,7 @@ function registerVideosCommand(program) {
171
104
  }
172
105
  }
173
106
  finally {
174
- await (0, auth_js_2.closeBrowserSafely)(browser, browserContext);
107
+ await (0, auth_js_1.closeBrowserSafely)(browser, browserContext);
175
108
  }
176
109
  });
177
110
  videosCmd
@@ -182,7 +115,7 @@ function registerVideosCommand(program) {
182
115
  .action(async (options, command) => {
183
116
  const output = (0, utils_js_1.getOutputFormat)(command);
184
117
  // Get API context for getting incomplete videos and completion
185
- const apiContext = await createApiContext(options, command);
118
+ const apiContext = await (0, auth_js_1.createApiContext)(options, command);
186
119
  if (!apiContext) {
187
120
  process.exitCode = 1;
188
121
  return;
@@ -227,7 +160,7 @@ function registerVideosCommand(program) {
227
160
  return;
228
161
  }
229
162
  // Need browser only for getting viewId and duration (not needed for dry-run)
230
- const context = await createSessionContext(options, command);
163
+ const context = await (0, auth_js_1.createBrowserContext)(options, command);
231
164
  if (!context) {
232
165
  process.exitCode = 1;
233
166
  return;
@@ -272,7 +205,7 @@ function registerVideosCommand(program) {
272
205
  }
273
206
  }
274
207
  finally {
275
- await (0, auth_js_2.closeBrowserSafely)(browser, browserContext);
208
+ await (0, auth_js_1.closeBrowserSafely)(browser, browserContext);
276
209
  }
277
210
  });
278
211
  videosCmd
@@ -282,7 +215,7 @@ function registerVideosCommand(program) {
282
215
  .option("--output-dir <path>", "Output directory", "./downloads/videos")
283
216
  .option("--incomplete-only", "Download only incomplete videos")
284
217
  .action(async (courseId, options, command) => {
285
- const context = await createSessionContext(options, command);
218
+ const context = await (0, auth_js_1.createBrowserContext)(options, command);
286
219
  if (!context) {
287
220
  process.exitCode = 1;
288
221
  return;
@@ -322,7 +255,7 @@ function registerVideosCommand(program) {
322
255
  const completed = downloaded.filter(d => d.success).length;
323
256
  const failed = downloaded.filter(d => !d.success).length;
324
257
  log.info(`\n執行結果: ${completed} 成功, ${failed} 失敗`);
325
- console.log(JSON.stringify({
258
+ (0, index_js_1.formatAndOutput)(downloaded, "json", log, {
326
259
  status: "success",
327
260
  timestamp: new Date().toISOString(),
328
261
  course_id: courseId,
@@ -330,13 +263,10 @@ function registerVideosCommand(program) {
330
263
  total_videos: videos.length,
331
264
  downloaded: completed,
332
265
  failed,
333
- }));
334
- for (const v of downloaded) {
335
- console.log(JSON.stringify(v));
336
- }
266
+ });
337
267
  }
338
268
  finally {
339
- await (0, auth_js_2.closeBrowserSafely)(browser, browserContext);
269
+ await (0, auth_js_1.closeBrowserSafely)(browser, browserContext);
340
270
  }
341
271
  });
342
272
  }
@@ -21,7 +21,8 @@ export declare function createSessionContext(options: {
21
21
  /**
22
22
  * Helper to output formatted data.
23
23
  * For JSON output (agent mode), exits immediately after output.
24
+ * If meta is provided, it is printed as the first line before items.
24
25
  */
25
- export declare function formatAndOutput<T extends Record<string, unknown>>(data: T | T[], format: OutputFormat, log: Logger): void;
26
+ export declare function formatAndOutput<T extends Record<string, unknown>>(data: T | T[], format: OutputFormat, log: Logger, meta?: Record<string, unknown>): void;
26
27
  export { createLogger, type AppConfig, type Logger, type SessionInfo, type OutputFormat };
27
28
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/src/index.ts"],"names":[],"mappings":";AACA,OAAO,sBAAsB,CAAC;AAO9B,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAwEnF;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE;IACP,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,GACA,OAAO,CAAC;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,iBAAiB,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,WAAW,CAAA;CAAE,GAAG,IAAI,CAAC,CAoBhH;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/D,IAAI,EAAE,CAAC,GAAG,CAAC,EAAE,EACb,MAAM,EAAE,YAAY,EACpB,GAAG,EAAE,MAAM,GACV,IAAI,CAyBN;AA2CD,OAAO,EAAE,YAAY,EAAE,KAAK,SAAS,EAAE,KAAK,MAAM,EAAE,KAAK,WAAW,EAAE,KAAK,YAAY,EAAE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/src/index.ts"],"names":[],"mappings":";AACA,OAAO,sBAAsB,CAAC;AAO9B,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAwEnF;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE;IACP,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,GACA,OAAO,CAAC;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,iBAAiB,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,WAAW,CAAA;CAAE,GAAG,IAAI,CAAC,CAoBhH;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/D,IAAI,EAAE,CAAC,GAAG,CAAC,EAAE,EACb,MAAM,EAAE,YAAY,EACpB,GAAG,EAAE,MAAM,EACX,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,IAAI,CA4BN;AA2CD,OAAO,EAAE,YAAY,EAAE,KAAK,SAAS,EAAE,KAAK,MAAM,EAAE,KAAK,WAAW,EAAE,KAAK,YAAY,EAAE,CAAC"}
@@ -94,9 +94,13 @@ async function createSessionContext(options) {
94
94
  /**
95
95
  * Helper to output formatted data.
96
96
  * For JSON output (agent mode), exits immediately after output.
97
+ * If meta is provided, it is printed as the first line before items.
97
98
  */
98
- function formatAndOutput(data, format, log) {
99
+ function formatAndOutput(data, format, log, meta) {
99
100
  if (format === "json") {
101
+ if (meta) {
102
+ console.log(JSON.stringify(meta));
103
+ }
100
104
  if (Array.isArray(data)) {
101
105
  for (const item of data) {
102
106
  console.log(JSON.stringify(item));
@@ -1,7 +1,7 @@
1
1
  import { type Browser, type BrowserContext, type Page } from "playwright-core";
2
- import type { AppConfig, Logger, OutputFormat } from "./types.js";
2
+ import type { AppConfig, Logger, OutputFormat, SessionInfo } from "./types.js";
3
3
  /**
4
- * Find a Chromium-based browser executable on Windows.
4
+ * Find a Chromium-based browser executable on Windows, macOS, or Linux.
5
5
  * Priority: Edge → Chrome → Brave
6
6
  */
7
7
  export declare function findEdgePath(): string;
@@ -44,4 +44,23 @@ export declare function createApiContext(options: {
44
44
  moodleBaseUrl: string;
45
45
  };
46
46
  } | null>;
47
+ /**
48
+ * Create an authenticated browser context for commands that need page access.
49
+ * Launches a browser, restores or creates a session, and extracts session info.
50
+ */
51
+ export declare function createBrowserContext(options: {
52
+ verbose?: boolean;
53
+ headed?: boolean;
54
+ }, command?: {
55
+ optsWithGlobals(): {
56
+ output?: OutputFormat;
57
+ verbose?: boolean;
58
+ };
59
+ }): Promise<{
60
+ log: Logger;
61
+ page: Page;
62
+ session: SessionInfo;
63
+ browser: Browser;
64
+ context: BrowserContext;
65
+ } | null>;
47
66
  //# sourceMappingURL=auth.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../../src/src/lib/auth.ts"],"names":[],"mappings":"AAEA,OAAO,EAAY,KAAK,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,IAAI,EAAE,MAAM,iBAAiB,CAAC;AACzF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAGlE;;;GAGG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAuBrC;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,SAAS,EACjB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,cAAc,CAAC;IAAC,IAAI,EAAE,IAAI,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA4DtF;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE,cAAc,EACxB,SAAS,GAAE,MAAa,EACxB,MAAM,GAAE,OAAe,GACtB,OAAO,CAAC,IAAI,CAAC,CA+Bf;AAoHD;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,EAChD,OAAO,CAAC,EAAE;IAAE,eAAe,IAAI;QAAE,MAAM,CAAC,EAAE,YAAY,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAA;CAAE,GAC5E,OAAO,CAAC;IACT,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAA;KAAE,CAAC;CACrD,GAAG,IAAI,CAAC,CA0BR"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../../src/src/lib/auth.ts"],"names":[],"mappings":"AAEA,OAAO,EAAY,KAAK,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,IAAI,EAAE,MAAM,iBAAiB,CAAC;AACzF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAM/E;;;GAGG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAiDrC;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,SAAS,EACjB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,cAAc,CAAC;IAAC,IAAI,EAAE,IAAI,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA4DtF;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE,cAAc,EACxB,SAAS,GAAE,MAAa,EACxB,MAAM,GAAE,OAAe,GACtB,OAAO,CAAC,IAAI,CAAC,CA+Bf;AAoHD;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,EAChD,OAAO,CAAC,EAAE;IAAE,eAAe,IAAI;QAAE,MAAM,CAAC,EAAE,YAAY,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAA;CAAE,GAC5E,OAAO,CAAC;IACT,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAA;KAAE,CAAC;CACrD,GAAG,IAAI,CAAC,CAoBR;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,EAChD,OAAO,CAAC,EAAE;IAAE,eAAe,IAAI;QAAE,MAAM,CAAC,EAAE,YAAY,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAA;CAAE,GAC5E,OAAO,CAAC;IACT,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,IAAI,CAAC;IACX,OAAO,EAAE,WAAW,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,cAAc,CAAC;CACzB,GAAG,IAAI,CAAC,CA0BR"}
@@ -1,37 +1,4 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
2
  var __importDefault = (this && this.__importDefault) || function (mod) {
36
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
4
  };
@@ -40,32 +7,66 @@ exports.findEdgePath = findEdgePath;
40
7
  exports.launchAuthenticated = launchAuthenticated;
41
8
  exports.closeBrowserSafely = closeBrowserSafely;
42
9
  exports.createApiContext = createApiContext;
10
+ exports.createBrowserContext = createBrowserContext;
43
11
  const node_fs_1 = __importDefault(require("node:fs"));
44
12
  const node_path_1 = __importDefault(require("node:path"));
45
13
  const playwright_core_1 = require("playwright-core");
46
14
  const token_js_1 = require("./token.js");
15
+ const logger_js_1 = require("./logger.js");
16
+ const utils_js_1 = require("./utils.js");
17
+ const session_js_1 = require("./session.js");
47
18
  /**
48
- * Find a Chromium-based browser executable on Windows.
19
+ * Find a Chromium-based browser executable on Windows, macOS, or Linux.
49
20
  * Priority: Edge → Chrome → Brave
50
21
  */
51
22
  function findEdgePath() {
52
- const roots = [
53
- process.env.PROGRAMFILES,
54
- process.env["PROGRAMFILES(X86)"],
55
- process.env.LOCALAPPDATA,
56
- ].filter(Boolean);
57
- const browsers = [
58
- { name: "Edge", suffix: "Microsoft\\Edge\\Application\\msedge.exe" },
59
- { name: "Chrome", suffix: "Google\\Chrome\\Application\\chrome.exe" },
60
- { name: "Brave", suffix: "BraveSoftware\\Brave-Browser\\Application\\brave.exe" },
61
- ];
62
- for (const { suffix } of browsers) {
63
- for (const root of roots) {
64
- const candidate = node_path_1.default.join(root, suffix);
23
+ const platform = process.platform;
24
+ if (platform === "darwin") {
25
+ const candidates = [
26
+ "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
27
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
28
+ "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
29
+ ];
30
+ for (const candidate of candidates) {
65
31
  if (node_fs_1.default.existsSync(candidate))
66
32
  return candidate;
67
33
  }
68
34
  }
35
+ else if (platform === "linux") {
36
+ const candidates = [
37
+ "/usr/bin/microsoft-edge",
38
+ "/usr/bin/google-chrome",
39
+ "/usr/bin/chromium-browser",
40
+ "/usr/bin/chromium",
41
+ "/usr/bin/brave-browser",
42
+ ];
43
+ for (const candidate of candidates) {
44
+ if (node_fs_1.default.existsSync(candidate))
45
+ return candidate;
46
+ }
47
+ }
48
+ else if (platform === "win32") {
49
+ const roots = [
50
+ process.env.PROGRAMFILES,
51
+ process.env["PROGRAMFILES(X86)"],
52
+ process.env.LOCALAPPDATA,
53
+ ].filter(Boolean);
54
+ const browsers = [
55
+ { suffix: "Microsoft\\Edge\\Application\\msedge.exe" },
56
+ { suffix: "Google\\Chrome\\Application\\chrome.exe" },
57
+ { suffix: "BraveSoftware\\Brave-Browser\\Application\\brave.exe" },
58
+ ];
59
+ for (const { suffix } of browsers) {
60
+ for (const root of roots) {
61
+ const candidate = node_path_1.default.join(root, suffix);
62
+ if (node_fs_1.default.existsSync(candidate))
63
+ return candidate;
64
+ }
65
+ }
66
+ }
67
+ else {
68
+ throw new Error(`不支援的作業系統:${platform}`);
69
+ }
69
70
  throw new Error("找不到可用的瀏覽器(Edge / Chrome / Brave)。請確認已安裝其中一種。");
70
71
  }
71
72
  /**
@@ -245,18 +246,14 @@ async function login(page, config, log) {
245
246
  * Returns null if session is invalid or WS token is missing.
246
247
  */
247
248
  async function createApiContext(options, command) {
248
- const { createLogger } = await Promise.resolve().then(() => __importStar(require("./logger.js")));
249
- const { loadWsToken } = await Promise.resolve().then(() => __importStar(require("./token.js")));
250
- const { getOutputFormat, getSessionPath } = await Promise.resolve().then(() => __importStar(require("./utils.js")));
251
249
  const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
252
- const outputFormat = command ? getOutputFormat(command) : "json";
250
+ const outputFormat = command ? (0, utils_js_1.getOutputFormat)(command) : "json";
253
251
  const silent = outputFormat === "json" && !opts.verbose;
254
- const log = createLogger(opts.verbose, silent, outputFormat);
255
- const sessionPath = getSessionPath();
256
- // Try to load WS token
257
- const wsToken = loadWsToken(sessionPath);
252
+ const log = (0, logger_js_1.createLogger)(opts.verbose, silent, outputFormat);
253
+ const sessionPath = (0, utils_js_1.getSessionPath)();
254
+ const wsToken = (0, token_js_1.loadWsToken)(sessionPath);
258
255
  if (!wsToken) {
259
- console.error("未找到 WS token。請先執行 'openape login' 進行登入。");
256
+ log.error("未找到 WS token。請先執行 'openape login' 進行登入。");
260
257
  return null;
261
258
  }
262
259
  return {
@@ -267,3 +264,33 @@ async function createApiContext(options, command) {
267
264
  },
268
265
  };
269
266
  }
267
+ /**
268
+ * Create an authenticated browser context for commands that need page access.
269
+ * Launches a browser, restores or creates a session, and extracts session info.
270
+ */
271
+ async function createBrowserContext(options, command) {
272
+ const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
273
+ const outputFormat = command ? (0, utils_js_1.getOutputFormat)(command) : "json";
274
+ const silent = outputFormat === "json" && !opts.verbose;
275
+ const log = (0, logger_js_1.createLogger)(opts.verbose, silent, outputFormat);
276
+ const sessionPath = (0, utils_js_1.getSessionPath)();
277
+ const headed = "headed" in options ? options.headed : false;
278
+ const config = {
279
+ courseUrl: "",
280
+ moodleBaseUrl: "https://ilearning.cycu.edu.tw",
281
+ headless: !headed,
282
+ slowMo: 0,
283
+ authStatePath: sessionPath,
284
+ ollamaBaseUrl: "",
285
+ };
286
+ try {
287
+ log.info("啟動瀏覽器...");
288
+ const { browser, context, page, wsToken } = await launchAuthenticated(config, log);
289
+ const session = await (0, session_js_1.extractSessionInfo)(page, config, log, wsToken);
290
+ return { log, page, session, browser, context };
291
+ }
292
+ catch (err) {
293
+ log.error(err instanceof Error ? err.message : String(err));
294
+ return null;
295
+ }
296
+ }
@@ -1,3 +1,3 @@
1
- import type { Logger } from "./types.js";
2
- export declare function createLogger(verbose?: boolean, silent?: boolean, outputFormat?: string): Logger;
1
+ import type { Logger, OutputFormat } from "./types.js";
2
+ export declare function createLogger(verbose?: boolean, silent?: boolean, outputFormat?: OutputFormat): Logger;
3
3
  //# sourceMappingURL=logger.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../../src/src/lib/logger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAOzC,wBAAgB,YAAY,CAAC,OAAO,UAAQ,EAAE,MAAM,UAAQ,EAAE,YAAY,GAAE,MAAe,GAAG,MAAM,CAwBnG"}
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../../src/src/lib/logger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAOvD,wBAAgB,YAAY,CAAC,OAAO,UAAQ,EAAE,MAAM,UAAQ,EAAE,YAAY,GAAE,YAAqB,GAAG,MAAM,CAuBzG"}
@@ -4,8 +4,7 @@ exports.createLogger = createLogger;
4
4
  const NO_COLOR = !!process.env.NO_COLOR;
5
5
  const c = (code, text) => NO_COLOR ? text : `\x1b[${code}m${text}\x1b[0m`;
6
6
  function createLogger(verbose = false, silent = false, outputFormat = "json") {
7
- const jsonError = outputFormat === "json";
8
- const errorFn = jsonError
7
+ const errorFn = outputFormat === "json"
9
8
  ? (msg) => console.error(JSON.stringify({ error: msg }))
10
9
  : (msg) => console.error(c("31", "[ERR]") + ` ${msg}`);
11
10
  if (silent) {
@@ -57,6 +57,20 @@ export declare function getForumsApi(session: {
57
57
  courseid: number;
58
58
  timemodified: number;
59
59
  }>>;
60
+ /**
61
+ * Resolve a forum ID (cmid or instance ID) to a forum instance ID.
62
+ * Tries cmid resolution first (via core_course_get_course_module) to get name/course info.
63
+ * Falls back to treating the ID as a raw forum instance ID.
64
+ */
65
+ export declare function resolveForumId(session: {
66
+ wsToken: string;
67
+ moodleBaseUrl: string;
68
+ }, id: string): Promise<{
69
+ forumId: number;
70
+ cmid?: number;
71
+ name?: string;
72
+ courseid?: number;
73
+ } | null>;
60
74
  /**
61
75
  * Get discussions in a forum via WS API (no browser required).
62
76
  * Uses mod_forum_get_forum_discussions