litura-app 0.1.0 → 0.2.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+
3
+ Litura follows [semantic versioning](https://semver.org). Until 1.0 the minor
4
+ number carries breaking changes.
5
+
6
+ ## Unreleased
7
+
8
+ ## 0.2.0 — 2026-09-06
9
+
10
+ - Settings can ask npm about new versions once a day, off by default. When a newer version is published the header shows a badge linking to this file; the badge links rather than installs, so updating stays a command you run. Left off, Litura opens no connection of its own.
11
+ - Releases are cut by tag, with GitHub Actions running the checks and publishing over OIDC.
12
+
13
+ ## 0.1.0 — 2026-09-06
14
+
15
+ First npm release, published as `litura-app`; the command it installs is `litura`.
16
+
17
+ - Run from any folder with `npx litura-app`. The draft and the style guide are read from the folder Litura starts in, not from its install directory.
18
+ - The browser opens on start, and a taken port falls through to the next free one.
19
+ - `litura --version` and `litura --check-update` report the running and published versions. Litura opens no connection of its own.
package/README.md CHANGED
@@ -21,7 +21,7 @@ Right-click a selection to attach it to the composer, or press `Cmd/Ctrl+K` to a
21
21
 
22
22
  The draft, findings, conversation, and selected model are stored in browser local storage and survive a reload.
23
23
 
24
- The draft is also mirrored to `draft.md` in the folder Litura was started from, so it survives cleared browser storage and opens in any editor. The browser copy stays authoritative while the app runs; if the file changes underneath it, Litura says so and offers to load it rather than merging or overwriting silently. Nothing leaves the machine except what a model request sends to the selected provider.
24
+ The draft is also mirrored to `draft.md` in the folder Litura was started from, so it survives cleared browser storage and opens in any editor. The browser copy stays authoritative while the app runs; if the file changes underneath it, Litura says so and offers to load it rather than merging or overwriting silently. Nothing leaves the machine except what a model request sends to the selected provider — and, if you switch it on in settings, a once-a-day question to the npm registry about new versions.
25
25
 
26
26
 
27
27
  ## Run
@@ -53,7 +53,11 @@ Running `npx litura-app` is the update: for a bare package name npm re-resolves
53
53
  npx litura-app --check-update
54
54
  ```
55
55
 
56
- prints the published version next to the running one. Litura opens no connection on its own — the check happens only when you run it, and nothing else phones home.
56
+ prints the published version next to the running one.
57
+
58
+ Settings has an `Ask npm about new versions` switch, off by default. Turn it on and Litura asks once a day and shows a badge in the header when a newer version is published; the badge links to the changelog rather than installing anything. Left off, Litura opens no connection of its own.
59
+
60
+ Releases are cut by tag: `npm version <patch|minor|major>`, then `git push` and `git push --tags`. GitHub Actions runs the checks, verifies the tag matches `package.json`, and publishes.
57
61
 
58
62
  An update leaves your draft, your `style.md`, and your model settings alone. Stored findings and chat history are dropped only when a release changes their format.
59
63
 
package/SPEC.md CHANGED
@@ -205,6 +205,8 @@ When no model is selected, the review, rewrite, and chat actions re-check Pi sta
205
205
  | Chat history | `localStorage["wa-chat"]`, last 20 turns | Until `Clear` or browser storage is cleared |
206
206
  | Findings | `localStorage["wa-findings"]` | Until a new review, `Clear`, or browser storage is cleared |
207
207
  | Automatic review setting | `localStorage["wa-autoreview"]` | Until browser storage is cleared |
208
+ | Update-check setting | `localStorage["wa-updatecheck"]`, off unless set to `on` | Until browser storage is cleared |
209
+ | Last seen published version | `localStorage["wa-update"]` as `{ latest, at }`, reused for 24 hours | Until browser storage is cleared |
208
210
  | Checked sentences | Browser memory | Until reload or a full review reset |
209
211
  | API credentials | Pi credential storage or environment | Managed by Pi |
210
212
 
@@ -265,6 +267,7 @@ All bodies and non-streaming responses are JSON unless noted.
265
267
  | `GET` | `/style.css`, `/app.js`, `/fonts/*` | Static assets |
266
268
  | `GET` | `/draft` | Current contents of the draft file as `{ text, path }` |
267
269
  | `PUT` | `/draft` | Overwrite the draft file with `{ text }` |
270
+ | `GET` | `/api/version` | `{ name, current }`; with `?check=1` also `latest` from the npm registry, or `error` if it is unreachable |
268
271
  | `GET` | `/api/agent/status` | Providers, models, auth status, and default selection |
269
272
  | `POST` | `/api/agent/credentials` | Save a provider API key through Pi |
270
273
  | `DELETE` | `/api/agent/credentials` | Remove a stored provider credential |
@@ -338,9 +341,17 @@ Each review pass validates diagnostic codes against its assigned levels and chec
338
341
 
339
342
  ## 9. Distribution and updates
340
343
 
341
- Litura is published to npm and run as `npx litura` from the folder holding the draft. For a bare package name npm re-resolves the registry manifest on every run, so starting Litura is the update; a global install (`npm i -g`) shadows that check and has to be updated by hand.
344
+ Litura is published to npm as `litura-app`; the command it installs is `litura`. It runs as `npx litura-app` from the folder holding the draft. For a bare package name npm re-resolves the registry manifest on every run, so starting Litura is the update; a global install (`npm i -g`) shadows that check and has to be updated by hand.
342
345
 
343
- `litura --version` prints the running version. `litura --check-update` prints the published version next to it. Neither the server nor the browser contacts the registry on its own: the check runs only when the command asks for it.
346
+ `litura --version` prints the running version. `litura --check-update` prints the published version next to it, and reports an unpublished package rather than comparing against nothing. Neither flag ranks the two versions: a local build legitimately runs ahead of the registry, and ordering semver correctly would be a dependency.
347
+
348
+ In the browser, `Ask npm about new versions` in settings is off by default and stored in `wa-updatecheck`. Off, the browser calls `/api/version` without `?check=1` and the server contacts nothing. On, it asks at most once every 24 hours, caches the answer in `wa-update`, and shows a header badge linking to the changelog when the published version differs from the running one. Switching it off hides the badge rather than leaving a stale one.
349
+
350
+ ### Releasing
351
+
352
+ A release is a tag. `npm version <patch|minor|major>` refuses a dirty tree, bumps `package.json`, commits, and tags. Push the branch and the tag as two pushes — `git push && git push --tags` — rather than `--follow-tags`: pushing v0.1.0 together with its branch produced a run for the branch only. The tag push triggers `.github/workflows/release.yml`, which runs `npm run check`, verifies the tag matches `package.json`, and publishes with npm trusted publishing (OIDC, no stored token). Every other push runs the checks without publishing. `npm run check` passes without provider credentials: the Pi section asserts the shape of the model list, not its contents.
353
+
354
+ ### What survives
344
355
 
345
356
  | Survives an update | Mechanism |
346
357
  |---|---|
package/index.js CHANGED
@@ -25,6 +25,15 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
25
25
  const MANIFEST = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
26
26
  const VERSION = MANIFEST.version;
27
27
 
28
+ // Ask npm what it publishes. `null` means the package is not published at all
29
+ // — a local build asking about itself — which is not the same as a failure.
30
+ async function latestPublished(name = MANIFEST.name) {
31
+ const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(name)}/latest`);
32
+ if (response.status === 404) return null;
33
+ if (!response.ok) throw new Error(`registry returned ${response.status}`);
34
+ return (await response.json()).version;
35
+ }
36
+
28
37
  // ─── CLI ───────────────────────────────────────────────────────────────────
29
38
  // Both flags run before the bundler and the server: they answer and exit.
30
39
  if (process.argv.includes('--version') || process.argv.includes('-v')) {
@@ -37,18 +46,10 @@ if (process.argv.includes('--version') || process.argv.includes('-v')) {
37
46
  // not something that happens quietly at startup.
38
47
  if (process.argv.includes('--check-update')) {
39
48
  const name = MANIFEST.name;
40
- // A 404 is the registry answering, not failing: an unpublished build asking
41
- // about itself. Say so instead of reporting version "undefined".
42
- const latest = await fetch(`https://registry.npmjs.org/${encodeURIComponent(name)}/latest`)
43
- .then(async response => {
44
- if (response.status === 404) return null;
45
- if (!response.ok) throw new Error(`registry returned ${response.status}`);
46
- return (await response.json()).version;
47
- })
48
- .catch(error => {
49
- console.error(`[update] npm registry unreachable — ${error.message}`);
50
- process.exit(1);
51
- });
49
+ const latest = await latestPublished(name).catch(error => {
50
+ console.error(`[update] npm registry unreachable ${error.message}`);
51
+ process.exit(1);
52
+ });
52
53
  if (latest === null) {
53
54
  console.log(`Litura ${VERSION} — ${name} is not published; nothing to compare against.`);
54
55
  process.exit(0);
@@ -208,6 +209,19 @@ function readBody(req) {
208
209
  const server = http.createServer(async (req, res) => {
209
210
  const url = new URL(req.url, `http://localhost`);
210
211
 
212
+ // Which version is running, and — only when the browser asks with ?check=1,
213
+ // which it does only if the writer turned the check on — which is published.
214
+ // A registry that is down is reported, not retried: this is a nicety.
215
+ if (req.method === 'GET' && url.pathname === '/api/version') {
216
+ const payload = { name: MANIFEST.name, current: VERSION };
217
+ if (url.searchParams.get('check') === '1') {
218
+ try { payload.latest = await latestPublished(); }
219
+ catch (error) { payload.error = error.message; }
220
+ }
221
+ sendJson(res, 200, payload);
222
+ return;
223
+ }
224
+
211
225
  if (req.method === 'GET' && url.pathname === '/api/agent/status') {
212
226
  sendJson(res, 200, await getAgentStatus());
213
227
  return;
package/package.json CHANGED
@@ -1,8 +1,15 @@
1
1
  {
2
2
  "name": "litura-app",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "A local AI-assisted editor for sharper, more human writing",
5
- "keywords": ["writing", "editor", "ai", "prose", "markdown", "local"],
5
+ "keywords": [
6
+ "writing",
7
+ "editor",
8
+ "ai",
9
+ "prose",
10
+ "markdown",
11
+ "local"
12
+ ],
6
13
  "license": "MIT",
7
14
  "author": "Vadim Chirkov",
8
15
  "homepage": "https://github.com/vadimchirkov/litura#readme",
@@ -28,7 +35,8 @@
28
35
  "review-prompt.js",
29
36
  "public",
30
37
  "style.md",
31
- "SPEC.md"
38
+ "SPEC.md",
39
+ "CHANGELOG.md"
32
40
  ],
33
41
  "scripts": {
34
42
  "start": "node index.js",
package/public/app.js CHANGED
@@ -16592,6 +16592,9 @@
16592
16592
  var scoreEl = document.getElementById("style-score");
16593
16593
  var chatPanel = document.getElementById("chat");
16594
16594
  var autoReviewEl = document.getElementById("auto-review");
16595
+ var updateCheckEl = document.getElementById("update-check");
16596
+ var updateBadge = document.getElementById("update-badge");
16597
+ var updateStatus = document.getElementById("update-status");
16595
16598
  var chatHeight = 140;
16596
16599
  var STORAGE_SCHEMA = "1";
16597
16600
  if (localStorage.getItem("wa-schema") !== STORAGE_SCHEMA) {
@@ -17001,6 +17004,38 @@
17001
17004
  autoReviewOn = autoReviewEl.checked;
17002
17005
  localStorage.setItem("wa-autoreview", autoReviewOn ? "on" : "off");
17003
17006
  });
17007
+ var UPDATE_INTERVAL = 24 * 60 * 60 * 1e3;
17008
+ var updateCheckOn = localStorage.getItem("wa-updatecheck") === "on";
17009
+ updateCheckEl.checked = updateCheckOn;
17010
+ function renderUpdate({ current, latest, error }) {
17011
+ const stale = Boolean(latest) && latest !== current;
17012
+ updateBadge.hidden = !stale;
17013
+ if (stale) {
17014
+ updateBadge.textContent = `${latest} available`;
17015
+ updateBadge.title = `Running ${current}. Restart with npx to pick it up; a global install needs npm i -g.`;
17016
+ }
17017
+ updateStatus.textContent = error ? `Running ${current} \u2014 npm could not be reached.` : stale ? `Running ${current}; npm publishes ${latest}.` : latest ? `Running ${current} \u2014 the published version.` : `Running ${current}.`;
17018
+ }
17019
+ async function refreshUpdate({ force = false } = {}) {
17020
+ let cached = null;
17021
+ try {
17022
+ cached = JSON.parse(localStorage.getItem("wa-update") || "null");
17023
+ } catch {
17024
+ }
17025
+ const check = updateCheckOn && (force || !(cached && Date.now() - cached.at < UPDATE_INTERVAL));
17026
+ try {
17027
+ const info = await api(`/api/version${check ? "?check=1" : ""}`);
17028
+ if (info.latest) localStorage.setItem("wa-update", JSON.stringify({ latest: info.latest, at: Date.now() }));
17029
+ renderUpdate({ ...info, latest: info.latest ?? (updateCheckOn ? cached?.latest : void 0) });
17030
+ } catch {
17031
+ }
17032
+ }
17033
+ updateCheckEl.addEventListener("change", () => {
17034
+ updateCheckOn = updateCheckEl.checked;
17035
+ localStorage.setItem("wa-updatecheck", updateCheckOn ? "on" : "off");
17036
+ refreshUpdate({ force: updateCheckOn });
17037
+ });
17038
+ refreshUpdate();
17004
17039
  function syncStyleScore() {
17005
17040
  const text = workView.state.doc.toString();
17006
17041
  const { score, structural } = styleScore(text);
package/public/index.html CHANGED
@@ -20,6 +20,12 @@
20
20
  <span class="wordmark">Litura</span>
21
21
  <span class="style-score" id="style-score" title="Local AI-tell score (0 = clean). Computed in the browser, no model call."></span>
22
22
  <button class="review-button" id="review-button" title="Review writing and structure">Review</button>
23
+ <!-- Only ever shown when the writer opted into the check and a newer
24
+ version exists. It links to the changelog, not to an installer:
25
+ updating is a command the writer runs. -->
26
+ <a class="update-badge" id="update-badge" hidden
27
+ href="https://github.com/vadimchirkov/litura/blob/main/CHANGELOG.md"
28
+ target="_blank" rel="noreferrer"></a>
23
29
  <span class="agent-status" id="agent-status">Pi</span>
24
30
  <button class="settings-open" id="settings-open" title="Model and access" aria-label="Model and access">⚙</button>
25
31
  </div>
@@ -81,6 +87,15 @@
81
87
  </label>
82
88
  </section>
83
89
 
90
+ <section>
91
+ <h3>Updates</h3>
92
+ <label class="settings-check">
93
+ <input type="checkbox" id="update-check">
94
+ <span>Ask npm about new versions<small>Off by default, once a day when on. It is the only request Litura makes that no writing of yours asked for.</small></span>
95
+ </label>
96
+ <p class="settings-hint" id="update-status"></p>
97
+ </section>
98
+
84
99
  <section>
85
100
  <h3>API keys</h3>
86
101
  <div class="key-entry">
package/public/style.css CHANGED
@@ -251,6 +251,21 @@ html, body {
251
251
  .style-score:empty { margin-left: auto; }
252
252
  .style-score.warn { color: var(--accent); }
253
253
 
254
+ /* Available update — quiet on purpose. Nothing is broken, and the writer is
255
+ mid-sentence; this must not compete with the draft. */
256
+ .update-badge {
257
+ padding: 2px 8px;
258
+ border-radius: var(--r-pill);
259
+ background: var(--fill);
260
+ color: var(--text-2);
261
+ font-size: 12px;
262
+ text-decoration: none;
263
+ white-space: nowrap;
264
+ }
265
+
266
+ .update-badge:hover { background: var(--fill-hover); color: var(--text); }
267
+ .update-badge[hidden] { display: none; }
268
+
254
269
  /* Secondary pill button — ChatGPT's action/secondary style */
255
270
  .review-button {
256
271
  display: inline-flex;