prism-mcp-server 20.13.0 → 20.14.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/dist/cli.js CHANGED
@@ -221,6 +221,7 @@ program
221
221
  .option('--dry-run', 'Preview configuration changes without writing files')
222
222
  .option('--refresh', 'Refresh only entries previously created by Prism; custom entries stay untouched')
223
223
  .option('--no-self-update', 'Skip the npm self-update check; configure with the currently installed version')
224
+ .option('--no-models', 'Skip model convergence; keep whatever model versions are installed')
224
225
  .action(async (options) => {
225
226
  try {
226
227
  // ── Converge the PACKAGE first, then the configs ──────────────
@@ -447,6 +448,17 @@ program
447
448
  if (!summary.usedApiKey) {
448
449
  console.log('PRISM_SYNALUX_API_KEY is not set; no Synalux subscription key was copied into new registrations.');
449
450
  }
451
+ // ── Converge the MODELS last ────────────────────────────────
452
+ // selfUpdate above is the package half of convergence; without this
453
+ // half a machine kept vision-less models for four days after the
454
+ // registry carried the fix (2026-08-18), and `ollama cp` aliases are
455
+ // snapshots that silently detach from their source on every re-pull.
456
+ // Failures here never fail connect — runOllamaConverge reports and
457
+ // returns.
458
+ if (options.models !== false) {
459
+ const { runOllamaConverge } = await import('./modelConvergeRunner.js');
460
+ await runOllamaConverge({ dryRun: options.dryRun === true });
461
+ }
450
462
  }
451
463
  catch (err) {
452
464
  console.error(`Connect failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -1028,6 +1040,20 @@ scmCmd
1028
1040
  process.exit(1);
1029
1041
  }
1030
1042
  });
1043
+ // ─── prism update-models ──────────────────────────────────────
1044
+ // Standalone model convergence: pull each installed prism-coder tier from
1045
+ // the registry and repair its local alias. connect runs this automatically;
1046
+ // this command is for updating models without touching host configuration.
1047
+ program
1048
+ .command('update-models')
1049
+ .description('Pull installed prism-coder models from the registry and repair stale local aliases')
1050
+ .option('--dry-run', 'Print what would be pulled/re-aliased without executing')
1051
+ .action(async (options) => {
1052
+ const { runOllamaConverge } = await import('./modelConvergeRunner.js');
1053
+ const outcomes = await runOllamaConverge({ dryRun: options.dryRun === true });
1054
+ if (outcomes.every(o => o.action === 'failed'))
1055
+ process.exitCode = 1;
1056
+ });
1031
1057
  // ─── prism register-models ────────────────────────────────────
1032
1058
  // Convenience: alias namespaced HF-style prism-coder tags
1033
1059
  // (`dcostenco/prism-coder:9b`) to the bare tags (`prism-coder:9b`)
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Production wiring for model convergence — real Ollama, real registry.
3
+ *
4
+ * Kept out of cli.ts so the pure logic (utils/modelConverge.ts) stays
5
+ * testable with injected deps, and out of utils/ because this half owns
6
+ * process spawning. `ollama pull` runs with inherited stdio deliberately:
7
+ * a multi-GB download with no progress bar reads as a hang, and ollama's
8
+ * own progress output is better than anything we would reimplement.
9
+ *
10
+ * Adversarial review 2026-08-18 (pre-20.14.0), two findings fixed here:
11
+ *
12
+ * 1. SPLIT-BRAIN DAEMON TARGETING: listTags read /api/tags from
13
+ * PRISM_LOCAL_LLM_URL while the spawned `ollama` CLI targeted whatever
14
+ * OLLAMA_HOST the user's shell had (default localhost). On a machine
15
+ * with a remote Ollama, convergence read tags from one daemon and
16
+ * pulled/re-aliased against another. The spawn env now pins OLLAMA_HOST
17
+ * to the SAME url the tags came from — one daemon, one truth.
18
+ *
19
+ * 2. SILENT MULTI-GB DOWNLOADS: connect gave no warning before pulling.
20
+ * Measured the day this shipped: the registry 27b had just changed
21
+ * bytes, so every 27b holder's first converge pulls ~16 GB. The plan
22
+ * is now announced up front, with the --no-models escape hatch named,
23
+ * before any network transfer starts.
24
+ */
25
+ import { spawn } from "node:child_process";
26
+ import { convergeModels, MODEL_NAMESPACE, LOCAL_PREFIX, CONVERGE_TIERS } from "./utils/modelConverge.js";
27
+ const OLLAMA_URL = process.env.PRISM_LOCAL_LLM_URL || "http://localhost:11434";
28
+ /**
29
+ * Env for spawned `ollama` processes. OLLAMA_HOST is pinned to the same
30
+ * daemon /api/tags was read from — never the shell's ambient value — so
31
+ * list, pull, and cp can never disagree about which Ollama they act on.
32
+ * Exported for tests.
33
+ */
34
+ export function convergeEnv(base, ollamaUrl = OLLAMA_URL) {
35
+ return { ...base, OLLAMA_HOST: ollamaUrl };
36
+ }
37
+ function runOllama(args, inheritStdio) {
38
+ return new Promise((resolve, reject) => {
39
+ const child = spawn("ollama", args, {
40
+ stdio: inheritStdio ? "inherit" : "ignore",
41
+ env: convergeEnv(process.env),
42
+ });
43
+ child.on("error", (err) => reject(new Error(`ollama ${args[0]}: ${err.message}`)));
44
+ child.on("close", (code) => {
45
+ if (code === 0)
46
+ resolve();
47
+ else
48
+ reject(new Error(`ollama ${args.join(" ")} exited ${code}`));
49
+ });
50
+ });
51
+ }
52
+ export async function runOllamaConverge(opts = {}) {
53
+ // Announce the plan BEFORE any network transfer: which tiers are
54
+ // installed (and will be checked), that pulls can be large, and how to
55
+ // opt out. A 16 GB download must never be the first sign convergence
56
+ // is running.
57
+ let installedTiers = [];
58
+ try {
59
+ const res = await fetch(`${OLLAMA_URL}/api/tags`, { signal: AbortSignal.timeout(5_000) });
60
+ if (res.ok) {
61
+ const data = (await res.json());
62
+ const names = new Set((data.models ?? []).map((m) => m.name));
63
+ installedTiers = CONVERGE_TIERS.filter((t) => names.has(`${MODEL_NAMESPACE}:${t}`) || names.has(`${LOCAL_PREFIX}:${t}`));
64
+ }
65
+ }
66
+ catch {
67
+ // convergeModels handles the unreachable case with its own message
68
+ }
69
+ if (installedTiers.length > 0) {
70
+ console.log(`\nConverging ${installedTiers.length} installed model tier(s) [${installedTiers.join(", ")}] against the registry.`);
71
+ console.log(" Updated models download in full (can be multiple GB). Skip with: prism connect --no-models");
72
+ }
73
+ else {
74
+ console.log("\nConverging local models against the registry…");
75
+ }
76
+ const outcomes = await convergeModels({
77
+ listTags: async () => {
78
+ const res = await fetch(`${OLLAMA_URL}/api/tags`, { signal: AbortSignal.timeout(5_000) });
79
+ if (!res.ok)
80
+ throw new Error(`Ollama /api/tags HTTP ${res.status}`);
81
+ const data = (await res.json());
82
+ return (data.models ?? []).map((m) => ({ name: m.name, digest: m.digest }));
83
+ },
84
+ pull: (ref) => runOllama(["pull", ref], true),
85
+ copy: (from, to) => runOllama(["cp", from, to], false),
86
+ log: (line) => console.log(` ${line}`),
87
+ dryRun: opts.dryRun,
88
+ });
89
+ const failed = outcomes.filter((o) => o.action === "failed" && o.detail !== "ollama_unreachable");
90
+ if (failed.length > 0) {
91
+ console.log(` ⚠ ${failed.length} tier(s) did not converge — re-run \`prism update-models\` when the network allows`);
92
+ }
93
+ return outcomes;
94
+ }
@@ -29,6 +29,8 @@
29
29
  * - getHistory → POST /api/v1/prism/memory action=memory_history
30
30
  * - patchLedger → POST /api/v1/prism/memory action=save_embedding
31
31
  * - getEntriesMissingEmbeddings → POST /api/v1/prism/memory action=list_missing_embeddings
32
+ * - listProjects → POST /api/v1/prism/memory action=list_projects
33
+ * - exportLedger → POST /api/v1/prism/memory action=export_memory (paginated)
32
34
  *
33
35
  * Methods still falling through to SupabaseStorage (Phase 3 Tier B+):
34
36
  * save_experience direct entrypoint, compactLedger, image ops,
@@ -449,6 +451,58 @@ export class SynaluxStorage extends SupabaseStorage {
449
451
  const entries = Array.isArray(result.entries) ? result.entries : [];
450
452
  return entries;
451
453
  }
454
+ // ─── Project inventory + export ──────────────────────────────
455
+ // Both portal actions shipped in Phase 3 but the client was never
456
+ // wired: listProjects and the export path fell through to
457
+ // SupabaseStorage and threw "Supabase not configured" on every
458
+ // paid-tier install (2026-08-18 audit).
459
+ async listProjects() {
460
+ const result = await this.portalPost("/api/v1/prism/memory", {
461
+ action: "list_projects",
462
+ });
463
+ // Strict on drift: a 200 without a projects ARRAY is a contract change,
464
+ // not "no projects" — the portal returns projects:[] for genuinely none.
465
+ // Coercing drift to [] would make callers report empty inventories while
466
+ // claiming success. (R1 adversarial review 2026-08-18.)
467
+ if (!Array.isArray(result.projects)) {
468
+ throw new Error("[SynaluxStorage] list_projects: portal response missing projects[] — contract drift");
469
+ }
470
+ return result.projects
471
+ .map((p) => (typeof p === "string" ? p : p?.project))
472
+ .filter((name) => typeof name === "string" && name.length > 0);
473
+ }
474
+ async exportLedger(project) {
475
+ // action=export_memory is paginated (EXPORT_PAGE_SIZE=1000). Follow
476
+ // next_offset until has_more is false, capped at 10 pages to match the
477
+ // local path's 10k-row OOM guard.
478
+ //
479
+ // Strict on drift (R1 adversarial review 2026-08-18): this feeds a
480
+ // BACKUP. A 200 missing ledger[] or page{} must throw, not degrade —
481
+ // coercing either would write an empty or silently-truncated export
482
+ // file that reports ✅ success, which is worse than any error.
483
+ const rows = [];
484
+ let offset = 0;
485
+ for (let page = 0; page < 10; page++) {
486
+ const result = await this.portalPost("/api/v1/prism/memory", {
487
+ action: "export_memory",
488
+ project,
489
+ offset,
490
+ limit: 1000,
491
+ });
492
+ if (!Array.isArray(result.ledger)) {
493
+ throw new Error("[SynaluxStorage] export_memory: portal response missing ledger[] — contract drift");
494
+ }
495
+ const pageInfo = result.page;
496
+ if (typeof pageInfo?.has_more !== "boolean") {
497
+ throw new Error("[SynaluxStorage] export_memory: portal response missing page.has_more — refusing a possibly-truncated export");
498
+ }
499
+ rows.push(...result.ledger);
500
+ if (!pageInfo.has_more || typeof pageInfo.next_offset !== "number")
501
+ break;
502
+ offset = pageInfo.next_offset;
503
+ }
504
+ return rows;
505
+ }
452
506
  // ─── Time Travel ─────────────────────────────────────────────
453
507
  // Phase 3 Tier B: route memory_history through portal instead of
454
508
  // falling through to SupabaseStorage (which requires a direct
@@ -2719,12 +2719,24 @@ export async function sessionExportMemoryHandler(args) {
2719
2719
  debugLog(`[session_export_memory] Exporting project "${project}" as ${format}`);
2720
2720
  // Fetch handoff (live context)
2721
2721
  const ctx = await storage.loadContext(project, "deep", PRISM_USER_ID);
2722
- // Fetch full ledger (all non-deleted entries, capped at 10k as OOM guard)
2723
- const ledger = await storage.getLedgerEntries({
2724
- project: `eq.${project}`,
2725
- order: "created_at.asc",
2726
- limit: "10000",
2727
- });
2722
+ // Fetch full ledger (all non-deleted entries, capped at 10k as OOM guard).
2723
+ // Portal-backed installs export via action=export_memory (exportLedger);
2724
+ // local/direct backends keep the getLedgerEntries assembly. Without this
2725
+ // branch, paid thin-client installs threw "Supabase not configured" here.
2726
+ const ledger = (typeof storage.exportLedger === "function"
2727
+ ? await storage.exportLedger(project)
2728
+ : await storage.getLedgerEntries({
2729
+ project: `eq.${project}`,
2730
+ // R1 adversarial review 2026-08-18: without this filter the
2731
+ // direct/local paths exported TOMBSTONED rows — content the user
2732
+ // had asked session_forget_memory to erase shipped in every
2733
+ // backup (GDPR Art. 17 leak; the portal export always excluded
2734
+ // them, so the two paths also disagreed). Both PostgREST and the
2735
+ // sqlite filter parser support is.null.
2736
+ deleted_at: "is.null",
2737
+ order: "created_at.asc",
2738
+ limit: "10000",
2739
+ }));
2728
2740
  // Strip raw embedding vectors from the export (large binary / not human-useful)
2729
2741
  // embedding: raw float32 JSON array (~12KB/entry)
2730
2742
  // embedding_compressed: TurboQuant binary blob (~400B/entry, base64 in JSON)
@@ -1090,7 +1090,9 @@ export async function runInfer(args, deps) {
1090
1090
  // describe the request rather than a specific backend.
1091
1091
  const maxTokens = cloudMaxTokens;
1092
1092
  // Cloud fallback only for paid plans
1093
- const allowCloud = args.cloud_fallback === true && ent.features.cloud_fallback;
1093
+ // let, not const: the reserved-image branch pins this off mid-call so no
1094
+ // later escalation path can carry even the prompt text off-device.
1095
+ let allowCloud = args.cloud_fallback === true && ent.features.cloud_fallback;
1094
1096
  // Verification only for paid plans (free users skip L3 grounding)
1095
1097
  const canVerify = ent.features.grounding_verifier;
1096
1098
  // The portal entitlement is authoritative. A paid plan alone must not
@@ -1106,7 +1108,10 @@ export async function runInfer(args, deps) {
1106
1108
  const verificationGatedArgs = canVerify
1107
1109
  ? args
1108
1110
  : { ...args, verify: false, evidence: undefined };
1109
- const gatedArgs = canUsePrivateRouteGuard
1111
+ // let, not const: re-pinned below once images are resolved — an image
1112
+ // request must not leave the device through ANY channel, including the
1113
+ // paid ones this gate would otherwise leave enabled.
1114
+ let gatedArgs = canUsePrivateRouteGuard
1110
1115
  ? verificationGatedArgs
1111
1116
  : { ...verificationGatedArgs, route_guard: "local" };
1112
1117
  // §5.2 failure contract: under escalation:"report", safety refusals return
@@ -1190,6 +1195,21 @@ export async function runInfer(args, deps) {
1190
1195
  // fail open: the original images are still in resolvedImages
1191
1196
  }
1192
1197
  }
1198
+ if (resolvedImages?.length) {
1199
+ // Adversarial review R1 (2026-08-18): serving image requests locally is
1200
+ // not enough — two paid side doors still carried content DERIVED from
1201
+ // the pixels off-device. The Synalux route guard POSTs the prompt and
1202
+ // the draft; the Synalux grounding verifier POSTs the draft and the
1203
+ // evidence. A draft written by a model that just read a clinical
1204
+ // screenshot can quote it. Pin both local for EVERY image request, not
1205
+ // just reserved-flagged ones: the content screen is FN-porous by
1206
+ // design, so a clean screen is not a leak clearance. Text-only
1207
+ // requests keep both features.
1208
+ const wouldVerify = gatedArgs.verify ?? ((gatedArgs.evidence?.length ?? 0) > 0);
1209
+ if (wouldVerify)
1210
+ attempts.push({ tier: "verifier", reason: "verifier_skipped_images_stay_local" });
1211
+ gatedArgs = { ...gatedArgs, route_guard: "local", verify: false };
1212
+ }
1193
1213
  if (installed && !layer1RecursionGuard) {
1194
1214
  const l1fn = deps.callLayer1 ?? defaultCallLayer1;
1195
1215
  const l1Model = resolveOllamaName("prism-coder:4b", installed);
@@ -1222,7 +1242,26 @@ export async function runInfer(args, deps) {
1222
1242
  // Null when the deterministic floor did not fire — the verdict then came
1223
1243
  // from the semantic classifier, which has no per-rule attribution.
1224
1244
  const reservedCat = reservedCategory(args.prompt);
1225
- if (l1 === "OBVIOUS_RESERVED" || l1 === "UNCERTAIN") {
1245
+ if ((l1 === "OBVIOUS_RESERVED" || l1 === "UNCERTAIN")
1246
+ && (resolvedImages?.length ?? 0) > 0) {
1247
+ // Clinical images are PROCESSED, never refused (ruling 2026-08-18:
1248
+ // the standard BCBA role works from scanned assessments and
1249
+ // screenshots — locally, or via the sanctioned prism cloud once it
1250
+ // has an image channel). Local inference is exactly where that
1251
+ // content is SAFE: nothing leaves the device. Refusing here broke
1252
+ // screenshot verification and assessment work for the clinical
1253
+ // enterprise tiers that need it most, while the actual no-leak
1254
+ // property — images never reach unsanctioned cloud — is enforced
1255
+ // architecturally either way. Serve locally with cloud pinned off
1256
+ // for the rest of the call. No text-policy bypass results: an
1257
+ // image-carrying request is STRICTER than the same words without
1258
+ // one (local-only), so attaching an image can only reduce
1259
+ // exposure. The verdict stays in attempts for the audit trail.
1260
+ debugLog(`[prism_infer] Layer 1 verdict=${l1} with images — serving locally, cloud disabled for this call`);
1261
+ attempts.push({ tier: "layer1", reason: `layer1_${l1.toLowerCase()}_image_local_only` });
1262
+ allowCloud = false;
1263
+ }
1264
+ else if (l1 === "OBVIOUS_RESERVED" || l1 === "UNCERTAIN") {
1226
1265
  debugLog(`[prism_infer] Layer 1 verdict=${l1} — reserved content detected`);
1227
1266
  attempts.push({ tier: "layer1", reason: `layer1_${l1.toLowerCase()}` });
1228
1267
  // Images never leave the device, and callCloud has no image channel
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Model convergence for `prism connect` — the missing half of self-update.
3
+ *
4
+ * selfUpdate.ts converges the PACKAGE; nothing converged the MODELS. The gap
5
+ * was measured on 2026-08-18: the vision-restored artifacts had been on the
6
+ * Ollama registry for four days while a laptop that pulled earlier kept
7
+ * refusing image requests (layer1_classifier_no_vision) — and on the machine
8
+ * where this was written, the `prism-coder:2b` alias pointed at different
9
+ * bytes than `dcostenco/prism-coder:2b`, because `ollama cp` makes a
10
+ * SNAPSHOT: a re-pull updates the source name and silently leaves every
11
+ * alias behind.
12
+ *
13
+ * Design decisions, in order of importance:
14
+ *
15
+ * 1. DELEGATE freshness to `ollama pull`. The registry serves no
16
+ * docker-content-digest header and its manifest body hash is not the
17
+ * digest ollama reports locally, so a client-side digest comparison
18
+ * would reimplement (and drift from) ollama's own logic. `ollama pull`
19
+ * of an up-to-date tag is a manifest check that downloads nothing.
20
+ *
21
+ * 2. REPAIR the alias after every pull. `prism-coder:<t>` must equal
22
+ * `dcostenco/prism-coder:<t>` byte-for-byte; when digests differ, re-cp.
23
+ * This is the trap register-models cannot fix on its own — it only
24
+ * aliases tags that are MISSING, not tags that are stale.
25
+ *
26
+ * 3. Converge only tiers the machine already has (either the alias or the
27
+ * namespaced source). Convergence updates what you chose to install; it
28
+ * never decides that a 16 GB model belongs on your laptop.
29
+ *
30
+ * 4. Models must never break connect. Ollama down, registry unreachable,
31
+ * one tier failing — report and continue. A machine with stale models
32
+ * and fresh config beats a machine with neither.
33
+ */
34
+ export const MODEL_NAMESPACE = "dcostenco/prism-coder";
35
+ export const LOCAL_PREFIX = "prism-coder";
36
+ export const CONVERGE_TIERS = ["2b", "4b", "9b", "27b"];
37
+ export async function convergeModels(deps) {
38
+ let tags;
39
+ try {
40
+ tags = await deps.listTags();
41
+ }
42
+ catch (err) {
43
+ deps.log(`− model convergence skipped: Ollama unreachable (${err instanceof Error ? err.message : String(err)})`);
44
+ return CONVERGE_TIERS.map((tier) => ({ tier, action: "failed", detail: "ollama_unreachable" }));
45
+ }
46
+ const byName = new Map(tags.map((t) => [t.name, t]));
47
+ const outcomes = [];
48
+ for (const tier of CONVERGE_TIERS) {
49
+ const source = `${MODEL_NAMESPACE}:${tier}`;
50
+ const alias = `${LOCAL_PREFIX}:${tier}`;
51
+ const hadSource = byName.has(source);
52
+ const hadAlias = byName.has(alias);
53
+ if (!hadSource && !hadAlias) {
54
+ outcomes.push({ tier, action: "skipped_not_installed" });
55
+ continue;
56
+ }
57
+ if (deps.dryRun) {
58
+ deps.log(`• would pull ${source} and repair the ${alias} alias if stale`);
59
+ outcomes.push({ tier, action: "up_to_date", detail: "dry_run" });
60
+ continue;
61
+ }
62
+ try {
63
+ const digestBefore = byName.get(source)?.digest;
64
+ await deps.pull(source);
65
+ // Re-list AFTER the pull: both the freshness answer and the alias
66
+ // comparison must come from post-pull state, or a pull that
67
+ // changed bytes looks identical to one that did nothing.
68
+ const after = await deps.listTags();
69
+ const afterByName = new Map(after.map((t) => [t.name, t]));
70
+ const sourceNow = afterByName.get(source);
71
+ const aliasNow = afterByName.get(alias);
72
+ if (!sourceNow) {
73
+ outcomes.push({ tier, action: "failed", detail: "source_missing_after_pull" });
74
+ deps.log(`⚠ ${source}: pull reported success but the tag is not installed`);
75
+ continue;
76
+ }
77
+ const pulledNewBytes = digestBefore !== undefined && digestBefore !== sourceNow.digest;
78
+ const aliasStale = !aliasNow || aliasNow.digest !== sourceNow.digest;
79
+ if (aliasStale) {
80
+ await deps.copy(source, alias);
81
+ const action = pulledNewBytes || !hadSource ? "pulled_and_aliased" : "aliased_only";
82
+ outcomes.push({ tier, action });
83
+ deps.log(`✓ ${alias} ${aliasNow ? "re-aliased (was a stale snapshot)" : "aliased"} → ${sourceNow.digest.slice(0, 12)}`);
84
+ }
85
+ else if (pulledNewBytes) {
86
+ // Alias digest already matches the fresh source — cp raced us
87
+ // or the user re-aliased by hand; either way converged.
88
+ outcomes.push({ tier, action: "pulled_and_aliased" });
89
+ deps.log(`✓ ${source} updated; ${alias} already matches`);
90
+ }
91
+ else {
92
+ outcomes.push({ tier, action: "up_to_date" });
93
+ deps.log(`= ${alias} up to date (${sourceNow.digest.slice(0, 12)})`);
94
+ }
95
+ }
96
+ catch (err) {
97
+ outcomes.push({ tier, action: "failed", detail: err instanceof Error ? err.message : String(err) });
98
+ deps.log(`⚠ ${source}: ${err instanceof Error ? err.message : String(err)} — continuing with the other tiers`);
99
+ }
100
+ }
101
+ return outcomes;
102
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prism-mcp-server",
3
- "version": "20.13.0",
3
+ "version": "20.14.0",
4
4
  "mcpName": "io.github.dcostenco/prism-coder",
5
5
  "description": "Persistent session memory for AI coding agents that never leaves your machine — including the on-device model that reasons over it. Restores your prior decisions, open TODOs, and changed files across sessions; adds associative recall of related past work, semantic drift detection, and local inference. Local-first by default. Works with Claude Code, Cursor, and Codex.",
6
6
  "module": "index.ts",