@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 +2 -0
- package/dist/index.d.ts +112 -1
- package/dist/index.js +130 -2
- package/package.json +1 -1
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
|
|
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
|
|
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.
|
|
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": {
|