@letterstory/cli 0.25.4 → 0.25.6

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/lib/cli.mjs CHANGED
@@ -44,7 +44,7 @@ import {
44
44
  } from "./commands.mjs";
45
45
 
46
46
  // Keep in sync with cli/package.json.
47
- export const VERSION = "0.25.4";
47
+ export const VERSION = "0.25.6";
48
48
 
49
49
  // Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
50
50
  // can't accidentally swallow the id as --json's value.
@@ -455,12 +455,22 @@ Seers (event-driven signals -> drafts):
455
455
  seers list List seers with recent activity
456
456
  seers get <seer-id> One seer's full config + poll state
457
457
  seers providers Provider catalog: config fields + behavior keys
458
- seers create --name <n> --collection <uuid> --provider <p>
458
+ seers create --name <n> --provider <p> [--collection <uuid>]
459
459
  [--mode suggest|auto] [--interval <min>] [--config '<json>'|--config-file <path>]
460
- seers update <seer-id> [--name] [--collection] [--mode] [--enabled true|false]
461
- [--interval <min>] [--config '<json>'|--config-file <path>]
460
+ --collection is optional: omit it for a
461
+ detect-only seer (scans + judges, never
462
+ produces). --mode auto requires one.
463
+ seers update <seer-id> [--name] [--collection <uuid> | --clear-collection] [--mode]
464
+ [--enabled true|false] [--interval <min>] [--config '<json>'|--config-file <path>]
462
465
  seers delete <seer-id> --yes
463
- seers run <seer-id> Poll + produce now (can take minutes; spends AI budget)
466
+ seers run <seer-id> [--dry-run] [--no-produce] [--async]
467
+ Run now — THE DOOR (one seer run from the balance).
468
+ --dry-run: scan + judge, persist nothing, print signals
469
+ --no-produce: detect + record events only
470
+ --async: queue for the heartbeat, print a run id
471
+ seers run-status <run-id> One run: status, counts, constraints, verification
472
+ seers runs [--seer <seer-id>] [--limit <n>] Recent runs (origin, caller, verification)
473
+ seers balance Seer runs granted / used / available
464
474
  seers events <seer-id> [--limit <n>] [--offset <n>] Detected signals w/ dedup + relevance
465
475
  seers dismiss <seer-id> <event-id> Dismiss a signal (feeds the relevance judge)
466
476
  seers headlines [--collection <uuid>] Seer-sourced suggestion feed
@@ -7,12 +7,24 @@
7
7
  // Connections are both headless: `seers connect github --token` (a PAT) and
8
8
  // `seers connect notion --token` (a Notion internal-integration secret; run
9
9
  // `seers connect notion` with no token for how to mint one and which pages to share).
10
- // One thing to know: `seers run` executes synchronously server-side (news/regulation
11
- // web scans can take minutes) and spends model budget — it is usage-gated per org.
10
+ // `seers run` is THE DOOR (lib/seers/run.ts): a direct, metered run (`seers balance`).
11
+ // Synchronous by default — news/regulation web scans can take minutes; pass --async to
12
+ // queue it and poll with `seers run-status`, --dry-run to see what it would record
13
+ // without recording anything, --no-produce to detect and record events only.
12
14
 
13
15
  import { readFileSync, writeFileSync } from "node:fs";
14
16
  import { CliError } from "../client.mjs";
15
- import { flagStr, flagNum, flagBool, requirePositional, printResult, compact, ok, argsFromFlags } from "./shared.mjs";
17
+ import { flagStr, flagBool, requirePositional, printResult, ok, argsFromFlags } from "./shared.mjs";
18
+
19
+ // A single-value flag the API clears with an explicit `null` (same idiom as
20
+ // strategy.mjs's clearableList): `--clear-collection` sends null, `--collection <uuid>`
21
+ // sends it, neither sends nothing (no change).
22
+ const clearableValue = (flag, clearFlag) => ({
23
+ value: (flags) => {
24
+ if (flags[clearFlag]) return null;
25
+ return flagStr(flags[flag]);
26
+ },
27
+ });
16
28
 
17
29
  /**
18
30
  * How the ratcheted commands in this file source their tool inputs — see `argsFromFlags` in
@@ -20,11 +32,93 @@ import { flagStr, flagNum, flagBool, requirePositional, printResult, compact, ok
20
32
  * ratchet and stays in UNMAPPED_TOOLS; that list only shrinks.
21
33
  */
22
34
  export const ARGS = {
35
+ // THE DOOR. `seers run <id>` — a direct, metered run. `--dry-run` scans + judges and
36
+ // persists nothing (returns the signals it would have recorded, with verdicts);
37
+ // `--no-produce` detects and records events only; `--async` queues it and prints a
38
+ // run id to poll with `seers run-status`.
23
39
  run_seer: {
24
40
  seer_id: { positional: 0, label: "seer-id", required: true },
41
+ dry_run: { flag: "dry-run", type: "bool" },
42
+ produce: { value: (flags) => (flags["no-produce"] === true ? false : undefined) },
43
+ async: { flag: "async", type: "bool" },
44
+ },
45
+ get_seer_run: {
46
+ run_id: { positional: 0, label: "run-id", required: true },
47
+ },
48
+ list_seer_runs: {
49
+ seer_id: { flag: "seer" },
50
+ limit: { flag: "limit", type: "num" },
51
+ },
52
+ get_seer_run_balance: {},
53
+ list_seers: {},
54
+ get_seer: {
55
+ seer_id: { positional: 0, label: "seer-id", required: true },
56
+ },
57
+ create_seer: {
58
+ name: { flag: "name", required: true },
59
+ // Optional (2026-09-18): omit it for a detect-only seer — it can scan and judge
60
+ // forever but never produce (auto mode is rejected without one).
61
+ collection_id: { flag: "collection" },
62
+ provider: { flag: "provider", required: true },
63
+ mode: { flag: "mode", enum: ["suggest", "auto"] },
64
+ poll_interval_minutes: { flag: "interval", type: "num" },
65
+ // The whole config document — provider source params plus the behavior keys
66
+ // (auto_draft_scope, template_key, mention_degree, kernel_family, relevance{…}).
67
+ config: { value: (flags) => readJsonObjectFlag(flags, "config") ?? {} },
68
+ },
69
+ update_seer: {
70
+ seer_id: { positional: 0, label: "seer-id", required: true },
71
+ name: { flag: "name" },
72
+ // --collection <uuid> sets it, --clear-collection sends null (the seer becomes
73
+ // detect-only — rejected if it's mode "auto"), neither leaves it unchanged.
74
+ collection_id: clearableValue("collection", "clear-collection"),
75
+ mode: { flag: "mode", enum: ["suggest", "auto"] },
76
+ enabled: { value: (flags) => readEnabledFlag(flags) },
77
+ poll_interval_minutes: { flag: "interval", type: "num" },
78
+ config: { value: (flags) => readJsonObjectFlag(flags, "config") },
79
+ },
80
+ delete_seer: {
81
+ seer_id: { positional: 0, label: "seer-id", required: true },
82
+ },
83
+ list_seer_events: {
84
+ seer_id: { positional: 0, label: "seer-id", required: true },
85
+ limit: { flag: "limit", type: "num" },
86
+ offset: { flag: "offset", type: "num" },
87
+ },
88
+ dismiss_seer_event: {
89
+ seer_id: { positional: 0, label: "seer-id", required: true },
90
+ event_id: { positional: 1, label: "event-id", required: true },
91
+ },
92
+ list_seer_headlines: {
93
+ collection_id: { flag: "collection" },
94
+ limit: { flag: "limit", type: "num" },
95
+ offset: { flag: "offset", type: "num" },
96
+ },
97
+ list_held_drafts: {
98
+ collection_id: { flag: "collection" },
99
+ },
100
+ resolve_held_draft: {
101
+ article_id: { positional: 0, label: "article-id", required: true },
102
+ // `seers release <id>` / `seers discard <id>` — the subcommand IS the action.
103
+ action: { value: (_flags, _positionals, extra) => extra.action },
104
+ },
105
+ list_seer_providers: {},
106
+ get_seer_connections: {},
107
+ set_github_connection: {
108
+ access_token: { flag: "token", required: true },
109
+ },
110
+ set_notion_connection: {
111
+ access_token: { flag: "token", required: true },
25
112
  },
26
113
  };
27
114
 
115
+ function readEnabledFlag(flags) {
116
+ const enabledRaw = flagStr(flags.enabled);
117
+ if (enabledRaw === undefined) return undefined;
118
+ if (enabledRaw !== "true" && enabledRaw !== "false") throw new CliError('--enabled must be "true" or "false"');
119
+ return enabledRaw === "true";
120
+ }
121
+
28
122
  export async function cmdSeers(ctx) {
29
123
  const sub = ctx.positionals[0];
30
124
  const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
@@ -45,6 +139,13 @@ export async function cmdSeers(ctx) {
45
139
  return seersDelete(rest);
46
140
  case "run":
47
141
  return seersRun(rest);
142
+ case "run-status":
143
+ case "run-get":
144
+ return seersRunStatus(rest);
145
+ case "runs":
146
+ return seersRuns(rest);
147
+ case "balance":
148
+ return seersBalance(rest);
48
149
  case "events":
49
150
  return seersEvents(rest);
50
151
  case "dismiss":
@@ -69,7 +170,7 @@ export async function cmdSeers(ctx) {
69
170
  return seersApply(rest);
70
171
  default:
71
172
  throw new CliError(
72
- `Unknown seers subcommand: ${sub ?? "(none)"}. Try: list, get, create, update, delete, run, events, dismiss, headlines, held, release, discard, providers, connections, connect, export, apply`
173
+ `Unknown seers subcommand: ${sub ?? "(none)"}. Try: list, get, create, update, delete, run, run-status, runs, balance, events, dismiss, headlines, held, release, discard, providers, connections, connect, export, apply`
73
174
  );
74
175
  }
75
176
  }
@@ -113,7 +214,7 @@ function formatSeerLine(s) {
113
214
 
114
215
  async function seersList(ctx) {
115
216
  const { client, flags, io } = ctx;
116
- const result = await client.callTool("list_seers", {});
217
+ const result = await client.callTool("list_seers", argsFromFlags(ARGS.list_seers, flags));
117
218
  printResult(io, flags, result, (r) => {
118
219
  const lines = (r.seers ?? []).map(formatSeerLine);
119
220
  if (!lines.length) lines.push("No seers yet. Create one with `seers create` (see `seers providers`).");
@@ -128,36 +229,15 @@ async function seersList(ctx) {
128
229
 
129
230
  async function seersGet(ctx) {
130
231
  const { client, positionals, flags, io } = ctx;
131
- const seerId = requirePositional(positionals, 0, "seer-id");
132
- const result = await client.callTool("get_seer", { seer_id: seerId });
232
+ const result = await client.callTool("get_seer", argsFromFlags(ARGS.get_seer, flags, positionals));
133
233
  printResult(io, flags, result);
134
234
  return 0;
135
235
  }
136
236
 
137
- function seerWriteArgsFromFlags(flags) {
138
- const enabledRaw = flagStr(flags.enabled);
139
- if (enabledRaw !== undefined && enabledRaw !== "true" && enabledRaw !== "false") {
140
- throw new CliError('--enabled must be "true" or "false"');
141
- }
142
- return compact({
143
- name: flagStr(flags.name),
144
- collection_id: flagStr(flags.collection),
145
- mode: flagStr(flags.mode),
146
- poll_interval_minutes: flagNum(flags.interval),
147
- enabled: enabledRaw === undefined ? undefined : enabledRaw === "true",
148
- config: readJsonObjectFlag(flags, "config"),
149
- });
150
- }
151
-
152
237
  async function seersCreate(ctx) {
153
238
  const { client, flags, io } = ctx;
154
- const args = seerWriteArgsFromFlags(flags);
155
- if (!args.name) throw new CliError("Missing required --name");
156
- if (!args.collection_id) throw new CliError("Missing required --collection <uuid>");
157
- const provider = flagStr(flags.provider);
158
- if (!provider) throw new CliError("Missing required --provider (run `seers providers` for the catalog)");
159
- delete args.enabled; // create has no enabled field; new seers start enabled
160
- const result = await client.callTool("create_seer", { ...args, provider, config: args.config ?? {} });
239
+ const args = argsFromFlags(ARGS.create_seer, flags);
240
+ const result = await client.callTool("create_seer", args);
161
241
  ok(ctx, "Seer created.");
162
242
  printResult(io, flags, result, (r) => formatSeerLine(r.seer));
163
243
  return 0;
@@ -165,12 +245,11 @@ async function seersCreate(ctx) {
165
245
 
166
246
  async function seersUpdate(ctx) {
167
247
  const { client, positionals, flags, io } = ctx;
168
- const seerId = requirePositional(positionals, 0, "seer-id");
169
- const args = seerWriteArgsFromFlags(flags);
170
- if (Object.keys(args).length === 0) {
248
+ const args = argsFromFlags(ARGS.update_seer, flags, positionals);
249
+ if (Object.keys(args).length <= 1) {
171
250
  throw new CliError("Nothing to update — pass --name/--collection/--mode/--enabled/--interval/--config");
172
251
  }
173
- const result = await client.callTool("update_seer", { seer_id: seerId, ...args });
252
+ const result = await client.callTool("update_seer", args);
174
253
  ok(ctx, "Seer updated.");
175
254
  printResult(io, flags, result, (r) => formatSeerLine(r.seer));
176
255
  return 0;
@@ -178,26 +257,84 @@ async function seersUpdate(ctx) {
178
257
 
179
258
  async function seersDelete(ctx) {
180
259
  const { client, positionals, flags, io } = ctx;
181
- const seerId = requirePositional(positionals, 0, "seer-id");
182
260
  if (!flagBool(flags.yes)) throw new CliError("Deleting a seer removes its detected events. Re-run with --yes.");
183
- const result = await client.callTool("delete_seer", { seer_id: seerId });
261
+ const result = await client.callTool("delete_seer", argsFromFlags(ARGS.delete_seer, flags, positionals));
184
262
  ok(ctx, "Seer deleted.");
185
263
  printResult(io, flags, result);
186
264
  return 0;
187
265
  }
188
266
 
267
+ function describeRun(r) {
268
+ const run = r.run ?? r;
269
+ const counts = `Detected ${r.detected ?? run.detected ?? 0}, produced ${r.produced ?? run.produced ?? 0} (auto-drafted ${
270
+ r.auto_drafted ?? run.auto_drafted ?? 0
271
+ }, suggested-only ${r.suggested_only ?? run.suggested_only ?? 0}, dismissed ${r.dismissed ?? run.dismissed ?? 0}).`;
272
+ const lines = [counts];
273
+ if (run.run_id)
274
+ lines.push(
275
+ `Run ${run.run_id} [${run.status}] origin=${run.origin} dry_run=${run.dry_run} produce=${run.produce}`
276
+ );
277
+ if (run.verification?.checks?.length) {
278
+ lines.push(
279
+ "Verification: " +
280
+ run.verification.checks.map((c) => `${c.id}=${c.verdict}${c.detail ? ` (${c.detail})` : ""}`).join("; ")
281
+ );
282
+ }
283
+ if (Array.isArray(run.signals) && run.signals.length) {
284
+ lines.push("", "Signals (dry run — nothing recorded):");
285
+ for (const sig of run.signals) {
286
+ lines.push(
287
+ ` [${sig.relevance?.decision ?? "?"} ${
288
+ typeof sig.relevance?.score === "number" ? sig.relevance.score.toFixed(2) : "-"
289
+ }] ${sig.title}${sig.url ? ` ${sig.url}` : ""}`
290
+ );
291
+ }
292
+ }
293
+ return lines.join("\n");
294
+ }
295
+
189
296
  async function seersRun(ctx) {
190
297
  const { client, positionals, flags, io } = ctx;
191
298
  const args = argsFromFlags(ARGS.run_seer, flags, positionals);
192
- ok(ctx, "Polling now — web-scan providers can take a few minutes…");
299
+ if (!args.async)
300
+ ok(ctx, `${args.dry_run ? "Dry run" : "Polling now"} — web-scan providers can take a few minutes…`);
193
301
  const result = await client.callTool("run_seer", args);
194
- printResult(
195
- io,
196
- flags,
197
- result,
198
- (r) =>
199
- `Detected ${r.detected}, produced ${r.produced} (auto-drafted ${r.auto_drafted}, suggested-only ${r.suggested_only}, dismissed ${r.dismissed}).`
200
- );
302
+ if (result?.status === "queued" && result?.run_id) {
303
+ ok(ctx, `Run queued as ${result.run_id}. Poll it with: ${ctx.bin} seers run-status ${result.run_id}`);
304
+ printResult(io, flags, result);
305
+ return 0;
306
+ }
307
+ printResult(io, flags, result, describeRun);
308
+ return 0;
309
+ }
310
+
311
+ async function seersRunStatus(ctx) {
312
+ const { client, positionals, flags, io } = ctx;
313
+ const result = await client.callTool("get_seer_run", argsFromFlags(ARGS.get_seer_run, flags, positionals));
314
+ printResult(io, flags, result, describeRun);
315
+ return 0;
316
+ }
317
+
318
+ async function seersRuns(ctx) {
319
+ const { client, flags, io } = ctx;
320
+ const result = await client.callTool("list_seer_runs", argsFromFlags(ARGS.list_seer_runs, flags));
321
+ printResult(io, flags, result, (r) => {
322
+ const lines = (r.runs ?? []).map(
323
+ (run) =>
324
+ `${run.run_id} [${run.status}] ${run.created_at} ${run.seer_name ?? run.seer_id ?? "?"} origin=${run.origin}${
325
+ run.dry_run ? " dry" : ""
326
+ } detected=${run.detected ?? "-"} produced=${run.produced ?? "-"}`
327
+ );
328
+ if (!lines.length) lines.push("No seer runs yet.");
329
+ return lines.join("\n");
330
+ });
331
+ return 0;
332
+ }
333
+
334
+ async function seersBalance(ctx) {
335
+ const { client, flags, io } = ctx;
336
+ const result = await client.callTool("get_seer_run_balance", argsFromFlags(ARGS.get_seer_run_balance, flags));
337
+ printResult(io, flags, result, (r) => `Seer runs: ${r.available} available (${r.used} of ${r.granted} used).`);
201
338
  return 0;
202
339
  }
203
340
 
@@ -205,8 +342,7 @@ async function seersRun(ctx) {
205
342
 
206
343
  async function seersEvents(ctx) {
207
344
  const { client, positionals, flags, io } = ctx;
208
- const seerId = requirePositional(positionals, 0, "seer-id");
209
- const args = compact({ seer_id: seerId, limit: flagNum(flags.limit), offset: flagNum(flags.offset) });
345
+ const args = argsFromFlags(ARGS.list_seer_events, flags, positionals);
210
346
  const result = await client.callTool("list_seer_events", args);
211
347
  printResult(io, flags, result, (r) => {
212
348
  const lines = (r.events ?? []).map((e) => {
@@ -223,9 +359,10 @@ async function seersEvents(ctx) {
223
359
 
224
360
  async function seersDismiss(ctx) {
225
361
  const { client, positionals, flags, io } = ctx;
226
- const seerId = requirePositional(positionals, 0, "seer-id");
227
- const eventId = requirePositional(positionals, 1, "event-id");
228
- const result = await client.callTool("dismiss_seer_event", { seer_id: seerId, event_id: eventId });
362
+ const result = await client.callTool(
363
+ "dismiss_seer_event",
364
+ argsFromFlags(ARGS.dismiss_seer_event, flags, positionals)
365
+ );
229
366
  ok(ctx, "Event dismissed.");
230
367
  printResult(io, flags, result);
231
368
  return 0;
@@ -233,12 +370,7 @@ async function seersDismiss(ctx) {
233
370
 
234
371
  async function seersHeadlines(ctx) {
235
372
  const { client, flags, io } = ctx;
236
- const args = compact({
237
- collection_id: flagStr(flags.collection),
238
- limit: flagNum(flags.limit),
239
- offset: flagNum(flags.offset),
240
- });
241
- const result = await client.callTool("list_seer_headlines", args);
373
+ const result = await client.callTool("list_seer_headlines", argsFromFlags(ARGS.list_seer_headlines, flags));
242
374
  printResult(io, flags, result, (r) => {
243
375
  const lines = (r.headlines ?? []).map(
244
376
  (h) => `${h.id} [${h.status}] ${h.title ?? h.headline ?? "(untitled)"}`
@@ -251,8 +383,7 @@ async function seersHeadlines(ctx) {
251
383
 
252
384
  async function seersHeld(ctx) {
253
385
  const { client, flags, io } = ctx;
254
- const args = compact({ collection_id: flagStr(flags.collection) });
255
- const result = await client.callTool("list_held_drafts", args);
386
+ const result = await client.callTool("list_held_drafts", argsFromFlags(ARGS.list_held_drafts, flags));
256
387
  printResult(io, flags, result, (r) => {
257
388
  const lines = (r.drafts ?? []).map(
258
389
  (d) => `${d.articleId} [${d.kind}] ${d.title} — ${d.reason} (dropped ${d.dropped}, unsure ${d.unsure})`
@@ -266,8 +397,10 @@ async function seersHeld(ctx) {
266
397
 
267
398
  async function seersResolveHeld(ctx, action) {
268
399
  const { client, positionals, flags, io } = ctx;
269
- const articleId = requirePositional(positionals, 0, "article-id");
270
- const result = await client.callTool("resolve_held_draft", { article_id: articleId, action });
400
+ const result = await client.callTool(
401
+ "resolve_held_draft",
402
+ argsFromFlags(ARGS.resolve_held_draft, flags, positionals, { action })
403
+ );
271
404
  ok(ctx, action === "release" ? "Draft released — it can publish through the normal path." : "Draft discarded.");
272
405
  printResult(io, flags, result);
273
406
  return 0;
@@ -277,7 +410,7 @@ async function seersResolveHeld(ctx, action) {
277
410
 
278
411
  async function seersProviders(ctx) {
279
412
  const { client, flags, io } = ctx;
280
- const result = await client.callTool("list_seer_providers", {});
413
+ const result = await client.callTool("list_seer_providers", argsFromFlags(ARGS.list_seer_providers, flags));
281
414
  printResult(io, flags, result, (r) => {
282
415
  const blocks = (r.providers ?? []).map((p) => {
283
416
  const fields = p.fields
@@ -296,7 +429,7 @@ async function seersProviders(ctx) {
296
429
 
297
430
  async function seersConnections(ctx) {
298
431
  const { client, flags, io } = ctx;
299
- const result = await client.callTool("get_seer_connections", {});
432
+ const result = await client.callTool("get_seer_connections", argsFromFlags(ARGS.get_seer_connections, flags));
300
433
  printResult(io, flags, result, (r) => {
301
434
  const gh = r.github.connected
302
435
  ? `GitHub: connected as ${r.github.account_login} (${r.github.connected_at})`
@@ -313,9 +446,11 @@ async function seersConnect(ctx) {
313
446
  const which = ctx.positionals[0];
314
447
  const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
315
448
  if (which === "github") {
316
- const token = flagStr(rest.flags.token);
317
- if (!token) throw new CliError("Missing required --token <github-pat> (repo read access)");
318
- const result = await rest.client.callTool("set_github_connection", { access_token: token });
449
+ if (!flagStr(rest.flags.token)) throw new CliError("Missing required --token <github-pat> (repo read access)");
450
+ const result = await rest.client.callTool(
451
+ "set_github_connection",
452
+ argsFromFlags(ARGS.set_github_connection, rest.flags)
453
+ );
319
454
  ok(rest, "GitHub connected.");
320
455
  printResult(rest.io, rest.flags, result);
321
456
  return 0;
@@ -340,7 +475,10 @@ async function seersConnect(ctx) {
340
475
  );
341
476
  return 0;
342
477
  }
343
- const result = await rest.client.callTool("set_notion_connection", { access_token: token });
478
+ const result = await rest.client.callTool(
479
+ "set_notion_connection",
480
+ argsFromFlags(ARGS.set_notion_connection, rest.flags)
481
+ );
344
482
  ok(rest, `Notion connected${result.workspace_name ? ` to ${result.workspace_name}` : ""}.`);
345
483
  ok(rest, "Reminder: the integration only watches pages you've shared with it in Notion.");
346
484
  printResult(rest.io, rest.flags, result);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.25.4",
3
+ "version": "0.25.6",
4
4
  "description": "Spin up and manage Letterstory phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",