@filamind-app/core 0.1.2 → 0.1.4

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
@@ -18,6 +18,7 @@ The shared foundation of the FilaMind suite — **framework-agnostic TypeScript*
18
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
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
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 |
21
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 |
22
23
  | `printer/prompt-parser.ts` | parses Klipper's `// action:prompt_*` protocol into structured modal dialogs (`PromptParser`) |
23
24
  | `session/session.ts` | **`FilaMindSession`** — the orchestrator: staged init (identify → capabilities → query/seed → subscribe), routes `notify_*`, owns the Klippy-aware `live`/stale gate |
package/dist/index.d.ts CHANGED
@@ -90,6 +90,85 @@ declare class Logger {
90
90
  /** A no-op logger for when none is supplied. */
91
91
  declare const NULL_LOGGER: Logger;
92
92
 
93
+ /** A JSON-RPC error that keeps Moonraker's numeric `code` and optional `data` payload. */
94
+ declare class RpcError extends Error {
95
+ readonly code?: number | undefined;
96
+ readonly data?: unknown | undefined;
97
+ constructor(message: string, code?: number | undefined, data?: unknown | undefined);
98
+ }
99
+ interface ServerInfo {
100
+ klippy_connected: boolean;
101
+ klippy_state: 'ready' | 'startup' | 'shutdown' | 'error' | 'disconnected';
102
+ components?: string[];
103
+ warnings?: string[];
104
+ [k: string]: unknown;
105
+ }
106
+ interface PrinterInfo {
107
+ state: 'ready' | 'startup' | 'shutdown' | 'error';
108
+ state_message: string;
109
+ hostname?: string;
110
+ software_version?: string;
111
+ [k: string]: unknown;
112
+ }
113
+ interface QueryResult {
114
+ eventtime: number;
115
+ status: Record<string, unknown>;
116
+ }
117
+ /** Result types for the methods the suite calls most. Unlisted methods still work via `call<T>()`. */
118
+ interface MoonrakerMethods {
119
+ 'server.info': ServerInfo;
120
+ 'server.connection.identify': {
121
+ connection_id: number;
122
+ };
123
+ 'printer.info': PrinterInfo;
124
+ 'printer.objects.list': {
125
+ objects: string[];
126
+ };
127
+ 'printer.objects.query': QueryResult;
128
+ 'printer.objects.subscribe': QueryResult;
129
+ 'printer.gcode.script': 'ok';
130
+ 'printer.emergency_stop': 'ok';
131
+ 'printer.restart': 'ok';
132
+ 'printer.firmware_restart': 'ok';
133
+ 'machine.system_info': {
134
+ system_info: Record<string, unknown>;
135
+ };
136
+ 'machine.reboot': 'ok';
137
+ 'machine.shutdown': 'ok';
138
+ 'server.database.get_item': {
139
+ namespace: string;
140
+ key?: string;
141
+ value: unknown;
142
+ };
143
+ 'server.database.post_item': {
144
+ namespace: string;
145
+ key?: string;
146
+ value: unknown;
147
+ };
148
+ }
149
+ /** A narrowed view of Moonraker's `notify_*` broadcasts (whose raw params are positional arrays). */
150
+ type NotifyEvent = {
151
+ method: 'notify_status_update';
152
+ status: Record<string, unknown>;
153
+ eventtime: number;
154
+ } | {
155
+ method: 'notify_gcode_response';
156
+ response: string;
157
+ } | {
158
+ method: 'notify_klippy_ready';
159
+ } | {
160
+ method: 'notify_klippy_shutdown';
161
+ } | {
162
+ method: 'notify_klippy_disconnected';
163
+ } | {
164
+ method: 'notify_agent_event';
165
+ agent: string;
166
+ event: string;
167
+ data: unknown;
168
+ };
169
+ /** Parse a raw (method, params) notify into a typed event, or null for an unhandled method. */
170
+ declare function parseNotifyEvent(method: string, params: unknown): NotifyEvent | null;
171
+
93
172
  /** Minimal WebSocket surface the client uses — the DOM WebSocket satisfies it; tests fake it. */
94
173
  interface WebSocketLike {
95
174
  send(data: string): void;
@@ -130,6 +209,7 @@ declare class MoonrakerClient implements Connector {
130
209
  setCallbacks(cb: ConnectorCallbacks): void;
131
210
  connect(): Promise<void>;
132
211
  close(): void;
212
+ call<M extends keyof MoonrakerMethods>(method: M, params?: Record<string, unknown>): Promise<MoonrakerMethods[M]>;
133
213
  call<T = unknown>(method: string, params?: Record<string, unknown>): Promise<T>;
134
214
  subscribe(objects: SubscriptionMap): Promise<void>;
135
215
  upload(root: string, file: File, onProgress?: (pct: number) => void): Promise<void>;
@@ -377,6 +457,42 @@ declare function aggregateSubscriptions(ids: string[]): SubscriptionMap;
377
457
  /** Test/host helper — clear the registry. */
378
458
  declare function _resetRegistry(): void;
379
459
 
460
+ type SurfaceHint = 'dense-desktop' | 'thumb-phone' | 'big-touch';
461
+ /** Derive the layout hint from the surface + viewport width (px). The touch screen is always
462
+ * big-touch; the 3d web UI is dense on wide viewports and a single thumb column when narrow. */
463
+ declare function surfaceHint(target: SurfaceTarget, viewportWidth: number): SurfaceHint;
464
+ /** One slot in the shared dashboard definition. */
465
+ interface DashboardSlot {
466
+ widgetId: string;
467
+ /** ascending display order; slots without it sort after ordered ones */
468
+ order?: number;
469
+ /** hide this widget on these hints (e.g. a dense-only widget hidden on thumb-phone) */
470
+ hideOn?: SurfaceHint[];
471
+ }
472
+ /** The single serializable dashboard definition (persisted in UserSettings, machineUUID-keyed). */
473
+ interface DashboardLayout {
474
+ version: number;
475
+ slots: DashboardSlot[];
476
+ }
477
+ declare const DASHBOARD_LAYOUT_VERSION = 1;
478
+ interface ResolvedDashboard {
479
+ hint: SurfaceHint;
480
+ columns: number;
481
+ /** the widgets to render, in order, already filtered for this hint */
482
+ widgets: WidgetDefinition[];
483
+ }
484
+ /**
485
+ * Adapt one layout definition + the available widgets to a surface hint.
486
+ *
487
+ * Slots are honoured first (ordered, minus those hidden on this hint); any registered widget the
488
+ * layout doesn't mention is appended (so newly added widgets still appear). A widget a slot hides
489
+ * on this hint stays hidden even though it is available. With no layout, all available widgets
490
+ * render in registry order.
491
+ */
492
+ declare function resolveDashboard(layout: DashboardLayout | undefined, available: WidgetDefinition[], hint: SurfaceHint): ResolvedDashboard;
493
+ /** Coerce an arbitrary blob into a valid DashboardLayout, or undefined. Used by settings migrate. */
494
+ declare function coerceDashboardLayout(raw: unknown): DashboardLayout | undefined;
495
+
380
496
  interface ThemeTokens {
381
497
  bg: string;
382
498
  surface: string;
@@ -502,6 +618,8 @@ interface UserSettings {
502
618
  motifDensity: 'off' | 'subtle' | 'full';
503
619
  /** reduce animation/illustration for low-power or accessibility */
504
620
  reducedMotion: boolean;
621
+ /** the single per-surface dashboard definition (resolved per surface + viewport at render) */
622
+ dashboardLayout?: DashboardLayout;
505
623
  }
506
624
  declare const DEFAULT_SETTINGS: UserSettings;
507
625
  /** Coerce an arbitrary (old / foreign / partial) blob into valid UserSettings — drops unknown keys,
@@ -543,4 +661,4 @@ declare function applySettings(s: UserSettings, el?: {
543
661
  dir: 'ltr' | 'rtl';
544
662
  };
545
663
 
546
- export { type AgentAllow, type AgentEvent, type BackendMessage, type Catalog, CommandSender, type CommandSenderOptions, type ConnectionState, type Connector, type ConnectorCallbacks, DEFAULT_LOCALE, DEFAULT_SETTINGS, DEFAULT_THEME, type DiscoveryOptions, FILAMIND_COMMAND_EVENT, FULL_CONTROL, FilaMindSession, type IdentifyInfo, type KlippyState, LOCALES, type Listener, type LocaleMeta, type LocationLike, type LogEntry, type LogLevel, Logger, MoonrakerClient, type MoonrakerClientOptions, NARROW_STATUS, NULL_LOGGER, Observable, type Params, type PluralCategory, type PrinterObjects, PrinterState, type PromptButton, type PromptDialog, type PromptEvent, PromptParser, REMOTE_MESSAGE_LEVELS, REMOTE_VIEWS, RTL_LOCALES, type RemoteCommand, type RemoteMessageLevel, type RemoteView, type RestorePoint, RestorePoints, type RestoreStore, SETTINGS_VERSION, SUBSCRIPTION_CONTRACT_VERSION, type SessionOptions, type SettingsPersistence, SettingsStore, type Source, type Stamped, type SubscriptionMap, type SubscriptionTier, type SurfaceTarget, type ThemeName, type ThemeTokens, Translator, UNKNOWN, type UserSettings, type WebSocketLike, type WidgetDefinition, WriteArbiter, type WriteGuard, WriteRefused, _resetRegistry, aggregateSubscriptions, applySettings, applyTheme, deepMerge, deriveCandidates, deriveKlippyState, deriveMachineId, fnv1a, freshness, getWidget, getWidgets, handleAgentCommand, isKlippyLive, isRtl, isStale, localStoragePersistence, localeMeta, memoryPersistence, memoryRestoreStore, mergeSubscriptions, migrate, moonrakerDbPersistence, parseAgentEvent, parseCommand, pluralCategory, registerWidget, resolveMessage, resolveMoonrakerUrl, roamSettings, stamp, themeToCssVars, themes, tier, translate };
664
+ export { type AgentAllow, type AgentEvent, type BackendMessage, type Catalog, CommandSender, type CommandSenderOptions, type ConnectionState, type Connector, type ConnectorCallbacks, DASHBOARD_LAYOUT_VERSION, DEFAULT_LOCALE, DEFAULT_SETTINGS, DEFAULT_THEME, type DashboardLayout, type DashboardSlot, type DiscoveryOptions, FILAMIND_COMMAND_EVENT, FULL_CONTROL, FilaMindSession, type IdentifyInfo, type KlippyState, LOCALES, type Listener, type LocaleMeta, type LocationLike, type LogEntry, type LogLevel, Logger, MoonrakerClient, type MoonrakerClientOptions, type MoonrakerMethods, NARROW_STATUS, NULL_LOGGER, type NotifyEvent, Observable, type Params, type PluralCategory, type PrinterInfo, type PrinterObjects, PrinterState, type PromptButton, type PromptDialog, type PromptEvent, PromptParser, type QueryResult, REMOTE_MESSAGE_LEVELS, REMOTE_VIEWS, RTL_LOCALES, type RemoteCommand, type RemoteMessageLevel, type RemoteView, type ResolvedDashboard, type RestorePoint, RestorePoints, type RestoreStore, RpcError, SETTINGS_VERSION, SUBSCRIPTION_CONTRACT_VERSION, type ServerInfo, type SessionOptions, type SettingsPersistence, SettingsStore, type Source, type Stamped, type SubscriptionMap, type SubscriptionTier, type SurfaceHint, type SurfaceTarget, type ThemeName, type ThemeTokens, Translator, UNKNOWN, type UserSettings, type WebSocketLike, type WidgetDefinition, WriteArbiter, type WriteGuard, WriteRefused, _resetRegistry, aggregateSubscriptions, applySettings, applyTheme, coerceDashboardLayout, deepMerge, deriveCandidates, deriveKlippyState, deriveMachineId, fnv1a, freshness, getWidget, getWidgets, handleAgentCommand, isKlippyLive, isRtl, isStale, localStoragePersistence, localeMeta, memoryPersistence, memoryRestoreStore, mergeSubscriptions, migrate, moonrakerDbPersistence, parseAgentEvent, parseCommand, parseNotifyEvent, pluralCategory, registerWidget, resolveDashboard, resolveMessage, resolveMoonrakerUrl, roamSettings, stamp, surfaceHint, themeToCssVars, themes, tier, translate };
package/dist/index.js CHANGED
@@ -105,6 +105,51 @@ function deepMerge(target, patch) {
105
105
  return out;
106
106
  }
107
107
 
108
+ // src/moonraker/rpc-types.ts
109
+ var RpcError = class extends Error {
110
+ constructor(message, code, data) {
111
+ super(message);
112
+ this.code = code;
113
+ this.data = data;
114
+ this.name = "RpcError";
115
+ }
116
+ code;
117
+ data;
118
+ };
119
+ function parseNotifyEvent(method, params) {
120
+ const arr = Array.isArray(params) ? params : [];
121
+ switch (method) {
122
+ case "notify_status_update":
123
+ return {
124
+ method: "notify_status_update",
125
+ status: arr[0] ?? {},
126
+ eventtime: typeof arr[1] === "number" ? arr[1] : 0
127
+ };
128
+ case "notify_gcode_response":
129
+ return {
130
+ method: "notify_gcode_response",
131
+ response: typeof arr[0] === "string" ? arr[0] : ""
132
+ };
133
+ case "notify_klippy_ready":
134
+ return { method: "notify_klippy_ready" };
135
+ case "notify_klippy_shutdown":
136
+ return { method: "notify_klippy_shutdown" };
137
+ case "notify_klippy_disconnected":
138
+ return { method: "notify_klippy_disconnected" };
139
+ case "notify_agent_event": {
140
+ const e = arr[0] ?? {};
141
+ return {
142
+ method: "notify_agent_event",
143
+ agent: typeof e.agent === "string" ? e.agent : "",
144
+ event: typeof e.event === "string" ? e.event : "",
145
+ data: e.data
146
+ };
147
+ }
148
+ default:
149
+ return null;
150
+ }
151
+ }
152
+
108
153
  // src/moonraker/client.ts
109
154
  var defaultWsFactory = (url) => new WebSocket(url);
110
155
  var MoonrakerClient = class {
@@ -169,7 +214,7 @@ var MoonrakerClient = class {
169
214
  return new Promise((resolve, reject) => {
170
215
  const timer = setTimeout(() => {
171
216
  this.pending.delete(id);
172
- reject(new Error(`moonraker request timed out: ${method}`));
217
+ reject(new RpcError(`moonraker request timed out: ${method}`));
173
218
  }, this.opts.requestTimeoutMs ?? 1e4);
174
219
  this.pending.set(id, { resolve, reject, timer });
175
220
  this.send({ jsonrpc: "2.0", method, params, id });
@@ -246,7 +291,7 @@ var MoonrakerClient = class {
246
291
  if (!p) return;
247
292
  clearTimeout(p.timer);
248
293
  this.pending.delete(msg.id);
249
- if (msg.error) p.reject(new Error(msg.error.message ?? "rpc error"));
294
+ if (msg.error) p.reject(new RpcError(msg.error.message ?? "rpc error", msg.error.code, msg.error.data));
250
295
  else p.resolve(msg.result);
251
296
  return;
252
297
  }
@@ -789,6 +834,58 @@ function _resetRegistry() {
789
834
  registry.clear();
790
835
  }
791
836
 
837
+ // src/registry/dashboard-resolver.ts
838
+ var COLUMNS = {
839
+ "dense-desktop": 3,
840
+ "thumb-phone": 1,
841
+ "big-touch": 2
842
+ };
843
+ function surfaceHint(target, viewportWidth) {
844
+ if (target === "screen") return "big-touch";
845
+ return viewportWidth < 640 ? "thumb-phone" : "dense-desktop";
846
+ }
847
+ var DASHBOARD_LAYOUT_VERSION = 1;
848
+ var HINTS = ["dense-desktop", "thumb-phone", "big-touch"];
849
+ function resolveDashboard(layout, available, hint) {
850
+ const byId = new Map(available.map((w) => [w.id, w]));
851
+ const slots = layout?.slots ?? [];
852
+ const hiddenHere = new Set(
853
+ slots.filter((s) => (s.hideOn ?? []).includes(hint)).map((s) => s.widgetId)
854
+ );
855
+ const used = /* @__PURE__ */ new Set();
856
+ 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))) {
858
+ const w = byId.get(slot.widgetId);
859
+ if (w && !used.has(w.id)) {
860
+ widgets.push(w);
861
+ used.add(w.id);
862
+ }
863
+ }
864
+ for (const w of available) {
865
+ if (!used.has(w.id) && !hiddenHere.has(w.id)) widgets.push(w);
866
+ }
867
+ return { hint, columns: COLUMNS[hint], widgets };
868
+ }
869
+ function coerceDashboardLayout(raw) {
870
+ if (!raw || typeof raw !== "object") return void 0;
871
+ const r = raw;
872
+ if (!Array.isArray(r.slots)) return void 0;
873
+ const slots = [];
874
+ for (const item of r.slots) {
875
+ if (!item || typeof item !== "object") continue;
876
+ const s = item;
877
+ if (typeof s.widgetId !== "string" || !s.widgetId) continue;
878
+ const slot = { widgetId: s.widgetId };
879
+ if (typeof s.order === "number" && Number.isFinite(s.order)) slot.order = s.order;
880
+ if (Array.isArray(s.hideOn)) {
881
+ const hideOn = s.hideOn.filter((h) => HINTS.includes(h));
882
+ if (hideOn.length) slot.hideOn = hideOn;
883
+ }
884
+ slots.push(slot);
885
+ }
886
+ return { version: DASHBOARD_LAYOUT_VERSION, slots };
887
+ }
888
+
792
889
  // src/theme/tokens.ts
793
890
  var themes = {
794
891
  // flagship default — obsidian + gold + lapis + turquoise
@@ -1099,7 +1196,7 @@ var DENSITIES = /* @__PURE__ */ new Set(["comfortable", "compact"]);
1099
1196
  var MOTIFS = /* @__PURE__ */ new Set(["off", "subtle", "full"]);
1100
1197
  function migrate(raw) {
1101
1198
  const r = raw && typeof raw === "object" ? raw : {};
1102
- return {
1199
+ const out = {
1103
1200
  version: SETTINGS_VERSION,
1104
1201
  theme: typeof r.theme === "string" && r.theme in themes ? r.theme : DEFAULT_SETTINGS.theme,
1105
1202
  locale: typeof r.locale === "string" && LOCALES.some((l) => l.code === r.locale) ? r.locale : DEFAULT_SETTINGS.locale,
@@ -1107,6 +1204,9 @@ function migrate(raw) {
1107
1204
  motifDensity: typeof r.motifDensity === "string" && MOTIFS.has(r.motifDensity) ? r.motifDensity : DEFAULT_SETTINGS.motifDensity,
1108
1205
  reducedMotion: typeof r.reducedMotion === "boolean" ? r.reducedMotion : DEFAULT_SETTINGS.reducedMotion
1109
1206
  };
1207
+ const layout = coerceDashboardLayout(r.dashboardLayout);
1208
+ if (layout) out.dashboardLayout = layout;
1209
+ return out;
1110
1210
  }
1111
1211
  function memoryPersistence() {
1112
1212
  let store;
@@ -1221,6 +1321,7 @@ function applySettings(s, el) {
1221
1321
  }
1222
1322
  export {
1223
1323
  CommandSender,
1324
+ DASHBOARD_LAYOUT_VERSION,
1224
1325
  DEFAULT_LOCALE,
1225
1326
  DEFAULT_SETTINGS,
1226
1327
  DEFAULT_THEME,
@@ -1239,6 +1340,7 @@ export {
1239
1340
  REMOTE_VIEWS,
1240
1341
  RTL_LOCALES,
1241
1342
  RestorePoints,
1343
+ RpcError,
1242
1344
  SETTINGS_VERSION,
1243
1345
  SUBSCRIPTION_CONTRACT_VERSION,
1244
1346
  SettingsStore,
@@ -1250,6 +1352,7 @@ export {
1250
1352
  aggregateSubscriptions,
1251
1353
  applySettings,
1252
1354
  applyTheme,
1355
+ coerceDashboardLayout,
1253
1356
  deepMerge,
1254
1357
  deriveCandidates,
1255
1358
  deriveKlippyState,
@@ -1271,12 +1374,15 @@ export {
1271
1374
  moonrakerDbPersistence,
1272
1375
  parseAgentEvent,
1273
1376
  parseCommand,
1377
+ parseNotifyEvent,
1274
1378
  pluralCategory,
1275
1379
  registerWidget,
1380
+ resolveDashboard,
1276
1381
  resolveMessage,
1277
1382
  resolveMoonrakerUrl,
1278
1383
  roamSettings,
1279
1384
  stamp,
1385
+ surfaceHint,
1280
1386
  themeToCssVars,
1281
1387
  themes,
1282
1388
  tier,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@filamind-app/core",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
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.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {