| name | wiener-cli |
| description | Read and write student academic data at Universidad Norbert Wiener (UNW) via the `wiener` CLI. Triggers when the user asks about notas, calificaciones, horario, asistencia, tareas, pendientes, qué tengo hoy/ahora, anuncios, archivos del curso, módulos, syllabus, calendario, deudas/pagos, plan de estudios, trámites, examenes próximos, OR wants to submit/upload an assignment, OR mentions Canvas/Aula Virtual/intranet/Wienernet/UWiener/UNW. The CLI surfaces what the Wiener admin-locked Canvas UI hides (Tareas, Discusiones, Páginas, Archivos, Quizzes, Conferencias, Syllabus tabs are removed from student view but the API exposes everything). Username format: letter + 9 digits (e.g. aXXXXXXXXX). Two backends with different passwords: intranet (ASP cookie) and Canvas (Personal Access Token via Microsoft Entra MFA). Always prefer this skill over manually scraping the Wiener portals or telling the user to navigate the web UI. |
wiener-cli
Agent-first CLI for Universidad Norbert Wiener student portals. One binary
(wiener), two backends (intranet ASP + Canvas LMS), single canonical JSON
contract. Built for an agent to autonomously answer day-to-day student
questions ("qué tengo ahora", "qué debo entregar pronto", "cómo voy", "qué
subió el profe") in one command.
Why this exists
Wiener admin hides 7 Canvas sidebar tabs from the student UI: Tareas,
Discusiones, Páginas, Archivos, Quizzes, Conferencias, Programa del curso. The
underlying REST API at /api/v1/* is NOT locked down — wiener surfaces what
the UI hides. Canvas Personal Access Token generation is enabled for students.
Backends
| Backend | Domain | Auth | Data |
|---|
| Intranet (Wienernet) | intranet.uwiener.edu.pe | ASP cookie session, intranet-only password | Notas oficiales, horario, asistencia, plan, pagos, trámites |
| Canvas (Aula Virtual) | campus.uwiener.edu.pe | Personal Access Token (Microsoft Entra–backed account) | Cursos, tareas, calificaciones, anuncios, archivos, módulos, syllabus, páginas, discusiones, quizzes, conferencias, calendario, inbox |
The two backends use different passwords. Intranet auth alone is sufficient
for intranet commands. Canvas commands require a PAT (one-time manual setup).
Trust Ladder
| Level | Commands | Friction |
|---|
| T0 (auto) | All read commands. wiener solo (panorama), hoy, ahora, semana, nuevo, doctor, schema, config, auth status/logout, cursos*, tareas (read), planner, calificaciones*, notas*, historial, horario*, asistencia, plan*, examenes, matricula, perfil, pagos*, tramite list, anuncios*, archivos list/arbol/download (≤50MB), modulos, syllabus, paginas, discusiones, quizzes, conferencias, calendario*, inbox* | None — runs silently |
| T2 (confirm) | auth login, auth canvas set-token, auth canvas pat new, cursos aliases (wizard), tramite generar (creates billable obligation), tareas submit (uploads assignment to Canvas), archivos download (>50MB), archivos sync (bulk), watch --detach | Show preview, require --yes (or interactive confirm in TTY) |
Pass --yes to skip T2 confirmations in scripted/agent flows. Always preview
with --dry-run first when uncertain.
Most common workflows (in order of frequency)
"What do I need to know right now?" — the morning ritual
wiener
Returns: bloque actual, próximo bloque, tareas vencidas/hoy, week summary, and
diff since last invocation (new grades posted, new files uploaded, new
announcements). This is the canonical entry point — most invocations.
Quick targeted queries
wiener hoy
wiener ahora
wiener semana
wiener nuevo
wiener nuevo --abrir
Course-specific (smart resolver)
The CLI accepts course code, alias, substring, or fuzzy match. All these work:
wiener tareas FB6N1
wiener tareas terapeutica
wiener tareas farma
wiener tareas terap
wiener tareas frama
In JSON/agent mode (--json or --no-input):
- Top-1 fuzzy score >0.92 → executes silently
- Otherwise → exit 1 with
error.code = "course-ambiguous" + candidates array (agent must disambiguate)
Force exact match for paranoid scripts:
wiener tareas FB6N1 --exact
Course aliases (one-time customization)
wiener cursos aliases
wiener cursos aliases list
wiener cursos aliases reset FB6M4
Hidden Canvas surfaces (what the UI doesn't show)
wiener tareas FB6N1
wiener archivos FB6N1
wiener archivos arbol FB6N1
wiener quizzes FB6N1
wiener discusiones FB6N1
wiener syllabus FB6N1
wiener conferencias FB6N1
wiener modulos FB6N1
wiener paginas FB6N1
Cross-course aggregation
wiener tareas
wiener tareas hoy
wiener tareas semana
wiener planner
wiener calificaciones
wiener notas
wiener anuncios --ultimos 3
wiener calendario --dias 14
wiener calendario --ics
Submit an assignment (T2 upload)
wiener tareas submit ciencia informe ./informe.pdf
wiener tareas submit ciencia informe doc1.pdf doc2.pdf cover.pdf --yes
cat reflexion.md | wiener tareas submit ciencia ensayo --type online_text_entry --yes
wiener tareas submit ciencia link --type online_url --url https://example.com --yes
wiener tareas submit ciencia informe ./informe.pdf --dry-run --json
The CLI auto-detects submission type from the assignment's submission_types
when there's only one. If multiple are allowed, agent must pass --type.
Hard errors: file doesn't exist, lock_at past, no attempts left, extension not
allowed.
Bulk download
wiener archivos download <file_id> --out ./material/
wiener archivos sync FB6N1 --dir ./farmaco/
Background watching (opt-in)
wiener watch
wiener watch --detach
wiener watch --detach --whatsapp
wiener watch stop
Composability with jq
wiener notas --json | jq '.cursos[] | select(.codigo=="AC4061") | {nombre, nota_final, estado}'
wiener tareas --json | jq '.tareas | map(select(.due_at <= (now + 172800 | todate))) | sort_by(.due_at)'
wiener --json | jq -c '{ahora, proximo: .ahora.proximo, pendientes: .pendiente_hoy | length}'
Initial setup (first run)
Step 1: Intranet login
wiener auth login
Step 2: Canvas PAT (one-time, requires browser + MFA)
wiener auth canvas pat new
wiener auth canvas set-token <pat>
Step 3: Verify everything works
wiener doctor --json
Mirror namespaces
wiener intranet * and wiener canvas * mirror the relevant subsets:
wiener intranet --help
wiener intranet notas
wiener canvas --help
wiener canvas tareas FB6N1
The canonical user invocation is top-level plano (wiener tareas FB6N1).
Namespaces exist for filtered help and disambiguation.
Output contract
Every command supports --json for machine-parseable output. Default is human
table with color (picocolors + cli-table3).
Success envelope
{
"ok": true,
"data": { },
"meta": {
"duration_ms": 412,
"rate_limit_remaining": 698.5,
"from_cache": false
}
}
Error envelope
{
"ok": false,
"error": {
"code": "course-ambiguous",
"message": "Multiple courses match \"labo\"",
"hint": "Try `wiener cursos` to see exact aliases",
"details": {
"candidates": [
{ "code": "FB6M4", "alias": "labo", "name": "LABORATORIO Y DIAGNÓSTICO II", "score": 0.98 }
]
},
"next_steps": ["wiener cursos --json"]
}
}
Canonical error codes (always use these in agent error handling)
| Code | When | Recovery |
|---|
auth-required | No session, command needs one | run wiener auth login (T2) |
auth-expired | Intranet session timed out | run wiener auth login (T2) |
auth-invalid-credentials | Wrong intranet password | check creds, retry login |
canvas-not-configured | No PAT set, Canvas command attempted | run wiener auth canvas pat new (T2) |
canvas-token-invalid | PAT rejected (401) | run wiener auth canvas pat new (T2) — token revoked |
course-not-found | Resolver returned 0 candidates | check wiener cursos |
course-ambiguous | Resolver returned >1 with no clear winner | use exact code or --exact |
assignment-not-found | Submit: assignment ref didn't resolve | use wiener tareas <course> to see ids |
submission-locked | Assignment lock_at is in the past | submission window closed; can't submit |
submission-no-attempts | All attempts used | check Canvas for instructor extension |
submission-invalid-extension | File ext not in allowed_extensions | use accepted format |
file-not-found | Local file path doesn't exist | check path, retry |
network-error | DNS/TCP/TLS issue | check network, retry |
rate-limited | Canvas rlr < 50 + retry-after present | wait, retry later |
parse-error | HTML parser failed (intranet shape changed) | run wiener doctor; may need CLI update |
validation-error | Input failed schema | check args |
not-implemented | Command exists but feature deferred | n/a |
Schema introspection (use this when constructing complex --params)
wiener schema notas
wiener schema --list
wiener schema tareas submit
Agents should call wiener schema <cmd> instead of relying on --help text
when constructing complex --params '<json>' invocations.
Gotchas (read before using)
-
Two passwords: intranet password ≠ Microsoft/Canvas password. If the
user only has intranet access, Canvas commands fail with
error.code = "canvas-not-configured". Suggest wiener auth canvas pat new.
-
MS Entra requires number-matching MFA: cannot be automated from CLI.
Always direct the user to do PAT generation manually once via
wiener auth canvas pat new. After that, the PAT works headlessly forever
(until revoked).
-
Wrong-password 200: intranet returns HTTP 200 for bad credentials with
estado: "0" JSON. CLI handles this — auth login aborts cleanly with
error.code = "auth-invalid-credentials".
-
Session expiry: intranet ASP session ~30 min idle. CLI detects expiry
via SiguNet.htm NXDOMAIN-redirect signature, wipes stored session,
returns error.code = "auth-expired". Agent should run wiener auth login
(T2, requires interactive confirm).
-
Course splits T/P/PD: each Wiener course splits into theory + practice
as separate Canvas courses with same course_code. CLI groups by default
(e.g. 8 logical courses from 11 Canvas courses). Use --all to flatten or
--seccion T / --seccion P1 to filter.
-
Wiener customizations in Canvas:
- 2 LTI tabs per course (
Microsoft Education, Búsqueda con IgniteAI) —
opaque to API. Use wiener cursos abrir <ref> to open in browser.
- 7 sidebar tabs admin-hidden but API works.
- 6 institutional global announcements visible on dashboard — reachable
via
wiener anuncios globales.
-
Wiener admin endpoint restrictions (v0.4.0+):
Some Canvas endpoints are blocked at the institutional level — not by the
PAT. The CLI distinguishes these from token errors:
GET /courses/{id}/files — blocked. wiener archivos list automatically
falls back to module items (/modules?include[]=items) as the source.
Files attached to modules appear; files uploaded directly to the Files tab
do not.
GET /courses/{id}/files/{id} — blocked. wiener archivos download
shows a helpful message and instructs the user to copy the download URL from
wiener archivos list output and use the --url flag instead.
- Per-course features (pages, quizzes, conferences) can be disabled by the
instructor. If so, those commands show a friendly message and redirect to
wiener modulos <ref>.
wiener doctor includes a Capabilities matrix that probes all
endpoints and shows what works vs. what is restricted, with 1h cache.
- Error codes:
wiener-restricted-endpoint (admin block) vs.
canvas-not-configured (token problem) — these are intentionally different.
-
Canvas rate limit: 3000 req/hour per token. CLI tracks via
X-Canvas-Meta rlr= header. Approaching limit (rlr < 100) surfaces a
warning in --verbose mode but continues.
-
No bulk write of tramite generar: rate-limited to 1/min internally
even with --yes. Same for tareas submit.
-
Submission upload preserves filenames: Canvas accepts files via 3-step
API (prepare → upload → submit). The displayed filename in Canvas is the
local filename, not a sanitized version.
-
watch is opt-in: never auto-starts at login. Single instance enforced
via ~/.wiener/watch.pid. Stop with wiener watch stop.
-
TLS strict: CLI never disables cert verification.
-
Dead sso.wienergroup.com domain: known Wiener intranet bug — unauth
deep-links 302-redirect to a meta-refresh into the dead sso.wienergroup.com
domain. CLI detects this signature and treats it as auth-failure.
Environment variables
| Var | Required | Description |
|---|
WIENER_INTRANET_USER | No | Auto-login bootstrap. Format: letter + 9 digits. |
WIENER_INTRANET_PASS | No | Auto-login bootstrap. Held in memory only, never persisted. |
WIENER_INTRANET_PERFIL | No | A (Alumno, default), D (Docente), P (Administrativo). |
WIENER_CANVAS_TOKEN | No | Per-invocation Canvas PAT override. |
WIENER_PROFILE | No | Default profile name (default default). |
WIENER_CONFIG_DIR | No | Override config dir. |
WIENER_WATCH_WHATSAPP_URL | No | Kapso webhook for wiener watch --whatsapp. |
Files and storage
OS-aware paths (cligentic xdg-paths block):
- macOS:
~/Library/Application Support/wiener/
- Linux:
$XDG_CONFIG_HOME/wiener/ (falls back to ~/.config/wiener/)
Layout:
config.json — global config
audit/YYYY-MM-DD.jsonl — daily-rotated audit log of T2 + verbose T0
state.json — snapshots backing wiener nuevo diff
aliases.json — custom course aliases per profile
watch.pid + watch.log — background watch
cache/ — 5-min TTL cache for Canvas responses
<profile>/ — per-profile state (CSRF token, periodos, doctor results, sessions if no keychain)
macOS Keychain items (preferred over file fallback):
wiener-cli.intranet.<profile> — ASP cookie + perfil + codigo
wiener-cli.canvas.<profile> — PAT + validatedAt + userId
Global flags (every command)
| Flag | Purpose |
|---|
--json | JSON envelope to stdout |
--ndjson | One JSON object per line (paginated/streaming) |
--dry-run | Preview T2 mutations without executing |
--verbose | Detailed stderr + audit-log T0 commands |
--quiet | Suppress stderr |
--no-input | Force non-interactive (auto-on if !isTTY(stdin)) |
--yes | Skip T2 confirmations |
--exact | Course resolver: exact match only, no fuzzy |
--fields a,b,c | Project specific keys in JSON output |
--params '<json>' | Canonical input override (wins over sugar flags) |
--config PATH | Override config dir |
--profile NAME | Use named profile |
--help, -h | Help |
--version, -v | Version |
When NOT to use this CLI
- Paying tuition or trámite obligations — generate the order with
wiener tramite generar then pay in the user's bank app. The CLI does not
handle banking integrations.
- Course enrollment / matrícula — separate portal at
matricula.uwiener.edu.pe, not in scope.
- Posting to discussions / replying to inbox — read-only in v0.2.
- Modifying any academic record — locked down by design and by API.
- Operating on another student's data — only authenticated user's own records.
- Bypassing Microsoft Entra MFA — manual PAT generation is mandatory.
Installation
bunx @railly/wiener-cli@latest
bun install -g @railly/wiener-cli
wiener --help
git clone https://github.com/Railly/wiener-cli
cd wiener-cli
bun install
bun run build:bin
./bin/wiener --help
Repo: https://github.com/Railly/wiener-cli (public)
npm: https://npmjs.com/package/@railly/wiener-cli
Agent decision tree
When user asks something Wiener-related, route as follows:
"qué tengo / qué se viene / qué hay / cómo va mi día" → wiener
"qué clase tengo ahora / a qué hora termina" → wiener ahora
"qué entregas tengo / qué debo hacer" → wiener tareas hoy or wiener tareas semana
"qué nota saqué en X" → wiener notas (intranet, official) or wiener calificaciones <ref> (Canvas, formative)
"cuándo es el examen de X" → wiener examenes
"cómo voy en X" → wiener calificaciones <ref>
"qué subió el profe / hay material nuevo" → wiener nuevo or wiener archivos <ref>
"descárgame todos los pdfs de X" → wiener archivos sync <ref> (T2)
"sube esta tarea por mí" → wiener tareas submit <course> <assignment> <files...> (T2)
"pago / debo / cuánto debo" → wiener pagos
"plan de estudios / cuántos créditos llevo" → wiener plan avance
"asistencias / cuántas faltas" → wiener asistencia
"cualquier cosa de Canvas que la web no muestra" → check the hidden-tabs commands above
Always pass --json when the agent needs to parse output. Always run
wiener doctor --json first if user reports something not working.