@wilm-ai/wilma-cli 2.1.0 → 2.1.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 +1 -1
- package/dist/index.js +2 -2
- package/dist/login-server.d.ts +15 -0
- package/dist/login-server.js +135 -33
- package/dist/mcp.js +6 -2
- package/dist/update-check.d.ts +8 -0
- package/dist/update-check.js +27 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -72,7 +72,7 @@ wilma find-school <city or school>
|
|
|
72
72
|
|
|
73
73
|
## Other
|
|
74
74
|
```bash
|
|
75
|
-
wilma update #
|
|
75
|
+
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)
|
|
76
76
|
wilma config clear # deletes every saved login and session
|
|
77
77
|
wilma --version
|
|
78
78
|
```
|
package/dist/index.js
CHANGED
|
@@ -17,7 +17,7 @@ import { ENV_VARS, isMfaFailure, mfaCallbackFor, resolveAccounts, saveLogin, ver
|
|
|
17
17
|
import { createUniqueDownloadFile } from "./downloads.js";
|
|
18
18
|
import { openBrowser, startLoginServer } from "./login-server.js";
|
|
19
19
|
import { runMcpServer } from "./mcp.js";
|
|
20
|
-
import { cliUpdateNotice, isNewerVersion, latestWithin, startUpdateCheck, updateChecksEnabled } from "./update-check.js";
|
|
20
|
+
import { claimNotice, cliUpdateNotice, isNewerVersion, latestWithin, startUpdateCheck, updateChecksEnabled } from "./update-check.js";
|
|
21
21
|
import { toAgentJson } from "./output-json.js";
|
|
22
22
|
import { clearSessions, fileSessionStore } from "./session-store.js";
|
|
23
23
|
import { normalizeTenantUrl, resolveTenant, searchTenants } from "./tenant-search.js";
|
|
@@ -757,7 +757,7 @@ async function showUpdateNotice(check) {
|
|
|
757
757
|
if (!latestVersion)
|
|
758
758
|
return;
|
|
759
759
|
const currentVersion = await readPackageVersion();
|
|
760
|
-
if (isNewerVersion(latestVersion, currentVersion)) {
|
|
760
|
+
if (isNewerVersion(latestVersion, currentVersion) && (await claimNotice(latestVersion))) {
|
|
761
761
|
process.stderr.write(`\n${cliUpdateNotice(currentVersion, latestVersion)}\n`);
|
|
762
762
|
}
|
|
763
763
|
}
|
package/dist/login-server.d.ts
CHANGED
|
@@ -14,9 +14,23 @@ export interface LoginServer {
|
|
|
14
14
|
* on different Wilmas can add another login on the same page; it closes when
|
|
15
15
|
* the parent presses Done or after 15 idle minutes.
|
|
16
16
|
*/
|
|
17
|
+
/**
|
|
18
|
+
* The assistant app that asked for the login (by the name it gives when it
|
|
19
|
+
* connects to the MCP server), named in the page's intro so the parent can
|
|
20
|
+
* connect the page to what they just did.
|
|
21
|
+
*/
|
|
22
|
+
export type OpenedBy = {
|
|
23
|
+
app: string;
|
|
24
|
+
};
|
|
25
|
+
/** A plain app name from the name an MCP client reports, or undefined if unknown. */
|
|
26
|
+
export declare function appName(client?: {
|
|
27
|
+
name?: string;
|
|
28
|
+
title?: string;
|
|
29
|
+
}): string | undefined;
|
|
17
30
|
export declare function startLoginServer(opts?: {
|
|
18
31
|
timeoutMs?: number;
|
|
19
32
|
onSaved?: (profile: StoredProfile) => void;
|
|
33
|
+
openedBy?: OpenedBy;
|
|
20
34
|
}): Promise<LoginServer>;
|
|
21
35
|
export type BrowserOpenResult = "opened" | "skipped" | "headless";
|
|
22
36
|
/** Best-effort: open a URL in the user's default browser. */
|
|
@@ -28,6 +42,7 @@ export interface LoginPageOptions {
|
|
|
28
42
|
apiBase: string;
|
|
29
43
|
/** Shown in the privacy note under the form. */
|
|
30
44
|
configPath?: string;
|
|
45
|
+
openedBy?: OpenedBy;
|
|
31
46
|
}
|
|
32
47
|
/** The login page. The login API answers {status: "ok", students, accounts}. */
|
|
33
48
|
export declare function renderLoginPage(opts: LoginPageOptions): string;
|
package/dist/login-server.js
CHANGED
|
@@ -16,13 +16,25 @@ function accountsSummary(profiles) {
|
|
|
16
16
|
students: (p.students ?? []).map((s) => s.name),
|
|
17
17
|
}));
|
|
18
18
|
}
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
/** A plain app name from the name an MCP client reports, or undefined if unknown. */
|
|
20
|
+
export function appName(client) {
|
|
21
|
+
const raw = `${client?.title ?? ""} ${client?.name ?? ""}`.toLowerCase();
|
|
22
|
+
if (raw.includes("claude-code") || raw.includes("claude code"))
|
|
23
|
+
return "Claude Code";
|
|
24
|
+
if (raw.includes("claude"))
|
|
25
|
+
return "Claude";
|
|
26
|
+
if (raw.includes("codex"))
|
|
27
|
+
return "Codex";
|
|
28
|
+
if (raw.includes("chatgpt") || raw.includes("openai"))
|
|
29
|
+
return "ChatGPT";
|
|
30
|
+
if (raw.includes("cursor"))
|
|
31
|
+
return "Cursor";
|
|
32
|
+
if (raw.includes("openclaw"))
|
|
33
|
+
return "OpenClaw";
|
|
34
|
+
// Another app: its own display name, if it gave one.
|
|
35
|
+
const title = client?.title?.trim();
|
|
36
|
+
return title && title.length <= 40 ? title : undefined;
|
|
37
|
+
}
|
|
26
38
|
export async function startLoginServer(opts = {}) {
|
|
27
39
|
const token = randomBytes(18).toString("base64url");
|
|
28
40
|
const nonce = randomBytes(16).toString("base64");
|
|
@@ -64,7 +76,7 @@ export async function startLoginServer(opts = {}) {
|
|
|
64
76
|
"base-uri 'none'",
|
|
65
77
|
].join("; "),
|
|
66
78
|
});
|
|
67
|
-
res.end(renderLoginPage({ nonce, apiBase: `${base}/api`, configPath: getConfigPath() }));
|
|
79
|
+
res.end(renderLoginPage({ nonce, apiBase: `${base}/api`, configPath: getConfigPath(), openedBy: opts.openedBy }));
|
|
68
80
|
return;
|
|
69
81
|
}
|
|
70
82
|
if (url.pathname === `${base}/api/tenants` && req.method === "GET") {
|
|
@@ -260,7 +272,18 @@ export function renderLoginPage(opts) {
|
|
|
260
272
|
tooMany: "Too many attempts. Close this tab and start again from your assistant.",
|
|
261
273
|
missing: "Pick your school's Wilma and fill in both fields.",
|
|
262
274
|
expired: "This login page is no longer active. Start the login again from your assistant, or run wilma login.",
|
|
263
|
-
privacy: "Your login is saved only on this computer, in {path}
|
|
275
|
+
privacy: "Your login is saved only on this computer, in {path}, readable only by your user account. To remove it, run wilma config clear or delete that file.",
|
|
276
|
+
introApp: "{app} asked to connect to your Wilma. Log in once, and {app} can then check schedules, homework, exams, messages and news for you.",
|
|
277
|
+
trustLocalTitle: "On your own computer",
|
|
278
|
+
trustLocal: "This page isn't a website. It runs on this computer (that's what 127.0.0.1 in the address bar means), and WilmAI has no server.",
|
|
279
|
+
trustWilmaTitle: "Only to your school's Wilma",
|
|
280
|
+
trustWilma: "Your password goes from this computer straight to your school's Wilma. Your assistant never sees it.",
|
|
281
|
+
trustWilmaHost: "Your password goes from this computer straight to {host}. Your assistant never sees it.",
|
|
282
|
+
trustReadTitle: "The same information you see",
|
|
283
|
+
trustRead: "WilmAI reads schedules, messages, news and the rest just as you see them in Wilma.",
|
|
284
|
+
sentTo: "Sent only to {host}",
|
|
285
|
+
learnMore: "How WilmAI handles your login",
|
|
286
|
+
sourceCode: "Source code",
|
|
264
287
|
disclaimer: "WilmAI is an independent open-source project, not affiliated with Visma or the Wilma service.",
|
|
265
288
|
},
|
|
266
289
|
fi: {
|
|
@@ -293,7 +316,18 @@ export function renderLoginPage(opts) {
|
|
|
293
316
|
tooMany: "Liian monta yritystä. Sulje välilehti ja aloita uudelleen avustajasi kautta.",
|
|
294
317
|
missing: "Valitse koulusi Wilma ja täytä molemmat kentät.",
|
|
295
318
|
expired: "Tämä kirjautumissivu ei ole enää käytössä. Aloita kirjautuminen uudelleen avustajasi kautta tai komennolla wilma login.",
|
|
296
|
-
privacy: "Kirjautumistietosi tallennetaan vain tälle tietokoneelle, tiedostoon {path}.
|
|
319
|
+
privacy: "Kirjautumistietosi tallennetaan vain tälle tietokoneelle, tiedostoon {path}, jota vain sinun käyttäjätilisi voi lukea. Voit poistaa ne komennolla wilma config clear tai poistamalla tiedoston.",
|
|
320
|
+
introApp: "{app} pyysi yhteyttä Wilmaasi. Kirjaudu kerran, niin {app} voi sen jälkeen tarkistaa lukujärjestykset, läksyt, kokeet, viestit ja tiedotteet puolestasi.",
|
|
321
|
+
trustLocalTitle: "Omalla koneellasi",
|
|
322
|
+
trustLocal: "Tämä sivu ei ole verkkosivusto. Se toimii tällä koneella (sitä osoiterivin 127.0.0.1 tarkoittaa), eikä WilmAI:lla ole palvelinta.",
|
|
323
|
+
trustWilmaTitle: "Vain koulusi Wilmaan",
|
|
324
|
+
trustWilma: "Salasanasi lähtee tältä koneelta suoraan koulusi Wilmaan. Avustajasi ei näe sitä.",
|
|
325
|
+
trustWilmaHost: "Salasanasi lähtee tältä koneelta suoraan osoitteeseen {host}. Avustajasi ei näe sitä.",
|
|
326
|
+
trustReadTitle: "Samat tiedot kuin sinulla",
|
|
327
|
+
trustRead: "WilmAI lukee lukujärjestykset, viestit, tiedotteet ja muut tiedot samoin kuin näet ne Wilmassa.",
|
|
328
|
+
sentTo: "Lähetetään vain osoitteeseen {host}",
|
|
329
|
+
learnMore: "Miten WilmAI käsittelee kirjautumisen (englanniksi)",
|
|
330
|
+
sourceCode: "Lähdekoodi",
|
|
297
331
|
disclaimer: "WilmAI on itsenäinen avoimen lähdekoodin projekti, eikä se liity Vismaan tai Wilma-palveluun.",
|
|
298
332
|
},
|
|
299
333
|
};
|
|
@@ -310,9 +344,14 @@ export function renderLoginPage(opts) {
|
|
|
310
344
|
@media (prefers-color-scheme: dark) { :root { --ink:#eef2f3; --muted:#a8b5bb; --paper:#141c21; --line:#2c3a42; --card:#1b252b; --err:#ff8a80; } }
|
|
311
345
|
* { box-sizing:border-box; }
|
|
312
346
|
body { margin:0; background:var(--paper); color:var(--ink); font:16px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; }
|
|
313
|
-
main { max-width:
|
|
347
|
+
main { max-width:920px; margin:0 auto; padding:32px 16px 48px; }
|
|
348
|
+
.layout { display:grid; grid-template-columns:minmax(0, 1fr) minmax(0, 1fr); gap:24px 48px; align-items:start; }
|
|
349
|
+
.layout .card { margin:0 0 16px; }
|
|
350
|
+
.foot { max-width:560px; margin-top:28px; }
|
|
351
|
+
@media (max-width:760px) { main { max-width:480px; } .layout { grid-template-columns:minmax(0, 1fr); } }
|
|
314
352
|
h1 { font-size:28px; line-height:1.2; margin:0 0 8px; }
|
|
315
353
|
h1 mark { background:linear-gradient(transparent 55%, var(--yellow) 55%); color:inherit; padding:0 2px; }
|
|
354
|
+
@media (prefers-color-scheme: dark) { h1 mark { background:linear-gradient(transparent 60%, rgba(255,216,77,.4) 60%); } }
|
|
316
355
|
p { margin:0 0 16px; }
|
|
317
356
|
.muted { color:var(--muted); font-size:14px; }
|
|
318
357
|
.card { background:var(--card); border:1px solid var(--line); border-radius:12px; padding:20px; margin:20px 0; }
|
|
@@ -343,13 +382,41 @@ export function renderLoginPage(opts) {
|
|
|
343
382
|
.add-hint { margin-top:8px; }
|
|
344
383
|
.error { color:var(--err); margin-top:12px; }
|
|
345
384
|
.done h2 { margin:0 0 8px; }
|
|
385
|
+
.top { display:flex; justify-content:space-between; align-items:center; gap:12px; margin-bottom:28px; }
|
|
386
|
+
.brand { font-weight:800; font-size:18px; letter-spacing:-0.01em; }
|
|
387
|
+
.lang { display:flex; gap:4px; }
|
|
388
|
+
.lang button { font:inherit; font-size:13px; padding:3px 10px; border-radius:999px; border:1px solid var(--line); background:transparent; color:var(--muted); cursor:pointer; }
|
|
389
|
+
.lang button[aria-pressed="true"] { background:var(--yellow); border-color:var(--yellow); color:#1b2b34; font-weight:700; }
|
|
390
|
+
.trust { list-style:none; margin:20px 0 0; padding:0; display:flex; flex-direction:column; gap:12px; }
|
|
391
|
+
.trust li { display:flex; gap:12px; align-items:flex-start; }
|
|
392
|
+
.trust svg { flex:none; width:22px; height:22px; margin-top:1px; fill:none; stroke:var(--teal); stroke-width:1.8; stroke-linecap:round; stroke-linejoin:round; }
|
|
393
|
+
.trust strong { display:block; font-size:15px; line-height:1.35; }
|
|
394
|
+
.trust span { display:block; font-size:14px; line-height:1.45; color:var(--muted); }
|
|
395
|
+
.sent-to { font-size:13px; color:var(--muted); margin:6px 0 0; }
|
|
396
|
+
.links a { color:var(--teal); }
|
|
346
397
|
[hidden] { display:none !important; }
|
|
347
398
|
</style>
|
|
348
399
|
</head>
|
|
349
400
|
<body>
|
|
350
401
|
<main>
|
|
402
|
+
<header class="top">
|
|
403
|
+
<span class="brand">WilmAI</span>
|
|
404
|
+
<span class="lang" role="group" aria-label="Kieli / Language">
|
|
405
|
+
<button type="button" id="lang-fi" lang="fi">Suomi</button>
|
|
406
|
+
<button type="button" id="lang-en" lang="en">English</button>
|
|
407
|
+
</span>
|
|
408
|
+
</header>
|
|
409
|
+
<div class="layout">
|
|
410
|
+
<div class="about">
|
|
351
411
|
<h1 id="heading"></h1>
|
|
352
412
|
<p id="intro"></p>
|
|
413
|
+
<ul class="trust">
|
|
414
|
+
<li><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="4" width="18" height="12" rx="2"/><path d="M8 20h8M12 16v4"/></svg><div><strong id="trust-local-title"></strong><span id="trust-local"></span></div></li>
|
|
415
|
+
<li><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/></svg><div><strong id="trust-wilma-title"></strong><span id="trust-wilma"></span></div></li>
|
|
416
|
+
<li><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12z"/><circle cx="12" cy="12" r="3"/></svg><div><strong id="trust-read-title"></strong><span id="trust-read"></span></div></li>
|
|
417
|
+
</ul>
|
|
418
|
+
</div>
|
|
419
|
+
<div class="forms">
|
|
353
420
|
<section class="card" id="form-card">
|
|
354
421
|
<div id="school-pick">
|
|
355
422
|
<label for="school" id="school-label"></label>
|
|
@@ -367,6 +434,7 @@ export function renderLoginPage(opts) {
|
|
|
367
434
|
<input id="username" autocomplete="username" autocapitalize="none" spellcheck="false">
|
|
368
435
|
<label for="password" id="password-label"></label>
|
|
369
436
|
<input id="password" type="password" autocomplete="current-password">
|
|
437
|
+
<p class="sent-to" id="sent-to" hidden></p>
|
|
370
438
|
<div id="mfa" hidden>
|
|
371
439
|
<label id="mfa-title"></label>
|
|
372
440
|
<p class="muted" id="mfa-body"></p>
|
|
@@ -388,37 +456,75 @@ export function renderLoginPage(opts) {
|
|
|
388
456
|
<h2 id="finished-title"></h2>
|
|
389
457
|
<p id="finished-body"></p>
|
|
390
458
|
</section>
|
|
459
|
+
</div>
|
|
460
|
+
</div>
|
|
461
|
+
<div class="foot">
|
|
391
462
|
<p class="muted" id="privacy"></p>
|
|
463
|
+
<p class="muted links"><a id="learn-more" href="https://github.com/aikarjal/wilmai#credentials--privacy" target="_blank" rel="noreferrer"></a> · <a id="source-code" href="https://github.com/aikarjal/wilmai" target="_blank" rel="noreferrer"></a></p>
|
|
392
464
|
<p class="muted" id="disclaimer"></p>
|
|
465
|
+
</div>
|
|
393
466
|
</main>
|
|
394
467
|
<script nonce="${nonce}">
|
|
395
468
|
(() => {
|
|
396
469
|
const STRINGS = ${jsonForScript(strings)};
|
|
397
470
|
const API = ${jsonForScript(opts.apiBase)};
|
|
398
471
|
const CONFIG_PATH = ${jsonForScript(opts.configPath ?? "")};
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
472
|
+
const OPENED_BY = ${jsonForScript(opts.openedBy ?? null)};
|
|
473
|
+
let lang = (navigator.language || "en").toLowerCase().startsWith("fi") ? "fi" : "en";
|
|
474
|
+
let t = STRINGS[lang];
|
|
475
|
+
let tenant = null;
|
|
476
|
+
let timer = null;
|
|
477
|
+
let seq = 0;
|
|
478
|
+
let shownAccounts = null;
|
|
403
479
|
const $ = (id) => document.getElementById(id);
|
|
404
480
|
const text = (id, value) => { $(id).textContent = value; };
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
text
|
|
408
|
-
|
|
409
|
-
|
|
481
|
+
const hostOf = (url) => { try { return new URL(url).host; } catch { return url; } };
|
|
482
|
+
|
|
483
|
+
// Every text on the page, in the chosen language (the switch at the top re-runs this).
|
|
484
|
+
function applyText() {
|
|
485
|
+
t = STRINGS[lang];
|
|
486
|
+
document.documentElement.lang = lang;
|
|
487
|
+
document.title = t.title;
|
|
488
|
+
$("heading").replaceChildren(t.heading.split(" ").slice(0, -1).join(" ") + " ");
|
|
489
|
+
const mark = document.createElement("mark"); mark.textContent = t.heading.split(" ").slice(-1)[0]; $("heading").append(mark);
|
|
490
|
+
text("intro", OPENED_BY ? t.introApp.split("{app}").join(OPENED_BY.app) : t.intro);
|
|
491
|
+
text("trust-local-title", t.trustLocalTitle); text("trust-local", t.trustLocal);
|
|
492
|
+
text("trust-wilma-title", t.trustWilmaTitle);
|
|
493
|
+
text("trust-read-title", t.trustReadTitle); text("trust-read", t.trustRead);
|
|
494
|
+
text("school-label", t.school); text("school-label-2", t.school);
|
|
495
|
+
$("school").placeholder = t.schoolHint; text("no-results", t.noResults); text("change", t.change);
|
|
496
|
+
text("school-help", t.schoolHelp);
|
|
497
|
+
text("username-label", t.username); text("password-label", t.password);
|
|
498
|
+
text("mfa-title", t.mfaTitle); text("mfa-body", t.mfaBody); text("mfa-label", t.mfaLabel); text("mfa-help", t.mfaHelp);
|
|
499
|
+
if (!$("submit").disabled) text("submit", t.submit);
|
|
500
|
+
text("done-title", t.doneTitle);
|
|
501
|
+
text("finish", t.finish); text("add-another", t.addAnother); text("add-hint", t.addAnotherHint);
|
|
502
|
+
text("finished-title", t.finishedTitle); text("finished-body", t.finishedBody);
|
|
503
|
+
text("privacy", t.privacy.replace("{path}", CONFIG_PATH));
|
|
504
|
+
text("learn-more", t.learnMore); text("source-code", t.sourceCode);
|
|
505
|
+
text("disclaimer", t.disclaimer);
|
|
506
|
+
for (const code of ["fi", "en"]) $("lang-" + code).setAttribute("aria-pressed", String(code === lang));
|
|
507
|
+
showHost();
|
|
508
|
+
if (shownAccounts) renderAccounts(shownAccounts);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Where the password goes, once the parent has picked their Wilma.
|
|
512
|
+
function showHost() {
|
|
513
|
+
const host = tenant ? hostOf(tenant.url) : "";
|
|
514
|
+
text("trust-wilma", host ? t.trustWilmaHost.replace("{host}", host) : t.trustWilma);
|
|
515
|
+
$("sent-to").hidden = !host;
|
|
516
|
+
if (host) text("sent-to", t.sentTo.replace("{host}", host));
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
for (const code of ["fi", "en"]) $("lang-" + code).addEventListener("click", () => { lang = code; applyText(); });
|
|
410
520
|
for (const city of ["Helsinki", "Espoo", "Tampere", "Vantaa", "Oulu", "Turku", "Jyväskylä"]) {
|
|
411
521
|
const b = document.createElement("button"); b.type = "button"; b.textContent = city;
|
|
412
522
|
b.addEventListener("click", () => { $("school").value = city; search(city).catch(() => showError(t.expired)); $("school").focus(); });
|
|
413
523
|
$("chips").append(b);
|
|
414
524
|
}
|
|
415
|
-
text("username-label", t.username); text("password-label", t.password);
|
|
416
|
-
text("mfa-title", t.mfaTitle); text("mfa-body", t.mfaBody); text("mfa-label", t.mfaLabel); text("mfa-help", t.mfaHelp);
|
|
417
|
-
text("submit", t.submit); text("done-title", t.doneTitle);
|
|
418
|
-
text("finish", t.finish); text("add-another", t.addAnother); text("add-hint", t.addAnotherHint);
|
|
419
|
-
text("finished-title", t.finishedTitle); text("finished-body", t.finishedBody);
|
|
420
525
|
|
|
421
526
|
function renderAccounts(accounts) {
|
|
527
|
+
shownAccounts = accounts;
|
|
422
528
|
const box = $("accounts"); box.replaceChildren();
|
|
423
529
|
const intro = document.createElement("p"); intro.textContent = t.doneIntro; box.append(intro);
|
|
424
530
|
for (const account of accounts) {
|
|
@@ -434,7 +540,7 @@ export function renderLoginPage(opts) {
|
|
|
434
540
|
}
|
|
435
541
|
|
|
436
542
|
$("add-another").addEventListener("click", () => {
|
|
437
|
-
tenant = null;
|
|
543
|
+
tenant = null; shownAccounts = null; showHost();
|
|
438
544
|
for (const id of ["school", "username", "password", "totp"]) $(id).value = "";
|
|
439
545
|
render([]); showError("");
|
|
440
546
|
$("mfa").hidden = true; $("school-chosen").hidden = true; $("school-pick").hidden = false;
|
|
@@ -457,21 +563,16 @@ export function renderLoginPage(opts) {
|
|
|
457
563
|
}
|
|
458
564
|
$("done").hidden = true; $("finished").hidden = false;
|
|
459
565
|
});
|
|
460
|
-
text("privacy", t.privacy.replace("{path}", CONFIG_PATH)); text("disclaimer", t.disclaimer);
|
|
461
|
-
|
|
462
|
-
let tenant = null;
|
|
463
|
-
let timer = null;
|
|
464
|
-
let seq = 0;
|
|
465
566
|
const showError = (msg) => { const el = $("error"); el.textContent = msg; el.hidden = !msg; };
|
|
466
567
|
|
|
467
568
|
function choose(t2) {
|
|
468
569
|
tenant = t2;
|
|
469
|
-
text("chosen-name", t2.name || t2.url); text("chosen-url", t2.url);
|
|
570
|
+
text("chosen-name", t2.name || t2.url); text("chosen-url", t2.url); showHost();
|
|
470
571
|
$("school-pick").hidden = true; $("school-chosen").hidden = false;
|
|
471
572
|
$("username").focus();
|
|
472
573
|
}
|
|
473
574
|
$("change").addEventListener("click", () => {
|
|
474
|
-
tenant = null; $("school-chosen").hidden = true; $("school-pick").hidden = false; $("school").focus();
|
|
575
|
+
tenant = null; showHost(); $("school-chosen").hidden = true; $("school-pick").hidden = false; $("school").focus();
|
|
475
576
|
});
|
|
476
577
|
|
|
477
578
|
async function search(q) {
|
|
@@ -565,6 +666,7 @@ export function renderLoginPage(opts) {
|
|
|
565
666
|
btn.disabled = false; btn.textContent = t.submit;
|
|
566
667
|
}
|
|
567
668
|
}
|
|
669
|
+
applyText();
|
|
568
670
|
$("school").focus();
|
|
569
671
|
})();
|
|
570
672
|
</script>
|
package/dist/mcp.js
CHANGED
|
@@ -10,7 +10,7 @@ import { createAccess } from "./access.js";
|
|
|
10
10
|
import { loadConfig } from "./config.js";
|
|
11
11
|
import { ENV_VARS, isMfaFailure, resolveAccounts } from "./credentials.js";
|
|
12
12
|
import { createUniqueDownloadFile } from "./downloads.js";
|
|
13
|
-
import { openBrowser, startLoginServer } from "./login-server.js";
|
|
13
|
+
import { appName, openBrowser, startLoginServer } from "./login-server.js";
|
|
14
14
|
import { z } from "zod";
|
|
15
15
|
import { INSTRUCTIONS, READ_ONLY, json, registerWilmaTools, textResult } from "./mcp-tools.js";
|
|
16
16
|
import { searchTenants } from "./tenant-search.js";
|
|
@@ -23,6 +23,8 @@ const SECRET_SETTINGS_HINT = [
|
|
|
23
23
|
"Never ask the user to type their Wilma password into the chat.",
|
|
24
24
|
].join("\n");
|
|
25
25
|
let loginServerPromise = null;
|
|
26
|
+
/** The running server, so the login page can name the app that asked for it. */
|
|
27
|
+
let mcpServer = null;
|
|
26
28
|
/** One login page at a time. Parallel tool calls share it; an explicit wilma_login starts a fresh one. */
|
|
27
29
|
async function ensureLoginServer(fresh) {
|
|
28
30
|
if (fresh && loginServerPromise) {
|
|
@@ -31,7 +33,8 @@ async function ensureLoginServer(fresh) {
|
|
|
31
33
|
}
|
|
32
34
|
if (loginServerPromise)
|
|
33
35
|
return { server: await loginServerPromise, created: false };
|
|
34
|
-
const
|
|
36
|
+
const app = appName(mcpServer?.server.getClientVersion());
|
|
37
|
+
const pending = startLoginServer(app ? { openedBy: { app } } : {}).then((server) => {
|
|
35
38
|
server.result.finally(() => {
|
|
36
39
|
if (loginServerPromise === pending)
|
|
37
40
|
loginServerPromise = null;
|
|
@@ -170,5 +173,6 @@ export async function runMcpServer(version) {
|
|
|
170
173
|
if (updateChecksEnabled())
|
|
171
174
|
updateCheck = startUpdateCheck();
|
|
172
175
|
const server = createWilmaMcpServer(version);
|
|
176
|
+
mcpServer = server;
|
|
173
177
|
await server.connect(new StdioServerTransport());
|
|
174
178
|
}
|
package/dist/update-check.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export declare function installKind(env?: NodeJS.ProcessEnv, scriptPath?: string
|
|
|
8
8
|
export declare function updateChecksEnabled(env?: NodeJS.ProcessEnv): boolean;
|
|
9
9
|
export declare function isNewerVersion(latest: string, current: string): boolean;
|
|
10
10
|
/** The CLI's notice, for a person or an agent reading stderr. */
|
|
11
|
+
/** Release notes: a wilm.ai redirect, so the target can move without a CLI release. */
|
|
12
|
+
export declare const CHANGES_URL = "https://wilm.ai/changes";
|
|
11
13
|
export declare function cliUpdateNotice(current: string, latest: string): string;
|
|
12
14
|
/** The note the MCP server adds for the assistant, who passes it on. */
|
|
13
15
|
export declare function mcpUpdateNote(kind: InstallKind, current: string, latest: string): string;
|
|
@@ -23,5 +25,11 @@ export interface UpdateCheck {
|
|
|
23
25
|
* a timeout on every command.
|
|
24
26
|
*/
|
|
25
27
|
export declare function startUpdateCheck(): UpdateCheck;
|
|
28
|
+
/**
|
|
29
|
+
* Whether to show the CLI's notice for `latest` now, and if so, record it: at
|
|
30
|
+
* most once a day, and right away for a version not announced before. An
|
|
31
|
+
* agent's morning run of ten commands should see it once, not ten times.
|
|
32
|
+
*/
|
|
33
|
+
export declare function claimNotice(latest: string, now?: number): Promise<boolean>;
|
|
26
34
|
/** The latest version if the check has an answer within `waitMs`, else null. */
|
|
27
35
|
export declare function latestWithin(check: UpdateCheck, waitMs: number): Promise<string | null>;
|
package/dist/update-check.js
CHANGED
|
@@ -3,10 +3,10 @@ import { dirname, resolve, sep } from "node:path";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { getConfigPath } from "./config.js";
|
|
5
5
|
/*
|
|
6
|
-
* "A newer WilmAI is available." The CLI prints it on stderr
|
|
7
|
-
* terminal and agents like OpenClaw both read it there); the MCP
|
|
8
|
-
* once to a tool result so the assistant can tell the user. How
|
|
9
|
-
* depends on how WilmAI was installed.
|
|
6
|
+
* "A newer WilmAI is available." The CLI prints it on stderr at most once a day
|
|
7
|
+
* (people in a terminal and agents like OpenClaw both read it there); the MCP
|
|
8
|
+
* server adds it once to a tool result so the assistant can tell the user. How
|
|
9
|
+
* to update depends on how WilmAI was installed.
|
|
10
10
|
*/
|
|
11
11
|
const VERSION_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
12
12
|
export function installKind(env = process.env, scriptPath = fileURLToPath(import.meta.url)) {
|
|
@@ -34,15 +34,17 @@ export function isNewerVersion(latest, current) {
|
|
|
34
34
|
return false;
|
|
35
35
|
}
|
|
36
36
|
/** The CLI's notice, for a person or an agent reading stderr. */
|
|
37
|
+
/** Release notes: a wilm.ai redirect, so the target can move without a CLI release. */
|
|
38
|
+
export const CHANGES_URL = "https://wilm.ai/changes";
|
|
37
39
|
export function cliUpdateNotice(current, latest) {
|
|
38
|
-
return `Update available: ${current} → ${latest}\nRun "wilma update" to update.`;
|
|
40
|
+
return `Update available: ${current} → ${latest}\nWhat's new: ${CHANGES_URL}\nRun "wilma update" to update.`;
|
|
39
41
|
}
|
|
40
42
|
/** The note the MCP server adds for the assistant, who passes it on. */
|
|
41
43
|
export function mcpUpdateNote(kind, current, latest) {
|
|
42
44
|
const how = kind === "claude-desktop"
|
|
43
45
|
? "download https://wilm.ai/get/claude and open the file; Claude Desktop asks to install it"
|
|
44
46
|
: 'run "wilma update" in a terminal';
|
|
45
|
-
return `Update note: WilmAI ${latest} is available (this computer has ${current}). After answering, tell the user in one sentence that they can update: ${how}.`;
|
|
47
|
+
return `Update note: WilmAI ${latest} is available (this computer has ${current}; what's new: ${CHANGES_URL}). After answering, tell the user in one sentence that they can update: ${how}.`;
|
|
46
48
|
}
|
|
47
49
|
function versionCachePath() {
|
|
48
50
|
return resolve(dirname(getConfigPath()), "version-check.json");
|
|
@@ -78,13 +80,13 @@ export function startUpdateCheck() {
|
|
|
78
80
|
const response = await fetch("https://registry.npmjs.org/@wilm-ai/wilma-cli/latest", { signal: controller.signal });
|
|
79
81
|
const data = response.ok ? (await response.json()) : {};
|
|
80
82
|
const latestVersion = data.version ?? cache?.latestVersion ?? null;
|
|
81
|
-
await writeVersionCache({ latestVersion, checkedAt: Date.now() });
|
|
83
|
+
await writeVersionCache({ ...cache, latestVersion, checkedAt: Date.now() });
|
|
82
84
|
return latestVersion;
|
|
83
85
|
}
|
|
84
86
|
catch {
|
|
85
87
|
// Cancelled because the command finished: try again next time.
|
|
86
88
|
if (!cancelled)
|
|
87
|
-
await writeVersionCache({ latestVersion: cache?.latestVersion ?? null, checkedAt: Date.now() }).catch(() => { });
|
|
89
|
+
await writeVersionCache({ ...cache, latestVersion: cache?.latestVersion ?? null, checkedAt: Date.now() }).catch(() => { });
|
|
88
90
|
return cache?.latestVersion ?? null;
|
|
89
91
|
}
|
|
90
92
|
finally {
|
|
@@ -99,6 +101,23 @@ export function startUpdateCheck() {
|
|
|
99
101
|
},
|
|
100
102
|
};
|
|
101
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Whether to show the CLI's notice for `latest` now, and if so, record it: at
|
|
106
|
+
* most once a day, and right away for a version not announced before. An
|
|
107
|
+
* agent's morning run of ten commands should see it once, not ten times.
|
|
108
|
+
*/
|
|
109
|
+
export async function claimNotice(latest, now = Date.now()) {
|
|
110
|
+
const cache = await readVersionCache();
|
|
111
|
+
if (cache?.notifiedVersion === latest && now - (cache.notifiedAt ?? 0) < VERSION_CHECK_INTERVAL_MS)
|
|
112
|
+
return false;
|
|
113
|
+
await writeVersionCache({
|
|
114
|
+
latestVersion: cache?.latestVersion ?? latest,
|
|
115
|
+
checkedAt: cache?.checkedAt ?? now,
|
|
116
|
+
notifiedVersion: latest,
|
|
117
|
+
notifiedAt: now,
|
|
118
|
+
});
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
102
121
|
/** The latest version if the check has an answer within `waitMs`, else null. */
|
|
103
122
|
export async function latestWithin(check, waitMs) {
|
|
104
123
|
let timer;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wilm-ai/wilma-cli",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.2",
|
|
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": {
|