campus-cli
Advanced tools
+10
-0
@@ -7,2 +7,12 @@ # Changelog | ||
| ## [Sin publicar] | ||
| ### Added | ||
| - `Dockerfile` para los verificadores de directorios MCP. Glama y similares arrancan el servidor y le piden `tools/list`; la imagen no descarga el navegador de Playwright porque el login real ocurre en la máquina del estudiante, no en un contenedor. | ||
| ### Fixed | ||
| - El servidor MCP anunciaba `version: 1.0.0` en el handshake, escrita a mano y congelada desde entonces. Ahora sale del `package.json`, así que cada cliente y cada directorio ve la versión real. | ||
| --- | ||
| ## [1.3.2] — 2026-08-06 | ||
@@ -9,0 +19,0 @@ |
@@ -27,2 +27,7 @@ "use strict"; | ||
| console.log((0, theme_js_1.ok)(`Sesión iniciada como ${chalk_1.default.bold(session.account.name)} (${session.account.email})`)); | ||
| // El registro es el momento de decirlo: es cuando el usuario nos da | ||
| // una cuenta y cuando esa cuenta pasa a identificar sus métricas de | ||
| // uso. Enterrarlo solo en el README no cuenta como avisar. | ||
| console.log(chalk_1.default.gray('\n Campus registra qué comandos y herramientas usas para mejorar el producto.')); | ||
| console.log(chalk_1.default.gray(' Nunca tus cursos, tareas ni calificaciones. Para desactivarlo: POSTHOG_DISABLED=1')); | ||
| } | ||
@@ -29,0 +34,0 @@ catch (err) { |
| /** Best-effort analytics: failures never affect the campus client. */ | ||
| export declare function track(event: string, properties?: Record<string, unknown>, userId?: string): void; | ||
| export declare function track(event: string, properties?: Record<string, unknown>): void; |
+57
-4
@@ -11,5 +11,31 @@ "use strict"; | ||
| const node_path_1 = __importDefault(require("node:path")); | ||
| const store_js_1 = require("./account/store.js"); | ||
| const POSTHOG_HOST = (process.env.POSTHOG_HOST ?? 'https://us.i.posthog.com').replace(/\/$/, ''); | ||
| const POSTHOG_KEY = process.env.POSTHOG_API_KEY ?? 'phc_mVYDii8qujKLxaCagZomJjR4B2Cd53FieYqDyBPe4zGw'; | ||
| const INSTALL_FILE = node_path_1.default.join(node_os_1.default.homedir(), '.blackboard-cli', 'analytics-id'); | ||
| // npm_package_version solo existe cuando algo corre vía npm scripts, así que | ||
| // en una instalación normal todos los eventos llegaban etiquetados con el | ||
| // fallback y no con la versión real. Desde dist/, '../' es la raíz del paquete. | ||
| const { version: VERSION } = require('../package.json'); | ||
| // Todo lo que puede viajar a PostHog. Es una lista blanca a propósito: antes | ||
| // reenviábamos `...properties` tal cual, así que cualquier evento nuevo podía | ||
| // llevarse por delante el nombre de un curso o de una tarea sin que nadie lo | ||
| // notara al revisar. Añadir aquí una clave es una decisión consciente. | ||
| const ALLOWED_PROPERTIES = new Set([ | ||
| 'app', | ||
| 'attempts_count', | ||
| 'command', | ||
| 'duration_ms', | ||
| 'error_type', | ||
| 'has_comments', | ||
| 'has_file', | ||
| 'has_text', | ||
| 'method', | ||
| 'mode', | ||
| 'parent_command', | ||
| 'status_code', | ||
| 'success', | ||
| 'tool', | ||
| 'version', | ||
| ]); | ||
| function installId() { | ||
@@ -28,7 +54,34 @@ try { | ||
| } | ||
| // Identificamos por la cuenta Campus, no por el usuario de Blackboard. | ||
| // | ||
| // El identificador de Blackboard es una credencial del sistema de la | ||
| // universidad y es reidentificable: con blackboard_whoami se convierte en el | ||
| // nombre y el correo del alumno. Mandarlo a un tercero no es algo que el | ||
| // estudiante nos haya autorizado al conectar su campus. | ||
| // | ||
| // La cuenta Campus sí es nuestra, el usuario se registró con ella y acepta la | ||
| // analítica en ese registro. Para validar sirve igual — usuarios únicos, | ||
| // retención, embudo, herramientas más usadas — y el vínculo con la persona | ||
| // vive en nuestro backend, no en PostHog. | ||
| // | ||
| // Sin cuenta (alguien que solo usa `campus login`) cae al id de instalación, | ||
| // que es un UUID local y no identifica a nadie. | ||
| function distinctId() { | ||
| try { | ||
| const accountId = (0, store_js_1.loadAccountSession)()?.account?.id; | ||
| if (accountId) | ||
| return `campus:${accountId}`; | ||
| } | ||
| catch { /* la analítica nunca puede romper un comando */ } | ||
| return `install:${installId()}`; | ||
| } | ||
| /** Best-effort analytics: failures never affect the campus client. */ | ||
| function track(event, properties = {}, userId) { | ||
| function track(event, properties = {}) { | ||
| if (process.env.POSTHOG_DISABLED === '1' || !POSTHOG_KEY) | ||
| return; | ||
| const distinctId = userId ? `bb:${userId}` : `install:${installId()}`; | ||
| const safe = {}; | ||
| for (const [key, value] of Object.entries(properties)) { | ||
| if (ALLOWED_PROPERTIES.has(key)) | ||
| safe[key] = value; | ||
| } | ||
| void fetch(`${POSTHOG_HOST}/capture/`, { | ||
@@ -40,6 +93,6 @@ method: 'POST', | ||
| event, | ||
| distinct_id: distinctId, | ||
| properties: { ...properties, app: 'campus-cli', version: process.env.npm_package_version ?? '1.1.2' }, | ||
| distinct_id: distinctId(), | ||
| properties: { ...safe, app: 'campus-cli', version: VERSION }, | ||
| }), | ||
| }).catch(() => { }); | ||
| } |
+4
-4
@@ -60,3 +60,3 @@ #!/usr/bin/env node | ||
| command: process.argv.slice(2, 4).filter((arg) => !arg.startsWith('-')).join(' ') || 'help', | ||
| }, (0, session_js_1.loadSession)()?.userId); | ||
| }); | ||
| // Commander hooks give every CLI command the same success/error/latency | ||
@@ -70,3 +70,3 @@ // coverage, including commands that do not call Blackboard directly here. | ||
| parent_command: thisCommand.name(), | ||
| }, (0, session_js_1.loadSession)()?.userId); | ||
| }); | ||
| }); | ||
@@ -79,3 +79,3 @@ program.hook('postAction', (thisCommand, actionCommand) => { | ||
| duration_ms: Date.now() - (actionCommand.__analyticsStartedAt ?? Date.now()), | ||
| }, (0, session_js_1.loadSession)()?.userId); | ||
| }); | ||
| }); | ||
@@ -179,5 +179,5 @@ program | ||
| program.parseAsync(process.argv).catch((err) => { | ||
| (0, analytics_js_1.track)('cli_error', { error_type: err instanceof Error ? err.name : 'CommandError', command: process.argv[2] ?? 'unknown' }, (0, session_js_1.loadSession)()?.userId); | ||
| (0, analytics_js_1.track)('cli_error', { error_type: err instanceof Error ? err.name : 'CommandError', command: process.argv[2] ?? 'unknown' }); | ||
| console.error(chalk_1.default.red(err.message)); | ||
| process.exit(1); | ||
| }); |
@@ -8,2 +8,7 @@ "use strict"; | ||
| const analytics_js_1 = require("../analytics.js"); | ||
| // La versión que anunciamos en el handshake sale del package.json. Estaba | ||
| // escrita a mano y se quedó en 1.0.0, así que cada cliente y cada directorio | ||
| // que la leía veía una versión que no existe. Desde dist/mcp/, '../..' es la | ||
| // raíz del paquete, que sí incluye package.json en lo publicado. | ||
| const { version: VERSION } = require('../../package.json'); | ||
| const INSTRUCTIONS = ` | ||
@@ -37,3 +42,3 @@ campus-cli conecta el campus universitario del estudiante con su agente de IA. | ||
| name: 'campus-cli', | ||
| version: '1.0.0', | ||
| version: VERSION, | ||
| }, { | ||
@@ -40,0 +45,0 @@ instructions: INSTRUCTIONS, |
@@ -65,3 +65,3 @@ "use strict"; | ||
| if (session.expiresAt && Date.now() > session.expiresAt) { | ||
| (0, analytics_js_1.track)('session_expired', {}, session.userId); | ||
| (0, analytics_js_1.track)('session_expired', {}); | ||
| return null; // expired | ||
@@ -68,0 +68,0 @@ } |
@@ -214,3 +214,3 @@ "use strict"; | ||
| const attempts = await (0, assignments_js_1.listAttempts)(client, courseId, columnId); | ||
| (0, analytics_js_1.track)('attempts_viewed', { success: true, attempts_count: attempts.length }, session.userId); | ||
| (0, analytics_js_1.track)('attempts_viewed', { success: true, attempts_count: attempts.length }); | ||
| spinner.succeed(`${attempts.length} attempt(s)`); | ||
@@ -249,3 +249,3 @@ if (opts.json) { | ||
| catch (err) { | ||
| (0, analytics_js_1.track)('attempts_view_error', { success: false, error_type: err?.name ?? 'AttemptsError' }, session.userId); | ||
| (0, analytics_js_1.track)('attempts_view_error', { success: false, error_type: err?.name ?? 'AttemptsError' }); | ||
| spinner.fail(err.message); | ||
@@ -269,3 +269,3 @@ process.exit(1); | ||
| const mode = opts.draft ? 'draft' : 'submit'; | ||
| (0, analytics_js_1.track)('assignment_submission_started', { mode, has_file: !!opts.file, has_text: !!opts.text, has_comments: !!opts.comments }, session.userId); | ||
| (0, analytics_js_1.track)('assignment_submission_started', { mode, has_file: !!opts.file, has_text: !!opts.text, has_comments: !!opts.comments }); | ||
| if (!opts.file && !opts.text && !opts.comments) { | ||
@@ -288,7 +288,7 @@ console.error(chalk_1.default.red('Provide at least --file, --text, or --comments')); | ||
| fileUploadIds = [uploadId]; | ||
| (0, analytics_js_1.track)('assignment_file_uploaded', { success: true, mode, duration_ms: Date.now() - startedAt }, session.userId); | ||
| (0, analytics_js_1.track)('assignment_file_uploaded', { success: true, mode, duration_ms: Date.now() - startedAt }); | ||
| uploadSpinner.succeed(`File uploaded (id: ${uploadId})`); | ||
| } | ||
| catch (e) { | ||
| (0, analytics_js_1.track)('assignment_file_upload_error', { success: false, mode, error_type: e?.name ?? 'UploadError', status_code: e?.response?.status }, session.userId); | ||
| (0, analytics_js_1.track)('assignment_file_upload_error', { success: false, mode, error_type: e?.name ?? 'UploadError', status_code: e?.response?.status }); | ||
| uploadSpinner.fail(`Upload failed: ${e.message}`); | ||
@@ -309,3 +309,3 @@ process.exit(1); | ||
| success: true, mode, duration_ms: Date.now() - startedAt, has_file: !!fileUploadIds, | ||
| }, session.userId); | ||
| }); | ||
| if (opts.json) { | ||
@@ -326,3 +326,3 @@ console.log(JSON.stringify(attempt, null, 2)); | ||
| error_type: err?.name ?? 'SubmissionError', status_code: err?.response?.status, | ||
| }, session.userId); | ||
| }); | ||
| const body = err.response?.data; | ||
@@ -329,0 +329,0 @@ console.error(chalk_1.default.red(`\n✗ ${err.message}`)); |
@@ -31,3 +31,3 @@ "use strict"; | ||
| console.log(chalk_1.default.gray('A browser window will open. Complete the login and it will close automatically.\n')); | ||
| (0, analytics_js_1.track)('login_started', { method: 'microsoft_sso' }, existing?.userId); | ||
| (0, analytics_js_1.track)('login_started', { method: 'microsoft_sso' }); | ||
| try { | ||
@@ -39,3 +39,3 @@ const session = await (0, login_js_1.login)({ | ||
| }); | ||
| (0, analytics_js_1.track)('login_success', { method: 'microsoft_sso' }, session.userId); | ||
| (0, analytics_js_1.track)('login_success', { method: 'microsoft_sso' }); | ||
| const ssoExpiresAt = (0, login_js_1.getSsoExpiry)(session.cookies); | ||
@@ -42,0 +42,0 @@ const { summary, note } = (0, theme_js_1.formatSessionLifetime)(session.expiresAt, ssoExpiresAt); |
@@ -34,3 +34,3 @@ "use strict"; | ||
| const result = await handler(...input); | ||
| (0, analytics_js_1.track)('mcp_tool_used', { tool: name, success: true, duration_ms: Date.now() - startedAt }, session?.userId); | ||
| (0, analytics_js_1.track)('mcp_tool_used', { tool: name, success: true, duration_ms: Date.now() - startedAt }); | ||
| return result; | ||
@@ -44,3 +44,3 @@ } | ||
| error_type: error instanceof Error ? error.name : 'UnknownError', | ||
| }, session?.userId); | ||
| }); | ||
| throw error; | ||
@@ -47,0 +47,0 @@ } |
+2
-4
| { | ||
| "name": "campus-cli", | ||
| "version": "1.3.2", | ||
| "version": "1.3.3", | ||
| "description": "Conecta Blackboard UPC con ChatGPT y Claude vía MCP, o úsalo desde la terminal — CLI/MCP no oficial para el campus universitario (Blackboard, Canvas, Moodle...)", | ||
@@ -36,5 +36,3 @@ "mcpName": "io.github.alejooroncoy/campus-cli", | ||
| "dev": "tsx src/index.ts", | ||
| "start": "node dist/index.js", | ||
| "landing:css": "esbuild landing/styles.css --minify --outfile=landing/styles.min.css --allow-overwrite", | ||
| "landing:build": "node landing/tools/build.mjs" | ||
| "start": "node dist/index.js" | ||
| }, | ||
@@ -41,0 +39,0 @@ "keywords": [ |
+7
-1
@@ -443,4 +443,10 @@ # campus-cli | ||
| El cliente registra de forma anónima el inicio de la CLI, los logins exitosos y la apertura del dashboard en PostHog. Se usa el ID de Blackboard únicamente como identificador estable; no se envían cookies, contraseñas, cursos, tareas ni calificaciones. | ||
| El cliente registra en PostHog el inicio de la CLI, los logins exitosos y la apertura del dashboard. No se envían cookies, contraseñas, cursos, tareas ni calificaciones. | ||
| Como identificador estable se usa el **ID de tu cuenta Campus**, la que creas con `campus account login`. Si no tienes cuenta Campus, se usa un UUID aleatorio generado en tu máquina que no identifica a nadie. En ningún caso se envía tu identificador de Blackboard: es una credencial de la universidad y no sale de tu equipo. | ||
| Esto es seudónimo, no anónimo: quien tenga acceso a nuestro PostHog puede distinguir a un usuario de otro y, cruzando con nuestra base de cuentas, saber de quién se trata. Lo decimos así de claro a propósito. | ||
| Solo viajan las propiedades de esta lista blanca: `app`, `attempts_count`, `command`, `duration_ms`, `error_type`, `has_comments`, `has_file`, `has_text`, `method`, `mode`, `parent_command`, `status_code`, `success`, `tool` y `version`. Cualquier otra clave se descarta antes de enviar, así que un evento nuevo no puede filtrar el nombre de un curso por descuido. El código está en [`src/analytics.ts`](src/analytics.ts) y son cuarenta líneas: léelas. | ||
| La clave pública del proyecto está configurada por defecto. Para cambiar el proyecto o desactivar la analítica: | ||
@@ -447,0 +453,0 @@ |
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 2 instances
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 3 instances
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
209053
1.84%3596
1.78%472
1.29%21
-8.7%