@subshell-ai/plugin-oh-my-pi 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,13 +1,12 @@
1
1
  # Oh My Pi plugin for Subshell
2
2
 
3
3
  `@subshell-ai/plugin-oh-my-pi` launches the `omp` CLI from Subshell. It provides
4
- model, provider, and thinking settings, plus automatic per-launch MCP
5
- configuration. Install and authenticate Oh My Pi separately on each node
6
- where you want to run it.
4
+ model, provider, and thinking settings, automatic per-launch MCP
5
+ configuration, and sidebar subagent monitoring. Install and authenticate Oh My
6
+ Pi separately on each node where you want to run it.
7
7
 
8
8
  See the [Oh My Pi guide](https://docs.subshell.sh/agents/oh-my-pi) for setup
9
9
  and supported behavior. The integration is verified with Oh My Pi 18.12.6.
10
- Automatic conversation resumption, attention hooks, and sidebar subagent
11
- monitoring aren't supported.
10
+ Automatic conversation resumption and attention hooks aren't supported.
12
11
 
13
12
  Licensed under Apache-2.0.
package/dist/index.d.ts CHANGED
@@ -500,6 +500,24 @@ interface InstallSpec {
500
500
  */
501
501
  docsUrl: string;
502
502
  }
503
+ /**
504
+ * How this plugin's harness CLI updates itself in place.
505
+ *
506
+ * The installer doubles as the updater for every built-in today (`curl … | sh`
507
+ * and `npm i -g` lines are idempotent), so this is OPTIONAL and falls back to
508
+ * {@link InstallSpec}. Declare it where the vendor ships a dedicated verb or a
509
+ * different channel — `claude update`, say — so pressing Update runs the
510
+ * vendor's own update path rather than reinstalling.
511
+ *
512
+ * Like `install.command`, this is a SHELL LINE the host runs through `sh -c`,
513
+ * on the node that holds the harness, and it may not need privileges.
514
+ */
515
+ interface UpdateSpec {
516
+ /** Shell line that updates the CLI in place. */
517
+ command: string;
518
+ /** Where the vendor documents it; http(s) only, when present. */
519
+ docsUrl?: string;
520
+ }
503
521
  /** One step an operator must run themselves, because the host may not. */
504
522
  interface PrivilegedStep {
505
523
  /** What it does, in a few words: "Install the daemon" */
@@ -643,6 +661,11 @@ interface SubshellManifest {
643
661
  detect?: DetectSpec;
644
662
  /** Install guidance shown when the binary is missing */
645
663
  install?: InstallSpec;
664
+ /**
665
+ * How the CLI updates itself, when the installer is not the updater.
666
+ * Absent means `install.command` is the update path too.
667
+ */
668
+ update?: UpdateSpec;
646
669
  /**
647
670
  * Environment variables this plugin computes against (spec 2026-09-10 §5).
648
671
  *
@@ -677,6 +700,11 @@ declare const manifest: SubshellManifest;
677
700
  * Oh My Pi uses its own executable and native MCP discovery, independently of
678
701
  * pi. An explicit extension directory contributes mcp.json without redirecting
679
702
  * the user's config or credentials. Verified against omp 18.12.6.
703
+ *
704
+ * The same directory carries the subagent bridge. omp keeps its child roster
705
+ * on the session event bus; the extension reports it so the sidebar can hang
706
+ * those children under the parent subshell. omp offers no extension API for
707
+ * moving the parent's terminal onto a child, so navigation stays unavailable.
680
708
  */
681
709
  declare const createPlugin: HarnessPluginFactory;
682
710
  //#endregion
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- //#region ../../plugin-api/dist/manifest-XGvw0ugx.js
1
+ //#region ../../plugin-api/dist/manifest-Cdy9tSLl.js
2
2
  /** Every {@link PluginType}, for anything that has to iterate or validate them. */
3
3
  const PLUGIN_TYPES = [
4
4
  "agent-harness",
@@ -158,6 +158,19 @@ function parseManifest(pkgJson) {
158
158
  docsUrl: i.docsUrl
159
159
  };
160
160
  }
161
+ let update;
162
+ if (block.update !== void 0) {
163
+ const u = block.update;
164
+ if (!isRecord(u) || typeof u.command !== "string" || u.command.trim() === "") return { error: "`subshell.update` needs a non-empty command" };
165
+ if (needsPrivileges(u.command)) return { error: "`subshell.update.command` must not need sudo, doas or pkexec — the host runs it through a shell and has no terminal for a password prompt" };
166
+ if (u.docsUrl !== void 0) {
167
+ if (typeof u.docsUrl !== "string" || !isDocsUrl(u.docsUrl)) return { error: "`subshell.update.docsUrl` must be an http(s) URL — it is rendered as a link on an admin page" };
168
+ update = {
169
+ command: u.command,
170
+ docsUrl: u.docsUrl
171
+ };
172
+ } else update = { command: u.command };
173
+ }
161
174
  let hostEnv;
162
175
  if (block.hostEnv !== void 0) {
163
176
  if (!Array.isArray(block.hostEnv) || !block.hostEnv.every((k) => typeof k === "string" && k.trim() !== "")) return { error: "`subshell.hostEnv` must be an array of environment variable names" };
@@ -176,6 +189,7 @@ function parseManifest(pkgJson) {
176
189
  entry: block.entry,
177
190
  ...detect ? { detect } : {},
178
191
  ...install ? { install } : {},
192
+ ...update ? { update } : {},
179
193
  ...hostEnv ? { hostEnv } : {},
180
194
  ...parsedNetwork ? { network: parsedNetwork } : {},
181
195
  ...block.pickable === false ? { pickable: false } : {}
@@ -273,6 +287,193 @@ function validateGenericPreset(preset) {
273
287
  };
274
288
  }
275
289
  //#endregion
290
+ //#region src/subagents.ts
291
+ /**
292
+ * omp's session event bus already carries the roster the task tool is running:
293
+ * `task:subagent:lifecycle` announces a child's start and settlement, and
294
+ * `task:subagent:progress` streams its status, generated one-line label, and
295
+ * latest intent. The extension below turns those frames into the snapshot
296
+ * reports the control plane reads, so the sidebar shows the children under the
297
+ * subshell that asked for them.
298
+ *
299
+ * Every frame is already bounded by omp; this module bounds again because the
300
+ * report is rendered in the sidebar of the owner and every viewer, and it is
301
+ * the pane's own porcelain that names them (160 characters, like the server).
302
+ * Progress frames arrive at the coalescing rate omp emits (about 150 ms), so
303
+ * they queue one snapshot per second; lifecycle changes report at once.
304
+ *
305
+ * A child session rebinds every extension factory, so this factory runs once
306
+ * per session. Each binding keeps its own roster, and `session_start` tells a
307
+ * child binding apart from the pane's main agent: only the main agent's roster
308
+ * is the set of children the sidebar should hang under the subshell.
309
+ *
310
+ * The roster repeats every two seconds while it is non-empty: the control
311
+ * plane keeps it in memory, so a restart has nothing until the harness reports
312
+ * again, and an idle child produces no other frames.
313
+ */
314
+ const SUBAGENT_BRIDGE = `
315
+ export default function (pi) {
316
+ const LIFECYCLE = 'task:subagent:lifecycle';
317
+ const PROGRESS = 'task:subagent:progress';
318
+ const MAX_AGENTS = 256;
319
+ const MAX_TEXT = 160;
320
+ const REPORT_DELAY_MS = 1000;
321
+ // A live child keeps reporting, so a control-plane restart (which forgets
322
+ // every roster) gets the set back without waiting for the next tool event.
323
+ const HEARTBEAT_MS = 2000;
324
+
325
+ const agents = new Map();
326
+ let role = 'main';
327
+ let timer = null;
328
+ let reporting = false;
329
+ let queued = false;
330
+ let heartbeat = null;
331
+ let warned = false;
332
+ const label = value => {
333
+ if (typeof value !== 'string') return undefined;
334
+ const trimmed = value.trim().slice(0, MAX_TEXT);
335
+ return trimmed || undefined;
336
+ };
337
+ const validId = id => (typeof id === 'string' && id.length > 0 && id.length <= 256 ? id : null);
338
+ const statusFor = status =>
339
+ status === 'pending' ? 'waiting' : status === 'running' ? 'running' : null;
340
+
341
+ const put = (id, status, activity) => {
342
+ const key = validId(id);
343
+ if (!key || role !== 'main') return false;
344
+ if (!agents.has(key) && agents.size >= MAX_AGENTS) return false;
345
+ const previous = agents.get(key);
346
+ // The report schema caps the name field at 160 while the id may run to
347
+ // 256; an over-long name would fail validation for the whole snapshot.
348
+ const next = { id: key, name: key.slice(0, MAX_TEXT), status };
349
+ const words = activity || previous?.activity;
350
+ if (words) next.activity = words;
351
+ agents.set(key, next);
352
+ if (heartbeat === null) heartbeat = setInterval(beat, HEARTBEAT_MS);
353
+ return true;
354
+ };
355
+
356
+ const drop = id => {
357
+ if (role !== 'main') return false;
358
+ const key = validId(id);
359
+ return key ? agents.delete(key) : false;
360
+ };
361
+
362
+ const report = async () => {
363
+ if (reporting) {
364
+ queued = true;
365
+ return;
366
+ }
367
+ const base = process.env.SUBSHELL_BASE_URL;
368
+ const token = process.env.SUBSHELL_API_KEY;
369
+ const id = process.env.SUBSHELL_ID;
370
+ if (!base || !token || !id) {
371
+ if (!warned) {
372
+ warned = true;
373
+ pi.logger?.debug?.('subshell subagent report skipped: the pane identity environment is missing');
374
+ }
375
+ return;
376
+ }
377
+ reporting = true;
378
+ try {
379
+ const response = await fetch(base + '/api/subshells/' + encodeURIComponent(id) + '/subagents/report', {
380
+ method: 'POST',
381
+ headers: { authorization: 'Bearer ' + token, 'content-type': 'application/json' },
382
+ body: JSON.stringify({
383
+ event: 'snapshot',
384
+ agents: [...agents.values()].slice(0, MAX_AGENTS),
385
+ selectedId: null,
386
+ canSelect: false,
387
+ }),
388
+ signal: AbortSignal.timeout(2000),
389
+ });
390
+ if (!response.ok && !warned) {
391
+ warned = true;
392
+ pi.logger?.debug?.('subshell subagent report failed', { status: response.status });
393
+ }
394
+ } catch {
395
+ // A failed report is not fatal: the next frame reports again, and a pane
396
+ // without the identity variables above has no endpoint to reach.
397
+ } finally {
398
+ reporting = false;
399
+ if (queued) {
400
+ queued = false;
401
+ void report();
402
+ }
403
+ }
404
+ };
405
+
406
+ const schedule = () => {
407
+ if (timer !== null) return;
408
+ timer = setTimeout(() => {
409
+ timer = null;
410
+ void report();
411
+ }, REPORT_DELAY_MS);
412
+ };
413
+
414
+ const beat = () => {
415
+ if (role !== 'main' || agents.size === 0) {
416
+ if (heartbeat !== null) {
417
+ clearInterval(heartbeat);
418
+ heartbeat = null;
419
+ }
420
+ return;
421
+ }
422
+ void report();
423
+ };
424
+
425
+ pi.events.on(LIFECYCLE, frame => {
426
+ if (!frame || typeof frame !== 'object' || typeof frame.status !== 'string') return;
427
+ if (frame.status === 'started') {
428
+ if (put(frame.id, 'running', label(frame.description) || label(frame.agent))) schedule();
429
+ } else if (drop(frame.id)) schedule();
430
+ });
431
+
432
+ pi.events.on(PROGRESS, frame => {
433
+ if (!frame || typeof frame !== 'object') return;
434
+ const progress = frame.progress;
435
+ if (!progress || typeof progress !== 'object') return;
436
+ const status = statusFor(progress.status);
437
+ if (status) {
438
+ if (put(progress.id ?? frame.id, status, label(progress.description) || label(progress.lastIntent))) schedule();
439
+ } else if (progress.status === 'completed' || progress.status === 'failed' || progress.status === 'aborted') {
440
+ if (drop(progress.id ?? frame.id)) schedule();
441
+ }
442
+ });
443
+
444
+ const reset = () => {
445
+ agents.clear();
446
+ if (timer !== null) {
447
+ clearTimeout(timer);
448
+ timer = null;
449
+ }
450
+ if (heartbeat !== null) {
451
+ clearInterval(heartbeat);
452
+ heartbeat = null;
453
+ }
454
+ return report();
455
+ };
456
+
457
+ pi.on('session_start', (_event, ctx) => {
458
+ role = ctx && ctx.agent && ctx.agent.kind === 'sub' ? 'sub' : 'main';
459
+ if (role !== 'main') return;
460
+ return reset();
461
+ });
462
+
463
+ // A switch (resume, fork, new) replaces the conversation the roster belonged
464
+ // to; its children are no longer this session's children.
465
+ pi.on('session_switch', () => {
466
+ if (role !== 'main') return;
467
+ return reset();
468
+ });
469
+
470
+ pi.on('session_shutdown', () => {
471
+ if (role !== 'main') return;
472
+ return reset();
473
+ });
474
+ }
475
+ `;
476
+ //#endregion
276
477
  //#region src/manifest.ts
277
478
  /**
278
479
  * This package's own `subshell` block, read from its package.json so the
@@ -285,7 +486,7 @@ function validateGenericPreset(preset) {
285
486
  const parsed = parseManifest({
286
487
  name: "@subshell-ai/plugin-oh-my-pi",
287
488
  description: "Oh My Pi harness plugin for Subshell",
288
- version: "0.2.1",
489
+ version: "0.2.3",
289
490
  license: "Apache-2.0",
290
491
  type: "module",
291
492
  main: "dist/index.js",
@@ -397,12 +598,182 @@ const SETTINGS = [
397
598
  */
398
599
  const TITLE_PREFIX = /^π(?:$|\s+(?:[>!:]|[⠁-⣿]|[◔◑◕○●]|[-\\/|])|:(?=\s|$))\s*/u;
399
600
  /**
601
+ * Loads that provider from the environment. The source is fixed: it names the
602
+ * variable and never interpolates its value, so no preset data (and no
603
+ * credential) is written to a node. A launch without the variable loads
604
+ * nothing, which keeps the module inert for every other omp preset.
605
+ *
606
+ * The value is preset-controlled, so malformed or incomplete JSON leaves the
607
+ * module inert too. It must match what `model-cli omp` generates and nothing
608
+ * else: the provider keeps model-cli's namespace, so a preset cannot override
609
+ * a built-in provider; the apiKey is the env-var name rather than a literal
610
+ * secret; and no field may carry omp's `!command` config syntax, which would
611
+ * run shell commands as the pane user. Registration is queued outside the
612
+ * extension factory's error guard, so a value omp would reject there would
613
+ * take the pane down at startup instead of warning. One debug line names the
614
+ * problem, never the value.
615
+ *
616
+ * A preset built from `model-cli omp` used to name its provider by an
617
+ * `--extension` path in the generating host's cache, so it only worked on
618
+ * machines where model-cli had run. This module rides the per-launch
619
+ * extension directory the host already ships to every node instead.
620
+ */
621
+ const MODEL_CLI_PROVIDER_SHIM = `export default function(pi) {
622
+ const raw = process.env.MODEL_CLI_OMP_CONFIG;
623
+ if (!raw) return;
624
+ let config;
625
+ try {
626
+ config = JSON.parse(raw);
627
+ } catch {
628
+ pi.logger?.debug?.("model-cli provider config ignored: MODEL_CLI_OMP_CONFIG is not JSON");
629
+ return;
630
+ }
631
+
632
+ const CONNECTION_KEYS = ["baseUrl", "api", "apiKey", "models"];
633
+ const MODEL_KEYS = [
634
+ "id",
635
+ "name",
636
+ "contextWindow",
637
+ "maxTokens",
638
+ "reasoning",
639
+ "input",
640
+ "cost",
641
+ "compat",
642
+ "thinking",
643
+ ];
644
+ const COST_KEYS = ["input", "output", "cacheRead", "cacheWrite"];
645
+ const COMPAT_KEYS = [
646
+ "maxTokensField",
647
+ "reasoningContentField",
648
+ "replayReasoningContent",
649
+ "requiresReasoningContentForAllAssistantTurns",
650
+ "supportsDeveloperRole",
651
+ "supportsStore",
652
+ "supportsStrictMode",
653
+ "thinkingFormat",
654
+ ];
655
+ const THINKING_KEYS = ["mode", "efforts", "effortMap"];
656
+
657
+ const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
658
+ const only = (value, keys) => Object.keys(value).every((key) => keys.includes(key));
659
+ const text = (value) => typeof value === "string" && value !== "";
660
+ const amount = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0;
661
+ const positive = (value) => amount(value) && value > 0;
662
+ const command = (value) => {
663
+ if (typeof value === "string") return value.startsWith("!");
664
+ if (Array.isArray(value)) return value.some(command);
665
+ if (isObject(value)) return Object.values(value).some(command);
666
+ return false;
667
+ };
668
+ const cost = (value) => isObject(value) && only(value, COST_KEYS) && Object.values(value).every(amount);
669
+ const compat = (value) =>
670
+ isObject(value) &&
671
+ only(value, COMPAT_KEYS) &&
672
+ Object.values(value).every((item) => typeof item === "string" || typeof item === "boolean");
673
+ const thinking = (value) =>
674
+ isObject(value) &&
675
+ only(value, THINKING_KEYS) &&
676
+ (value.mode === "anthropic-adaptive" || value.mode === "effort") &&
677
+ Array.isArray(value.efforts) &&
678
+ value.efforts.every(text) &&
679
+ isObject(value.effortMap) &&
680
+ Object.values(value.effortMap).every(text);
681
+ const model = (value) =>
682
+ isObject(value) &&
683
+ only(value, MODEL_KEYS) &&
684
+ text(value.id) &&
685
+ (value.name === undefined || text(value.name)) &&
686
+ (value.contextWindow === undefined || positive(value.contextWindow)) &&
687
+ (value.maxTokens === undefined || positive(value.maxTokens)) &&
688
+ (value.reasoning === undefined || typeof value.reasoning === "boolean") &&
689
+ (value.input === undefined ||
690
+ (Array.isArray(value.input) && value.input.every((item) => item === "text" || item === "image"))) &&
691
+ (value.cost === undefined || cost(value.cost)) &&
692
+ (value.compat === undefined || compat(value.compat)) &&
693
+ (value.thinking === undefined || thinking(value.thinking));
694
+
695
+ const connection = config?.connection;
696
+ const models = connection?.models;
697
+ const usable =
698
+ typeof config?.provider === "string" &&
699
+ /^model-cli-omp-(?:litellm|direct)-\\S+$/.test(config.provider) &&
700
+ isObject(connection) &&
701
+ only(connection, CONNECTION_KEYS) &&
702
+ typeof connection.baseUrl === "string" &&
703
+ /^https?:\\/\\/.+/.test(connection.baseUrl) &&
704
+ (connection.api === "anthropic-messages" || connection.api === "openai-completions") &&
705
+ connection.apiKey === "MODEL_CLI_OMP_API_KEY" &&
706
+ Array.isArray(models) &&
707
+ models.length > 0 &&
708
+ models.every(model) &&
709
+ !command(config);
710
+ if (!usable) {
711
+ pi.logger?.debug?.("model-cli provider config ignored: MODEL_CLI_OMP_CONFIG is not the model-cli shape");
712
+ return;
713
+ }
714
+ pi.registerProvider(config.provider, connection);
715
+ }
716
+ `;
717
+ /**
718
+ * A preset built from `model-cli omp` used to prove its provider with an
719
+ * extension path in the generating host's cache. That file is writable by
720
+ * anything running as the pane user and persists between launches, so the
721
+ * provider module below is a replacement, not a companion: the legacy flag is
722
+ * dropped when this launch writes its own module. omp accepts the path as
723
+ * `--extension`, `--trusted-extension`, `-e` or `--hook`, so every spelling is
724
+ * matched, and the path is normalized first because `./` and doubled
725
+ * separators name the same file.
726
+ */
727
+ const LEGACY_PROVIDER_FLAGS = [
728
+ "--extension",
729
+ "--trusted-extension",
730
+ "-e",
731
+ "--hook"
732
+ ];
733
+ const LEGACY_PROVIDER_INLINE = [
734
+ "--extension=",
735
+ "--trusted-extension=",
736
+ "-e=",
737
+ "--hook="
738
+ ];
739
+ function isLegacyProviderPath(value) {
740
+ const segments = value.trim().replace(/\\/g, "/").split("/").filter((segment) => segment !== "" && segment !== ".");
741
+ return /(?:^|\/)model-cli\/omp-provider\.mjs$/.test(segments.join("/"));
742
+ }
743
+ function withoutLegacyProviderExtension(flags) {
744
+ const kept = [];
745
+ for (let i = 0; i < flags.length; i++) {
746
+ const flag = flags[i];
747
+ if (LEGACY_PROVIDER_FLAGS.includes(flag) && isLegacyProviderPath(flags[i + 1] ?? "")) {
748
+ i += 1;
749
+ continue;
750
+ }
751
+ const inline = LEGACY_PROVIDER_INLINE.find((prefix) => flag.startsWith(prefix));
752
+ if (inline && isLegacyProviderPath(flag.slice(inline.length))) continue;
753
+ kept.push(flag);
754
+ }
755
+ return kept;
756
+ }
757
+ /**
400
758
  * Oh My Pi uses its own executable and native MCP discovery, independently of
401
759
  * pi. An explicit extension directory contributes mcp.json without redirecting
402
760
  * the user's config or credentials. Verified against omp 18.12.6.
761
+ *
762
+ * The same directory carries the subagent bridge. omp keeps its child roster
763
+ * on the session event bus; the extension reports it so the sidebar can hang
764
+ * those children under the parent subshell. omp offers no extension API for
765
+ * moving the parent's terminal onto a child, so navigation stays unavailable.
403
766
  */
404
767
  const createPlugin = () => ({
405
- capabilities: () => ["mcp", "settings"],
768
+ subagents: {
769
+ discovery: "extension",
770
+ navigation: "unavailable"
771
+ },
772
+ capabilities: () => [
773
+ "subagents",
774
+ "mcp",
775
+ "settings"
776
+ ],
406
777
  /** `π <state> <label>`: the label behind the brand and run state is the name. */
407
778
  parsePaneTitle: (raw) => raw.replace(TITLE_PREFIX, "").trim(),
408
779
  buildCommand({ binary, preset, extraFlags, mcp }) {
@@ -416,7 +787,7 @@ const createPlugin = () => ({
416
787
  const value = settings[key];
417
788
  if (typeof value === "string" && value) args.push(`--${key}`, value);
418
789
  }
419
- args.push(...preset.flags, ...extraFlags ?? []);
790
+ args.push(...mcp ? withoutLegacyProviderExtension(preset.flags) : preset.flags, ...extraFlags ?? []);
420
791
  return args;
421
792
  },
422
793
  mcpRegistration(launch, configPath) {
@@ -432,9 +803,15 @@ const createPlugin = () => ({
432
803
  name: "subshell-mcp",
433
804
  private: true,
434
805
  type: "module",
435
- omp: { extensions: ["bridge.mjs"] }
806
+ omp: { extensions: [
807
+ "bridge.mjs",
808
+ "subagents.mjs",
809
+ "provider.mjs"
810
+ ] }
436
811
  }),
437
812
  "bridge.mjs": "export default function () {}\n",
813
+ "subagents.mjs": SUBAGENT_BRIDGE,
814
+ "provider.mjs": MODEL_CLI_PROVIDER_SHIM,
438
815
  "mcp.json": fileContent
439
816
  },
440
817
  args: ["--extension", `${configPath}.d`]
@@ -443,7 +820,7 @@ const createPlugin = () => ({
443
820
  mcpSetup() {
444
821
  return {
445
822
  mode: "auto",
446
- summary: "Subshell registers its MCP server through a per-launch Oh My Pi extension. Your existing configuration stays in place."
823
+ summary: "Subshell registers its MCP server and reports the pane's subagents through a per-launch Oh My Pi extension. Your existing configuration stays in place."
447
824
  };
448
825
  },
449
826
  validatePreset: validateGenericPreset,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@subshell-ai/plugin-oh-my-pi",
3
3
  "description": "Oh My Pi harness plugin for Subshell",
4
- "version": "0.2.1",
4
+ "version": "0.2.3",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",