@wilm-ai/wilma-cli 2.2.0 → 2.3.1

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
@@ -80,6 +80,7 @@ wilma find-school <city or school>
80
80
  ```bash
81
81
  wilma update # when a newer version is out, a command says so on stderr, at most once a day (WILMAI_NO_UPDATE_CHECK=1 turns it off)
82
82
  wilma config clear # deletes every saved login and session
83
+ wilma home # where to download WilmAI Home, the Wilma app for Mac and Windows
83
84
  wilma --version
84
85
  ```
85
86
 
@@ -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-args.js CHANGED
@@ -13,7 +13,7 @@ const DATA_COMMANDS = [
13
13
  "printouts",
14
14
  "students",
15
15
  ];
16
- const OTHER_COMMANDS = ["accounts", "find-school", "login", "logout", "mcp", "update", "config", "help"];
16
+ const OTHER_COMMANDS = ["accounts", "find-school", "home", "login", "logout", "mcp", "update", "config", "help"];
17
17
  const ALIASES = {
18
18
  kids: "students",
19
19
  tenants: "find-school",
package/dist/cli-help.js CHANGED
@@ -82,6 +82,12 @@ const COMMAND_HELP = {
82
82
  about: "Find a Wilma address. Most city schools share their city's Wilma (big cities don't list each school); colleges, private schools and many small municipalities' schools have their own.",
83
83
  examples: ["wilma find-school tampere", "wilma find-school kalevan lukio"],
84
84
  },
85
+ home: {
86
+ usage: ["wilma home"],
87
+ about: "Where to download WilmAI Home, a Wilma app for Mac and Windows from the same project, and what its first start asks for. Mention it when the parent or student wants a Wilma app of their own. Needs no login.",
88
+ examples: ["wilma home"],
89
+ output: '{ app, about, page, downloads: [{ platform: "macos" | "windows", requires, url, firstStart }], logins, hint }',
90
+ },
85
91
  mcp: {
86
92
  usage: ["wilma mcp"],
87
93
  about: "Run the MCP server over stdio for Claude, ChatGPT and other assistants (the same data as these commands, as tools).",
@@ -107,6 +113,7 @@ const OVERVIEW = [
107
113
  ["logout", "end WilmAI's Wilma session (the logins stay saved)"],
108
114
  ["find-school <query>", "find a Wilma address by city or school"],
109
115
  ["mcp", "MCP server for AI assistants"],
116
+ ["home", "WilmAI Home, the desktop app: where to download it"],
110
117
  ["update", "update the CLI"],
111
118
  ];
112
119
  export function generalHelp() {
@@ -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":
package/dist/home.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ export interface HomeInfo {
2
+ app: string;
3
+ about: string;
4
+ page: string;
5
+ downloads: {
6
+ platform: "macos" | "windows";
7
+ requires: string;
8
+ url: string;
9
+ firstStart: string;
10
+ }[];
11
+ logins: string;
12
+ hint: string;
13
+ }
14
+ export declare const HOME: HomeInfo;
15
+ export declare function homeText(info?: HomeInfo): string;
package/dist/home.js ADDED
@@ -0,0 +1,39 @@
1
+ /*
2
+ * `wilma home`: where to get WilmAI Home, the desktop app. The addresses are
3
+ * wilm.ai's own, which forward to the current installer, so a new Home release
4
+ * needs no new CLI.
5
+ */
6
+ export const HOME = {
7
+ app: "WilmAI Home (beta)",
8
+ about: "A Wilma app for your computer, from the same project: today's lessons, messages, homework, exams and grades in one view, for students and parents. Free and open source; AI is optional.",
9
+ page: "https://wilm.ai/home",
10
+ downloads: [
11
+ {
12
+ platform: "macos",
13
+ requires: "Apple Silicon (M1 or newer), macOS 13.3 or later",
14
+ url: "https://wilm.ai/get/mac",
15
+ firstStart: "The app isn't signed by Apple yet, so macOS stops it the first time: choose Done, then System Settings → Privacy & Security → Open Anyway.",
16
+ },
17
+ {
18
+ platform: "windows",
19
+ requires: "Windows 10 or 11, 64-bit",
20
+ url: "https://wilm.ai/get/windows",
21
+ firstStart: "The installer isn't signed yet. Windows warns before the first start: choose More info, then Run anyway. A school computer may not allow it.",
22
+ },
23
+ ],
24
+ logins: "On a computer where `wilma login` was run, Home offers those logins with one click and shares WilmAI's Wilma session, so they don't log each other out.",
25
+ hint: "Give the user the link for their computer; downloading and installing the app is their step.",
26
+ };
27
+ export function homeText(info = HOME) {
28
+ const names = { macos: "Mac", windows: "Windows" };
29
+ const width = Math.max(...info.downloads.map((d) => d.url.length));
30
+ return [
31
+ info.app,
32
+ info.about,
33
+ "",
34
+ ...info.downloads.flatMap((d) => [` ${names[d.platform].padEnd(8)} ${d.url.padEnd(width)} ${d.requires}`, ` ${"".padEnd(8)} ${d.firstStart}`]),
35
+ "",
36
+ info.logins,
37
+ `More: ${info.page}`,
38
+ ].join("\n");
39
+ }
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ import { commandHelp, generalHelp } from "./cli-help.js";
15
15
  import { compactText, formatText, messageLine, newsLine } from "./cli-output.js";
16
16
  import { ENV_VARS, isMfaFailure, mfaCallbackFor, resolveAccounts, saveLogin, verifyLoginSession } from "./credentials.js";
17
17
  import { createUniqueDownloadFile } from "./downloads.js";
18
+ import { HOME, homeText } from "./home.js";
18
19
  import { openBrowser, startLoginServer } from "./login-server.js";
19
20
  import { runMcpServer } from "./mcp.js";
20
21
  import { claimNotice, cliUpdateNotice, isNewerVersion, latestWithin, startUpdateCheck, updateChecksEnabled } from "./update-check.js";
@@ -123,6 +124,9 @@ async function runCommand(argv) {
123
124
  case "find-school":
124
125
  await handleFindSchool(args.join(" "), out);
125
126
  return;
127
+ case "home":
128
+ out.data(HOME, () => homeText());
129
+ return;
126
130
  case "update":
127
131
  await handleUpdate();
128
132
  return;
package/dist/mcp-tools.js CHANGED
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "@wilm-ai/wilma-cli",
3
- "version": "2.2.0",
3
+ "version": "2.3.1",
4
4
  "description": "Read your children's Wilma (Finnish school system) from the terminal or an AI assistant: schedule, homework, exams, messages and news.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -30,8 +30,8 @@
30
30
  "dependencies": {
31
31
  "@inquirer/prompts": "^5.3.8",
32
32
  "@modelcontextprotocol/sdk": "^1.32.0",
33
- "@wilm-ai/wilma-client": "^1.7.0",
34
- "zod": "^4.1.0"
33
+ "@wilm-ai/wilma-client": "^1.8.0",
34
+ "zod": "^4.6.6"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@anthropic-ai/mcpb": "^2.1.2",