teapot-coding-agent 0.29.9 → 0.29.10

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.
@@ -74,6 +74,43 @@ export const PROGRESS_REQUEST =
74
74
  * meta tools instead. The cache rationale itself stays HERE in a comment —
75
75
  * the model does not need our cost-engineering notes every turn.
76
76
  */
77
+ /**
78
+ * Recent context kept VERBATIM through a compaction, in tokens.
79
+ *
80
+ * Pi defaults `keepRecentTokens` to 20000 and opencode to `keep: 15000` for the
81
+ * same reason: the newest exchanges are what the model needs to continue
82
+ * accurately, and a verbatim copy beats a summary of them.
83
+ */
84
+ const KEEP_RECENT_TOKENS = 15_000;
85
+ /**
86
+ * The summarize call is skipped when the summarizable prefix is a small share of
87
+ * the budget, because the call cannot reuse any prefix cache (its system prompt is
88
+ * its instructions) and so re-bills the whole history uncached. Below this share,
89
+ * dropping the prefix outright reclaims the budget for free and the model can
90
+ * recover the details by re-reading the goal or the files.
91
+ *
92
+ * A SHARE rather than an absolute token count: what counts as "a lot to lose"
93
+ * scales with the window a session is configured for. An absolute constant
94
+ * calibrated for the 96k default would disable summarization entirely for a
95
+ * session with a small budget (measured: a 200-token budget with a 654-token
96
+ * history is over every bar, yet its prefix is a few hundred tokens).
97
+ *
98
+ * Manual compaction and overflow recovery bypass this — those are not cost
99
+ * decisions.
100
+ */
101
+ const SUMMARIZE_MIN_SHARE = 0.02;
102
+ /** the absolute floor for the bar above (see the note where it is applied) */
103
+ const SUMMARIZE_MIN_TOKENS = 2_000;
104
+ /**
105
+ * How much of the budget to KEEP on each successive overflow recovery.
106
+ *
107
+ * An overflow means the estimate was too low, so a single fixed cut can still
108
+ * leave the request too large — and the old code turned that second failure into a
109
+ * hard error. opencode uses the same shape (`SHRINK_STEPS [0.7, 0.5, 0.35]`): each
110
+ * retry keeps less, so the request shrinks even when the first attempt did not
111
+ * shrink it enough. The last entry repeats for any further attempt.
112
+ */
113
+ const OVERFLOW_SHRINK_STEPS = [0.5, 0.35, 0.2];
77
114
  const SYSTEM_TEMPLATE = `You are a coding agent working autonomously inside a workspace.
78
115
 
79
116
  Most session state is not injected into prompts — fetch it with tools instead:
@@ -334,6 +371,17 @@ export class Agent {
334
371
  skillsCache = [];
335
372
  /** fingerprint of the roots that produced skillsCache ("" = not scanned yet) */
336
373
  skillsStamp = "";
374
+ /**
375
+ * The notes the LAST compaction produced, if any.
376
+ *
377
+ * Compaction UPDATES these rather than re-deriving notes from scratch: a fresh
378
+ * summary each pass loses whatever the previous one distilled (every
379
+ * implementation surveyed — opencode, Kilo, Pi — updates for this reason).
380
+ * Recovered from the log so the continuity survives a restart.
381
+ */
382
+ lastCompactSummary;
383
+ /** overflow recoveries used in the current turn; reset when a turn succeeds */
384
+ overflowRecoveries = 0;
337
385
  /**
338
386
  * Rescan skill roots — but only when something actually changed (#14).
339
387
  * This used to run a readdir per root plus a read of every SKILL.md at the
@@ -856,6 +904,13 @@ export class Agent {
856
904
  if (d.event === "context-compacted")
857
905
  this.stats.compactions++;
858
906
  }
907
+ else if (e.type === "compaction") {
908
+ // keep the NEWEST notes: they are what the next compaction updates, and
909
+ // without this a restart would silently fall back to re-summarizing
910
+ const d = e.data;
911
+ if (typeof d.summary === "string" && d.summary)
912
+ this.lastCompactSummary = d.summary;
913
+ }
859
914
  }
860
915
  // newest usage event seeds the context gauge right after reload —
861
916
  // inputTokens from the LAST turn is the live context size, and cached
@@ -1918,20 +1973,27 @@ export class Agent {
1918
1973
  detail: "stopped before any output arrived",
1919
1974
  });
1920
1975
  }
1921
- // Provider context overflow → compact ONCE and retry the turn
1922
- // (OpenCode-style overflow recovery). even with autoCompact off, an
1923
- // actual overflow is proof the estimate missed; a second overflow is
1924
- // returned as a real error instead of looping.
1976
+ // Provider context overflow → compact and retry (OpenCode-style recovery).
1977
+ // Even with autoCompact off, an actual overflow is proof the estimate
1978
+ // missed. Staged rather than single-shot (opencode: SHRINK_STEPS
1979
+ // [0.7, 0.5, 0.35]): one fixed cut can still be too shallow, and the old
1980
+ // code returned the second overflow as a hard error even though cutting
1981
+ // deeper would have worked.
1925
1982
  if (isContextOverflow(err) && !this.stopRequested) {
1983
+ const attempt = (this.overflowRecoveries += 1);
1926
1984
  await this.log.append("system_note", this.currentSession, this.currentBranch, {
1927
1985
  event: "overflow-recovery",
1986
+ attempt,
1928
1987
  detail: String(err.message).slice(0, 200),
1929
1988
  });
1930
1989
  const before = this.stats.compactions;
1931
- await this.maybeCompact(true);
1990
+ // each retry cuts deeper: 1x, then the staged fractions of the budget
1991
+ const shrink = OVERFLOW_SHRINK_STEPS[Math.min(attempt - 1, OVERFLOW_SHRINK_STEPS.length - 1)];
1992
+ await this.maybeCompact(true, shrink);
1932
1993
  if (this.stats.compactions > before) {
1933
1994
  await this.log.append("system_note", this.currentSession, this.currentBranch, {
1934
1995
  event: "overflow-recovered",
1996
+ attempt,
1935
1997
  });
1936
1998
  guard--; // the retry does not count against this round's cap
1937
1999
  continue;
@@ -1939,6 +2001,9 @@ export class Agent {
1939
2001
  }
1940
2002
  throw err;
1941
2003
  }
2004
+ // a request the provider accepted: the current depth is enough, so the next
2005
+ // overflow starts its staircase over rather than cutting from the deepest step
2006
+ this.overflowRecoveries = 0;
1942
2007
  if (res.usage) {
1943
2008
  const inTok = res.usage.inputTokens ?? 0;
1944
2009
  const cached = res.usage.cachedInputTokens ?? 0;
@@ -2908,7 +2973,7 @@ export class Agent {
2908
2973
  * token/char — the old flat /4 underestimated Japanese sessions ~4x.
2909
2974
  * Adds a small per-message overhead for role/framing tokens.
2910
2975
  */
2911
- estimateTokens() {
2976
+ estimateTokens(msgs = this.messages) {
2912
2977
  let ascii = 0;
2913
2978
  let wide = 0;
2914
2979
  const count = (s) => {
@@ -2919,14 +2984,14 @@ export class Agent {
2919
2984
  ascii++;
2920
2985
  }
2921
2986
  };
2922
- for (const m of this.messages) {
2987
+ for (const m of msgs) {
2923
2988
  count(m.content ?? "");
2924
2989
  for (const t of m.tool_calls ?? []) {
2925
2990
  count(t.function.name);
2926
2991
  count(t.function.arguments);
2927
2992
  }
2928
2993
  }
2929
- return Math.ceil(ascii / 4 + wide * 1.2 + this.messages.length * 8);
2994
+ return Math.ceil(ascii / 4 + wide * 1.2 + msgs.length * 8);
2930
2995
  }
2931
2996
  /**
2932
2997
  * Latest index whose message may START the kept tail of a compacted
@@ -3032,8 +3097,13 @@ export class Agent {
3032
3097
  });
3033
3098
  return count;
3034
3099
  }
3035
- /** Compact history when the real prompt size exceeds the budget. */
3036
- async maybeCompact(force = false) {
3100
+ /**
3101
+ * Compact history when the real prompt size exceeds the budget.
3102
+ *
3103
+ * @param keepFraction share of the budget to keep verbatim; the overflow path
3104
+ * lowers it on each retry so a too-shallow cut is not fatal
3105
+ */
3106
+ async maybeCompact(force = false, keepFraction) {
3037
3107
  // prefer the provider's own count from the last response — it is exactly
3038
3108
  // what would overflow the window; the char heuristic is only a fallback
3039
3109
  // for providers that omit usage
@@ -3050,12 +3120,27 @@ export class Agent {
3050
3120
  return;
3051
3121
  this.lastUsage = undefined; // stale after compaction — re-armed next turn
3052
3122
  // keep roughly the most recent quarter of the budget as live context
3053
- const keepCharBudget = (this.opts.contextTokenBudget / 4) * 4;
3054
- let keepChars = 0;
3123
+ // Keep the most recent stretch VERBATIM, as every implementation surveyed
3124
+ // does (Pi: keepRecentTokens 20000, opencode: keep 15000). Recency is what the
3125
+ // model needs to continue accurately, and verbatim beats a summary of it.
3126
+ //
3127
+ // Size: a quarter of the budget, but never less than KEEP_RECENT_TOKENS — a
3128
+ // small budget would otherwise keep so little that the model loses the turn it
3129
+ // is in the middle of. Measured in TOKENS rather than characters, so the choice
3130
+ // matches the published defaults instead of depending on the char heuristic.
3131
+ const keepTokens = keepFraction !== undefined
3132
+ // an explicit retry (overflow recovery): shrink toward a share of the budget
3133
+ ? Math.max(1, Math.floor(this.opts.contextTokenBudget * keepFraction))
3134
+ // otherwise keep the newest stretch verbatim, but never more than a quarter
3135
+ // of the budget — a fixed floor GREATER than the budget would keep everything
3136
+ // and compaction would never fire at all (measured: a 500-token budget kept
3137
+ // a 7.5k-token history intact)
3138
+ : Math.min(KEEP_RECENT_TOKENS, Math.max(1, Math.floor(this.opts.contextTokenBudget / 4)));
3055
3139
  let cut = -1;
3056
3140
  for (let i = this.messages.length - 1; i >= 1; i--) {
3057
- keepChars += (this.messages[i].content?.length ?? 0) + JSON.stringify(this.messages[i].tool_calls ?? "").length;
3058
- if (keepChars > keepCharBudget)
3141
+ // token-aware boundary, using the SAME estimate the budget uses — a raw char
3142
+ // ratio would disagree with it for wide (CJK) text and per-message overhead
3143
+ if (this.estimateTokens(this.messages.slice(i)) > keepTokens)
3059
3144
  break;
3060
3145
  cut = i;
3061
3146
  }
@@ -3064,10 +3149,62 @@ export class Agent {
3064
3149
  return; // nothing safely compactable (phase never armed yet)
3065
3150
  const old = this.messages.slice(0, cut);
3066
3151
  const oldCount = old.length;
3152
+ // The prefix's share of the size the budget check ACTED on (which floors our
3153
+ // estimate against the provider's reported prompt_tokens). Scaling by character
3154
+ // share rather than message count: messages vary hugely in size, and the point
3155
+ // is to avoid treating an unmeasurably large context as small.
3156
+ const totalChars = this.messages.reduce((n, m) => n + (m.content?.length ?? 0), 0);
3157
+ const oldChars = old.reduce((n, m) => n + (m.content?.length ?? 0), 0);
3158
+ const oldShareOfBefore = totalChars > 0 ? Math.round((before * oldChars) / totalChars) : 0;
3067
3159
  let summary = "";
3068
3160
  let mode = "summarize";
3069
3161
  let summarizedCount = oldCount;
3070
3162
  let droppedCount = 0;
3163
+ // COST GATE (measured). The summarize call shares NO cacheable prefix with the
3164
+ // main loop: the summarizer's system prompt IS its instructions, so message[0]
3165
+ // differs and the entire history is re-prefilled and re-billed at full size.
3166
+ // Sharing the prefix would mean gutting the summarizer's role, so the lever is
3167
+ // not to PAY for a shallow saving.
3168
+ //
3169
+ // The criterion is the ABSOLUTE amount of context the cut would discard, not
3170
+ // how far over the budget we are. A ratio test gets it backwards: a small
3171
+ // window overflowing slightly is exactly the case where a full-history call
3172
+ // buys almost nothing (there is little knowledge to lose), while a large
3173
+ // window overflowing slightly still has a lot to lose. `oldTokens` is the
3174
+ // summarizable prefix — the thing the call is actually paid for.
3175
+ //
3176
+ // Below the threshold the DROP alone reclaims the budget, for free.
3177
+ //
3178
+ // MANUAL is exempt: an explicit /compact is a user command, not a cost
3179
+ // decision, and opencode draws the line in the same place ("only the user
3180
+ // compacts when automatic compaction is off"). An overflow recovery is exempt
3181
+ // too — the provider just told us the prompt was too big, so the question is
3182
+ // not whether the saving is worth a call.
3183
+ // Use BOTH sizes. `oldTokens` is our estimate of the prefix being summarized;
3184
+ // `before` is the size the budget check acted on, which already floors that
3185
+ // estimate against the provider's REPORTED prompt_tokens. Gating on the local
3186
+ // estimate alone would silently truncate a context the provider says is large
3187
+ // but we cannot measure (caught by a test: provider reports 5000 tokens for a
3188
+ // 300-token local estimate). The provider tokenizes for real; our count is a
3189
+ // guess, so the larger of the two is the honest figure.
3190
+ const oldTokens = Math.max(this.estimateTokens(old), oldShareOfBefore);
3191
+ // The bar is a SHARE of the budget with an absolute floor, so it scales with a
3192
+ // realistic window while never sitting above a small session's entire history
3193
+ // (measured: a budget of 200 made a pure-share bar 10 tokens, while the whole
3194
+ // summarizable prefix in that fixture was an 8-token prompt — summarization
3195
+ // would have been silently disabled).
3196
+ //
3197
+ // Below the bar the prefix is a turn or two, and dropping it costs the model
3198
+ // nothing it cannot recover by re-reading the goal or the files; a full-history
3199
+ // LLM call to preserve that is not worth its price.
3200
+ const summarizeBar = Math.max(SUMMARIZE_MIN_TOKENS, this.opts.contextTokenBudget * SUMMARIZE_MIN_SHARE);
3201
+ const worthSummarizing = force || oldTokens >= summarizeBar;
3202
+ // UPDATE mode: carry the previous notes forward instead of re-deriving them.
3203
+ // Every implementation surveyed (opencode, Kilo, Pi) updates rather than
3204
+ // re-summarizes, because a fresh summary each time loses whatever the previous
3205
+ // pass had already distilled. Recovered from the LAST compaction event, so it
3206
+ // survives a restart.
3207
+ const previousSummary = this.lastCompactSummary;
3071
3208
  // progress is visible: announce the phase up front, then stream the
3072
3209
  // summarizer's output as it generates (a long summarize used to look like
3073
3210
  // the agent silently hung — hundreds of thousands of tokens take a while)
@@ -3101,18 +3238,32 @@ export class Agent {
3101
3238
  tokensBefore: before,
3102
3239
  messages: oldCount,
3103
3240
  reason: force ? "manual" : "auto",
3241
+ worthSummarizing, // makes the cost decision auditable
3242
+ updating: Boolean(previousSummary),
3104
3243
  });
3105
3244
  try {
3106
- summary = await this.summarize(old, (text) => {
3107
- bus.emit("update", {
3108
- kind: "llm-delta",
3109
- agentId: this.opts.id,
3110
- // #146: the bubble is session-scoped, so the stream must say which session
3111
- sessionId: this.currentSession,
3112
- text: `[compact] ${text}`,
3113
- reasoning: "",
3245
+ if (worthSummarizing) {
3246
+ summary = await this.summarize(old, (text) => {
3247
+ bus.emit("update", {
3248
+ kind: "llm-delta",
3249
+ agentId: this.opts.id,
3250
+ // #146: the bubble is session-scoped, so the stream must say which session
3251
+ sessionId: this.currentSession,
3252
+ text: `[compact] ${text}`,
3253
+ reasoning: "",
3254
+ });
3255
+ }, previousSummary);
3256
+ }
3257
+ else {
3258
+ // shallow saving: truncation reclaims the budget for free, so skip the
3259
+ // one full-history LLM call the summary would cost
3260
+ await this.log.append("system_note", this.currentSession, this.currentBranch, {
3261
+ event: "context-compaction-skipped-summarize",
3262
+ reason: "shallow: truncation reclaims the budget without a full-history LLM call",
3263
+ tokensBefore: before,
3264
+ budget: this.opts.contextTokenBudget,
3114
3265
  });
3115
- });
3266
+ }
3116
3267
  this.compactPhase = "harvesting";
3117
3268
  bus.emit("update", { kind: "compaction-progress", agentId: this.opts.id, phase: "harvesting" });
3118
3269
  if (summary)
@@ -3145,7 +3296,23 @@ export class Agent {
3145
3296
  return; // the finally clears the phase and tells the UI it ended
3146
3297
  }
3147
3298
  droppedCount = halfCut;
3148
- this.messages = this.messages.slice(halfCut);
3299
+ // Tell the model its history was cut. Dropping a prefix SILENTLY is the
3300
+ // one failure it cannot detect or work around: it would see a conversation
3301
+ // that simply starts mid-way and could not distinguish "I forgot this" from
3302
+ // "this never happened", then confidently act on a gap. Truncation is now
3303
+ // the COMMON path (the cost gate keeps the summarize call for deep cuts
3304
+ // only), which makes this the difference between a known gap and a wrong
3305
+ // belief. The dropped text stays in the log — nothing durable is lost.
3306
+ this.messages = [
3307
+ {
3308
+ role: "user",
3309
+ content: `[harness] ${droppedCount} earlier messages were DROPPED, not summarized (no room to ` +
3310
+ "summarize, or the saving would not have justified the call). Your earlier work is " +
3311
+ "in the session log but is NOT in your context any more: re-read files or the goal " +
3312
+ "before assuming what happened, and do not rely on memory of the dropped part.",
3313
+ },
3314
+ ...this.messages.slice(halfCut),
3315
+ ];
3149
3316
  }
3150
3317
  else {
3151
3318
  this.messages = [
@@ -3226,6 +3393,9 @@ export class Agent {
3226
3393
  mode,
3227
3394
  ...(summary ? { summary: String(summary).slice(0, 20_000) } : {}),
3228
3395
  });
3396
+ // remember the notes for the NEXT compaction to update (and for a restore)
3397
+ if (summary)
3398
+ this.lastCompactSummary = summary;
3229
3399
  await this.log.append("system_note", this.currentSession, this.currentBranch, {
3230
3400
  event: "context-compacted",
3231
3401
  tokensBefore: before,
@@ -3245,7 +3415,7 @@ export class Agent {
3245
3415
  }
3246
3416
  }
3247
3417
  /** Ask the model for dense continuation notes over the compacted range. */
3248
- async summarize(old, onDelta) {
3418
+ async summarize(old, onDelta, previousSummary) {
3249
3419
  const full = old
3250
3420
  .map((m) => {
3251
3421
  const who = m.role === "tool" ? "tool" : m.role;
@@ -3286,10 +3456,25 @@ export class Agent {
3286
3456
  // where "pending tasks" vs "user messages" live, so nothing silently
3287
3457
  // merges away. The <analysis> scratchpad gives the model a thinking space;
3288
3458
  // it is stripped from the stored summary (formatCompactSummary).
3289
- const res = await fn(this.opts.llm, [
3459
+ const res = await fn({ ...this.opts.llm, oneOff: true }, // one-off prompt: ask the provider not to retain it
3460
+ [
3290
3461
  {
3291
3462
  role: "system",
3292
- content: "You compress a coding agent's conversation into dense notes for it to continue working.\n\n" +
3463
+ content:
3464
+ // Both variants OPEN with the same sentence. Tests (and any future caller)
3465
+ // identify the summarizer by this text, and a shared prefix keeps that
3466
+ // detection working across the initial/update split — the first version of
3467
+ // update mode changed it to "You UPDATE…", which silently made every
3468
+ // update pass look like an ordinary turn.
3469
+ "You compress a coding agent's conversation into dense notes for it to continue working.\n\n" +
3470
+ (previousSummary
3471
+ ? "This is an UPDATE: the current notes are under `## Current notes`. They are your\n" +
3472
+ "output from an earlier compaction and are the ONLY record of everything before the\n" +
3473
+ "messages you are about to read — so carry them forward. PRESERVE what is still\n" +
3474
+ "true, ADD what the new messages establish, and REMOVE only what they resolve or\n" +
3475
+ "supersede. Never drop a constraint or a user request just because the new\n" +
3476
+ "messages do not repeat it: an unanswered request stays, an answered one goes.\n\n"
3477
+ : "") +
3293
3478
  "First think inside an <analysis> block: what mattered, what changed, what is unresolved.\n" +
3294
3479
  "Then output ONLY the following sections, each starting with its exact heading:\n\n" +
3295
3480
  "## Primary request and intent\n" +
@@ -3307,7 +3492,14 @@ export class Agent {
3307
3492
  'Durable lessons lists reusable insights worth keeping forever (gotchas, preferences, what worked); omit the section if empty. ' +
3308
3493
  "The <analysis> block is discarded before your notes are stored, so think freely there.",
3309
3494
  },
3310
- { role: "user", content: `Conversation:\n\n${transcript}\n\nWrite the continuation notes now.` },
3495
+ {
3496
+ role: "user",
3497
+ content: (previousSummary ? `## Current notes\n\n${previousSummary}\n\n` : "") +
3498
+ `## Conversation\n\n${transcript}\n\n` +
3499
+ (previousSummary
3500
+ ? "Update the current notes to cover this conversation as well. Return the FULL updated notes."
3501
+ : "Write the continuation notes now."),
3502
+ },
3311
3503
  ], [], undefined, // never abortable by stop(): losing the summary would lose history
3312
3504
  onDelta
3313
3505
  ? (s) => onDelta(s.text)
package/dist/agent/llm.js CHANGED
@@ -37,6 +37,27 @@ function isOpenRouterEndpoint(baseUrl) {
37
37
  return false;
38
38
  }
39
39
  }
40
+ /**
41
+ * Ask the endpoint NOT to retain a one-off request (Pi does the same with
42
+ * `cacheRetention: "none"`; its docs say "summarization requests disable
43
+ * prompt-cache writes because these one-off prompts are unlikely to be reused").
44
+ *
45
+ * Sent ONLY to OpenRouter, which is the one endpoint in use here that both
46
+ * accepts the field and documents it. Every other OpenAI-compatible endpoint gets
47
+ * nothing: an unrecognised body field is a 400 on strict providers, and "send
48
+ * nothing" is exactly today's behaviour, so this can only ever add a saving.
49
+ *
50
+ * `24h` is the value that means "do not retain beyond this request's lifetime" for
51
+ * an organisation with zero-data-retention off; with ZDR on the default is already
52
+ * `in_memory` and this is a no-op.
53
+ */
54
+ export function cacheControlForRequest(cfg) {
55
+ if (!cfg.oneOff)
56
+ return {};
57
+ if (!isOpenRouterEndpoint(cfg.baseUrl))
58
+ return {};
59
+ return { prompt_cache_retention: "24h" };
60
+ }
40
61
  const REASONING_EFFORTS = ["minimal", "low", "medium", "high"];
41
62
  /**
42
63
  * OpenRouter app attribution (https://openrouter.ai/docs/app-attribution):
@@ -194,6 +215,7 @@ export async function chat(cfg, messages, tools, signal, onDelta) {
194
215
  messages: sanitize(messages),
195
216
  ...(tools.length ? { tools } : {}),
196
217
  ...effortForRequest(cfg),
218
+ ...cacheControlForRequest(cfg),
197
219
  }, { signal });
198
220
  // #50: a gateway may answer with `null` (or any non-object) on some proxy
199
221
  // error paths, so the guard is on `rm`, not on `res` being truthy —
@@ -289,6 +311,7 @@ export async function chatStream(cfg, messages, tools, signal, onDelta) {
289
311
  messages: sanitize(messages),
290
312
  ...(tools.length ? { tools } : {}),
291
313
  ...effortForRequest(cfg),
314
+ ...cacheControlForRequest(cfg),
292
315
  stream: true,
293
316
  // ask providers to include the final usage chunk in streaming mode
294
317
  // (OpenAI-compatible; harmless where unsupported)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teapot-coding-agent",
3
- "version": "0.29.9",
3
+ "version": "0.29.10",
4
4
  "description": "A lightweight, always-on multi-agent harness for AI coding agents",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-or-later",