@wilm-ai/wilma-cli 2.3.0 → 2.3.2

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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @wilm-ai/wilma-cli
2
2
 
3
- Command line interface for Wilma (Finnish school system), built for parents and AI agents.
3
+ Command line interface for Wilma (Finnish school system), built for parents and AI agents. Students can use it with their own Wilma login too.
4
4
 
5
5
  ## Install
6
6
  Needs Node.js 20.18.1 or newer.
@@ -479,6 +479,7 @@ export declare class WilmaAccess {
479
479
  students: ({
480
480
  student: StudentRef;
481
481
  } & {
482
+ unavailable?: string[] | undefined;
482
483
  news: NewsItem[];
483
484
  })[];
484
485
  problems: {
@@ -489,6 +490,7 @@ export declare class WilmaAccess {
489
490
  students: ({
490
491
  student: StudentRef;
491
492
  } & {
493
+ unavailable?: string[] | undefined;
492
494
  news: NewsItem[];
493
495
  })[];
494
496
  problems?: undefined;
@@ -187,9 +187,10 @@ function selectNews(news, opts = {}) {
187
187
  return [...dated, ...pinnedUndated, ...older];
188
188
  }
189
189
  /**
190
- * What a daily summary needs, fetched at once. The front page, bulletins and
191
- * inbox are required; meeting invitations, lesson notes and exam times are
192
- * extras — if Wilma can't give one, the summary says so in `unavailable`.
190
+ * What a daily summary needs, fetched at once. The front page and inbox are
191
+ * required; bulletins (some Wilmas don't show them to a guardian), meeting
192
+ * invitations, lesson notes and exam times aren't — if Wilma can't give one,
193
+ * the summary says so in `unavailable`.
193
194
  */
194
195
  async function fetchSummaryInputs(client, opts) {
195
196
  const unavailable = [];
@@ -202,14 +203,16 @@ async function fetchSummaryInputs(client, opts) {
202
203
  const [overview, calendar, news, inbox, appointments, lessonNotes] = await Promise.all([
203
204
  client.overview.get(),
204
205
  client.exams.calendarOrEmpty(),
205
- client.news.list(),
206
+ client.news.listIfShown(),
206
207
  client.messages.list("inbox"),
207
208
  optional("appointments", client.messages.list("appointments"), []),
208
209
  optional("lessonNotes", client.attendance.list({ from: opts.notesFrom, to: todayString() }), []),
209
210
  ]);
211
+ if (news === null)
212
+ unavailable.push("news");
210
213
  return {
211
214
  overview: { ...overview, upcomingExams: addExamTimes(overview.upcomingExams, calendar) },
212
- news,
215
+ news: news ?? [],
213
216
  messages: [...inbox, ...appointments],
214
217
  lessonNotes,
215
218
  unavailable,
@@ -591,9 +594,11 @@ export class WilmaAccess {
591
594
  return { student, message: item };
592
595
  }
593
596
  async news(opts = {}) {
594
- return this.perStudent(opts.student, async (client) => ({
595
- news: selectNews(await client.news.list(), { limit: opts.limit, includeOlder: opts.includeOlder }),
596
- }));
597
+ return this.perStudent(opts.student, async (client) => {
598
+ const list = await client.news.listIfShown();
599
+ // A Wilma that doesn't show this account its bulletins: none, and it says so (not the same as "no bulletins").
600
+ return { news: selectNews(list ?? [], { limit: opts.limit, includeOlder: opts.includeOlder }), ...(list === null ? { unavailable: ["news"] } : {}) };
601
+ });
597
602
  }
598
603
  async newsItem(opts) {
599
604
  const { student, item } = await this.firstStudentWith(opts.student, (client) => client.news.get(opts.id));
package/dist/cli-help.js CHANGED
@@ -63,7 +63,7 @@ const COMMAND_HELP = {
63
63
  },
64
64
  login: {
65
65
  usage: ["wilma login [--no-browser]", "wilma login --tenant <url|city> --username <name> [--password-stdin] [--totp-secret <key>]"],
66
- about: "Log in to Wilma. Opens a one-time login page in the browser where the parent picks their school's Wilma; logins are saved on this computer. Families with children on different Wilmas add each one there. Without a browser, pass --tenant and --username with the password on stdin or in WILMA_PASSWORD.",
66
+ about: "Log in to Wilma, with a parent's account or a student's own. Opens a one-time login page in the browser where the user picks their school's Wilma; logins are saved on this computer. Families with children on different Wilmas add each one there. Without a browser, pass --tenant and --username with the password on stdin or in WILMA_PASSWORD.",
67
67
  examples: ["wilma login"],
68
68
  },
69
69
  accounts: {
@@ -38,7 +38,7 @@ function perStudent(result, key, render) {
38
38
  const several = result.students.length > 1;
39
39
  const lines = [];
40
40
  for (const entry of result.students) {
41
- lines.push(...heading(entry.student, several), ...render(entry[key], entry.student));
41
+ lines.push(...heading(entry.student, several), ...render(entry[key], entry.student, entry));
42
42
  }
43
43
  for (const problem of result.problems ?? [])
44
44
  lines.push("", `Couldn't read ${problem.wilma}: ${problem.message}`);
@@ -168,9 +168,10 @@ export function formatText(command, action, result) {
168
168
  : []),
169
169
  ].join("\n");
170
170
  }
171
- return perStudent(result, "news", (items) => {
172
- const lines = items.length ? items.map((n) => ` ${newsLine(n)}`) : [" No bulletins."];
173
- return lines;
171
+ return perStudent(result, "news", (items, _student, entry) => {
172
+ if (entry.unavailable?.includes("news"))
173
+ return [" This Wilma doesn't show bulletins to this account."];
174
+ return items.length ? items.map((n) => ` ${newsLine(n)}`) : [" No bulletins."];
174
175
  });
175
176
  }
176
177
  case "printouts":
@@ -260,7 +260,7 @@ export function renderLoginPage(opts) {
260
260
  submit: "Log in",
261
261
  working: "Checking with Wilma…",
262
262
  doneTitle: "You're connected",
263
- doneIntro: "Children found:",
263
+ doneIntro: "Found on this login:",
264
264
  addAnother: "Add another Wilma",
265
265
  addAnotherHint: "If one of your children goes to a school with a different Wilma, add that login too.",
266
266
  finish: "Done",
@@ -304,7 +304,7 @@ export function renderLoginPage(opts) {
304
304
  submit: "Kirjaudu",
305
305
  working: "Tarkistetaan Wilmasta…",
306
306
  doneTitle: "Yhteys on valmis",
307
- doneIntro: "Löytyneet lapset:",
307
+ doneIntro: "Näillä tunnuksilla löytyi:",
308
308
  addAnother: "Lisää toinen Wilma",
309
309
  addAnotherHint: "Jos joku lapsistasi käy koulua, jolla on eri Wilma, lisää myös sen tunnukset.",
310
310
  finish: "Valmis",
@@ -1,7 +1,7 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3
3
  import type { FetchedAttachment, WilmaAccess } from "./agent-data.js";
4
- export declare const INSTRUCTIONS = "Read-only access to Finland's Wilma school system for a parent/guardian.\n- Start with wilma_summary: today's and tomorrow's lessons, upcoming exams, recent homework, news and messages for every child in one call.\n- Tools cover all children by default; pass \"student\" (name or student number) to narrow to one child.\n- Wilma content is usually in Finnish. Answer in the user's language and translate as needed. Write dates the way that language does: Finnish \"ke 7.10.\" (with the final period), English \"Wed 7 Oct\".\n- Message and news lists return ids; use wilma_read_message / wilma_read_news for full text. Bulletins can link attachments; fetch them with wilma_get_news_attachment.\n- Teachers' feedback (\"forgot books\", \"did well\") and absences are lesson notes: wilma_lesson_notes for recent days, wilma_lesson_notes_summary for counts. Course and report-card grades are in wilma_gradebook.\n- If a tool says the user isn't logged in, relay the login link or instructions to the user exactly; never ask the user to type their Wilma password into the chat.\n- Wilma allows one session per account, so each WilmAI login logs the user out of Wilma in their browser. If they want to use Wilma in the browser, call wilma_logout and then call Wilma tools only when they ask again.\n- If a result ends with an \"Update note\", answer the question first, then pass the note on in one sentence.";
4
+ export declare const INSTRUCTIONS = "Read-only access to Finland's Wilma school system for a parent/guardian, or for a student with their own login.\n- Start with wilma_summary: today's and tomorrow's lessons, upcoming exams, recent homework, news and messages for every child in one call.\n- Tools cover all children by default (a student's own login has just that student); pass \"student\" (name or student number) to narrow to one child.\n- Wilma content is usually in Finnish. Answer in the user's language and translate as needed. Write dates the way that language does: Finnish \"ke 7.10.\" (with the final period), English \"Wed 7 Oct\".\n- Message and news lists return ids; use wilma_read_message / wilma_read_news for full text. Bulletins can link attachments; fetch them with wilma_get_news_attachment.\n- Teachers' feedback (\"forgot books\", \"did well\") and absences are lesson notes: wilma_lesson_notes for recent days, wilma_lesson_notes_summary for counts. Course and report-card grades are in wilma_gradebook.\n- If a tool says the user isn't logged in, relay the login link or instructions to the user exactly; never ask the user to type their Wilma password into the chat.\n- Wilma allows one session per account, so each WilmAI login logs the user out of Wilma in their browser. If they want to use Wilma in the browser, call wilma_logout and then call Wilma tools only when they ask again.\n- If a result ends with an \"Update note\", answer the question first, then pass the note on in one sentence.";
5
5
  export declare const READ_ONLY: {
6
6
  readonly readOnlyHint: true;
7
7
  readonly destructiveHint: false;
package/dist/mcp-tools.js CHANGED
@@ -5,9 +5,9 @@ import { toAgentJson } from "./output-json.js";
5
5
  * Wilma data tools for the local stdio server (`wilma mcp`) and any other
6
6
  * host. Each host decides how a tool call gets a WilmaAccess.
7
7
  */
8
- export const INSTRUCTIONS = `Read-only access to Finland's Wilma school system for a parent/guardian.
8
+ export const INSTRUCTIONS = `Read-only access to Finland's Wilma school system for a parent/guardian, or for a student with their own login.
9
9
  - Start with wilma_summary: today's and tomorrow's lessons, upcoming exams, recent homework, news and messages for every child in one call.
10
- - Tools cover all children by default; pass "student" (name or student number) to narrow to one child.
10
+ - Tools cover all children by default (a student's own login has just that student); pass "student" (name or student number) to narrow to one child.
11
11
  - Wilma content is usually in Finnish. Answer in the user's language and translate as needed. Write dates the way that language does: Finnish "ke 7.10." (with the final period), English "Wed 7 Oct".
12
12
  - Message and news lists return ids; use wilma_read_message / wilma_read_news for full text. Bulletins can link attachments; fetch them with wilma_get_news_attachment.
13
13
  - Teachers' feedback ("forgot books", "did well") and absences are lesson notes: wilma_lesson_notes for recent days, wilma_lesson_notes_summary for counts. Course and report-card grades are in wilma_gradebook.
@@ -74,7 +74,7 @@ function attachmentResult(fetched, savedPath) {
74
74
  export function registerWilmaTools(server, ctx) {
75
75
  server.registerTool("wilma_summary", {
76
76
  title: "School summary",
77
- description: "Daily briefing per child: today's and the next school day's lessons, upcoming exams (with start times when given), recent homework, lesson notes since the previous school day (teachers' feedback and absences), recent bulletins, and recent and unread messages (ids, unread, replyCount). The best starting point for any question about school.",
77
+ description: "Daily briefing per child: today's and the next school day's lessons, upcoming exams (with start times when given), recent homework, lesson notes since the previous school day (teachers' feedback and absences), recent bulletins, and recent and unread messages (ids, unread, replyCount). What Wilma didn't give is listed in unavailable (\"news\": this Wilma doesn't show this account its bulletins). The best starting point for any question about school.",
78
78
  inputSchema: {
79
79
  student: studentArg,
80
80
  days: z.number().int().min(1).max(60).optional().describe("How many days back to include news and messages (default 7)."),
@@ -191,7 +191,7 @@ export function registerWilmaTools(server, ctx) {
191
191
  }, async ({ id, student }) => ctx.withAccess(async (a) => json(await a.message({ id, student }))));
192
192
  server.registerTool("wilma_list_news", {
193
193
  title: "List school news",
194
- description: "School bulletins (tiedotteet): the newest dated ones, then pinned ones (pinned: true, e.g. the school-year bulletin), and with include_older also older bulletins (archived: true). Use wilma_read_news for the full text and attachments.",
194
+ description: "School bulletins (tiedotteet): the newest dated ones, then pinned ones (pinned: true, e.g. the school-year bulletin), and with include_older also older bulletins (archived: true). Use wilma_read_news for the full text and attachments. unavailable: [\"news\"] means this Wilma doesn't show this account its bulletins, which isn't the same as having none.",
195
195
  inputSchema: {
196
196
  student: studentArg,
197
197
  limit: z.number().int().min(1).max(100).optional().describe("How many dated bulletins (default 20). Pinned ones are always included."),
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@wilm-ai/wilma-cli",
3
- "version": "2.3.0",
4
- "description": "Read your children's Wilma (Finnish school system) from the terminal or an AI assistant: schedule, homework, exams, messages and news.",
3
+ "version": "2.3.2",
4
+ "description": "Read your children's Wilma (Finnish school system) from the terminal or an AI assistant: schedule, homework, exams, messages and news. Students can use their own login too.",
5
+ "mcpName": "ai.wilm/wilma",
5
6
  "license": "MIT",
6
7
  "repository": {
7
8
  "type": "git",
@@ -21,7 +22,7 @@
21
22
  "scripts": {
22
23
  "build": "tsc -p tsconfig.json",
23
24
  "lint": "tsc -p tsconfig.json --noEmit",
24
- "test": "pnpm --filter @wilm-ai/wilma-client build && pnpm run build && node test/news-resources.mjs && node test/mcp-login.mjs && node test/mfa.mjs && node test/logout.mjs && node test/shared-session.mjs && node test/audit.mjs && node test/update-notice.mjs",
25
+ "test": "pnpm --filter @wilm-ai/wilma-client build && pnpm run build && node test/news-resources.mjs && node test/mcp-login.mjs && node test/mfa.mjs && node test/logout.mjs && node test/shared-session.mjs && node test/audit.mjs && node test/update-notice.mjs && node test/registry.mjs",
25
26
  "start": "node dist/index.js",
26
27
  "prepack": "pnpm --filter @wilm-ai/wilma-client build && pnpm --filter @wilm-ai/wilma-cli build",
27
28
  "test:live": "tsc -p tsconfig.json && node test/live.spec.mjs",
@@ -30,7 +31,7 @@
30
31
  "dependencies": {
31
32
  "@inquirer/prompts": "^5.3.8",
32
33
  "@modelcontextprotocol/sdk": "^1.32.0",
33
- "@wilm-ai/wilma-client": "^1.7.1",
34
+ "@wilm-ai/wilma-client": "^1.8.0",
34
35
  "zod": "^4.6.6"
35
36
  },
36
37
  "devDependencies": {