@filamind-app/core 0.1.4 → 0.1.6

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,40 +1,68 @@
1
+ <div align="center">
2
+
1
3
  # @filamind-app/core
2
4
 
3
- The shared foundation of the FilaMind suite **framework-agnostic TypeScript** consumed by FilaMind 3d
4
- (web), FilaMind screen (touch), and FilaMind flow. Phase 0.
5
+ The shared foundation of the FilaMind suite - one framework-agnostic TypeScript library
6
+ that talks to the printer, holds its state honestly, and looks the same everywhere.
7
+
8
+ **Built by Egyptian makers, for world makers. Happy printing.** 🇪🇬
9
+
10
+ A small-team hobby project, built and tested on real printers. The code is all here to read.
11
+
12
+ [![Support on Ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I119XEIV)
5
13
 
6
14
  [![CI](https://github.com/filamind-app/filamind-core/actions/workflows/ci.yml/badge.svg)](https://github.com/filamind-app/filamind-core/actions/workflows/ci.yml)
7
15
  [![npm](https://img.shields.io/npm/v/@filamind-app/core?color=111111&label=npm)](https://www.npmjs.com/package/@filamind-app/core)
8
16
  [![License: GPLv3](https://img.shields.io/badge/License-GPLv3-111111.svg)](LICENSE)
9
17
  [![TypeScript](https://img.shields.io/badge/TypeScript-strict-111111?logo=typescript&logoColor=white)](https://www.typescriptlang.org)
18
+ [![Klipper](https://img.shields.io/badge/Klipper-compatible-111111)](https://www.klipper3d.org)
19
+ [![Moonraker](https://img.shields.io/badge/Moonraker-API-111111)](https://moonraker.readthedocs.io)
20
+
21
+ [Install](#install) · [What's inside](#whats-inside) · [Quick start](#quick-start) · [Tested on real printers](#tested-on-real-printers) · [Languages](#languages) · [Docs](#documentation) · [Support](#support)
10
22
 
11
- ## What's here
12
- | Module | Purpose |
23
+ </div>
24
+
25
+ `@filamind-app/core` is the shared core of the FilaMind suite. It is the one place that knows how
26
+ to open a Moonraker connection, keep printer state honest, remember a user's settings, theme the UI,
27
+ and translate it - so the three FilaMind surfaces (**3d** on the web, **screen** on a touch panel,
28
+ and the **flow** touch app) all behave the same way without copying that logic three times. It is
29
+ pure TypeScript with no UI framework inside: the reactive layer is a plain observable, and each
30
+ surface adapts it to Vue, signals, or whatever it uses. One core, many faces.
31
+
32
+ ## Install
33
+
34
+ It is published to npm as `@filamind-app/core`:
35
+
36
+ ```bash
37
+ npm install @filamind-app/core
38
+ ```
39
+
40
+ The package is ESM-only and ships its own type declarations. It has **zero runtime dependencies** -
41
+ nothing is pulled in behind it. Inside the FilaMind suite it is consumed through the workspace during
42
+ development, so changes to the core are picked up by 3d, screen, and flow without a publish step.
43
+
44
+ ## What's inside
45
+
46
+ | Piece | What it gives you |
13
47
  | --- | --- |
14
- | `provenance.ts` | `Stamped<T>` "never-lie" values (`value · ts · source · staleAfter`) + `isStale` / `freshness` |
15
- | `state/observable.ts` | a tiny framework-agnostic observable (consumers bind it to Pinia/signals) |
16
- | `state/printer.ts` | `PrinterState` merge-patch from `notify_status_update`, **1 s coalescing + `motion_report` fast-path** |
17
- | `moonraker/connector.ts` | the backend-agnostic `Connector` seam (Moonraker first; remote/others later) |
18
- | `moonraker/client.ts` | `MoonrakerClient` reconnecting JSON-RPC WS (**injectable socket**, backoff **+ jitter + max-attempts**), id-correlated, notify fan-out, re-subscribe on reconnect, **REST file channel** (`upload`/`download`) |
19
- | `moonraker/discovery.ts` | **zero-config endpoint discovery** `resolveMoonrakerUrl()` races candidate endpoints (same-origin proxy / direct `:7125` / localhost) first to open wins; runtime `override` short-circuits. Socket-injectable |
20
- | `moonraker/subscriptions.ts` | the **versioned** canonical subscription contract `NARROW_STATUS` / `FULL_CONTROL` tiers + `tier()` |
21
- | `moonraker/rpc-types.ts` | **RPC type safety** `RpcError` (keeps JSON-RPC `code`/`data`) · `MoonrakerMethods` typed-`call` result map · `NotifyEvent` + `parseNotifyEvent()` discriminated notify union |
22
- | `printer/klippy.ts` | the Klippy lifecycle (`ready`/`startup`/`shutdown`/`error`/`disconnected`) tracked apart from the socket so a FIRMWARE_RESTART re-seeds instead of showing stale-as-live |
23
- | `printer/prompt-parser.ts` | parses Klipper's `// action:prompt_*` protocol into structured modal dialogs (`PromptParser`) |
24
- | `session/session.ts` | **`FilaMindSession`** the orchestrator: staged init (identify capabilities query/seed subscribe), routes `notify_*`, owns the Klippy-aware `live`/stale gate |
25
- | `safety/write-arbiter.ts` | fail-closed **`WriteArbiter`** the single chokepoint for every mutation; enforces safe-mode + a caller guard |
26
- | `identity/machine.ts` | `deriveMachineId` → stable `fm-<hash>` from `machine.system_info` (the roaming-settings key) |
27
- | `backup/restore-points.ts` | reversible, retention-bounded **`RestorePoints`** (back-up-first substrate for config/flash/migrate) |
28
- | `observability/logger.ts` | pluggable ring-buffer `Logger` (the diagnostics-bundle source; replaces silent `catch {}`) |
29
- | `registry/widget-registry.ts` | the cross-surface widget/plugin registry + `aggregateSubscriptions` |
30
- | `remote/commands.ts` · `remote/command-sender.ts` | the cross-surface command bus — **UI-only** commands (navigate / message / locate) over a Moonraker **agent** connection. `CommandSender` identifies as an agent, re-identifies on reconnect, is single-flight, and sanitizes control/bidi text; it is never a mutation path (those still go through `WriteArbiter`) |
31
- | `theme/tokens.ts` | the theme palettes as `--fm-*` design tokens — 3 signature Pharaonic themes (Tutankhamun · Horus · Anubis) + neutral **light** / **dark** |
32
- | `i18n/locale-meta.ts` | the 19 shipped locales `{ code, name, rtl, dir }` + RTL list + CLDR `pluralCategory` (`Intl.PluralRules`) |
33
- | `i18n/translator.ts` | framework-agnostic `translate` / `Translator` + the backend message-code contract (`resolveMessage`) |
34
- | `i18n/locales/*.json` | drop-in catalogs (en + ar shipped as proof, incl. the full Arabic plural set) |
35
- | `settings/settings.ts` | unified user settings (theme · locale · density · motif · reduced-motion): `SettingsStore` (patch/hydrate/reset/export/import), **`migrate()`** (versioned, enum-coercing — `import()` trusts no foreign JSON), `applySettings` (re-theme + RTL dir), persistence seam (Moonraker-DB / localStorage) |
36
-
37
- ## Wiring (the consumer entry point)
48
+ | **Moonraker client** | A reconnecting JSON-RPC WebSocket plus a REST file channel, behind a backend-agnostic connector seam. Backs off with jitter, re-subscribes on reconnect, and times out stuck requests. |
49
+ | **Zero-config discovery** | Finds the printer's WebSocket for you by racing the likely endpoints; the first to open wins, and an explicit override always wins first. |
50
+ | **FilaMindSession** | The orchestrator. It connects, identifies, queries, subscribes, and routes every update into one place - and re-seeds itself after a firmware restart so the UI never shows stale data as live. |
51
+ | **Printer state** | One reactive, merge-patched model of the printer, coalesced so the UI isn't thrashed, with a fast path so live motion stays smooth. |
52
+ | **Provenance** | "Never-lie" stamped values that carry where they came from and when, so a widget can dim or strike anything that is stale or unknown. |
53
+ | **WriteArbiter** | A single fail-closed chokepoint every mutation passes through: if state isn't trustworthy, or safe-mode is on, the write is refused. |
54
+ | **Settings store** | One unified, persisted user-settings model (theme, language, density, motifs) that roams across every surface on the same printer, with strict versioned migration. |
55
+ | **Theme tokens** | The FilaMind palette as `--fm-*` CSS variables - three signature Pharaonic themes plus a neutral light and dark - so one switch restyles everything. |
56
+ | **i18n** | Metadata for 19 locales (including right-to-left), correct plural rules, and a tiny framework-agnostic translator that 3d, screen, and flow all share. |
57
+ | **Widget registry & dashboard** | Features register themselves without touching the core, declare which surfaces they target, and resolve into one adaptive dashboard per screen size. |
58
+ | **Remote control bus** | A UI-only command bus so one FilaMind surface can steer another (navigate, message, locate). It can never move the printer - those still go through the WriteArbiter. |
59
+ | **Backup, identity, logging** | Reversible restore points, a stable per-machine id, and a ring-buffer logger that feeds the diagnostics bundle. |
60
+
61
+ The full public surface is exported from a single entry point; the details of how each piece works
62
+ live in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
63
+
64
+ ## Quick start
65
+
38
66
  ```ts
39
67
  import { MoonrakerClient, FilaMindSession, FULL_CONTROL } from '@filamind-app/core'
40
68
 
@@ -45,33 +73,100 @@ const session = new FilaMindSession(connector, {
45
73
  })
46
74
  await session.start()
47
75
 
48
- // reactive, framework-agnostic bind these to Pinia/signals/etc.
49
- session.live.subscribe((v) => {/* dim the UI when not trustworthy-live */})
50
- session.printer.objects.subscribe((o) => {/* render telemetry */})
51
- session.prompt.subscribe((ev) => {/* show a Klipper modal */})
76
+ // reactive and framework-agnostic - bind these to Pinia, signals, or anything:
77
+ session.live.subscribe((live) => {/* dim the UI when the data isn't trustworthy-live */})
78
+ session.printer.objects.subscribe((objects) => {/* render telemetry */})
79
+ session.prompt.subscribe((dialog) => {/* show a Klipper modal */})
80
+ ```
81
+
82
+ Don't know the printer's address? Let the core find it:
83
+
84
+ ```ts
85
+ import { resolveMoonrakerUrl, MoonrakerClient } from '@filamind-app/core'
86
+
87
+ const url = await resolveMoonrakerUrl() // races the likely endpoints, first to open wins
88
+ const connector = new MoonrakerClient({ url })
52
89
  ```
53
90
 
54
- ## Scripts
91
+ There is a worked example for settings, theming, and adding a widget in
92
+ [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md#using-the-core).
93
+
94
+ ## Tested on real printers
95
+
96
+ The core is exercised by the FilaMind suite on two machines that disagree on almost everything that
97
+ matters to a control surface. The first is a Sovol SV08: an STM32F103 mainboard, TMC2209 drivers
98
+ over UART, a USB toolhead, and a BTT CB1 host. The second is a Voron-class CoreXY: an STM32H723
99
+ mainboard, six TMC5160 drivers on a shared software-SPI bus, a CAN toolhead, and a Raspberry Pi 4.
100
+ The pieces here that hurt to get wrong - the reconnect and re-seed logic, the Klippy-aware live gate,
101
+ the fail-closed write path - are the ones those two printers stress, and the library ships **101
102
+ pure-logic tests** that pin that behaviour down without needing a printer attached.
103
+
104
+ ## How it's built
105
+
106
+ Pure TypeScript in `strict` mode, no UI framework imported anywhere in the core. Reactivity is a
107
+ small hand-written observable that each surface adapts to its own framework. Everything that touches
108
+ the network - the WebSocket, the discovery probes - is injectable, so the whole library is unit-tested
109
+ without a real socket. It builds to a single ESM bundle with type declarations via
110
+ [tsup](https://tsup.egoist.dev), and a CI guard keeps the published code free of any reference to
111
+ other projects. The deeper design is in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
112
+
113
+ ## Languages
114
+
115
+ The core carries the suite's shared internationalization foundation: metadata for **19 locales**,
116
+ the correct CLDR plural rules for each, and a tiny translator that 3d, screen, and flow all use so
117
+ they translate identically. The catalogs themselves are drop-in JSON, one folder per locale - adding
118
+ a language needs no code change. English and Arabic (right-to-left, with the full Arabic plural set)
119
+ ship in the package as a working proof; the surfaces carry the rest.
120
+
121
+ | | | | |
122
+ | --- | --- | --- | --- |
123
+ | English | Español | 简体中文 (Simplified Chinese) | 日本語 (Japanese) |
124
+ | العربية (Arabic, RTL) | Français | 繁體中文 (Traditional Chinese) | 한국어 (Korean) |
125
+ | Deutsch (German) | Русский (Russian) | Português (Brasil) | Tiếng Việt (Vietnamese) |
126
+ | Italiano | Nederlands (Dutch) | Polski (Polish) | Bahasa Indonesia |
127
+ | Türkçe (Turkish) | Українська (Ukrainian) | हिन्दी (Hindi) | |
128
+
129
+ ## Develop
130
+
55
131
  ```bash
56
132
  npm install
57
- npm run type-check # tsc --noEmit
58
- npm test # vitest (76 pure-logic tests)
59
- npm run build # tsup dist/ (ESM + .d.ts)
133
+ npm run type-check # tsc --noEmit (strict)
134
+ npm test # vitest run (101 tests, 13 files)
135
+ npm run build # tsup > dist/ (ESM + .d.ts)
60
136
  ```
61
137
 
62
- ## Notes
63
- - Consumed by the suite apps (FilaMind 3d / screen / flow) via the workspace in development.
64
- - Framework-agnostic on purpose: no Vue/React import in core. The reactive layer is a plain `Observable`;
65
- Vue/Pinia adapts it on the consumer side.
66
- - Versions are pinned to current real latest-stable; bump deliberately at first real install.
67
- - GPL-3.0-or-later. FilaMind's own — no third-party project named (R1).
138
+ `npm run dev` rebuilds on change. CI runs the type check, the tests, the build, and the
139
+ no-external-references guard on every push and pull request. Releases are cut by bumping the
140
+ version and pushing a matching `vX.Y.Z` tag; the workflow builds, publishes to npm (with provenance,
141
+ when a token is configured), and creates a GitHub Release from the changelog. The full contributor
142
+ and release flow is in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md#building-testing-and-releasing).
143
+
144
+ ## Documentation
145
+
146
+ | Document | What's inside |
147
+ | -------- | ------------- |
148
+ | [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | Design, key modules, data flow, build/release internals, and conventions |
149
+ | [CHANGELOG.md](CHANGELOG.md) | Release history |
150
+ | [SECURITY.md](SECURITY.md) | How to report a vulnerability privately |
151
+
152
+ ## Support
153
+
154
+ `@filamind-app/core` is free and open source, built and maintained in spare time. If the suite it
155
+ powers saved you a tuning session, or you just want to see it grow, a coffee helps keep the work
156
+ going. Code, data, and ideas are just as welcome.
157
+
158
+ <div align="center">
159
+
160
+ [![Support on Ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I119XEIV)
161
+
162
+ </div>
68
163
 
69
164
  ## Credits
70
165
 
71
- Built and maintained by the DeltaFabs team:
166
+ Built and maintained by the DeltaFabs team. **Built by Egyptian makers, for world makers.**
72
167
 
73
- - abdelmonem awad - <eg2@live.com>
74
- - Ahmed bebars - <Ahmedbebars1@gmail.com>
168
+ - Abdelmonem Awad - <eg2@live.com>
169
+ - Ahmed Bebars - <Ahmedbebars1@gmail.com>
75
170
  - Kareem Salama - <Golden.kiko@gmail.com>
76
171
 
77
172
  ## License
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  type Source = 'live' | 'cache' | 'optimistic' | 'unknown';
2
2
  interface Stamped<T> {
3
3
  value: T;
4
- /** when this value arrived (ms epoch, client clock Moonraker has no server timestamp) */
4
+ /** when this value arrived (ms epoch, client clock - Moonraker has no server timestamp) */
5
5
  ts: number;
6
6
  source: Source;
7
7
  /** ms after `ts` when the value should be treated as stale (omit = never goes stale on its own) */
@@ -169,7 +169,7 @@ type NotifyEvent = {
169
169
  /** Parse a raw (method, params) notify into a typed event, or null for an unhandled method. */
170
170
  declare function parseNotifyEvent(method: string, params: unknown): NotifyEvent | null;
171
171
 
172
- /** Minimal WebSocket surface the client uses the DOM WebSocket satisfies it; tests fake it. */
172
+ /** Minimal WebSocket surface the client uses - the DOM WebSocket satisfies it; tests fake it. */
173
173
  interface WebSocketLike {
174
174
  send(data: string): void;
175
175
  close(): void;
@@ -185,7 +185,7 @@ interface MoonrakerClientOptions {
185
185
  url: string;
186
186
  requestTimeoutMs?: number;
187
187
  maxBackoffMs?: number;
188
- /** stop reconnecting after this many consecutive attempts terminal 'closed' (default: unlimited) */
188
+ /** stop reconnecting after this many consecutive attempts > terminal 'closed' (default: unlimited) */
189
189
  maxReconnectAttempts?: number;
190
190
  wsFactory?: (url: string) => WebSocketLike;
191
191
  logger?: Logger;
@@ -223,13 +223,13 @@ declare class MoonrakerClient implements Connector {
223
223
  private onMessage;
224
224
  }
225
225
 
226
- /** A minimal view of `location` the DOM Location satisfies it; tests pass a plain object. */
226
+ /** A minimal view of `location` - the DOM Location satisfies it; tests pass a plain object. */
227
227
  interface LocationLike {
228
228
  protocol?: string;
229
229
  hostname?: string;
230
230
  }
231
231
  interface DiscoveryOptions {
232
- /** Explicit runtime override (e.g. from settings). If set, it wins immediately no probing. */
232
+ /** Explicit runtime override (e.g. from settings). If set, it wins immediately - no probing. */
233
233
  override?: string;
234
234
  /** Candidate ws(s):// URLs to race. If omitted, they are derived from `location`. */
235
235
  candidates?: string[];
@@ -262,7 +262,7 @@ declare const FULL_CONTROL: SubscriptionMap;
262
262
  type SubscriptionTier = 'narrow' | 'full';
263
263
  declare function tier(t: SubscriptionTier): SubscriptionMap;
264
264
  /** Union of subscription maps (`null` = "all fields" wins). The single source for the
265
- * null-wins merge rule consumers compose a baseline tier with widget-declared needs. */
265
+ * null-wins merge rule - consumers compose a baseline tier with widget-declared needs. */
266
266
  declare function mergeSubscriptions(...maps: SubscriptionMap[]): SubscriptionMap;
267
267
 
268
268
  type KlippyState = 'ready' | 'startup' | 'shutdown' | 'error' | 'disconnected';
@@ -344,7 +344,7 @@ interface SessionOptions {
344
344
  subscriptions?: SubscriptionMap;
345
345
  identify?: IdentifyInfo;
346
346
  logger?: Logger;
347
- /** Receive cross-surface agent events (notify_agent_event) e.g. remote-control commands from
347
+ /** Receive cross-surface agent events (notify_agent_event) - e.g. remote-control commands from
348
348
  * another FilaMind surface. Discrete (fired per event, never replayed), so no stale re-fire. */
349
349
  onAgentEvent?: (ev: AgentEvent) => void;
350
350
  }
@@ -366,7 +366,7 @@ declare class FilaMindSession {
366
366
  setSubscriptions(subs: SubscriptionMap): void;
367
367
  start(): Promise<void>;
368
368
  stop(): void;
369
- /** Staged init runs on first connect AND on reconnect AND on notify_klippy_ready.
369
+ /** Staged init - runs on first connect AND on reconnect AND on notify_klippy_ready.
370
370
  * Guarded so overlapping triggers (reconnect + klippy_ready) can't interleave two seeds. */
371
371
  private bootstrap;
372
372
  private onUpdate;
@@ -396,7 +396,7 @@ declare class WriteArbiter {
396
396
  run<T>(action: string, fn: () => Promise<T>): Promise<T>;
397
397
  }
398
398
 
399
- /** FNV-1a 32-bit hex deterministic, dependency-free. */
399
+ /** FNV-1a 32-bit hex - deterministic, dependency-free. */
400
400
  declare function fnv1a(s: string): string;
401
401
  /** Derive a stable `fm-<hash>` machine id from Moonraker. Falls back to `fm-unknown`. */
402
402
  declare function deriveMachineId(connector: Connector): Promise<string>;
@@ -438,7 +438,7 @@ interface WidgetDefinition {
438
438
  title: string;
439
439
  icon?: string;
440
440
  description?: string;
441
- /** lazy component loader the host framework (Vue) resolves it (kept as `unknown` here) */
441
+ /** lazy component loader - the host framework (Vue) resolves it (kept as `unknown` here) */
442
442
  component: () => Promise<unknown>;
443
443
  defaultSize?: {
444
444
  w: number;
@@ -454,7 +454,7 @@ declare function getWidget(id: string): WidgetDefinition | undefined;
454
454
  declare function getWidgets(target?: SurfaceTarget): WidgetDefinition[];
455
455
  /** The union of the given widgets' declared subscriptions (subscribe once). */
456
456
  declare function aggregateSubscriptions(ids: string[]): SubscriptionMap;
457
- /** Test/host helper clear the registry. */
457
+ /** Test/host helper - clear the registry. */
458
458
  declare function _resetRegistry(): void;
459
459
 
460
460
  type SurfaceHint = 'dense-desktop' | 'thumb-phone' | 'big-touch';
@@ -562,14 +562,14 @@ interface BackendMessage {
562
562
  declare function resolveMessage(catalog: Catalog, m: BackendMessage, locale?: string): string;
563
563
 
564
564
  interface CommandSenderOptions {
565
- /** Stable agent name (pinned a drifting name registers phantom duplicates on reconnect). */
565
+ /** Stable agent name (pinned - a drifting name registers phantom duplicates on reconnect). */
566
566
  client_name: string;
567
567
  version: string;
568
568
  /** Required: Moonraker documents `url` as mandatory for identify; omitting it can make a strict
569
569
  * host reject the agent identify, silently stranding the bus. */
570
570
  url: string;
571
571
  logger?: Logger;
572
- /** Fires whenever readiness flips (connected+identified) lets a UI gate its send affordances. */
572
+ /** Fires whenever readiness flips (connected+identified) - lets a UI gate its send affordances. */
573
573
  onReadyChange?: (ready: boolean) => void;
574
574
  }
575
575
  declare class CommandSender {
@@ -582,14 +582,14 @@ declare class CommandSender {
582
582
  private retryDelayMs;
583
583
  private readonly log;
584
584
  constructor(connector: Connector, opts: CommandSenderOptions);
585
- /** true only when the bus is connected AND identified gate remote-control affordances on this. */
585
+ /** true only when the bus is connected AND identified - gate remote-control affordances on this. */
586
586
  get ready(): boolean;
587
587
  /** Open the agent connection and identify once. Call alongside the app's main session start. */
588
588
  start(): Promise<void>;
589
589
  /** Close the agent connection (call on app teardown so the socket doesn't linger). */
590
590
  stop(): void;
591
591
  /** Broadcast a UI-only command to the other FilaMind surfaces. Best-effort: a command issued
592
- * while the bus is down is dropped (logged), never queued/replayed a stale "navigate" fired
592
+ * while the bus is down is dropped (logged), never queued/replayed - a stale "navigate" fired
593
593
  * minutes later would yank a screen unexpectedly. */
594
594
  send(cmd: RemoteCommand): Promise<void>;
595
595
  navigate(view: RemoteView): Promise<void>;
@@ -622,10 +622,10 @@ interface UserSettings {
622
622
  dashboardLayout?: DashboardLayout;
623
623
  }
624
624
  declare const DEFAULT_SETTINGS: UserSettings;
625
- /** Coerce an arbitrary (old / foreign / partial) blob into valid UserSettings drops unknown keys,
625
+ /** Coerce an arbitrary (old / foreign / partial) blob into valid UserSettings - drops unknown keys,
626
626
  * validates every enum, stamps the current version. Used by hydrate() + import(). */
627
627
  declare function migrate(raw: unknown): UserSettings;
628
- /** App-provided persistence the Moonraker-DB impl is keyed by machineUUID; tests use memory. */
628
+ /** App-provided persistence - the Moonraker-DB impl is keyed by machineUUID; tests use memory. */
629
629
  interface SettingsPersistence {
630
630
  load(): Promise<Partial<UserSettings>>;
631
631
  save(settings: UserSettings): Promise<void>;
@@ -633,7 +633,7 @@ interface SettingsPersistence {
633
633
  declare function memoryPersistence(): SettingsPersistence;
634
634
  declare function localStoragePersistence(storageKey?: string): SettingsPersistence;
635
635
  /** Settings persisted in the printer's Moonraker database, so they roam across every FilaMind
636
- * surface on that printer (3d, screen, …) the F10 remote-config substrate. */
636
+ * surface on that printer (3d, screen, …) - the F10 remote-config substrate. */
637
637
  declare function moonrakerDbPersistence(connector: Connector): SettingsPersistence;
638
638
  /** Roam a (locally-persisted) SettingsStore across surfaces via a shared remote store: pull the
639
639
  * shared settings whenever the connection goes live (the shared copy wins on connect), and
package/dist/index.js CHANGED
@@ -291,7 +291,8 @@ var MoonrakerClient = class {
291
291
  if (!p) return;
292
292
  clearTimeout(p.timer);
293
293
  this.pending.delete(msg.id);
294
- if (msg.error) p.reject(new RpcError(msg.error.message ?? "rpc error", msg.error.code, msg.error.data));
294
+ if (msg.error)
295
+ p.reject(new RpcError(msg.error.message ?? "rpc error", msg.error.code, msg.error.data));
295
296
  else p.resolve(msg.result);
296
297
  return;
297
298
  }
@@ -617,18 +618,18 @@ var FilaMindSession = class {
617
618
  this.connector.close();
618
619
  this.setLive(false);
619
620
  }
620
- /** Staged init runs on first connect AND on reconnect AND on notify_klippy_ready.
621
+ /** Staged init - runs on first connect AND on reconnect AND on notify_klippy_ready.
621
622
  * Guarded so overlapping triggers (reconnect + klippy_ready) can't interleave two seeds. */
622
623
  async bootstrap() {
623
624
  if (this.bootstrapping) return;
624
625
  this.bootstrapping = true;
625
626
  try {
626
627
  if (this.opts.identify) {
627
- await this.connector.call("server.connection.identify", { ...this.opts.identify }).catch(
628
- (e) => this.log.warn("identify failed", String(e))
629
- );
628
+ await this.connector.call("server.connection.identify", { ...this.opts.identify }).catch((e) => this.log.warn("identify failed", String(e)));
630
629
  }
631
- const info = await this.connector.call("server.info");
630
+ const info = await this.connector.call(
631
+ "server.info"
632
+ );
632
633
  this.capabilities.set(info?.components ?? []);
633
634
  const ks = deriveKlippyState(info?.klippy_state);
634
635
  this.klippy.set(ks);
@@ -754,7 +755,10 @@ function memoryRestoreStore() {
754
755
  byMachine.set(m, arr);
755
756
  },
756
757
  remove: async (m, id) => {
757
- byMachine.set(m, (byMachine.get(m) ?? []).filter((p) => p.id !== id));
758
+ byMachine.set(
759
+ m,
760
+ (byMachine.get(m) ?? []).filter((p) => p.id !== id)
761
+ );
758
762
  }
759
763
  };
760
764
  }
@@ -854,7 +858,9 @@ function resolveDashboard(layout, available, hint) {
854
858
  );
855
859
  const used = /* @__PURE__ */ new Set();
856
860
  const widgets = [];
857
- for (const slot of [...slots].filter((s) => !(s.hideOn ?? []).includes(hint)).sort((a, b) => (a.order ?? Number.POSITIVE_INFINITY) - (b.order ?? Number.POSITIVE_INFINITY))) {
861
+ for (const slot of [...slots].filter((s) => !(s.hideOn ?? []).includes(hint)).sort(
862
+ (a, b) => (a.order ?? Number.POSITIVE_INFINITY) - (b.order ?? Number.POSITIVE_INFINITY)
863
+ )) {
858
864
  const w = byId.get(slot.widgetId);
859
865
  if (w && !used.has(w.id)) {
860
866
  widgets.push(w);
@@ -888,7 +894,7 @@ function coerceDashboardLayout(raw) {
888
894
 
889
895
  // src/theme/tokens.ts
890
896
  var themes = {
891
- // flagship default obsidian + gold + lapis + turquoise
897
+ // flagship default - obsidian + gold + lapis + turquoise
892
898
  tutankhamun: {
893
899
  bg: "#0E0F12",
894
900
  surface: "#17171C",
@@ -936,7 +942,7 @@ var themes = {
936
942
  warning: "#D98E2B",
937
943
  danger: "#8C1F1A"
938
944
  },
939
- // neutral light a conventional bright UI for users who prefer it
945
+ // neutral light - a conventional bright UI for users who prefer it
940
946
  light: {
941
947
  bg: "#F7F8FA",
942
948
  surface: "#FFFFFF",
@@ -952,7 +958,7 @@ var themes = {
952
958
  warning: "#B45309",
953
959
  danger: "#DC2626"
954
960
  },
955
- // neutral dark a conventional dark UI, distinct from the warm Pharaonic palettes
961
+ // neutral dark - a conventional dark UI, distinct from the warm Pharaonic palettes
956
962
  dark: {
957
963
  bg: "#0F1115",
958
964
  surface: "#171A21",
@@ -1068,7 +1074,7 @@ var CommandSender = class {
1068
1074
  this.opts = opts;
1069
1075
  this.log = opts.logger ?? NULL_LOGGER;
1070
1076
  connector.setCallbacks({
1071
- // Moonraker forgets the agent identity on disconnect re-identify on every genuine reconnect.
1077
+ // Moonraker forgets the agent identity on disconnect > re-identify on every genuine reconnect.
1072
1078
  onReconnected: () => {
1073
1079
  this.setIdentified(false);
1074
1080
  void this.ensureIdentified().catch(() => this.scheduleRetry());
@@ -1088,7 +1094,7 @@ var CommandSender = class {
1088
1094
  retryTimer;
1089
1095
  retryDelayMs = 500;
1090
1096
  log;
1091
- /** true only when the bus is connected AND identified gate remote-control affordances on this. */
1097
+ /** true only when the bus is connected AND identified - gate remote-control affordances on this. */
1092
1098
  get ready() {
1093
1099
  return this._ready;
1094
1100
  }
@@ -1104,7 +1110,7 @@ var CommandSender = class {
1104
1110
  this.setIdentified(false);
1105
1111
  }
1106
1112
  /** Broadcast a UI-only command to the other FilaMind surfaces. Best-effort: a command issued
1107
- * while the bus is down is dropped (logged), never queued/replayed a stale "navigate" fired
1113
+ * while the bus is down is dropped (logged), never queued/replayed - a stale "navigate" fired
1108
1114
  * minutes later would yank a screen unexpectedly. */
1109
1115
  async send(cmd) {
1110
1116
  if (this.connector.state !== "ready") {
@@ -1113,7 +1119,10 @@ var CommandSender = class {
1113
1119
  }
1114
1120
  try {
1115
1121
  await this.ensureIdentified();
1116
- await this.connector.call("connection.send_event", { event: FILAMIND_COMMAND_EVENT, data: cmd });
1122
+ await this.connector.call("connection.send_event", {
1123
+ event: FILAMIND_COMMAND_EVENT,
1124
+ data: cmd
1125
+ });
1117
1126
  } catch (e) {
1118
1127
  this.log.warn("command send failed", String(e));
1119
1128
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@filamind-app/core",
3
- "version": "0.1.4",
4
- "description": "Shared foundation for the FilaMind suite Moonraker client, reactive printer state, provenance-stamped values, design tokens, and the widget/plugin registry. Framework-agnostic TypeScript.",
3
+ "version": "0.1.6",
4
+ "description": "Shared foundation for the FilaMind suite - Moonraker client, reactive printer state, provenance-stamped values, design tokens, and the widget/plugin registry. Framework-agnostic TypeScript.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
7
7
  "type": "git",
@@ -33,11 +33,14 @@
33
33
  "dev": "tsup src/index.ts --format esm --dts --watch",
34
34
  "type-check": "tsc --noEmit",
35
35
  "test": "vitest run",
36
- "prepublishOnly": "npm run build"
36
+ "prepublishOnly": "npm run build",
37
+ "format": "prettier --write \"src/**/*.ts\" \"*.{ts,js,json}\"",
38
+ "format:check": "prettier --check \"src/**/*.ts\" \"*.{ts,js,json}\""
37
39
  },
38
40
  "devDependencies": {
41
+ "prettier": "3.9.6",
39
42
  "tsup": "^8.5.1",
40
43
  "typescript": "~6.0.3",
41
- "vitest": "^4.1.9"
44
+ "vitest": "^4.1.10"
42
45
  }
43
46
  }