pi-cliffcompaction 0.1.0 → 0.2.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/CHANGELOG.md +4 -0
- package/README.md +16 -15
- package/index.ts +36 -3
- package/lib/cliff.ts +32 -2
- package/lib/config.ts +15 -3
- package/lib/decode.ts +5 -5
- package/lib/dialects/anthropic.ts +50 -3
- package/lib/dialects/base.ts +28 -0
- package/lib/dialects/index.ts +8 -1
- package/lib/dialects/openai-chat.ts +39 -10
- package/lib/dialects/openai-responses.ts +99 -10
- package/lib/engine.ts +120 -108
- package/lib/images.ts +29 -10
- package/lib/index.ts +4 -1
- package/lib/json.ts +9 -14
- package/lib/pi-hook.ts +144 -73
- package/lib/store.ts +18 -4
- package/package.json +3 -3
package/lib/json.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* JSON serializers
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* billable character counts.
|
|
2
|
+
* JavaScript-local JSON serializers. Canonical hashes use UTF-16 key order
|
|
3
|
+
* and ECMAScript numbers; they are not generally interchangeable with Python
|
|
4
|
+
* json.dumps hashes. Default formatting uses Python-style separators only.
|
|
6
5
|
*
|
|
7
|
-
* dumpsLen counts
|
|
6
|
+
* dumpsLen counts UTF-16 code units, not UTF-8 bytes or tokenizer tokens.
|
|
8
7
|
*/
|
|
9
8
|
|
|
10
9
|
import {
|
|
@@ -103,7 +102,7 @@ function pushCanonical(value: JsonValue, parts: { push(chunk: string): void }):
|
|
|
103
102
|
parts.push("}");
|
|
104
103
|
}
|
|
105
104
|
|
|
106
|
-
/**
|
|
105
|
+
/** Sorted compact JSON for local digest identity; not a full JCS validator. */
|
|
107
106
|
export function canonicalJson(value: JsonValue): string {
|
|
108
107
|
const parts: string[] = [];
|
|
109
108
|
pushCanonical(value, parts);
|
|
@@ -173,7 +172,7 @@ function pushDefault(value: JsonValue, parts: string[]): void {
|
|
|
173
172
|
parts.push("}");
|
|
174
173
|
}
|
|
175
174
|
|
|
176
|
-
/**
|
|
175
|
+
/** Insertion-order JSON with ", " / ": " separators and JS number/string semantics. */
|
|
177
176
|
export function dumpsDefault(value: JsonValue): string {
|
|
178
177
|
const parts: string[] = [];
|
|
179
178
|
pushDefault(value, parts);
|
|
@@ -244,13 +243,9 @@ export function dumpsLen(value: JsonValue): number {
|
|
|
244
243
|
}
|
|
245
244
|
|
|
246
245
|
export function objectWithoutKey(obj: JsonObject, key: string): JsonObject {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if (k !== key) {
|
|
251
|
-
out[k] = obj[k];
|
|
252
|
-
}
|
|
253
|
-
}
|
|
246
|
+
// Object spread creates data properties, including an own "__proto__" key.
|
|
247
|
+
const out: JsonObject = { ...obj };
|
|
248
|
+
delete out[key];
|
|
254
249
|
|
|
255
250
|
return out;
|
|
256
251
|
}
|
package/lib/pi-hook.ts
CHANGED
|
@@ -5,13 +5,15 @@
|
|
|
5
5
|
* from firstKeptEntryId. There is no separate head slot, so the verbatim
|
|
6
6
|
* head (task) is folded into the summary text. Previous compaction
|
|
7
7
|
* entries are skipped so we never compact a compaction: each pass
|
|
8
|
-
* compresses only original live-session messages (Lt)
|
|
8
|
+
* compresses only original live-session messages (Lt), with the original
|
|
9
|
+
* head retained across cliffs. Multimodal heads fall back to Pi because a
|
|
10
|
+
* text summary cannot preserve them.
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
|
-
import { compact } from "./cliff.ts";
|
|
13
|
+
import { compact, type CompactResult } from "./cliff.ts";
|
|
12
14
|
import { replaceConfig, type Config } from "./config.ts";
|
|
13
15
|
import { isRecord, isString, type JsonObject } from "./decode.ts";
|
|
14
|
-
import { SUMMARY_HEADER } from "./dialects/base.ts";
|
|
16
|
+
import { BRANCH_SUMMARY_PREFIX, BRANCH_SUMMARY_SUFFIX, fitSummary, SUMMARY_HEADER } from "./dialects/base.ts";
|
|
15
17
|
import { DIALECT as piDialect } from "./dialects/pi.ts";
|
|
16
18
|
import { estimateTokens } from "./engine.ts";
|
|
17
19
|
|
|
@@ -20,14 +22,33 @@ export type SessionMessageRef = {
|
|
|
20
22
|
message: JsonObject;
|
|
21
23
|
};
|
|
22
24
|
|
|
25
|
+
export type CompactReason = "manual" | "threshold" | "overflow";
|
|
26
|
+
|
|
23
27
|
export type CompactSessionInput = {
|
|
24
28
|
live: SessionMessageRef[];
|
|
25
29
|
tokensBefore: number;
|
|
26
30
|
fallbackFirstKeptEntryId: string;
|
|
27
|
-
reason:
|
|
31
|
+
reason: CompactReason;
|
|
28
32
|
cfg: Config;
|
|
29
33
|
};
|
|
30
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Pi's session_before_compact event has no reason field. Auto-compaction
|
|
37
|
+
* emits auto_compaction_start first with threshold | overflow | idle |
|
|
38
|
+
* incomplete. Manual `/compact` never fires that start event.
|
|
39
|
+
*/
|
|
40
|
+
export function resolveCompactReason(raw: unknown): CompactReason {
|
|
41
|
+
if (raw === "overflow" || raw === "incomplete") {
|
|
42
|
+
return "overflow";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (raw === "threshold" || raw === "idle") {
|
|
46
|
+
return "threshold";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return "manual";
|
|
50
|
+
}
|
|
51
|
+
|
|
31
52
|
export type CliffDetails = {
|
|
32
53
|
version: number;
|
|
33
54
|
method: "cliffcompaction";
|
|
@@ -39,6 +60,7 @@ export type CliffDetails = {
|
|
|
39
60
|
liveMessages: number;
|
|
40
61
|
keptMessages: number;
|
|
41
62
|
estTokensOut: number;
|
|
63
|
+
overBudget: boolean;
|
|
42
64
|
};
|
|
43
65
|
|
|
44
66
|
export type CompactSessionOutput = {
|
|
@@ -77,27 +99,35 @@ function summaryBody(summary: JsonObject): string {
|
|
|
77
99
|
return "";
|
|
78
100
|
}
|
|
79
101
|
|
|
80
|
-
function
|
|
81
|
-
|
|
82
|
-
const headCfg = replaceConfig(cfg, { thoughtMaxChars: 0, thinkingMaxChars: 0, humanMaxChars: cfg.humanMaxChars });
|
|
102
|
+
function headText(message: JsonObject): string | null {
|
|
103
|
+
if (message.role !== "user" && message.role !== "system") return null;
|
|
83
104
|
|
|
84
|
-
|
|
85
|
-
const piece = piDialect.summarizeMessage(resultMessages[i], headCfg);
|
|
105
|
+
if (isString(message.content)) return message.content;
|
|
86
106
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
}
|
|
107
|
+
if (!Array.isArray(message.content)) return null;
|
|
108
|
+
const parts: string[] = [];
|
|
91
109
|
|
|
92
|
-
|
|
93
|
-
|
|
110
|
+
for (const block of message.content) {
|
|
111
|
+
if (!isRecord(block) || block.type !== "text" || !isString(block.text)) return null;
|
|
112
|
+
parts.push(block.text);
|
|
94
113
|
}
|
|
95
114
|
|
|
96
|
-
|
|
97
|
-
|
|
115
|
+
return parts.join("\n");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function foldHeadIntoSummary(resultMessages: JsonObject[], headLen: number, body: string): string {
|
|
119
|
+
const parts: string[] = [];
|
|
120
|
+
|
|
121
|
+
for (let i = 0; i < headLen; i++) {
|
|
122
|
+
const text = headText(resultMessages[i]);
|
|
123
|
+
|
|
124
|
+
if (text === null) throw new Error("protected head cannot be represented in a text summary");
|
|
125
|
+
parts.push(String(resultMessages[i].role) + ": " + text);
|
|
98
126
|
}
|
|
99
127
|
|
|
100
|
-
|
|
128
|
+
if (body) parts.push(body);
|
|
129
|
+
|
|
130
|
+
return parts.length === 0 ? SUMMARY_HEADER : SUMMARY_HEADER + "\n\n" + parts.join("\n\n---\n\n");
|
|
101
131
|
}
|
|
102
132
|
|
|
103
133
|
/**
|
|
@@ -116,71 +146,85 @@ export function compactSession(input: CompactSessionInput): CompactSessionOutput
|
|
|
116
146
|
return null;
|
|
117
147
|
}
|
|
118
148
|
|
|
149
|
+
const headLen = msgs.findIndex(msg => piDialect.isAssistant(msg));
|
|
150
|
+
|
|
151
|
+
if (headLen < 0) return null;
|
|
152
|
+
const head = msgs.slice(0, headLen);
|
|
153
|
+
|
|
154
|
+
// Pi stores a text summary plus one contiguous suffix. Let its default
|
|
155
|
+
// summarizer handle protected multimodal heads instead of dropping images.
|
|
156
|
+
if (head.some(message => headText(message) === null)) return null;
|
|
157
|
+
|
|
158
|
+
const compactLive = (knobs: Config): CompactResult | null => {
|
|
159
|
+
const value = compact(msgs.slice(headLen), piDialect, knobs);
|
|
160
|
+
|
|
161
|
+
if (value === null) return null;
|
|
162
|
+
|
|
163
|
+
return { ...value, messages: [...head, ...value.messages], headLen, cut: headLen + value.cut };
|
|
164
|
+
};
|
|
165
|
+
|
|
119
166
|
const force = input.reason === "overflow";
|
|
167
|
+
const threshold = input.cfg.thresholdTokens;
|
|
120
168
|
let rung = 0;
|
|
121
169
|
let cfg = input.cfg;
|
|
122
|
-
let result =
|
|
170
|
+
let result = compactLive(cfg);
|
|
123
171
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
172
|
+
const render = (value: CompactResult): string =>
|
|
173
|
+
foldHeadIntoSummary(value.messages, value.headLen, summaryBody(value.summary));
|
|
174
|
+
|
|
175
|
+
const tokens = (value: CompactResult, summary: string): number =>
|
|
176
|
+
estimateTokens({ messages: [piDialect.userMessage(summary), ...value.messages.slice(value.headLen + 1)] });
|
|
177
|
+
|
|
178
|
+
for (let step = 1; step <= 2; step++) {
|
|
179
|
+
const missing = result === null && (force || (step === 1 && input.reason === "threshold"));
|
|
180
|
+
// Overflow already failed the provider window. Do not wait for the library
|
|
181
|
+
// thresholdTokens (200k default) before tightening keepRecent / thoughts.
|
|
182
|
+
const oversized = result !== null && (force || (input.cfg.strict && tokens(result, render(result)) > threshold));
|
|
183
|
+
|
|
184
|
+
if (!missing && !oversized) {
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
129
187
|
|
|
130
|
-
if (result === null && force) {
|
|
131
|
-
rung = 2;
|
|
132
188
|
const cap = input.cfg.thoughtMaxChars;
|
|
133
|
-
|
|
134
|
-
|
|
189
|
+
|
|
190
|
+
const keepRecent = Math.min(input.cfg.keepRecent, 1);
|
|
191
|
+
const knobs = replaceConfig(input.cfg, step === 1 ? { keepRecent } : {
|
|
192
|
+
keepRecent,
|
|
135
193
|
thoughtMaxChars: cap <= 0 ? 300 : Math.min(cap, 300),
|
|
136
194
|
keepThinking: false,
|
|
137
195
|
});
|
|
138
|
-
|
|
196
|
+
|
|
197
|
+
const candidate = compactLive(knobs);
|
|
198
|
+
|
|
199
|
+
if (candidate !== null && (result === null || tokens(candidate, render(candidate)) <= tokens(result, render(result)))) {
|
|
200
|
+
result = candidate;
|
|
201
|
+
cfg = knobs;
|
|
202
|
+
rung = step;
|
|
203
|
+
}
|
|
139
204
|
}
|
|
140
205
|
|
|
141
206
|
if (result === null) {
|
|
142
207
|
return null;
|
|
143
208
|
}
|
|
144
209
|
|
|
145
|
-
|
|
146
|
-
let
|
|
147
|
-
const kept = result.messages.length - result.headLen - 1;
|
|
148
|
-
const cut = result.cut;
|
|
149
|
-
let firstKeptEntryId = input.fallbackFirstKeptEntryId;
|
|
210
|
+
let summary = render(result);
|
|
211
|
+
let est = tokens(result, summary);
|
|
150
212
|
|
|
151
|
-
if (
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (input.cfg.strict && est > input.cfg.thresholdTokens && rung < 3) {
|
|
213
|
+
if (input.cfg.strict && est > threshold) {
|
|
214
|
+
const prefix = foldHeadIntoSummary(result.messages, result.headLen, "");
|
|
215
|
+
const body = summaryBody(result.summary);
|
|
216
|
+
const parts = body ? body.split("\n\n---\n\n") : [];
|
|
217
|
+
const protectedResult = result;
|
|
218
|
+
summary = fitSummary(prefix, parts, (text) => tokens(protectedResult, text) <= threshold);
|
|
219
|
+
est = tokens(result, summary);
|
|
159
220
|
rung = 3;
|
|
160
|
-
const header = SUMMARY_HEADER;
|
|
161
|
-
const rest = summary.slice(header.length).trim();
|
|
162
|
-
const parts = rest.length > 0 ? rest.split("\n\n---\n\n") : [];
|
|
163
|
-
const budget = input.cfg.thresholdTokens * 4 - 64 - header.length;
|
|
164
|
-
const keptParts: string[] = [];
|
|
165
|
-
let used = 0;
|
|
166
|
-
|
|
167
|
-
for (let i = parts.length - 1; i >= 0; i--) {
|
|
168
|
-
if (used + parts[i].length > Math.max(budget, 0)) {
|
|
169
|
-
break;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
keptParts.push(parts[i]);
|
|
173
|
-
used += parts[i].length + 9;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
keptParts.reverse();
|
|
177
|
-
summary = keptParts.length > 0 ? header + "\n\n" + keptParts.join("\n\n---\n\n") : header;
|
|
178
|
-
est = estimateTokens({ messages: [piDialect.userMessage(summary), ...result.messages.slice(result.headLen + 1)] });
|
|
179
221
|
}
|
|
180
222
|
|
|
181
223
|
return {
|
|
182
224
|
summary,
|
|
183
|
-
|
|
225
|
+
// cut === live.length means keepRecent 0: no suffix. A found fallback id
|
|
226
|
+
// (Pi's preparation cut, often the head) would revive the compacted middle.
|
|
227
|
+
firstKeptEntryId: input.live[result.cut]?.entryId ?? "",
|
|
184
228
|
tokensBefore: input.tokensBefore,
|
|
185
229
|
rung,
|
|
186
230
|
details: {
|
|
@@ -188,27 +232,47 @@ export function compactSession(input: CompactSessionInput): CompactSessionOutput
|
|
|
188
232
|
method: "cliffcompaction",
|
|
189
233
|
reason: input.reason,
|
|
190
234
|
headLen: result.headLen,
|
|
191
|
-
cut,
|
|
192
|
-
keepRecent:
|
|
235
|
+
cut: result.cut,
|
|
236
|
+
keepRecent: cfg.keepRecent,
|
|
193
237
|
rung,
|
|
194
238
|
liveMessages: msgs.length,
|
|
195
|
-
keptMessages:
|
|
239
|
+
keptMessages: result.messages.length - result.headLen - 1,
|
|
196
240
|
estTokensOut: est,
|
|
241
|
+
overBudget: est > threshold,
|
|
197
242
|
},
|
|
198
243
|
};
|
|
199
244
|
}
|
|
200
245
|
|
|
201
246
|
export type HookEntry = {
|
|
202
247
|
id: string;
|
|
203
|
-
kind: "message" | "compaction" | "other";
|
|
248
|
+
kind: "message" | "compaction" | "branch_summary" | "other";
|
|
204
249
|
message?: JsonObject;
|
|
205
250
|
firstKeptEntryId?: string;
|
|
251
|
+
summary?: string;
|
|
206
252
|
};
|
|
207
253
|
|
|
254
|
+
function liveRefFromEntry(entry: HookEntry): SessionMessageRef | null {
|
|
255
|
+
if (entry.kind === "message" && entry.message) {
|
|
256
|
+
return { entryId: entry.id, message: entry.message };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Pi puts /tree branch_summary entries in model context. Dropping them here
|
|
260
|
+
// would omit that text from both the mechanical recap and the kept suffix.
|
|
261
|
+
if (entry.kind === "branch_summary" && entry.summary) {
|
|
262
|
+
return {
|
|
263
|
+
entryId: entry.id,
|
|
264
|
+
message: { role: "user", content: BRANCH_SUMMARY_PREFIX + entry.summary + BRANCH_SUMMARY_SUFFIX },
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
|
|
208
271
|
/**
|
|
209
|
-
* Live session Lt: original messages from the previous
|
|
210
|
-
* firstKeptEntryId
|
|
211
|
-
* entries themselves are skipped so prior summaries are discarded.
|
|
272
|
+
* Live session Lt: the original head plus original messages from the previous
|
|
273
|
+
* cliff's firstKeptEntryId forward, without duplicating overlap. Compaction
|
|
274
|
+
* entries themselves are skipped so prior summaries are discarded. /tree
|
|
275
|
+
* branch_summary entries stay, matching Pi's model context.
|
|
212
276
|
*/
|
|
213
277
|
export function liveFromEntries(entries: HookEntry[]): SessionMessageRef[] {
|
|
214
278
|
let start = 0;
|
|
@@ -239,13 +303,20 @@ export function liveFromEntries(entries: HookEntry[]): SessionMessageRef[] {
|
|
|
239
303
|
start = idx >= 0 ? idx : i + 1;
|
|
240
304
|
}
|
|
241
305
|
|
|
306
|
+
const firstAssistant = entries.findIndex((e) => {
|
|
307
|
+
const ref = liveRefFromEntry(e);
|
|
308
|
+
|
|
309
|
+
return ref !== null && piDialect.isAssistant(ref.message);
|
|
310
|
+
});
|
|
311
|
+
const headEnd = firstAssistant < 0 ? entries.length : firstAssistant;
|
|
242
312
|
const live: SessionMessageRef[] = [];
|
|
243
313
|
|
|
244
|
-
for (let i =
|
|
245
|
-
|
|
314
|
+
for (let i = 0; i < entries.length; i++) {
|
|
315
|
+
if (i >= headEnd && i < start) continue;
|
|
316
|
+
const ref = liveRefFromEntry(entries[i]);
|
|
246
317
|
|
|
247
|
-
if (
|
|
248
|
-
live.push(
|
|
318
|
+
if (ref) {
|
|
319
|
+
live.push(ref);
|
|
249
320
|
}
|
|
250
321
|
}
|
|
251
322
|
|
package/lib/store.ts
CHANGED
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
* request extending prefix S (|S| = cut) is msgs[:headLen] + [summary] +
|
|
6
6
|
* msgs[cut:]. Head bytes come from the CURRENT request. The store is a
|
|
7
7
|
* cache: the compactor is deterministic, so lost entries are recomputed.
|
|
8
|
+
* Engine entries also carry the dialect and budget/configuration policy.
|
|
9
|
+
* The byte cap charges serialized UTF-8 summaries, not JavaScript heap overhead.
|
|
8
10
|
*/
|
|
9
11
|
|
|
10
|
-
import {
|
|
12
|
+
import { dumpsDefault } from "./json.ts";
|
|
11
13
|
import type { JsonObject } from "./decode.ts";
|
|
12
14
|
|
|
13
15
|
export type Entry = {
|
|
@@ -15,6 +17,8 @@ export type Entry = {
|
|
|
15
17
|
summary: JsonObject;
|
|
16
18
|
cut: number;
|
|
17
19
|
size: number;
|
|
20
|
+
policy?: string;
|
|
21
|
+
dialect?: object;
|
|
18
22
|
};
|
|
19
23
|
|
|
20
24
|
export function makeEntry(headLen: number, summary: JsonObject, cut: number): Entry {
|
|
@@ -22,7 +26,11 @@ export function makeEntry(headLen: number, summary: JsonObject, cut: number): En
|
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
export function entrySize(summary: JsonObject): number {
|
|
25
|
-
|
|
29
|
+
try {
|
|
30
|
+
return Buffer.byteLength(dumpsDefault(summary), "utf8");
|
|
31
|
+
} catch {
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
26
34
|
}
|
|
27
35
|
|
|
28
36
|
export class PrefixStore {
|
|
@@ -48,7 +56,12 @@ export class PrefixStore {
|
|
|
48
56
|
}
|
|
49
57
|
|
|
50
58
|
put(chainHash: string, entry: Entry): void {
|
|
51
|
-
|
|
59
|
+
const size = entrySize(entry.summary);
|
|
60
|
+
|
|
61
|
+
if (this.maxEntries <= 0 || this.maxBytes <= 0 || size > this.maxBytes) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
52
65
|
const old = this.data.get(chainHash);
|
|
53
66
|
|
|
54
67
|
if (old !== undefined) {
|
|
@@ -56,10 +69,11 @@ export class PrefixStore {
|
|
|
56
69
|
this.bytes -= old.size;
|
|
57
70
|
}
|
|
58
71
|
|
|
72
|
+
entry.size = size;
|
|
59
73
|
this.data.set(chainHash, entry);
|
|
60
74
|
this.bytes += entry.size;
|
|
61
75
|
|
|
62
|
-
while (this.data.size >
|
|
76
|
+
while (this.data.size > 0 && (this.data.size > this.maxEntries || this.bytes > this.maxBytes)) {
|
|
63
77
|
const first = this.data.keys().next().value;
|
|
64
78
|
|
|
65
79
|
if (first === undefined) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-cliffcompaction",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "CliffCompaction for Pi/OMP: mechanical autocompaction. Truncate or drop, never rephrase, never compact a compaction. Port of arXiv:2609.26779.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "AdityaVG13",
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
"config.example.json"
|
|
37
37
|
],
|
|
38
38
|
"scripts": {
|
|
39
|
-
"test": "node --test
|
|
40
|
-
"prepublishOnly": "
|
|
39
|
+
"test": "node --test tests/*.test.mjs",
|
|
40
|
+
"prepublishOnly": "node ../../scripts/preflight.mjs"
|
|
41
41
|
},
|
|
42
42
|
"pi": {
|
|
43
43
|
"extensions": [
|