@gamaze/hicortex 0.19.4 → 0.19.5

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.
@@ -38,10 +38,24 @@ export declare function extractConversationText(messages: unknown[], redactionCo
38
38
  * `droppedOut`, when provided, is filled with every entry the substance gate
39
39
  * discarded (full text). Callers use it to build a durable audit trail (#156);
40
40
  * omitting it leaves gate behaviour unchanged.
41
+ *
42
+ * `segmentLabel` (optional) identifies the caller's segment in the #339
43
+ * over-firing warning (e.g. the capture pipeline's segment_id). Purely for
44
+ * log correlation — omitting it falls back to "chunk".
41
45
  */
42
46
  export declare function distillSession(llm: LlmClient, conversation: string, projectName: string, date: string, chunkSizeChars?: number, droppedOut?: string[],
43
47
  /** Called with each chunk's token usage (#5 budget metering). Optional. */
44
- onUsage?: (usage: LlmUsage) => void): Promise<DistilledEntry[]>;
48
+ onUsage?: (usage: LlmUsage) => void,
49
+ /** Segment identifier for the #339 NO_EXTRACT warning. Optional. */
50
+ segmentLabel?: string): Promise<DistilledEntry[]>;
51
+ /**
52
+ * The NO_EXTRACT check distillChunk applies to an LLM response. EXPORTED and
53
+ * shared (not copy-pasted) with scripts/distill-ab-check/, whose counts must
54
+ * classify empty verdicts exactly as production does (#339 CR finding 3).
55
+ * Tolerant by design: a literal NO_EXTRACT anywhere in the first 20 chars
56
+ * counts (models prepend stray whitespace or a short phrase).
57
+ */
58
+ export declare function isNoExtractResponse(result: string): boolean;
45
59
  /**
46
60
  * Reject ONLY structurally-empty distiller fragments before they become
47
61
  * memories (#156). The distiller occasionally emits leftovers that parse into
@@ -76,3 +90,34 @@ export interface DistilledEntry {
76
90
  content: string;
77
91
  memoryType: "experience" | "knowledge" | "decisions";
78
92
  }
93
+ /**
94
+ * Map a single-letter type tag to the stored memory_type. Unknown/absent →
95
+ * experience (the pre-#216 default). `[L]` is explicitly rejected →
96
+ * experience: the distiller must NEVER emit learnings (that's the reflection
97
+ * stage's job), so a model that emits `[L]` is wrong and we do not propagate
98
+ * it as a learning.
99
+ *
100
+ * The single-letter tags ([E]/[K]/[D]) are unchanged from the raw-enum era —
101
+ * the model is taught these as "EXPERIENCE/KNOWLEDGE/DECISIONS" concepts in prompts.ts
102
+ * (ordinary English the model understands), and only the resulting STORED
103
+ * value changed in #264 (episode→experience, fact→knowledge, decision→
104
+ * decisions). The tag letters stay stable so neither the prompt nor the
105
+ * parser needs to change; only this mapping table moves.
106
+ *
107
+ * EXPORTED (with parseDistilledEntries) for scripts/distill-ab-check/ (#339 CR
108
+ * finding 3): the A/B harness computes its counts from each variant build's own
109
+ * parser instead of a copy-pasted mirror, so harness numbers are by construction
110
+ * the numbers that build's production would store. tests/distill-ab-parser-contract.test.ts
111
+ * pins the src and dist parsers against the same corpus.
112
+ */
113
+ export declare function typeFromTag(letter: string | undefined): DistilledEntry["memoryType"];
114
+ /**
115
+ * Parse distilled markdown into individual memory entries with type tags.
116
+ * Each bullet becomes a separate memory. The leading `[E]`/`[F]`/`[D]` type
117
+ * tag is extracted (→ memoryType), stripped from the stored content, and
118
+ * passed to `insertMemory` via the `memoryType` option (#216). Bullets with
119
+ * no tag default to "experience" (backward compatible with pre-#216 distiller
120
+ * output that never carried a tag). EXPORTED for the A/B harness — see
121
+ * typeFromTag's comment (#339 CR finding 3).
122
+ */
123
+ export declare function parseDistilledEntries(markdown: string): DistilledEntry[];
package/dist/distiller.js CHANGED
@@ -8,11 +8,28 @@ Object.defineProperty(exports, "__esModule", { value: true });
8
8
  exports.detectChunkSize = detectChunkSize;
9
9
  exports.extractConversationText = extractConversationText;
10
10
  exports.distillSession = distillSession;
11
+ exports.isNoExtractResponse = isNoExtractResponse;
11
12
  exports.hasMinimalSubstance = hasMinimalSubstance;
13
+ exports.typeFromTag = typeFromTag;
14
+ exports.parseDistilledEntries = parseDistilledEntries;
12
15
  const prompts_js_1 = require("./prompts.js");
13
16
  const redact_js_1 = require("./redact.js");
14
17
  const MAX_TRANSCRIPT_CHARS = 80_000;
15
18
  const MIN_CONVERSATION_CHARS = 200;
19
+ // #339 (2026-08-24 postmortem): NO_EXTRACT over-firing visibility threshold.
20
+ // Real summary-led segments that the model wrongly abandoned ran 38-64K
21
+ // denoised chars; genuine pure-status noise is ~2.6K. A segment larger than
22
+ // this whose every chunk returns an empty LLM verdict is far more likely the
23
+ // model pattern-matching a bookkeeping-heavy OPENING to the ephemera gate than
24
+ // a legitimately empty segment — so it gets a warning line. Warning ONLY: no
25
+ // auto-retry (cost); the goal is that silent segment loss shows up in the
26
+ // nightly log instead of in a weeks-later eval.
27
+ //
28
+ // The threshold compares the PRE-CHUNKING conversation length, never the chunk
29
+ // length: default ollama chunking (numCtx 8192 → ~19.6K chars) and the
30
+ // small-model speed cap (20K) both keep every chunk at or below this number,
31
+ // so a chunk-level check would be unreachable exactly where the incident lived.
32
+ const NO_EXTRACT_WARN_MIN_CHARS = 20_000;
16
33
  // Chunk size limits by model parameter count (for local/CPU inference)
17
34
  // Small models are slow on CPU — cap input size to keep inference under ~60s
18
35
  const SMALL_MODEL_PARAMS = 8_000_000_000; // 8B — threshold for "small"
@@ -225,10 +242,16 @@ function extractConversationText(messages, redactionConfig) {
225
242
  * `droppedOut`, when provided, is filled with every entry the substance gate
226
243
  * discarded (full text). Callers use it to build a durable audit trail (#156);
227
244
  * omitting it leaves gate behaviour unchanged.
245
+ *
246
+ * `segmentLabel` (optional) identifies the caller's segment in the #339
247
+ * over-firing warning (e.g. the capture pipeline's segment_id). Purely for
248
+ * log correlation — omitting it falls back to "chunk".
228
249
  */
229
250
  async function distillSession(llm, conversation, projectName, date, chunkSizeChars, droppedOut,
230
251
  /** Called with each chunk's token usage (#5 budget metering). Optional. */
231
- onUsage) {
252
+ onUsage,
253
+ /** Segment identifier for the #339 NO_EXTRACT warning. Optional. */
254
+ segmentLabel) {
232
255
  if (conversation.length < MIN_CONVERSATION_CHARS) {
233
256
  return [];
234
257
  }
@@ -241,9 +264,11 @@ onUsage) {
241
264
  const chunkSize = chunkSizeChars ?? MAX_TRANSCRIPT_CHARS;
242
265
  // If transcript fits in one chunk, distill directly (errors propagate)
243
266
  if (transcript.length <= chunkSize) {
244
- const { entries, dropped } = await distillChunk(llm, transcript, projectName, date, onUsage);
267
+ const { entries, dropped, emptyVerdict } = await distillChunk(llm, transcript, projectName, date, onUsage);
245
268
  if (droppedOut)
246
269
  droppedOut.push(...dropped);
270
+ if (emptyVerdict)
271
+ warnSuspiciousEmptySegment(segmentLabel, conversation.length);
247
272
  return entries;
248
273
  }
249
274
  // Chunk large transcripts and distill each segment.
@@ -259,11 +284,14 @@ onUsage) {
259
284
  const allEntries = [];
260
285
  const seen = new Set();
261
286
  let chunkFailures = 0;
287
+ let emptyVerdicts = 0;
262
288
  let lastError = null;
263
289
  for (let i = 0; i < chunks.length; i++) {
264
290
  console.log(`[hicortex] Chunk ${i + 1}/${chunks.length} (${chunks[i].length} chars)`);
265
291
  try {
266
- const { entries, dropped } = await distillChunk(llm, chunks[i], projectName, date, onUsage);
292
+ const { entries, dropped, emptyVerdict } = await distillChunk(llm, chunks[i], projectName, date, onUsage);
293
+ if (emptyVerdict)
294
+ emptyVerdicts++;
267
295
  if (droppedOut)
268
296
  droppedOut.push(...dropped);
269
297
  for (const entry of entries) {
@@ -292,8 +320,38 @@ onUsage) {
292
320
  if (chunkFailures > 0) {
293
321
  console.warn(`[hicortex] Partial distillation: ${chunks.length - chunkFailures}/${chunks.length} chunks succeeded`);
294
322
  }
323
+ // #339 segment-level net (CR finding 1): fire when the WHOLE segment produced
324
+ // zero memories and every processed chunk returned an empty LLM verdict. Fires
325
+ // only with zero chunk failures — failed chunks are already loudly visible,
326
+ // and "all failed" throws above. The size gate lives INSIDE
327
+ // warnSuspiciousEmptySegment (strict >, pre-chunking conversation length).
328
+ if (allEntries.length === 0 &&
329
+ chunkFailures === 0 &&
330
+ emptyVerdicts === chunks.length) {
331
+ warnSuspiciousEmptySegment(segmentLabel, conversation.length);
332
+ }
295
333
  return allEntries;
296
334
  }
335
+ /**
336
+ * #339 over-firing visibility net: an empty result this large is the silent-loss
337
+ * signature (real summary-led segments are 38-64K chars; legitimate pure-status
338
+ * noise is ~2.6K). Warning-only, content-free — segment id + size, nothing from
339
+ * the transcript. The empty SUCCESS semantics are unchanged (no throw, no
340
+ * retry): the cursor advances, but the loss is now VISIBLE in the nightly log
341
+ * instead of surfacing weeks later in an eval.
342
+ *
343
+ * The size check lives here, not at call sites, so no path can skip it. Strict
344
+ * comparison: exactly NO_EXTRACT_WARN_MIN_CHARS chars is a legitimate small
345
+ * segment and stays silent.
346
+ */
347
+ function warnSuspiciousEmptySegment(segmentLabel, segmentChars) {
348
+ if (segmentChars <= NO_EXTRACT_WARN_MIN_CHARS)
349
+ return;
350
+ console.warn(`[hicortex] Suspicious empty distillation: zero memories for a ${segmentChars}-char ` +
351
+ `${segmentLabel ? `segment ${segmentLabel}` : "segment"} ` +
352
+ `(every chunk returned NO_EXTRACT or nothing parseable) — segments this large almost always ` +
353
+ `contain extractable material; if this repeats, suspect gate over-firing (logged only, not retried)`);
354
+ }
297
355
  /**
298
356
  * Distill a single chunk of conversation text.
299
357
  *
@@ -309,6 +367,13 @@ onUsage) {
309
367
  *
310
368
  * `dropped` carries entries the substance gate rejected (full text) so the
311
369
  * caller can surface them in a durable audit trail (#156).
370
+ *
371
+ * `emptyVerdict` is true when the chunk was processed successfully but the LLM's
372
+ * verdict contained nothing extractable: bare NO_EXTRACT, an empty response, or
373
+ * text that parses to zero bullets (prose — the silent twin of NO_EXTRACT,
374
+ * #339 CR finding 2). The caller aggregates these for the segment-level
375
+ * over-firing warning; entries extracted then dropped by the substance gate do
376
+ * NOT count (their drops are already logged).
312
377
  */
313
378
  async function distillChunk(llm, transcript, projectName, date, onUsage) {
314
379
  const prompt = (0, prompts_js_1.distillation)(projectName, date, transcript);
@@ -323,10 +388,8 @@ async function distillChunk(llm, transcript, projectName, date, onUsage) {
323
388
  // trip a budget.
324
389
  if (usage && onUsage)
325
390
  onUsage(usage);
326
- if (!result)
327
- return { entries: [], dropped: [] };
328
- if (result === "NO_EXTRACT" || result.slice(0, 20).includes("NO_EXTRACT")) {
329
- return { entries: [], dropped: [] };
391
+ if (!result || isNoExtractResponse(result)) {
392
+ return { entries: [], dropped: [], emptyVerdict: true };
330
393
  }
331
394
  const parsed = parseDistilledEntries(result);
332
395
  // Smoke alarm (PR #218 review): the prompt enforces topic-first, but models
@@ -356,7 +419,20 @@ async function distillChunk(llm, transcript, projectName, date, onUsage) {
356
419
  }
357
420
  console.log(`[hicortex] Substance gate: dropped ${dropped.length}/${parsed.length} content-free fragment(s)`);
358
421
  }
359
- return { entries, dropped };
422
+ // Parsed-zero bypass (#339 CR finding 2): a non-empty response with no
423
+ // NO_EXTRACT token that still parses to zero bullets is the silent twin of
424
+ // NO_EXTRACT — an empty verdict for the over-firing net.
425
+ return { entries, dropped, emptyVerdict: parsed.length === 0 };
426
+ }
427
+ /**
428
+ * The NO_EXTRACT check distillChunk applies to an LLM response. EXPORTED and
429
+ * shared (not copy-pasted) with scripts/distill-ab-check/, whose counts must
430
+ * classify empty verdicts exactly as production does (#339 CR finding 3).
431
+ * Tolerant by design: a literal NO_EXTRACT anywhere in the first 20 chars
432
+ * counts (models prepend stray whitespace or a short phrase).
433
+ */
434
+ function isNoExtractResponse(result) {
435
+ return result === "NO_EXTRACT" || result.slice(0, 20).includes("NO_EXTRACT");
360
436
  }
361
437
  /**
362
438
  * Split transcript text into chunks at natural boundaries (double newlines).
@@ -445,6 +521,12 @@ function hasMinimalSubstance(entry) {
445
521
  * value changed in #264 (episode→experience, fact→knowledge, decision→
446
522
  * decisions). The tag letters stay stable so neither the prompt nor the
447
523
  * parser needs to change; only this mapping table moves.
524
+ *
525
+ * EXPORTED (with parseDistilledEntries) for scripts/distill-ab-check/ (#339 CR
526
+ * finding 3): the A/B harness computes its counts from each variant build's own
527
+ * parser instead of a copy-pasted mirror, so harness numbers are by construction
528
+ * the numbers that build's production would store. tests/distill-ab-parser-contract.test.ts
529
+ * pins the src and dist parsers against the same corpus.
448
530
  */
449
531
  function typeFromTag(letter) {
450
532
  switch (letter) {
@@ -467,7 +549,8 @@ function typeFromTag(letter) {
467
549
  * tag is extracted (→ memoryType), stripped from the stored content, and
468
550
  * passed to `insertMemory` via the `memoryType` option (#216). Bullets with
469
551
  * no tag default to "experience" (backward compatible with pre-#216 distiller
470
- * output that never carried a tag).
552
+ * output that never carried a tag). EXPORTED for the A/B harness — see
553
+ * typeFromTag's comment (#339 CR finding 3).
471
554
  */
472
555
  function parseDistilledEntries(markdown) {
473
556
  const entries = [];
@@ -1198,7 +1198,14 @@ async function startServer(options = {}) {
1198
1198
  distillUsage.prompt += u.prompt_tokens ?? 0;
1199
1199
  distillUsage.completion += u.completion_tokens ?? 0;
1200
1200
  distillUsage.total += u.total_tokens ?? 0;
1201
- });
1201
+ },
1202
+ // #339: identify this POST in the NO_EXTRACT over-firing warning —
1203
+ // segment_id (incremental capture), else session_id (legacy), else none.
1204
+ typeof segment_id === "string" && segment_id
1205
+ ? segment_id
1206
+ : typeof session_id === "string" && session_id
1207
+ ? session_id
1208
+ : undefined);
1202
1209
  // Phase 1 — embed every chunk up front (async). If ANY embed fails we
1203
1210
  // never reach the insert, so nothing is stored.
1204
1211
  const createdAt = new Date(date).toISOString();
package/dist/prompts.d.ts CHANGED
@@ -19,20 +19,23 @@ export declare function reflection(memoriesBlock: string, recentLessons?: string
19
19
  /**
20
20
  * Distillation prompt. Extracts knowledge from a session transcript.
21
21
  *
22
- * LAYOUT (#329 item 6): the ~6.3KB of static instructions come FIRST and the
23
- * transcript LAST, so every distill call shares a byte-identical instruction
24
- * prefix and provider-side prompt prefix caching can actually hit (per-session
25
- * calls with the same project/date share everything up to the transcript; a
26
- * multi-segment session the common capture shape re-uses the cached prefix
27
- * for every segment after the first). The static block is the transcript-first
28
- * block MOVED plus ONE deliberate addition in the same change: the #329 item-5
29
- * [D]-override sentence in the NEVER-RECORD section ("a version bump, merge,
30
- * or count is NEVER [D]"). Anyone diffing distill behavior across this change
31
- * must baseline against BOTH the reorder and that wording addition. NOTE the
32
- * prefix is only
33
- * fully shared while project/date agree: "# Session Memory: ${date} -
34
- * ${projectName}" and the (${date}) format examples interpolate inside the
35
- * static block by design (the model needs the real date in its output format).
22
+ * LAYOUT (REVERTED 2026-08-24): transcript BEFORE the static instruction
23
+ * block the pre-0.19.4 order. The #329 item-6 reorder (static-first, for
24
+ * provider prefix caching) was REVERTED after a deterministic A/B on real
25
+ * segments: with instructions first, the model over-fires NO_EXTRACT on
26
+ * summary-led and long mixed sessions (a real coding segment: 15 memories
27
+ * 0; a real Hermes session: rich 0; isolation proved the LAYOUT caused it,
28
+ * not the item-5 sentence, which is KEPT). Silent shape-dependent segment
29
+ * loss beats any caching win. Re-attempting instructions-first requires a
30
+ * gate fix that passes the A/B matrix harness first.
31
+ *
32
+ * #339 gate hardening (same day): the NO_EXTRACT rule now carries an explicit
33
+ * whole-transcript guard + counter-example (a summary-led session that
34
+ * contains later decisions MUST be extracted) the over-firing mechanism was
35
+ * the model pattern-matching a bookkeeping-heavy OPENING to the ephemera gate
36
+ * and abandoning the whole segment. Companion visibility net (warning on
37
+ * large empty results) lives in distiller.ts; the release-gate harness is
38
+ * scripts/distill-ab-check/.
36
39
  */
37
40
  export declare function distillation(projectName: string, date: string, transcript: string): string;
38
41
  /**
package/dist/prompts.js CHANGED
@@ -107,25 +107,31 @@ Respond with a JSON array. Empty array [] is a valid response.`;
107
107
  /**
108
108
  * Distillation prompt. Extracts knowledge from a session transcript.
109
109
  *
110
- * LAYOUT (#329 item 6): the ~6.3KB of static instructions come FIRST and the
111
- * transcript LAST, so every distill call shares a byte-identical instruction
112
- * prefix and provider-side prompt prefix caching can actually hit (per-session
113
- * calls with the same project/date share everything up to the transcript; a
114
- * multi-segment session the common capture shape re-uses the cached prefix
115
- * for every segment after the first). The static block is the transcript-first
116
- * block MOVED plus ONE deliberate addition in the same change: the #329 item-5
117
- * [D]-override sentence in the NEVER-RECORD section ("a version bump, merge,
118
- * or count is NEVER [D]"). Anyone diffing distill behavior across this change
119
- * must baseline against BOTH the reorder and that wording addition. NOTE the
120
- * prefix is only
121
- * fully shared while project/date agree: "# Session Memory: ${date} -
122
- * ${projectName}" and the (${date}) format examples interpolate inside the
123
- * static block by design (the model needs the real date in its output format).
110
+ * LAYOUT (REVERTED 2026-08-24): transcript BEFORE the static instruction
111
+ * block the pre-0.19.4 order. The #329 item-6 reorder (static-first, for
112
+ * provider prefix caching) was REVERTED after a deterministic A/B on real
113
+ * segments: with instructions first, the model over-fires NO_EXTRACT on
114
+ * summary-led and long mixed sessions (a real coding segment: 15 memories
115
+ * 0; a real Hermes session: rich 0; isolation proved the LAYOUT caused it,
116
+ * not the item-5 sentence, which is KEPT). Silent shape-dependent segment
117
+ * loss beats any caching win. Re-attempting instructions-first requires a
118
+ * gate fix that passes the A/B matrix harness first.
119
+ *
120
+ * #339 gate hardening (same day): the NO_EXTRACT rule now carries an explicit
121
+ * whole-transcript guard + counter-example (a summary-led session that
122
+ * contains later decisions MUST be extracted) the over-firing mechanism was
123
+ * the model pattern-matching a bookkeeping-heavy OPENING to the ephemera gate
124
+ * and abandoning the whole segment. Companion visibility net (warning on
125
+ * large empty results) lives in distiller.ts; the release-gate harness is
126
+ * scripts/distill-ab-check/.
124
127
  */
125
128
  function distillation(projectName, date, transcript) {
126
129
  return `You are a memory extraction agent. Analyze this AI session transcript and extract
127
130
  knowledge worth remembering long-term.
128
131
 
132
+ SESSION TRANSCRIPT (project: ${projectName}, date: ${date}):
133
+ ${transcript}
134
+
129
135
  EXTRACT into this markdown format:
130
136
 
131
137
  # Session Memory: ${date} - ${projectName}
@@ -213,6 +219,23 @@ user-confirmed standardization it embodies qualifies.
213
219
  If EVERY item in the transcript is never-record ephemera, output ONLY:
214
220
  "NO_EXTRACT" — zero memories is the correct result for a pure-status segment.
215
221
 
222
+ NO_EXTRACT guard (a verdict on the WHOLE transcript, never on its opening):
223
+ "NO_EXTRACT" requires that NO durable decision, knowledge, or correction
224
+ appears ANYWHERE in the transcript — including after long bookkeeping
225
+ stretches. The opening is not evidence about the rest: real sessions often
226
+ OPEN with bookkeeping (a compaction summary, a task notification, a status
227
+ recap) and CONTAIN extractable material later. Read to the END of the
228
+ transcript before deciding; NO_EXTRACT on a long, mixed session is almost
229
+ always a mistake — when in doubt, extract the durable items.
230
+ Counter-example (MUST be extracted, never NO_EXTRACT): a session opens with
231
+ "Session summary: continuing the API migration; prior PR merged, tests
232
+ green" but later the user confirms "standardize on the queue-based worker —
233
+ make it the documented default" and corrects the assistant: "no, don't gate
234
+ retries behind a flag — remove the flag entirely". That session yields at
235
+ least a [D] standardization and an [E] correction; the summary opening
236
+ changes nothing. Emitting NO_EXTRACT there would lose the only record of
237
+ both.
238
+
216
239
  RULES:
217
240
  - Extract MAX 20 items total (quality over quantity)
218
241
  - Use EXACT names/versions/paths/numbers as they appear in the transcript —
@@ -231,9 +254,6 @@ RULES:
231
254
  "[Strong Negative] User rejected per-agent billing"). The subject always comes first.
232
255
  - Omit any section that has zero items (don't include empty sections)
233
256
  - If nothing worth extracting, output ONLY: "NO_EXTRACT"
234
-
235
- SESSION TRANSCRIPT (project: ${projectName}, date: ${date}):
236
- ${transcript}
237
257
  `;
238
258
  }
239
259
  /**
@@ -2,7 +2,7 @@
2
2
  "id": "hicortex",
3
3
  "name": "Hicortex — Long-term Memory That Learns",
4
4
  "description": "Your agents remember past decisions, avoid repeated mistakes, and get smarter every day. Nightly reflection generates actionable lessons that automatically update agent behavior.",
5
- "version": "0.19.4",
5
+ "version": "0.19.5",
6
6
  "kind": "lifecycle",
7
7
  "skills": ["./skills/hicortex-memory"],
8
8
  "configSchema": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gamaze/hicortex",
3
- "version": "0.19.4",
3
+ "version": "0.19.5",
4
4
  "description": "Persistent agent identity for AI agents \u2014 a hand-edited identity layer, nightly-distilled experience, and lessons injected every session, shared across your whole fleet. Works with Hermes, OpenClaw, Claude Code, and Pi.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {