@filamind-app/core 0.1.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/LICENSE +675 -0
- package/README.md +71 -0
- package/dist/index.d.ts +515 -0
- package/dist/index.js +1171 -0
- package/package.json +31 -0
package/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# @filamind-app/core
|
|
2
|
+
|
|
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
|
+
|
|
6
|
+
## What's here
|
|
7
|
+
| Module | Purpose |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| `provenance.ts` | `Stamped<T>` "never-lie" values (`value · ts · source · staleAfter`) + `isStale` / `freshness` |
|
|
10
|
+
| `state/observable.ts` | a tiny framework-agnostic observable (consumers bind it to Pinia/signals) |
|
|
11
|
+
| `state/printer.ts` | `PrinterState` — merge-patch from `notify_status_update`, **1 s coalescing + `motion_report` fast-path** |
|
|
12
|
+
| `moonraker/connector.ts` | the backend-agnostic `Connector` seam (Moonraker first; remote/others later) |
|
|
13
|
+
| `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`) |
|
|
14
|
+
| `moonraker/subscriptions.ts` | the **versioned** canonical subscription contract — `NARROW_STATUS` / `FULL_CONTROL` tiers + `tier()` |
|
|
15
|
+
| `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 |
|
|
16
|
+
| `printer/prompt-parser.ts` | parses Klipper's `// action:prompt_*` protocol into structured modal dialogs (`PromptParser`) |
|
|
17
|
+
| `session/session.ts` | **`FilaMindSession`** — the orchestrator: staged init (identify → capabilities → query/seed → subscribe), routes `notify_*`, owns the Klippy-aware `live`/stale gate |
|
|
18
|
+
| `safety/write-arbiter.ts` | fail-closed **`WriteArbiter`** — the single chokepoint for every mutation; enforces safe-mode + a caller guard |
|
|
19
|
+
| `identity/machine.ts` | `deriveMachineId` → stable `fm-<hash>` from `machine.system_info` (the roaming-settings key) |
|
|
20
|
+
| `backup/restore-points.ts` | reversible, retention-bounded **`RestorePoints`** (back-up-first substrate for config/flash/migrate) |
|
|
21
|
+
| `observability/logger.ts` | pluggable ring-buffer `Logger` (the diagnostics-bundle source; replaces silent `catch {}`) |
|
|
22
|
+
| `registry/widget-registry.ts` | the cross-surface widget/plugin registry + `aggregateSubscriptions` |
|
|
23
|
+
| `theme/tokens.ts` | the exclusive Pharaonic themes (Tutankhamun · Horus · Anubis) as `--fm-*` design tokens |
|
|
24
|
+
| `i18n/locale-meta.ts` | the 19 shipped locales `{ code, name, rtl, dir }` + RTL list + CLDR `pluralCategory` (`Intl.PluralRules`) |
|
|
25
|
+
| `i18n/translator.ts` | framework-agnostic `translate` / `Translator` + the backend message-code contract (`resolveMessage`) |
|
|
26
|
+
| `i18n/locales/*.json` | drop-in catalogs (en + ar shipped as proof, incl. the full Arabic plural set) |
|
|
27
|
+
| `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) |
|
|
28
|
+
|
|
29
|
+
## Wiring (the consumer entry point)
|
|
30
|
+
```ts
|
|
31
|
+
import { MoonrakerClient, FilaMindSession, FULL_CONTROL } from '@filamind-app/core'
|
|
32
|
+
|
|
33
|
+
const connector = new MoonrakerClient({ url: 'ws://printer.local:7125/websocket' })
|
|
34
|
+
const session = new FilaMindSession(connector, {
|
|
35
|
+
subscriptions: FULL_CONTROL,
|
|
36
|
+
identify: { client_name: 'FilaMind 3d', version: '0.1.0', type: 'web' },
|
|
37
|
+
})
|
|
38
|
+
await session.start()
|
|
39
|
+
|
|
40
|
+
// reactive, framework-agnostic — bind these to Pinia/signals/etc.
|
|
41
|
+
session.live.subscribe((v) => {/* dim the UI when not trustworthy-live */})
|
|
42
|
+
session.printer.objects.subscribe((o) => {/* render telemetry */})
|
|
43
|
+
session.prompt.subscribe((ev) => {/* show a Klipper modal */})
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Scripts
|
|
47
|
+
```bash
|
|
48
|
+
npm install
|
|
49
|
+
npm run type-check # tsc --noEmit
|
|
50
|
+
npm test # vitest (76 pure-logic tests)
|
|
51
|
+
npm run build # tsup → dist/ (ESM + .d.ts)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Notes
|
|
55
|
+
- Consumed by the suite apps (FilaMind 3d / screen / flow) via the workspace in development.
|
|
56
|
+
- Framework-agnostic on purpose: no Vue/React import in core. The reactive layer is a plain `Observable`;
|
|
57
|
+
Vue/Pinia adapts it on the consumer side.
|
|
58
|
+
- Versions are pinned to current real latest-stable; bump deliberately at first real install.
|
|
59
|
+
- GPL-3.0-or-later. FilaMind's own — no third-party project named (R1).
|
|
60
|
+
|
|
61
|
+
## Credits
|
|
62
|
+
|
|
63
|
+
Built and maintained by the DeltaFabs team:
|
|
64
|
+
|
|
65
|
+
- abdelmonem awad - <eg2@live.com>
|
|
66
|
+
- Ahmed bebars - <Ahmedbebars1@gmail.com>
|
|
67
|
+
- Kareem Salama - <Golden.kiko@gmail.com>
|
|
68
|
+
|
|
69
|
+
## License
|
|
70
|
+
|
|
71
|
+
[GPL-3.0-or-later](LICENSE) © 2026 DeltaFabs team.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
type Source = 'live' | 'cache' | 'optimistic' | 'unknown';
|
|
2
|
+
interface Stamped<T> {
|
|
3
|
+
value: T;
|
|
4
|
+
/** when this value arrived (ms epoch, client clock — Moonraker has no server timestamp) */
|
|
5
|
+
ts: number;
|
|
6
|
+
source: Source;
|
|
7
|
+
/** ms after `ts` when the value should be treated as stale (omit = never goes stale on its own) */
|
|
8
|
+
staleAfter?: number;
|
|
9
|
+
}
|
|
10
|
+
declare function stamp<T>(value: T, source?: Source, staleAfter?: number, now?: number): Stamped<T>;
|
|
11
|
+
declare const UNKNOWN: Stamped<undefined>;
|
|
12
|
+
declare function isStale(s: Stamped<unknown> | undefined, now?: number): boolean;
|
|
13
|
+
/** UI hint a widget can map to a class: 'ok' | 'stale' | 'unknown'. */
|
|
14
|
+
declare function freshness(s: Stamped<unknown> | undefined, now?: number): 'ok' | 'stale' | 'unknown';
|
|
15
|
+
|
|
16
|
+
type Listener<T> = (value: T) => void;
|
|
17
|
+
declare class Observable<T> {
|
|
18
|
+
private _value;
|
|
19
|
+
private listeners;
|
|
20
|
+
constructor(_value: T);
|
|
21
|
+
get value(): T;
|
|
22
|
+
set(v: T): void;
|
|
23
|
+
update(fn: (v: T) => T): void;
|
|
24
|
+
/** Subscribe and receive the current value immediately. Returns an unsubscribe fn. */
|
|
25
|
+
subscribe(fn: Listener<T>): () => void;
|
|
26
|
+
private emit;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type PrinterObjects = Record<string, Record<string, unknown>>;
|
|
30
|
+
declare class PrinterState {
|
|
31
|
+
readonly objects: Observable<PrinterObjects>;
|
|
32
|
+
private pendingPatch;
|
|
33
|
+
private flushTimer;
|
|
34
|
+
private readonly coalesceMs;
|
|
35
|
+
constructor(coalesceMs?: number);
|
|
36
|
+
/** Seed from a printer.objects.query result. */
|
|
37
|
+
seed(objects: PrinterObjects): void;
|
|
38
|
+
/** Feed notify_status_update params: `[ { <object>: { <field>: value } }, eventtime ]`. */
|
|
39
|
+
applyNotify(params: unknown): void;
|
|
40
|
+
/** Force-apply any pending coalesced patch now (e.g. on tab focus). */
|
|
41
|
+
flush(): void;
|
|
42
|
+
}
|
|
43
|
+
/** Recursive merge of plain objects; arrays + primitives are replaced. Returns a new object. */
|
|
44
|
+
declare function deepMerge<T extends Record<string, unknown>>(target: T, patch: unknown): T;
|
|
45
|
+
|
|
46
|
+
type ConnectionState = 'idle' | 'connecting' | 'identifying' | 'ready' | 'reconnecting' | 'closed';
|
|
47
|
+
interface ConnectorCallbacks {
|
|
48
|
+
/** notify_* fan-out (e.g. notify_status_update, notify_gcode_response) */
|
|
49
|
+
onUpdate?: (method: string, params: unknown) => void;
|
|
50
|
+
onConnectProgress?: (state: ConnectionState) => void;
|
|
51
|
+
onError?: (err: Error) => void;
|
|
52
|
+
onReconnected?: () => void;
|
|
53
|
+
}
|
|
54
|
+
/** Objects subscription map: { "<object>": ["<field>", ...] | null } (null = all fields). */
|
|
55
|
+
type SubscriptionMap = Record<string, string[] | null>;
|
|
56
|
+
interface Connector {
|
|
57
|
+
readonly state: ConnectionState;
|
|
58
|
+
connect(): Promise<void>;
|
|
59
|
+
close(): void;
|
|
60
|
+
/** JSON-RPC request/response. */
|
|
61
|
+
call<T = unknown>(method: string, params?: Record<string, unknown>): Promise<T>;
|
|
62
|
+
/** Subscribe to printer objects; the impl remembers the set and restores it on reconnect. */
|
|
63
|
+
subscribe(objects: SubscriptionMap): Promise<void>;
|
|
64
|
+
upload(root: string, file: File, onProgress?: (pct: number) => void): Promise<void>;
|
|
65
|
+
download(path: string): Promise<Blob>;
|
|
66
|
+
setCallbacks(cb: ConnectorCallbacks): void;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
70
|
+
interface LogEntry {
|
|
71
|
+
ts: number;
|
|
72
|
+
level: LogLevel;
|
|
73
|
+
msg: string;
|
|
74
|
+
data?: unknown;
|
|
75
|
+
}
|
|
76
|
+
declare class Logger {
|
|
77
|
+
private readonly max;
|
|
78
|
+
private readonly sink?;
|
|
79
|
+
private ring;
|
|
80
|
+
constructor(max?: number, sink?: ((e: LogEntry) => void) | undefined);
|
|
81
|
+
log(level: LogLevel, msg: string, data?: unknown): void;
|
|
82
|
+
debug(msg: string, data?: unknown): void;
|
|
83
|
+
info(msg: string, data?: unknown): void;
|
|
84
|
+
warn(msg: string, data?: unknown): void;
|
|
85
|
+
error(msg: string, data?: unknown): void;
|
|
86
|
+
/** Snapshot of recent events (for the diagnostics bundle). */
|
|
87
|
+
recent(): LogEntry[];
|
|
88
|
+
clear(): void;
|
|
89
|
+
}
|
|
90
|
+
/** A no-op logger for when none is supplied. */
|
|
91
|
+
declare const NULL_LOGGER: Logger;
|
|
92
|
+
|
|
93
|
+
/** Minimal WebSocket surface the client uses — the DOM WebSocket satisfies it; tests fake it. */
|
|
94
|
+
interface WebSocketLike {
|
|
95
|
+
send(data: string): void;
|
|
96
|
+
close(): void;
|
|
97
|
+
onopen: ((ev?: unknown) => void) | null;
|
|
98
|
+
onmessage: ((ev: {
|
|
99
|
+
data: unknown;
|
|
100
|
+
}) => void) | null;
|
|
101
|
+
onerror: ((ev?: unknown) => void) | null;
|
|
102
|
+
onclose: ((ev?: unknown) => void) | null;
|
|
103
|
+
}
|
|
104
|
+
interface MoonrakerClientOptions {
|
|
105
|
+
/** ws(s)://host:port/websocket */
|
|
106
|
+
url: string;
|
|
107
|
+
requestTimeoutMs?: number;
|
|
108
|
+
maxBackoffMs?: number;
|
|
109
|
+
/** stop reconnecting after this many consecutive attempts → terminal 'closed' (default: unlimited) */
|
|
110
|
+
maxReconnectAttempts?: number;
|
|
111
|
+
wsFactory?: (url: string) => WebSocketLike;
|
|
112
|
+
logger?: Logger;
|
|
113
|
+
}
|
|
114
|
+
declare class MoonrakerClient implements Connector {
|
|
115
|
+
private readonly opts;
|
|
116
|
+
private ws?;
|
|
117
|
+
private _state;
|
|
118
|
+
private nextId;
|
|
119
|
+
private pending;
|
|
120
|
+
private cb;
|
|
121
|
+
private subs;
|
|
122
|
+
private backoff;
|
|
123
|
+
private reconnectAttempts;
|
|
124
|
+
private closedByUser;
|
|
125
|
+
private hasOpened;
|
|
126
|
+
private reconnectTimer?;
|
|
127
|
+
private readonly makeWs;
|
|
128
|
+
constructor(opts: MoonrakerClientOptions);
|
|
129
|
+
get state(): ConnectionState;
|
|
130
|
+
setCallbacks(cb: ConnectorCallbacks): void;
|
|
131
|
+
connect(): Promise<void>;
|
|
132
|
+
close(): void;
|
|
133
|
+
call<T = unknown>(method: string, params?: Record<string, unknown>): Promise<T>;
|
|
134
|
+
subscribe(objects: SubscriptionMap): Promise<void>;
|
|
135
|
+
upload(root: string, file: File, onProgress?: (pct: number) => void): Promise<void>;
|
|
136
|
+
download(path: string): Promise<Blob>;
|
|
137
|
+
private httpBase;
|
|
138
|
+
private send;
|
|
139
|
+
private setState;
|
|
140
|
+
private restoreSubs;
|
|
141
|
+
private scheduleReconnect;
|
|
142
|
+
private failAll;
|
|
143
|
+
private onMessage;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
declare const SUBSCRIPTION_CONTRACT_VERSION = 1;
|
|
147
|
+
/** Status-only tier (farm overview / ambient screen): the minimum to show state + progress. */
|
|
148
|
+
declare const NARROW_STATUS: SubscriptionMap;
|
|
149
|
+
/** Full-control tier (the daily-driver UI): everything a control surface needs. */
|
|
150
|
+
declare const FULL_CONTROL: SubscriptionMap;
|
|
151
|
+
type SubscriptionTier = 'narrow' | 'full';
|
|
152
|
+
declare function tier(t: SubscriptionTier): SubscriptionMap;
|
|
153
|
+
/** Union of subscription maps (`null` = "all fields" wins). The single source for the
|
|
154
|
+
* null-wins merge rule — consumers compose a baseline tier with widget-declared needs. */
|
|
155
|
+
declare function mergeSubscriptions(...maps: SubscriptionMap[]): SubscriptionMap;
|
|
156
|
+
|
|
157
|
+
type KlippyState = 'ready' | 'startup' | 'shutdown' | 'error' | 'disconnected';
|
|
158
|
+
/** Map a Moonraker `server.info.klippy_state` string to a KlippyState (default: disconnected). */
|
|
159
|
+
declare function deriveKlippyState(raw: unknown): KlippyState;
|
|
160
|
+
/** Printer data is trustworthy as "live" only when Klippy is ready. */
|
|
161
|
+
declare function isKlippyLive(state: KlippyState): boolean;
|
|
162
|
+
|
|
163
|
+
type ButtonStyle = 'primary' | 'secondary' | 'warning';
|
|
164
|
+
interface PromptButton {
|
|
165
|
+
label: string;
|
|
166
|
+
gcode?: string;
|
|
167
|
+
style?: ButtonStyle;
|
|
168
|
+
}
|
|
169
|
+
interface PromptDialog {
|
|
170
|
+
title?: string;
|
|
171
|
+
text: string[];
|
|
172
|
+
buttons: PromptButton[];
|
|
173
|
+
footer: PromptButton[];
|
|
174
|
+
}
|
|
175
|
+
type PromptEvent = {
|
|
176
|
+
type: 'show';
|
|
177
|
+
dialog: PromptDialog;
|
|
178
|
+
} | {
|
|
179
|
+
type: 'end';
|
|
180
|
+
} | null;
|
|
181
|
+
declare class PromptParser {
|
|
182
|
+
private current;
|
|
183
|
+
/** Feed one notify_gcode_response line. Returns a PromptEvent at show/end, else null. */
|
|
184
|
+
feed(line: string): PromptEvent;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** The single agent-event name FilaMind uses; everything else on the bus is ignored. */
|
|
188
|
+
declare const FILAMIND_COMMAND_EVENT = "filamind:command";
|
|
189
|
+
/** The views a screen-style surface can be told to show (matches the touch app's tabs). */
|
|
190
|
+
declare const REMOTE_VIEWS: readonly ["status", "control", "settings"];
|
|
191
|
+
type RemoteView = (typeof REMOTE_VIEWS)[number];
|
|
192
|
+
declare const REMOTE_MESSAGE_LEVELS: readonly ["info", "warn"];
|
|
193
|
+
type RemoteMessageLevel = (typeof REMOTE_MESSAGE_LEVELS)[number];
|
|
194
|
+
/** A UI-only instruction sent from one surface to another. */
|
|
195
|
+
type RemoteCommand = {
|
|
196
|
+
kind: 'navigate';
|
|
197
|
+
view: RemoteView;
|
|
198
|
+
} | {
|
|
199
|
+
kind: 'message';
|
|
200
|
+
level: RemoteMessageLevel;
|
|
201
|
+
text: string;
|
|
202
|
+
} | {
|
|
203
|
+
kind: 'locate';
|
|
204
|
+
};
|
|
205
|
+
/** The shape Moonraker delivers in notify_agent_event params[0]. */
|
|
206
|
+
interface AgentEvent {
|
|
207
|
+
agent: string;
|
|
208
|
+
event: string;
|
|
209
|
+
data?: unknown;
|
|
210
|
+
}
|
|
211
|
+
/** Validate an arbitrary blob (from the broadcast bus) into a RemoteCommand, or null if it isn't one. */
|
|
212
|
+
declare function parseCommand(raw: unknown): RemoteCommand | null;
|
|
213
|
+
/** Parse Moonraker's notify_agent_event params (an array holding one event object) into an AgentEvent. */
|
|
214
|
+
declare function parseAgentEvent(params: unknown): AgentEvent | null;
|
|
215
|
+
/** Optional sender allow-list: an array of accepted agent names, or a predicate over the name. */
|
|
216
|
+
type AgentAllow = readonly string[] | ((agent: string) => boolean);
|
|
217
|
+
/** If an agent event is a (validated) FilaMind command from an allowed sender, hand it to the surface's
|
|
218
|
+
* dispatcher. The allow-list is best-effort defence-in-depth: client_name is self-asserted, which is
|
|
219
|
+
* WHY these commands must stay strictly UI-only and never reach the printer-write path. */
|
|
220
|
+
declare function handleAgentCommand(ev: AgentEvent, dispatch: (cmd: RemoteCommand) => void, opts?: {
|
|
221
|
+
allowFrom?: AgentAllow;
|
|
222
|
+
}): void;
|
|
223
|
+
|
|
224
|
+
interface IdentifyInfo {
|
|
225
|
+
client_name: string;
|
|
226
|
+
version: string;
|
|
227
|
+
type: 'web' | 'display' | 'desktop' | 'bot' | 'agent';
|
|
228
|
+
url?: string;
|
|
229
|
+
access_token?: string;
|
|
230
|
+
}
|
|
231
|
+
interface SessionOptions {
|
|
232
|
+
/** the active subscription set (a tier or aggregateSubscriptions(active widgets)) */
|
|
233
|
+
subscriptions?: SubscriptionMap;
|
|
234
|
+
identify?: IdentifyInfo;
|
|
235
|
+
logger?: Logger;
|
|
236
|
+
/** Receive cross-surface agent events (notify_agent_event) — e.g. remote-control commands from
|
|
237
|
+
* another FilaMind surface. Discrete (fired per event, never replayed), so no stale re-fire. */
|
|
238
|
+
onAgentEvent?: (ev: AgentEvent) => void;
|
|
239
|
+
}
|
|
240
|
+
declare class FilaMindSession {
|
|
241
|
+
private readonly connector;
|
|
242
|
+
private readonly opts;
|
|
243
|
+
readonly printer: PrinterState;
|
|
244
|
+
readonly klippy: Observable<KlippyState>;
|
|
245
|
+
readonly capabilities: Observable<string[]>;
|
|
246
|
+
/** true only when data is trustworthy as live (connected + Klippy ready). The UI dims when false. */
|
|
247
|
+
readonly live: Observable<boolean>;
|
|
248
|
+
readonly prompt: Observable<PromptEvent>;
|
|
249
|
+
private subs;
|
|
250
|
+
private readonly log;
|
|
251
|
+
private readonly prompts;
|
|
252
|
+
private bootstrapping;
|
|
253
|
+
constructor(connector: Connector, opts?: SessionOptions);
|
|
254
|
+
/** Set/replace the active subscription set (e.g. when the dashboard's widgets change). */
|
|
255
|
+
setSubscriptions(subs: SubscriptionMap): void;
|
|
256
|
+
start(): Promise<void>;
|
|
257
|
+
stop(): void;
|
|
258
|
+
/** Staged init — runs on first connect AND on reconnect AND on notify_klippy_ready.
|
|
259
|
+
* Guarded so overlapping triggers (reconnect + klippy_ready) can't interleave two seeds. */
|
|
260
|
+
private bootstrap;
|
|
261
|
+
private onUpdate;
|
|
262
|
+
private handleGcodeResponse;
|
|
263
|
+
private setLive;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
interface GuardResult {
|
|
267
|
+
ok: boolean;
|
|
268
|
+
reason?: string;
|
|
269
|
+
}
|
|
270
|
+
type WriteGuard = (action: string) => GuardResult;
|
|
271
|
+
declare class WriteRefused extends Error {
|
|
272
|
+
readonly action: string;
|
|
273
|
+
constructor(action: string, reason: string);
|
|
274
|
+
}
|
|
275
|
+
declare class WriteArbiter {
|
|
276
|
+
/** caller supplies the live-trust check (Klippy ready + connection ready, not printing-locked, …) */
|
|
277
|
+
private readonly guard;
|
|
278
|
+
private readonly logger?;
|
|
279
|
+
readonly safeMode: Observable<boolean>;
|
|
280
|
+
constructor(
|
|
281
|
+
/** caller supplies the live-trust check (Klippy ready + connection ready, not printing-locked, …) */
|
|
282
|
+
guard?: WriteGuard, logger?: Logger | undefined);
|
|
283
|
+
setSafeMode(on: boolean, reason?: string): void;
|
|
284
|
+
/** Run a mutation through the gate. Throws WriteRefused if blocked. */
|
|
285
|
+
run<T>(action: string, fn: () => Promise<T>): Promise<T>;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** FNV-1a 32-bit hex — deterministic, dependency-free. */
|
|
289
|
+
declare function fnv1a(s: string): string;
|
|
290
|
+
/** Derive a stable `fm-<hash>` machine id from Moonraker. Falls back to `fm-unknown`. */
|
|
291
|
+
declare function deriveMachineId(connector: Connector): Promise<string>;
|
|
292
|
+
|
|
293
|
+
interface RestorePoint {
|
|
294
|
+
id: string;
|
|
295
|
+
/** what was snapshotted, e.g. 'printer.cfg' | 'settings' | 'config-templates' */
|
|
296
|
+
scope: string;
|
|
297
|
+
/** why, e.g. 'pre-config-save' | 'pre-plugin-install' | 'pre-migrate' */
|
|
298
|
+
trigger: string;
|
|
299
|
+
ts: number;
|
|
300
|
+
/** monotonic tiebreak so prune is deterministic even when two points share a ts (ms clock) */
|
|
301
|
+
seq: number;
|
|
302
|
+
reversible: boolean;
|
|
303
|
+
data: unknown;
|
|
304
|
+
}
|
|
305
|
+
interface RestoreStore {
|
|
306
|
+
list(machineId: string): Promise<RestorePoint[]>;
|
|
307
|
+
save(machineId: string, p: RestorePoint): Promise<void>;
|
|
308
|
+
remove(machineId: string, id: string): Promise<void>;
|
|
309
|
+
}
|
|
310
|
+
declare function memoryRestoreStore(): RestoreStore;
|
|
311
|
+
declare class RestorePoints {
|
|
312
|
+
private readonly store;
|
|
313
|
+
private readonly machineId;
|
|
314
|
+
private readonly retention;
|
|
315
|
+
private seq;
|
|
316
|
+
constructor(store: RestoreStore, machineId: string, retention?: number);
|
|
317
|
+
snapshot(scope: string, trigger: string, data: unknown, reversible?: boolean): Promise<RestorePoint>;
|
|
318
|
+
list(): Promise<RestorePoint[]>;
|
|
319
|
+
get(id: string): Promise<RestorePoint | undefined>;
|
|
320
|
+
/** Keep only the newest `retention` points per scope. */
|
|
321
|
+
private prune;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
type SurfaceTarget = '3d' | 'screen';
|
|
325
|
+
interface WidgetDefinition {
|
|
326
|
+
id: string;
|
|
327
|
+
title: string;
|
|
328
|
+
icon?: string;
|
|
329
|
+
description?: string;
|
|
330
|
+
/** lazy component loader — the host framework (Vue) resolves it (kept as `unknown` here) */
|
|
331
|
+
component: () => Promise<unknown>;
|
|
332
|
+
defaultSize?: {
|
|
333
|
+
w: number;
|
|
334
|
+
h: number;
|
|
335
|
+
};
|
|
336
|
+
/** declared data needs, aggregated across active widgets to subscribe once */
|
|
337
|
+
subscriptions?: SubscriptionMap;
|
|
338
|
+
/** which surfaces this widget renders on (default: both) */
|
|
339
|
+
targets?: SurfaceTarget[];
|
|
340
|
+
}
|
|
341
|
+
declare function registerWidget(def: WidgetDefinition): void;
|
|
342
|
+
declare function getWidget(id: string): WidgetDefinition | undefined;
|
|
343
|
+
declare function getWidgets(target?: SurfaceTarget): WidgetDefinition[];
|
|
344
|
+
/** The union of the given widgets' declared subscriptions (subscribe once). */
|
|
345
|
+
declare function aggregateSubscriptions(ids: string[]): SubscriptionMap;
|
|
346
|
+
/** Test/host helper — clear the registry. */
|
|
347
|
+
declare function _resetRegistry(): void;
|
|
348
|
+
|
|
349
|
+
interface ThemeTokens {
|
|
350
|
+
bg: string;
|
|
351
|
+
surface: string;
|
|
352
|
+
surface2: string;
|
|
353
|
+
border: string;
|
|
354
|
+
text: string;
|
|
355
|
+
textMuted: string;
|
|
356
|
+
primary: string;
|
|
357
|
+
primaryContrast: string;
|
|
358
|
+
secondary: string;
|
|
359
|
+
accent: string;
|
|
360
|
+
success: string;
|
|
361
|
+
warning: string;
|
|
362
|
+
danger: string;
|
|
363
|
+
}
|
|
364
|
+
type ThemeName = 'tutankhamun' | 'horus' | 'anubis';
|
|
365
|
+
declare const themes: Record<ThemeName, ThemeTokens>;
|
|
366
|
+
declare const DEFAULT_THEME: ThemeName;
|
|
367
|
+
/** `{ "--fm-bg": "#0E0F12", ... }` for a theme. */
|
|
368
|
+
declare function themeToCssVars(t: ThemeTokens): Record<string, string>;
|
|
369
|
+
/** Apply a theme to an element (default: document root) by setting the `--fm-*` variables. */
|
|
370
|
+
declare function applyTheme(name: ThemeName, el?: {
|
|
371
|
+
style: {
|
|
372
|
+
setProperty(p: string, v: string): void;
|
|
373
|
+
};
|
|
374
|
+
}): void;
|
|
375
|
+
|
|
376
|
+
interface LocaleMeta {
|
|
377
|
+
code: string;
|
|
378
|
+
/** endonym (shown in the language picker) */
|
|
379
|
+
name: string;
|
|
380
|
+
rtl: boolean;
|
|
381
|
+
dir: 'ltr' | 'rtl';
|
|
382
|
+
}
|
|
383
|
+
declare const LOCALES: readonly LocaleMeta[];
|
|
384
|
+
declare const DEFAULT_LOCALE = "en";
|
|
385
|
+
declare const RTL_LOCALES: readonly string[];
|
|
386
|
+
declare function localeMeta(code: string): LocaleMeta | undefined;
|
|
387
|
+
declare function isRtl(code: string): boolean;
|
|
388
|
+
type PluralCategory = 'zero' | 'one' | 'two' | 'few' | 'many' | 'other';
|
|
389
|
+
/** CLDR plural category via the platform's Intl.PluralRules (handles ar/ru/pl/… correctly). */
|
|
390
|
+
declare function pluralCategory(locale: string, n: number): PluralCategory;
|
|
391
|
+
|
|
392
|
+
/** A nested catalog: { app: { name: "FilaMind 3d" }, job: { layers: { one, other } } }. */
|
|
393
|
+
type Catalog = Record<string, unknown>;
|
|
394
|
+
type Params = Record<string, string | number>;
|
|
395
|
+
/**
|
|
396
|
+
* Translate `key` against `catalog` for `locale`. Plural: if the entry is an object
|
|
397
|
+
* (`{ one, other, … }`) and `params.count` is a number, the right CLDR form is chosen.
|
|
398
|
+
* Missing keys return the key itself (so gaps are visible, never blank).
|
|
399
|
+
*/
|
|
400
|
+
declare function translate(catalog: Catalog, key: string, params?: Params, locale?: string): string;
|
|
401
|
+
/** Convenience holder for a (catalog, locale) pair. */
|
|
402
|
+
declare class Translator {
|
|
403
|
+
catalog: Catalog;
|
|
404
|
+
locale: string;
|
|
405
|
+
constructor(catalog: Catalog, locale?: string);
|
|
406
|
+
t(key: string, params?: Params): string;
|
|
407
|
+
}
|
|
408
|
+
/** Backend strings travel as a message-code contract so the UI translates them. */
|
|
409
|
+
interface BackendMessage {
|
|
410
|
+
code: string;
|
|
411
|
+
params?: Params;
|
|
412
|
+
/** server-side fallback text if the UI lacks the key */
|
|
413
|
+
message?: string;
|
|
414
|
+
}
|
|
415
|
+
declare function resolveMessage(catalog: Catalog, m: BackendMessage, locale?: string): string;
|
|
416
|
+
|
|
417
|
+
interface CommandSenderOptions {
|
|
418
|
+
/** Stable agent name (pinned — a drifting name registers phantom duplicates on reconnect). */
|
|
419
|
+
client_name: string;
|
|
420
|
+
version: string;
|
|
421
|
+
/** Required: Moonraker documents `url` as mandatory for identify; omitting it can make a strict
|
|
422
|
+
* host reject the agent identify, silently stranding the bus. */
|
|
423
|
+
url: string;
|
|
424
|
+
logger?: Logger;
|
|
425
|
+
/** Fires whenever readiness flips (connected+identified) — lets a UI gate its send affordances. */
|
|
426
|
+
onReadyChange?: (ready: boolean) => void;
|
|
427
|
+
}
|
|
428
|
+
declare class CommandSender {
|
|
429
|
+
private readonly connector;
|
|
430
|
+
private readonly opts;
|
|
431
|
+
private identified;
|
|
432
|
+
private identifying?;
|
|
433
|
+
private _ready;
|
|
434
|
+
private retryTimer?;
|
|
435
|
+
private retryDelayMs;
|
|
436
|
+
private readonly log;
|
|
437
|
+
constructor(connector: Connector, opts: CommandSenderOptions);
|
|
438
|
+
/** true only when the bus is connected AND identified — gate remote-control affordances on this. */
|
|
439
|
+
get ready(): boolean;
|
|
440
|
+
/** Open the agent connection and identify once. Call alongside the app's main session start. */
|
|
441
|
+
start(): Promise<void>;
|
|
442
|
+
/** Close the agent connection (call on app teardown so the socket doesn't linger). */
|
|
443
|
+
stop(): void;
|
|
444
|
+
/** Broadcast a UI-only command to the other FilaMind surfaces. Best-effort: a command issued
|
|
445
|
+
* while the bus is down is dropped (logged), never queued/replayed — a stale "navigate" fired
|
|
446
|
+
* minutes later would yank a screen unexpectedly. */
|
|
447
|
+
send(cmd: RemoteCommand): Promise<void>;
|
|
448
|
+
navigate(view: RemoteView): Promise<void>;
|
|
449
|
+
message(level: RemoteMessageLevel, text: string): Promise<void>;
|
|
450
|
+
locate(): Promise<void>;
|
|
451
|
+
/** Identify as an agent once per connection; single-flight so concurrent sends don't double-identify. */
|
|
452
|
+
private ensureIdentified;
|
|
453
|
+
private setIdentified;
|
|
454
|
+
/** Self-heal a transient identify failure while the socket stays up (Moonraker busy during a
|
|
455
|
+
* klippy/print transition can time out identify without dropping the connection). Bounded backoff;
|
|
456
|
+
* a genuine reconnect re-drives identify on its own, so we only retry while state is 'ready'. */
|
|
457
|
+
private scheduleRetry;
|
|
458
|
+
private clearRetry;
|
|
459
|
+
/** Recompute readiness (connected + identified) and notify on change. */
|
|
460
|
+
private refreshReady;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
declare const SETTINGS_VERSION = 1;
|
|
464
|
+
interface UserSettings {
|
|
465
|
+
/** schema version (for migration) */
|
|
466
|
+
version: number;
|
|
467
|
+
theme: ThemeName;
|
|
468
|
+
locale: string;
|
|
469
|
+
density: 'comfortable' | 'compact';
|
|
470
|
+
/** Pharaonic motif density (§10.4) */
|
|
471
|
+
motifDensity: 'off' | 'subtle' | 'full';
|
|
472
|
+
/** reduce animation/illustration for low-power or accessibility */
|
|
473
|
+
reducedMotion: boolean;
|
|
474
|
+
}
|
|
475
|
+
declare const DEFAULT_SETTINGS: UserSettings;
|
|
476
|
+
/** Coerce an arbitrary (old / foreign / partial) blob into valid UserSettings — drops unknown keys,
|
|
477
|
+
* validates every enum, stamps the current version. Used by hydrate() + import(). */
|
|
478
|
+
declare function migrate(raw: unknown): UserSettings;
|
|
479
|
+
/** App-provided persistence — the Moonraker-DB impl is keyed by machineUUID; tests use memory. */
|
|
480
|
+
interface SettingsPersistence {
|
|
481
|
+
load(): Promise<Partial<UserSettings>>;
|
|
482
|
+
save(settings: UserSettings): Promise<void>;
|
|
483
|
+
}
|
|
484
|
+
declare function memoryPersistence(): SettingsPersistence;
|
|
485
|
+
declare function localStoragePersistence(storageKey?: string): SettingsPersistence;
|
|
486
|
+
/** Settings persisted in the printer's Moonraker database, so they roam across every FilaMind
|
|
487
|
+
* surface on that printer (3d, screen, …) — the F10 remote-config substrate. */
|
|
488
|
+
declare function moonrakerDbPersistence(connector: Connector): SettingsPersistence;
|
|
489
|
+
/** Roam a (locally-persisted) SettingsStore across surfaces via a shared remote store: pull the
|
|
490
|
+
* shared settings whenever the connection goes live (the shared copy wins on connect), and
|
|
491
|
+
* write local changes back. Lets one surface (e.g. FilaMind 3d) reconfigure another (the screen). */
|
|
492
|
+
declare function roamSettings(store: SettingsStore, remote: SettingsPersistence, live: Observable<boolean>): void;
|
|
493
|
+
declare class SettingsStore {
|
|
494
|
+
private readonly persistence;
|
|
495
|
+
readonly settings: Observable<UserSettings>;
|
|
496
|
+
constructor(persistence?: SettingsPersistence, initial?: Partial<UserSettings>);
|
|
497
|
+
get value(): UserSettings;
|
|
498
|
+
/** Load persisted settings over the defaults (e.g. on app start). */
|
|
499
|
+
hydrate(): Promise<void>;
|
|
500
|
+
/** Change one or more settings + persist. */
|
|
501
|
+
patch(p: Partial<UserSettings>): void;
|
|
502
|
+
reset(): void;
|
|
503
|
+
export(): string;
|
|
504
|
+
import(json: string): void;
|
|
505
|
+
}
|
|
506
|
+
/** Apply theme to the DOM and return the text direction the app should set for the locale. */
|
|
507
|
+
declare function applySettings(s: UserSettings, el?: {
|
|
508
|
+
style: {
|
|
509
|
+
setProperty(p: string, v: string): void;
|
|
510
|
+
};
|
|
511
|
+
}): {
|
|
512
|
+
dir: 'ltr' | 'rtl';
|
|
513
|
+
};
|
|
514
|
+
|
|
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 };
|