@filamind-app/core 0.1.1 → 0.1.3

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
@@ -16,7 +16,9 @@ The shared foundation of the FilaMind suite — **framework-agnostic TypeScript*
16
16
  | `state/printer.ts` | `PrinterState` — merge-patch from `notify_status_update`, **1 s coalescing + `motion_report` fast-path** |
17
17
  | `moonraker/connector.ts` | the backend-agnostic `Connector` seam (Moonraker first; remote/others later) |
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
+ | `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 |
19
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 |
20
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 |
21
23
  | `printer/prompt-parser.ts` | parses Klipper's `// action:prompt_*` protocol into structured modal dialogs (`PromptParser`) |
22
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>;
@@ -143,6 +223,37 @@ declare class MoonrakerClient implements Connector {
143
223
  private onMessage;
144
224
  }
145
225
 
226
+ /** A minimal view of `location` — the DOM Location satisfies it; tests pass a plain object. */
227
+ interface LocationLike {
228
+ protocol?: string;
229
+ hostname?: string;
230
+ }
231
+ interface DiscoveryOptions {
232
+ /** Explicit runtime override (e.g. from settings). If set, it wins immediately — no probing. */
233
+ override?: string;
234
+ /** Candidate ws(s):// URLs to race. If omitted, they are derived from `location`. */
235
+ candidates?: string[];
236
+ /** Overall budget before discovery gives up (ms). Default 3000. */
237
+ timeoutMs?: number;
238
+ /** Injectable socket factory (tests / non-DOM). Default: `new WebSocket`. */
239
+ wsFactory?: (url: string) => WebSocketLike;
240
+ /** Source for deriving candidates. Default: `globalThis.location`. */
241
+ location?: LocationLike;
242
+ }
243
+ /**
244
+ * Ordered, de-duplicated candidate endpoints:
245
+ * 1. same-origin `/websocket` (the suite deploy reverse-proxies Moonraker here),
246
+ * 2. the page host on Moonraker's default `:7125` (direct, no proxy),
247
+ * 3. `localhost:7125` (on-printer / Tauri webview, whose origin is `tauri.localhost`).
248
+ */
249
+ declare function deriveCandidates(location?: LocationLike): string[];
250
+ /**
251
+ * Resolve a reachable Moonraker WebSocket URL. `override` wins immediately; otherwise the
252
+ * candidates are raced and the first socket to open wins (the rest are closed). Rejects if
253
+ * none open within `timeoutMs`.
254
+ */
255
+ declare function resolveMoonrakerUrl(opts?: DiscoveryOptions): Promise<string>;
256
+
146
257
  declare const SUBSCRIPTION_CONTRACT_VERSION = 1;
147
258
  /** Status-only tier (farm overview / ambient screen): the minimum to show state + progress. */
148
259
  declare const NARROW_STATUS: SubscriptionMap;
@@ -512,4 +623,4 @@ declare function applySettings(s: UserSettings, el?: {
512
623
  dir: 'ltr' | 'rtl';
513
624
  };
514
625
 
515
- export { type AgentAllow, type AgentEvent, type BackendMessage, type Catalog, CommandSender, type CommandSenderOptions, type ConnectionState, type Connector, type ConnectorCallbacks, DEFAULT_LOCALE, DEFAULT_SETTINGS, DEFAULT_THEME, FILAMIND_COMMAND_EVENT, FULL_CONTROL, FilaMindSession, type IdentifyInfo, type KlippyState, LOCALES, type Listener, type LocaleMeta, 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, deriveKlippyState, deriveMachineId, fnv1a, freshness, getWidget, getWidgets, handleAgentCommand, isKlippyLive, isRtl, isStale, localStoragePersistence, localeMeta, memoryPersistence, memoryRestoreStore, mergeSubscriptions, migrate, moonrakerDbPersistence, parseAgentEvent, parseCommand, pluralCategory, registerWidget, resolveMessage, roamSettings, stamp, themeToCssVars, themes, tier, translate };
626
+ 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, 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 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 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, parseNotifyEvent, pluralCategory, registerWidget, resolveMessage, resolveMoonrakerUrl, roamSettings, stamp, 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
  }
@@ -270,6 +315,85 @@ function xhrUpload(url, form, onProgress) {
270
315
  });
271
316
  }
272
317
 
318
+ // src/moonraker/discovery.ts
319
+ var defaultWsFactory2 = (url) => new WebSocket(url);
320
+ function deriveCandidates(location) {
321
+ const loc = location ?? globalThis.location ?? {};
322
+ const wsProto = loc.protocol === "https:" ? "wss" : "ws";
323
+ const host = loc.hostname && loc.hostname.length > 0 ? loc.hostname : "localhost";
324
+ return [
325
+ .../* @__PURE__ */ new Set([
326
+ `${wsProto}://${host}/websocket`,
327
+ `${wsProto}://${host}:7125/websocket`,
328
+ `ws://localhost:7125/websocket`
329
+ ])
330
+ ];
331
+ }
332
+ function resolveMoonrakerUrl(opts = {}) {
333
+ const override = opts.override?.trim();
334
+ if (override) return Promise.resolve(override);
335
+ const candidates = opts.candidates ?? deriveCandidates(opts.location);
336
+ if (candidates.length === 0) {
337
+ return Promise.reject(new Error("moonraker discovery: no candidate endpoints"));
338
+ }
339
+ const makeWs = opts.wsFactory ?? defaultWsFactory2;
340
+ const timeoutMs = opts.timeoutMs ?? 3e3;
341
+ return new Promise((resolve, reject) => {
342
+ let settled = false;
343
+ let remaining = candidates.length;
344
+ const sockets = [];
345
+ const timer = setTimeout(finishFail, timeoutMs);
346
+ function cleanup() {
347
+ clearTimeout(timer);
348
+ for (const s of sockets) {
349
+ s.onopen = null;
350
+ s.onerror = null;
351
+ s.onclose = null;
352
+ try {
353
+ s.close();
354
+ } catch {
355
+ }
356
+ }
357
+ }
358
+ function finishOk(url) {
359
+ if (settled) return;
360
+ settled = true;
361
+ cleanup();
362
+ resolve(url);
363
+ }
364
+ function finishFail() {
365
+ if (settled) return;
366
+ settled = true;
367
+ cleanup();
368
+ reject(new Error(`moonraker discovery: no endpoint responded (${candidates.join(", ")})`));
369
+ }
370
+ function lose() {
371
+ remaining -= 1;
372
+ if (remaining <= 0) finishFail();
373
+ }
374
+ for (const url of candidates) {
375
+ let s;
376
+ try {
377
+ s = makeWs(url);
378
+ } catch {
379
+ lose();
380
+ continue;
381
+ }
382
+ sockets.push(s);
383
+ let done = false;
384
+ const fail = () => {
385
+ if (!done) {
386
+ done = true;
387
+ lose();
388
+ }
389
+ };
390
+ s.onopen = () => finishOk(url);
391
+ s.onerror = fail;
392
+ s.onclose = fail;
393
+ }
394
+ });
395
+ }
396
+
273
397
  // src/moonraker/subscriptions.ts
274
398
  var SUBSCRIPTION_CONTRACT_VERSION = 1;
275
399
  var NARROW_STATUS = {
@@ -1160,6 +1284,7 @@ export {
1160
1284
  REMOTE_VIEWS,
1161
1285
  RTL_LOCALES,
1162
1286
  RestorePoints,
1287
+ RpcError,
1163
1288
  SETTINGS_VERSION,
1164
1289
  SUBSCRIPTION_CONTRACT_VERSION,
1165
1290
  SettingsStore,
@@ -1172,6 +1297,7 @@ export {
1172
1297
  applySettings,
1173
1298
  applyTheme,
1174
1299
  deepMerge,
1300
+ deriveCandidates,
1175
1301
  deriveKlippyState,
1176
1302
  deriveMachineId,
1177
1303
  fnv1a,
@@ -1191,9 +1317,11 @@ export {
1191
1317
  moonrakerDbPersistence,
1192
1318
  parseAgentEvent,
1193
1319
  parseCommand,
1320
+ parseNotifyEvent,
1194
1321
  pluralCategory,
1195
1322
  registerWidget,
1196
1323
  resolveMessage,
1324
+ resolveMoonrakerUrl,
1197
1325
  roamSettings,
1198
1326
  stamp,
1199
1327
  themeToCssVars,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@filamind-app/core",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
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": {