Sign In

campus-cli

Package Overview
Dependencies
Maintainers
1
Versions
14
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

campus-cli - npm Package Compare versions

Comparing version
1.3.2
to
1.3.3
+10
-0
CHANGELOG.md

@@ -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) {

+1
-1
/** 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;

@@ -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(() => { });
}

@@ -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 @@ }

{
"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": [

@@ -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 @@