@fraqjs/fraq 0.1.0 → 0.2.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/README.md +57 -1
- package/dist/index.d.mts +50 -23
- package/dist/index.mjs +76 -45
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,3 +1,59 @@
|
|
|
1
1
|
# Fraq
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Fraq 是一个 [Milky](https://milky.ntqqrev.org/) 协议的聊天机器人框架,在协议之上提供了更方便的上下文、路由、消息构造、插件和服务依赖注入能力,让你可以用 TypeScript 更轻松地组织一个机器人应用。
|
|
4
|
+
|
|
5
|
+
## 安装 Fraq
|
|
6
|
+
|
|
7
|
+
可以通过任意的包管理器安装 Fraq:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @fraqjs/fraq
|
|
11
|
+
# or
|
|
12
|
+
yarn add @fraqjs/fraq
|
|
13
|
+
# or
|
|
14
|
+
pnpm add @fraqjs/fraq
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Fraq 需要在 Node.js 22+ 环境(或其他的支持 WebSocket API 的服务端 JS 环境)中运行。强烈建议使用 TypeScript 来编写 Fraq 代码,并使用 [tsx](https://www.npmjs.com/package/tsx) 来运行你的项目。
|
|
18
|
+
|
|
19
|
+
## 第一个 Fraq 机器人
|
|
20
|
+
|
|
21
|
+
我们从一个 Echo 机器人开始。首先,启动你的 Milky 协议端,并确认它监听的地址与端口。然后,在你的项目中创建一个 `index.ts` 文件,并添加以下代码:
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { Context, msg, param } from '@fraqjs/fraq';
|
|
25
|
+
|
|
26
|
+
const ctx = Context.create({
|
|
27
|
+
connect: {
|
|
28
|
+
baseUrl: '<替换成你的 Milky 协议端地址>',
|
|
29
|
+
// accessToken: '<如果你的 Milky 协议端配置了 token,请在这里添加>',
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
ctx.router.command(
|
|
34
|
+
'echo',
|
|
35
|
+
{
|
|
36
|
+
content: param.greedy(),
|
|
37
|
+
},
|
|
38
|
+
(session, { content }) => {
|
|
39
|
+
session.reply(msg`You said: ${content}`);
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
ctx.start();
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
在一切完成之后,用 `tsx` 执行你的 `index.ts` 文件:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npx tsx index.ts
|
|
50
|
+
# 或 tsx index.ts, 如果你全局安装了 tsx
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
你可以在任何一个包含它的会话中发送 `echo Hello` 来测试它是否正常工作。
|
|
54
|
+
|
|
55
|
+
## 下一步
|
|
56
|
+
|
|
57
|
+
上面的例子只是 Fraq 的最小使用方式,你还可以继续了解如何构造更丰富的消息、定义更复杂的指令参数、编写可复用的插件等。
|
|
58
|
+
|
|
59
|
+
完整文档请见 [fraq.ntqqrev.org](https://fraq.ntqqrev.org/)。
|
package/dist/index.d.mts
CHANGED
|
@@ -5526,13 +5526,32 @@ type Filter = { [K in keyof EventMap]?: (event: EventMap[K]) => boolean };
|
|
|
5526
5526
|
declare namespace filter {
|
|
5527
5527
|
function define(filter: Filter): Filter;
|
|
5528
5528
|
function allPass(): Filter;
|
|
5529
|
-
function friend(uinList: number[]): Filter;
|
|
5530
|
-
function group(uinList: number[]): Filter;
|
|
5529
|
+
function friend(...uinList: number[]): Filter;
|
|
5530
|
+
function group(...uinList: number[]): Filter;
|
|
5531
5531
|
function admin(): Filter;
|
|
5532
5532
|
function or(...filters: Filter[]): Filter;
|
|
5533
5533
|
function and(...filters: Filter[]): Filter;
|
|
5534
5534
|
}
|
|
5535
5535
|
//#endregion
|
|
5536
|
+
//#region src/core/logging.d.ts
|
|
5537
|
+
interface LogMessage {
|
|
5538
|
+
level: 'debug' | 'info' | 'warn' | 'error';
|
|
5539
|
+
module: string;
|
|
5540
|
+
message: string;
|
|
5541
|
+
error?: unknown;
|
|
5542
|
+
time: number;
|
|
5543
|
+
}
|
|
5544
|
+
declare class Logger {
|
|
5545
|
+
private readonly logHandler;
|
|
5546
|
+
private readonly module;
|
|
5547
|
+
constructor(logHandler: (message: LogMessage) => void, module: string);
|
|
5548
|
+
private log;
|
|
5549
|
+
debug(message: string): void;
|
|
5550
|
+
info(message: string): void;
|
|
5551
|
+
warn(message: string, error?: unknown): void;
|
|
5552
|
+
error(message: string, error?: unknown): void;
|
|
5553
|
+
}
|
|
5554
|
+
//#endregion
|
|
5536
5555
|
//#region src/core/service.d.ts
|
|
5537
5556
|
/** biome-ignore-all lint/suspicious/noExplicitAny: Service constructors may accept any arguments. */
|
|
5538
5557
|
type ServiceClass<T extends object = object> = abstract new (...args: any[]) => T;
|
|
@@ -5540,21 +5559,35 @@ type ServiceClass<T extends object = object> = abstract new (...args: any[]) =>
|
|
|
5540
5559
|
//#region src/core/plugin.d.ts
|
|
5541
5560
|
type ParameterList = Array<any>;
|
|
5542
5561
|
interface Plugin<T extends ParameterList> {
|
|
5562
|
+
name: string;
|
|
5543
5563
|
requires?: readonly ServiceClass[];
|
|
5544
5564
|
provides?: readonly ServiceClass[];
|
|
5545
|
-
apply(
|
|
5565
|
+
apply(ctx: Context, ...args: T): void | Promise<void>;
|
|
5546
5566
|
}
|
|
5547
5567
|
declare function definePlugin<T extends ParameterList>(plugin: Plugin<T>): Plugin<T>;
|
|
5548
5568
|
//#endregion
|
|
5549
5569
|
//#region src/core/context.d.ts
|
|
5570
|
+
interface ContextOptions {
|
|
5571
|
+
connect: {
|
|
5572
|
+
baseUrl: string;
|
|
5573
|
+
accessToken?: string;
|
|
5574
|
+
initialReconnectDelay?: number;
|
|
5575
|
+
maxReconnectDelay?: number;
|
|
5576
|
+
};
|
|
5577
|
+
logHandler?: (message: LogMessage) => void;
|
|
5578
|
+
}
|
|
5550
5579
|
declare class Context {
|
|
5551
5580
|
readonly client: MilkyClient;
|
|
5552
|
-
private readonly parent?;
|
|
5553
5581
|
readonly router: Router;
|
|
5582
|
+
readonly logger: Logger;
|
|
5583
|
+
private readonly parent?;
|
|
5554
5584
|
private readonly eventBus;
|
|
5555
5585
|
private readonly plugins;
|
|
5556
5586
|
private readonly services;
|
|
5557
5587
|
private readonly subContexts;
|
|
5588
|
+
private readonly initialReconnectDelayMs;
|
|
5589
|
+
private readonly maxReconnectDelayMs;
|
|
5590
|
+
private readonly logHandler?;
|
|
5558
5591
|
private isStarted;
|
|
5559
5592
|
private constructor();
|
|
5560
5593
|
on<K extends keyof EventMap>(type: K, handler: (event: EventMap[K]) => void | Promise<void>): void;
|
|
@@ -5567,21 +5600,12 @@ declare class Context {
|
|
|
5567
5600
|
start(): Promise<void>;
|
|
5568
5601
|
private applyPlugins;
|
|
5569
5602
|
private sortPlugins;
|
|
5570
|
-
private validateUniqueProvidedServices;
|
|
5571
5603
|
private collectAvailableServices;
|
|
5572
|
-
private areRequirementsAvailable;
|
|
5573
5604
|
private createUnresolvablePluginError;
|
|
5574
|
-
private
|
|
5605
|
+
private createProxyContextForPlugin;
|
|
5575
5606
|
private connectEventWebSocket;
|
|
5576
|
-
private handleWebSocketMessage;
|
|
5577
|
-
private delay;
|
|
5578
5607
|
private createSession;
|
|
5579
|
-
static create(options:
|
|
5580
|
-
connect: {
|
|
5581
|
-
baseUrl: string;
|
|
5582
|
-
accessToken?: string;
|
|
5583
|
-
};
|
|
5584
|
-
}): Context;
|
|
5608
|
+
static create(options: ContextOptions): Context;
|
|
5585
5609
|
}
|
|
5586
5610
|
//#endregion
|
|
5587
5611
|
//#region src/protocol/segment.d.ts
|
|
@@ -5625,24 +5649,27 @@ declare namespace seg {
|
|
|
5625
5649
|
}
|
|
5626
5650
|
//#endregion
|
|
5627
5651
|
export {
|
|
5628
|
-
|
|
5652
|
+
ApiEndpoints,
|
|
5629
5653
|
Context,
|
|
5630
|
-
|
|
5631
|
-
|
|
5632
|
-
|
|
5654
|
+
ContextOptions,
|
|
5655
|
+
EventMap,
|
|
5656
|
+
Filter,
|
|
5657
|
+
LogMessage,
|
|
5658
|
+
Logger,
|
|
5659
|
+
MilkyClient,
|
|
5633
5660
|
Parameter,
|
|
5634
|
-
|
|
5635
|
-
|
|
5661
|
+
ParameterList,
|
|
5662
|
+
Plugin,
|
|
5636
5663
|
Router,
|
|
5637
5664
|
type ServiceClass,
|
|
5638
|
-
|
|
5665
|
+
Session,
|
|
5639
5666
|
createMilkyClient,
|
|
5640
5667
|
definePlugin,
|
|
5641
5668
|
filter,
|
|
5669
|
+
type types_d_exports as milky,
|
|
5642
5670
|
milkyPackageVersion,
|
|
5643
5671
|
milkyVersion,
|
|
5644
5672
|
msg,
|
|
5645
5673
|
param,
|
|
5646
5674
|
seg,
|
|
5647
|
-
type types_d_exports as types,
|
|
5648
5675
|
};
|
package/dist/index.mjs
CHANGED
|
@@ -246,25 +246,63 @@ var Router = class Router {
|
|
|
246
246
|
}
|
|
247
247
|
};
|
|
248
248
|
//#endregion
|
|
249
|
+
//#region src/core/logging.ts
|
|
250
|
+
var Logger = class {
|
|
251
|
+
logHandler;
|
|
252
|
+
module;
|
|
253
|
+
constructor(logHandler, module) {
|
|
254
|
+
this.logHandler = logHandler;
|
|
255
|
+
this.module = module;
|
|
256
|
+
}
|
|
257
|
+
log(level, message, error) {
|
|
258
|
+
this.logHandler({
|
|
259
|
+
level,
|
|
260
|
+
module: this.module,
|
|
261
|
+
message,
|
|
262
|
+
error,
|
|
263
|
+
time: Date.now()
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
debug(message) {
|
|
267
|
+
this.log("debug", message);
|
|
268
|
+
}
|
|
269
|
+
info(message) {
|
|
270
|
+
this.log("info", message);
|
|
271
|
+
}
|
|
272
|
+
warn(message, error) {
|
|
273
|
+
this.log("warn", message, error);
|
|
274
|
+
}
|
|
275
|
+
error(message, error) {
|
|
276
|
+
this.log("error", message, error);
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
//#endregion
|
|
249
280
|
//#region src/core/service.ts
|
|
250
281
|
function getServiceName(service) {
|
|
251
282
|
return service.name || "<anonymous service>";
|
|
252
283
|
}
|
|
253
284
|
//#endregion
|
|
254
285
|
//#region src/core/context.ts
|
|
255
|
-
const
|
|
256
|
-
const
|
|
286
|
+
const DEFAULT_INITIAL_RECONNECT_DELAY_MS = 1e3;
|
|
287
|
+
const DEFAULT_MAX_RECONNECT_DELAY_MS = 3e4;
|
|
257
288
|
var Context = class Context {
|
|
258
289
|
client;
|
|
259
|
-
parent;
|
|
260
290
|
router = new Router();
|
|
291
|
+
logger = new Logger((message) => this.logHandler?.(message), "context");
|
|
292
|
+
parent;
|
|
261
293
|
eventBus = mitt();
|
|
262
294
|
plugins = [];
|
|
263
295
|
services = /* @__PURE__ */ new Map();
|
|
264
296
|
subContexts = [];
|
|
297
|
+
initialReconnectDelayMs;
|
|
298
|
+
maxReconnectDelayMs;
|
|
299
|
+
logHandler;
|
|
265
300
|
isStarted = false;
|
|
266
|
-
constructor(client, parent, filter) {
|
|
301
|
+
constructor(client, options, parent, filter) {
|
|
267
302
|
this.client = client;
|
|
303
|
+
this.initialReconnectDelayMs = options?.connect.initialReconnectDelay ?? DEFAULT_INITIAL_RECONNECT_DELAY_MS;
|
|
304
|
+
this.maxReconnectDelayMs = options?.connect.maxReconnectDelay ?? DEFAULT_MAX_RECONNECT_DELAY_MS;
|
|
305
|
+
this.logHandler = options?.logHandler ?? parent?.logHandler;
|
|
268
306
|
this.parent = parent;
|
|
269
307
|
parent?.eventBus.on("*", (type, event) => {
|
|
270
308
|
if (filter) {
|
|
@@ -313,7 +351,7 @@ var Context = class Context {
|
|
|
313
351
|
return this.tryResolve(service) !== void 0;
|
|
314
352
|
}
|
|
315
353
|
fork(filter) {
|
|
316
|
-
const subContext = new Context(this.client, this, filter);
|
|
354
|
+
const subContext = new Context(this.client, void 0, this, filter);
|
|
317
355
|
this.subContexts.push(subContext);
|
|
318
356
|
return subContext;
|
|
319
357
|
}
|
|
@@ -327,18 +365,23 @@ var Context = class Context {
|
|
|
327
365
|
async applyPlugins() {
|
|
328
366
|
for (const { plugin, args } of this.sortPlugins()) {
|
|
329
367
|
const providedBeforeApply = new Set(this.services.keys());
|
|
330
|
-
await plugin.apply(this, ...args);
|
|
331
|
-
for (const service of plugin.provides ?? []) if (!this.services.has(service) || providedBeforeApply.has(service)) throw new Error(`${
|
|
368
|
+
await plugin.apply(this.createProxyContextForPlugin(plugin), ...args);
|
|
369
|
+
for (const service of plugin.provides ?? []) if (!this.services.has(service) || providedBeforeApply.has(service)) throw new Error(`${plugin.name} declares service ${getServiceName(service)} but did not provide it.`);
|
|
332
370
|
}
|
|
333
371
|
}
|
|
334
372
|
sortPlugins() {
|
|
335
|
-
|
|
373
|
+
const providers = /* @__PURE__ */ new Map();
|
|
374
|
+
for (const { plugin } of this.plugins) for (const service of plugin.provides ?? []) {
|
|
375
|
+
const existingProvider = providers.get(service);
|
|
376
|
+
if (existingProvider) throw new Error(`Service ${getServiceName(service)} is declared by multiple plugins: ${existingProvider.name} and ${plugin.name}.`);
|
|
377
|
+
providers.set(service, plugin);
|
|
378
|
+
}
|
|
336
379
|
const pending = [...this.plugins];
|
|
337
380
|
const sorted = [];
|
|
338
381
|
const available = /* @__PURE__ */ new Set();
|
|
339
382
|
for (const service of this.collectAvailableServices()) available.add(service);
|
|
340
383
|
while (pending.length > 0) {
|
|
341
|
-
const nextIndex = pending.findIndex(({ plugin }) =>
|
|
384
|
+
const nextIndex = pending.findIndex(({ plugin }) => (plugin.requires ?? []).every((service) => available.has(service)));
|
|
342
385
|
if (nextIndex === -1) throw this.createUnresolvablePluginError(pending, available);
|
|
343
386
|
const [next] = pending.splice(nextIndex, 1);
|
|
344
387
|
sorted.push(next);
|
|
@@ -346,22 +389,11 @@ var Context = class Context {
|
|
|
346
389
|
}
|
|
347
390
|
return sorted;
|
|
348
391
|
}
|
|
349
|
-
validateUniqueProvidedServices() {
|
|
350
|
-
const providers = /* @__PURE__ */ new Map();
|
|
351
|
-
for (const { plugin } of this.plugins) for (const service of plugin.provides ?? []) {
|
|
352
|
-
const existingProvider = providers.get(service);
|
|
353
|
-
if (existingProvider) throw new Error(`Service ${getServiceName(service)} is declared by multiple plugins: ${this.describePlugin(existingProvider)} and ${this.describePlugin(plugin)}.`);
|
|
354
|
-
providers.set(service, plugin);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
392
|
collectAvailableServices() {
|
|
358
393
|
const services = [...this.services.keys()];
|
|
359
394
|
if (this.parent) services.push(...this.parent.collectAvailableServices());
|
|
360
395
|
return services;
|
|
361
396
|
}
|
|
362
|
-
areRequirementsAvailable(plugin, available) {
|
|
363
|
-
return (plugin.requires ?? []).every((service) => available.has(service));
|
|
364
|
-
}
|
|
365
397
|
createUnresolvablePluginError(pending, available) {
|
|
366
398
|
const missingRequirements = /* @__PURE__ */ new Map();
|
|
367
399
|
const pendingProviders = /* @__PURE__ */ new Set();
|
|
@@ -372,44 +404,43 @@ var Context = class Context {
|
|
|
372
404
|
missingRequirements.set(service, plugins);
|
|
373
405
|
}
|
|
374
406
|
const lines = [...missingRequirements].map(([service, plugins]) => {
|
|
375
|
-
const dependents = plugins.map((plugin) =>
|
|
407
|
+
const dependents = plugins.map((plugin) => plugin.name).join(", ");
|
|
376
408
|
const reason = pendingProviders.has(service) ? "blocked by a dependency cycle" : "no installed plugin provides it";
|
|
377
409
|
return `${getServiceName(service)} required by ${dependents} (${reason})`;
|
|
378
410
|
});
|
|
379
411
|
return /* @__PURE__ */ new Error(`Unable to resolve plugin service dependencies: ${lines.join("; ")}.`);
|
|
380
412
|
}
|
|
381
|
-
|
|
382
|
-
const
|
|
383
|
-
return
|
|
413
|
+
createProxyContextForPlugin(plugin) {
|
|
414
|
+
const proxyLogger = new Logger((message) => this.logHandler?.(message), plugin.name);
|
|
415
|
+
return new Proxy(this, { get(target, prop) {
|
|
416
|
+
if (prop === "logger") return proxyLogger;
|
|
417
|
+
else return target[prop];
|
|
418
|
+
} });
|
|
384
419
|
}
|
|
385
420
|
async connectEventWebSocket() {
|
|
386
|
-
let reconnectDelay =
|
|
421
|
+
let reconnectDelay = this.initialReconnectDelayMs;
|
|
387
422
|
while (this.isStarted) {
|
|
388
423
|
try {
|
|
389
|
-
const ws = await this.client.openEventWebSocket((event) =>
|
|
390
|
-
|
|
424
|
+
const ws = await this.client.openEventWebSocket((event) => {
|
|
425
|
+
try {
|
|
426
|
+
if (typeof event.data !== "string") throw new Error(`Expected text frame, got ${typeof event.data}`);
|
|
427
|
+
const payload = JSON.parse(event.data);
|
|
428
|
+
this.eventBus.emit(payload.event_type, payload);
|
|
429
|
+
} catch (error) {
|
|
430
|
+
console.error("Error handling WebSocket event:", error);
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
reconnectDelay = this.initialReconnectDelayMs;
|
|
391
434
|
await new Promise((resolve) => {
|
|
392
435
|
ws.addEventListener("close", () => resolve(), { once: true });
|
|
393
436
|
});
|
|
394
437
|
} catch (error) {
|
|
395
438
|
console.error("Error connecting event WebSocket:", error);
|
|
396
439
|
}
|
|
397
|
-
await
|
|
398
|
-
reconnectDelay = Math.min(reconnectDelay * 2,
|
|
440
|
+
await new Promise((resolve) => setTimeout(resolve, reconnectDelay));
|
|
441
|
+
reconnectDelay = Math.min(reconnectDelay * 2, this.maxReconnectDelayMs);
|
|
399
442
|
}
|
|
400
443
|
}
|
|
401
|
-
handleWebSocketMessage(event) {
|
|
402
|
-
try {
|
|
403
|
-
if (typeof event.data !== "string") throw new Error(`Expected text frame, got ${typeof event.data}`);
|
|
404
|
-
const payload = JSON.parse(event.data);
|
|
405
|
-
this.eventBus.emit(payload.event_type, payload);
|
|
406
|
-
} catch (error) {
|
|
407
|
-
console.error("Error handling WebSocket event:", error);
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
delay(ms) {
|
|
411
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
412
|
-
}
|
|
413
444
|
createSession(message) {
|
|
414
445
|
return {
|
|
415
446
|
raw: message,
|
|
@@ -436,7 +467,7 @@ var Context = class Context {
|
|
|
436
467
|
};
|
|
437
468
|
}
|
|
438
469
|
static create(options) {
|
|
439
|
-
return new Context(createMilkyClient(options.connect.baseUrl, { accessToken: options.connect.accessToken }));
|
|
470
|
+
return new Context(createMilkyClient(options.connect.baseUrl, { accessToken: options.connect.accessToken }), options);
|
|
440
471
|
}
|
|
441
472
|
};
|
|
442
473
|
//#endregion
|
|
@@ -476,7 +507,7 @@ let filter;
|
|
|
476
507
|
return result;
|
|
477
508
|
}
|
|
478
509
|
_filter.allPass = allPass;
|
|
479
|
-
function friend(uinList) {
|
|
510
|
+
function friend(...uinList) {
|
|
480
511
|
const uinSet = new Set(uinList);
|
|
481
512
|
return {
|
|
482
513
|
message_receive: ({ data }) => data.message_scene === "friend" && uinSet.has(data.peer_id),
|
|
@@ -488,7 +519,7 @@ let filter;
|
|
|
488
519
|
};
|
|
489
520
|
}
|
|
490
521
|
_filter.friend = friend;
|
|
491
|
-
function group(uinList) {
|
|
522
|
+
function group(...uinList) {
|
|
492
523
|
const uinSet = new Set(uinList);
|
|
493
524
|
return {
|
|
494
525
|
message_receive: ({ data }) => data.message_scene === "group" && uinSet.has(data.peer_id),
|
|
@@ -774,4 +805,4 @@ let param;
|
|
|
774
805
|
_param.segment = segment;
|
|
775
806
|
})(param || (param = {}));
|
|
776
807
|
//#endregion
|
|
777
|
-
export { Context, Parameter, Router, createMilkyClient, definePlugin, filter, milkyPackageVersion, milkyVersion, msg, param, seg };
|
|
808
|
+
export { Context, Logger, Parameter, Router, createMilkyClient, definePlugin, filter, milkyPackageVersion, milkyVersion, msg, param, seg };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fraqjs/fraq",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"description": "Milky Bot framework",
|
|
6
6
|
"files": [
|
|
7
7
|
"dist"
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"type": "git",
|
|
15
15
|
"url": "git+https://github.com/Wesley-Young/fraq.git"
|
|
16
16
|
},
|
|
17
|
+
"homepage": "https://fraq.ntqqrev.org/",
|
|
17
18
|
"license": "MIT",
|
|
18
19
|
"dependencies": {
|
|
19
20
|
"mitt": "^3.0.1"
|