@mjasnikovs/pi-task 0.40.40 → 0.40.42
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/task/enforce-guidelines.d.ts +12 -8
- package/dist/task/enforce-guidelines.js +20 -8
- package/dist/task/final-gate-fix.js +5 -2
- package/dist/task/gate-child.js +4 -3
- package/dist/task/lint-fix.js +7 -8
- package/dist/task/verify-work.js +4 -0
- package/dist/workers/pi-worker-core.d.ts +3 -2
- package/dist/workers/pi-worker.d.ts +7 -1
- package/dist/workers/pi-worker.js +36 -4
- package/dist/workers/worker-failure.d.ts +37 -8
- package/dist/workers/worker-failure.js +29 -0
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SpawnFn } from '../shared/child-process.js';
|
|
2
|
-
import { type WorkerFailureInput } from '../workers/worker-failure.js';
|
|
2
|
+
import { type WorkerAnswerInput, type WorkerFailureInput } from '../workers/worker-failure.js';
|
|
3
3
|
/** Filenames discovered in the working directory (cwd only — no tree walk). */
|
|
4
4
|
export declare const GUIDELINE_FILENAMES: readonly ["AGENTS.md", "CLAUDE.md"];
|
|
5
5
|
/**
|
|
@@ -94,8 +94,7 @@ export declare function parseEnforceVerdict(text: string): {
|
|
|
94
94
|
detail: string;
|
|
95
95
|
};
|
|
96
96
|
/** The subset of a runWorker result the enforcement-child mapping reads. */
|
|
97
|
-
export interface EnforceChildResult extends WorkerFailureInput {
|
|
98
|
-
text: string;
|
|
97
|
+
export interface EnforceChildResult extends WorkerFailureInput, WorkerAnswerInput {
|
|
99
98
|
}
|
|
100
99
|
/**
|
|
101
100
|
* Map the enforcement child's runWorker result to a fatal error message, or null
|
|
@@ -114,11 +113,16 @@ export interface EnforceChildResult extends WorkerFailureInput {
|
|
|
114
113
|
* user as their own cancel. The switch below is exhaustive, so the next cause
|
|
115
114
|
* added to the union is a compile error here rather than a silent mislabel.
|
|
116
115
|
*
|
|
117
|
-
* A loop is NOT fatal: enforce attaches the detector in nudge-then-warn
|
|
118
|
-
* loop that survived its restart-with-hint nudges
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
116
|
+
* A loop is NOT fatal on its own: enforce attaches the detector in nudge-then-warn
|
|
117
|
+
* mode, so a loop that survived its restart-with-hint nudges is a warning (the
|
|
118
|
+
* caller notifies it) and the answer still decides. It still has to be matched
|
|
119
|
+
* before `aborted`/`exitCode` so the kill's side effects don't get re-classified
|
|
120
|
+
* as a user cancel or a crash.
|
|
121
|
+
*
|
|
122
|
+
* After the kill ladder, the answer itself: a model error or a child that never
|
|
123
|
+
* spoke is fatal — left unread, either parses as "no verdict" and a dead provider
|
|
124
|
+
* is blamed on the work (issue #19). Only a genuinely empty answer reaches the
|
|
125
|
+
* verdict parser.
|
|
122
126
|
*/
|
|
123
127
|
export declare function classifyEnforceChildFailure(r: EnforceChildResult): string | null;
|
|
124
128
|
/**
|
|
@@ -25,7 +25,7 @@ import * as fsp from 'node:fs/promises';
|
|
|
25
25
|
import * as path from 'node:path';
|
|
26
26
|
import { makeGit } from '../shared/git-runner.js';
|
|
27
27
|
import { USER_CANCELLED } from './child-runner.js';
|
|
28
|
-
import { classifyWorkerFailure } from '../workers/worker-failure.js';
|
|
28
|
+
import { classifyWorkerAnswer, classifyWorkerFailure, describeNoAnswer } from '../workers/worker-failure.js';
|
|
29
29
|
import { TASKS_DIR_NAME } from './task-types.js';
|
|
30
30
|
import { findProbeGamingInDiff } from './probe-gaming.js';
|
|
31
31
|
/** Filenames discovered in the working directory (cwd only — no tree walk). */
|
|
@@ -229,16 +229,28 @@ export function parseEnforceVerdict(text) {
|
|
|
229
229
|
* user as their own cancel. The switch below is exhaustive, so the next cause
|
|
230
230
|
* added to the union is a compile error here rather than a silent mislabel.
|
|
231
231
|
*
|
|
232
|
-
* A loop is NOT fatal: enforce attaches the detector in nudge-then-warn
|
|
233
|
-
* loop that survived its restart-with-hint nudges
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
232
|
+
* A loop is NOT fatal on its own: enforce attaches the detector in nudge-then-warn
|
|
233
|
+
* mode, so a loop that survived its restart-with-hint nudges is a warning (the
|
|
234
|
+
* caller notifies it) and the answer still decides. It still has to be matched
|
|
235
|
+
* before `aborted`/`exitCode` so the kill's side effects don't get re-classified
|
|
236
|
+
* as a user cancel or a crash.
|
|
237
|
+
*
|
|
238
|
+
* After the kill ladder, the answer itself: a model error or a child that never
|
|
239
|
+
* spoke is fatal — left unread, either parses as "no verdict" and a dead provider
|
|
240
|
+
* is blamed on the work (issue #19). Only a genuinely empty answer reaches the
|
|
241
|
+
* verdict parser.
|
|
237
242
|
*/
|
|
238
243
|
export function classifyEnforceChildFailure(r) {
|
|
239
|
-
const
|
|
240
|
-
|
|
244
|
+
const kill = classifyWorkerFailure(r);
|
|
245
|
+
const named = kill ? describeKill(kill) : null;
|
|
246
|
+
if (named !== null)
|
|
247
|
+
return named;
|
|
248
|
+
const noAnswer = classifyWorkerAnswer(r);
|
|
249
|
+
if (!noAnswer || noAnswer.kind === 'empty-answer')
|
|
241
250
|
return null;
|
|
251
|
+
return describeNoAnswer(noAnswer);
|
|
252
|
+
}
|
|
253
|
+
function describeKill(failure) {
|
|
242
254
|
switch (failure.kind) {
|
|
243
255
|
case 'stalled':
|
|
244
256
|
// Otherwise a long silence is reported as a user cancel.
|
|
@@ -199,8 +199,8 @@ export async function runFinalGateAutofix(deps) {
|
|
|
199
199
|
signal: deps.signal,
|
|
200
200
|
marker: 'FINAL-GATE-FIX'
|
|
201
201
|
});
|
|
202
|
-
|
|
203
|
-
|
|
202
|
+
// A child that threw is NOT an early return from here: the guards below catch
|
|
203
|
+
// what it wrote before it died, and a thrown child has still written it.
|
|
204
204
|
// What the child wrote to gitignored paths. Recorded on the trail IMMEDIATELY —
|
|
205
205
|
// before any guard can reject the attempt — because `discard` reverts tracked
|
|
206
206
|
// edits only: an ignored file the pass wrote survives a rejection, and the trail
|
|
@@ -300,6 +300,9 @@ export async function runFinalGateAutofix(deps) {
|
|
|
300
300
|
return r;
|
|
301
301
|
}
|
|
302
302
|
}
|
|
303
|
+
if (end.kind === 'error') {
|
|
304
|
+
return withIgnored({ ok: false, reason: `fix child failed: ${end.msg}` });
|
|
305
|
+
}
|
|
303
306
|
if (end.kind === 'blocked') {
|
|
304
307
|
// Self-declared blocked: skip the (expensive) gate re-run; nothing converged.
|
|
305
308
|
return withIgnored({ ok: false, reason: `fix child blocked: ${end.note}` });
|
package/dist/task/gate-child.js
CHANGED
|
@@ -152,15 +152,16 @@ export function makeGateChild(deps) {
|
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
|
+
const failure = classifyEnforceChildFailure(r);
|
|
155
156
|
// A loop that survived the restart-with-hint nudges is a WARNING, not a
|
|
156
157
|
// failure: log it and tell the user, but let the verdict gate be the
|
|
157
|
-
// only thing that can block.
|
|
158
|
-
|
|
158
|
+
// only thing that can block. Unless the same child also failed — then
|
|
159
|
+
// "continuing" would contradict the throw below.
|
|
160
|
+
if (r.loopHit && failure === null) {
|
|
159
161
|
log(`=== ${deps.kind} LOOP WARNING — ${formatLoopHint(r.loopHit)} ===`);
|
|
160
162
|
notifyRun(deps.ctx, `${deps.taskTitle}: ${deps.kind} worker looped past the nudges — `
|
|
161
163
|
+ 'continuing (not blocked).', 'warning');
|
|
162
164
|
}
|
|
163
|
-
const failure = classifyEnforceChildFailure(r);
|
|
164
165
|
log(failure ?
|
|
165
166
|
`=== ${deps.kind} end: FAIL — ${failure} ===`
|
|
166
167
|
: `=== ${deps.kind} end: ${row.okMarker} ===`);
|
package/dist/task/lint-fix.js
CHANGED
|
@@ -219,14 +219,9 @@ export async function runBoundedLintFix(deps) {
|
|
|
219
219
|
signal: deps.signal,
|
|
220
220
|
marker: 'LINT-FIX'
|
|
221
221
|
});
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}
|
|
226
|
-
// A BLOCKED child is NOT an early return from here: the guards below exist to
|
|
227
|
-
// catch a child that discarded work, and a child can discard work and then
|
|
228
|
-
// block. The marker is consulted after them, in place of the re-run — which is
|
|
229
|
-
// where the twin consults its own.
|
|
222
|
+
// Neither a BLOCKED nor a thrown child is an early return from here: the
|
|
223
|
+
// guards below exist to catch a child that discarded work, and a child can
|
|
224
|
+
// discard work and then block, or die. Both are consulted after them.
|
|
230
225
|
// REVERT-GUARD: every pre-existing work file must still differ from HEAD, and
|
|
231
226
|
// every pre-existing untracked file must still exist. Trip → restore snapshot.
|
|
232
227
|
// Every comparison requires git to have actually SUCCEEDED. A git error after
|
|
@@ -325,6 +320,10 @@ export async function runBoundedLintFix(deps) {
|
|
|
325
320
|
}
|
|
326
321
|
}
|
|
327
322
|
}
|
|
323
|
+
if (end.kind === 'error') {
|
|
324
|
+
deps.log?.(`lint-fix child failed — ${end.msg}`);
|
|
325
|
+
return { ok: false, reason: `fix child failed: ${end.msg}` };
|
|
326
|
+
}
|
|
328
327
|
if (end.kind === 'blocked')
|
|
329
328
|
deps.log?.(`lint-fix BLOCKED — ${end.note}`);
|
|
330
329
|
// The CHECK is the arbiter, including after a BLOCKED marker.
|
package/dist/task/verify-work.js
CHANGED
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
* can FAIL before the spec is looked at.
|
|
38
38
|
*/
|
|
39
39
|
import { USER_CANCELLED } from './child-runner.js';
|
|
40
|
+
import { isModelErrorMessage } from '../workers/worker-failure.js';
|
|
40
41
|
import { buildEnvNotesBlock, ENV_NOTE_EMIT_INSTRUCTION, extractEnvNotes } from './env-notes.js';
|
|
41
42
|
import { buildContractsVerifyBlock } from './contracts.js';
|
|
42
43
|
import { findSkipEscapes, skipEscapeVerifyFindings } from './skip-escape.js';
|
|
@@ -823,6 +824,9 @@ export async function runWorkVerification(deps) {
|
|
|
823
824
|
if (err instanceof Error && err.message === USER_CANCELLED)
|
|
824
825
|
throw err;
|
|
825
826
|
const msg = err instanceof Error ? err.message : String(err);
|
|
827
|
+
// A provider that died under the child never judged the work either.
|
|
828
|
+
if (isModelErrorMessage(msg) && attempt === 1)
|
|
829
|
+
continue;
|
|
826
830
|
return {
|
|
827
831
|
ok: false,
|
|
828
832
|
failClass: 'harness-fault',
|
|
@@ -272,8 +272,9 @@ export interface RunWorkerResult {
|
|
|
272
272
|
* surface this through child-runner.ts; without it a swallowed provider error
|
|
273
273
|
* reaches the caller as an indistinguishable empty answer and gets reported as
|
|
274
274
|
* the useless "produced no output".
|
|
275
|
-
*
|
|
276
|
-
* recovered is a success
|
|
275
|
+
* Survives next to text only when the error came AFTER that text: a turn
|
|
276
|
+
* that produced text after pi recovered is a success and the sink drops the
|
|
277
|
+
* earlier error. See `classifyWorkerAnswer` (worker-failure.ts).
|
|
277
278
|
*/
|
|
278
279
|
modelError?: string;
|
|
279
280
|
/**
|
|
@@ -12,4 +12,10 @@
|
|
|
12
12
|
* — the tool whitelist, not the extension flag, is the bound that holds.
|
|
13
13
|
*/
|
|
14
14
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
15
|
-
|
|
15
|
+
import type { SpawnFn } from '../shared/child-process.js';
|
|
16
|
+
/** Test seams: a fake child, and no real backoff sleep between its attempts. */
|
|
17
|
+
export interface PiWorkerInternals {
|
|
18
|
+
spawn?: SpawnFn;
|
|
19
|
+
sleepFor?: (ms: number) => Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
export declare function registerPiWorker(pi: ExtensionAPI, internals?: PiWorkerInternals): void;
|
|
@@ -17,12 +17,34 @@ import { getConfig } from '../config/config.js';
|
|
|
17
17
|
import { groupChildArgs } from '../config/group-args.js';
|
|
18
18
|
import { runWorker } from './pi-worker-core.js';
|
|
19
19
|
import { contextWindowForGroup } from '../task/context-usage.js';
|
|
20
|
+
import { classifyWorkerAnswer, describeNoAnswer } from './worker-failure.js';
|
|
20
21
|
import { childFailureReason, formatChildFailure, makeWorkerTool, workerAnswer, workerUnavailable } from './shared.js';
|
|
21
22
|
const RENDER_PROMPT_MAX = 120;
|
|
23
|
+
const STDERR_TAIL = 500;
|
|
24
|
+
function workerDetails(r) {
|
|
25
|
+
return {
|
|
26
|
+
exitCode: r.exitCode,
|
|
27
|
+
attempts: r.attempts,
|
|
28
|
+
restarts: r.restarts.map(x => ({
|
|
29
|
+
reason: x.reason,
|
|
30
|
+
...(x.detail !== undefined ? { detail: x.detail } : {})
|
|
31
|
+
})),
|
|
32
|
+
...(r.modelError !== undefined ? { modelError: r.modelError } : {}),
|
|
33
|
+
...(r.stderr ? { stderr: r.stderr.slice(-STDERR_TAIL) } : {})
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** An empty final answer, with the spawns it cost, so the model can tell a
|
|
37
|
+
* retried failure from a child that simply had nothing to say. */
|
|
38
|
+
function describeEmptyAnswer(r) {
|
|
39
|
+
if (r.restarts.length === 0)
|
|
40
|
+
return '(no output)';
|
|
41
|
+
const reasons = r.restarts.map(x => x.reason).join(', ');
|
|
42
|
+
return `(no output after ${r.attempts} attempts; discarded: ${reasons})`;
|
|
43
|
+
}
|
|
22
44
|
const WorkerParams = Type.Object({
|
|
23
45
|
prompt: Type.String({ description: 'Task for the worker to perform.' })
|
|
24
46
|
});
|
|
25
|
-
export function registerPiWorker(pi) {
|
|
47
|
+
export function registerPiWorker(pi, internals = {}) {
|
|
26
48
|
makeWorkerTool(pi, {
|
|
27
49
|
name: 'pi-worker',
|
|
28
50
|
label: 'Pi Worker',
|
|
@@ -67,14 +89,24 @@ export function registerPiWorker(pi) {
|
|
|
67
89
|
// model and its window is the honest one. Without this the churn
|
|
68
90
|
// rule cannot fire — see RunWorkerInput.contextWindow.
|
|
69
91
|
contextWindow: contextWindowForGroup(ctx, 'research') || 'unknown',
|
|
70
|
-
groupArgs: groupChildArgs('research')
|
|
92
|
+
groupArgs: groupChildArgs('research'),
|
|
93
|
+
...(internals.spawn ? { spawn: internals.spawn } : {}),
|
|
94
|
+
...(internals.sleepFor ? { sleepFor: internals.sleepFor } : {})
|
|
71
95
|
});
|
|
72
|
-
const details =
|
|
96
|
+
const details = workerDetails(result);
|
|
73
97
|
const failure = formatChildFailure(result, 'Worker aborted.');
|
|
74
98
|
if (failure !== null) {
|
|
75
99
|
return workerUnavailable(failure, details, childFailureReason(result));
|
|
76
100
|
}
|
|
77
|
-
|
|
101
|
+
const noAnswer = classifyWorkerAnswer(result);
|
|
102
|
+
if (noAnswer === undefined)
|
|
103
|
+
return workerAnswer(result.text, details);
|
|
104
|
+
// Only the model sees the text, never `details`, so the diagnosis
|
|
105
|
+
// has to be in the text.
|
|
106
|
+
const text = noAnswer.kind === 'empty-answer' ?
|
|
107
|
+
describeEmptyAnswer(result)
|
|
108
|
+
: `Worker failed: ${describeNoAnswer(noAnswer)}`;
|
|
109
|
+
return workerUnavailable(text, details, noAnswer.kind);
|
|
78
110
|
},
|
|
79
111
|
renderCall(args, theme) {
|
|
80
112
|
const prompt = args.prompt.replace(/\s+/g, ' ').trim();
|
|
@@ -30,9 +30,9 @@ import type { WorkerKillId } from './worker-kill.js';
|
|
|
30
30
|
* The subset of a finished child result this classification reads.
|
|
31
31
|
*
|
|
32
32
|
* Structural on purpose: `runWorker` returns a superset, and `EnforceChildResult`
|
|
33
|
-
* extends this interface with the
|
|
34
|
-
* what is actually READ lets both pass without either importing the
|
|
35
|
-
* interface.
|
|
33
|
+
* extends this interface with the answer fields enforcement adds. Typing the
|
|
34
|
+
* input as what is actually READ lets both pass without either importing the
|
|
35
|
+
* other's interface.
|
|
36
36
|
*/
|
|
37
37
|
export interface WorkerFailureInput {
|
|
38
38
|
exitCode: number;
|
|
@@ -53,11 +53,11 @@ export interface WorkerFailureInput {
|
|
|
53
53
|
/**
|
|
54
54
|
* Why the child died, or `undefined` when it finished under its own power.
|
|
55
55
|
*
|
|
56
|
-
* Note what is NOT here:
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
56
|
+
* Note what is NOT here: a reported `modelError`, a child that never spoke, and
|
|
57
|
+
* an empty answer. None is a kill — whether they count as a failure is the
|
|
58
|
+
* consumer's policy — research-worker.ts, for one, accepts an explicit empty
|
|
59
|
+
* section after a retry. `classifyWorkerAnswer` below names them; each consumer
|
|
60
|
+
* decides what they mean.
|
|
61
61
|
*/
|
|
62
62
|
export type WorkerFailure = {
|
|
63
63
|
kind: 'stalled';
|
|
@@ -143,3 +143,32 @@ stderr?: string): string;
|
|
|
143
143
|
* and that judgement belongs to the caller.
|
|
144
144
|
*/
|
|
145
145
|
export declare function classifyWorkerFailure(r: WorkerFailureInput): WorkerFailure | undefined;
|
|
146
|
+
/** The answer-side fields `classifyWorkerAnswer` reads off a finished child. */
|
|
147
|
+
export interface WorkerAnswerInput {
|
|
148
|
+
text: string;
|
|
149
|
+
modelError?: string;
|
|
150
|
+
sawOutput?: boolean;
|
|
151
|
+
stderr?: string;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Why a child that was NOT killed still has no answer, or `undefined` for an answer.
|
|
155
|
+
*
|
|
156
|
+
* `model-error` wins even next to text. The event sink already drops an error
|
|
157
|
+
* that a LATER turn answered past, so a `modelError` that survives is one that
|
|
158
|
+
* came AFTER the last text: the provider died mid-run and the text is partial.
|
|
159
|
+
* Two consumers once required empty text here and each shipped a truncated
|
|
160
|
+
* verdict as a whole one.
|
|
161
|
+
*/
|
|
162
|
+
export type WorkerNoAnswer = {
|
|
163
|
+
kind: 'model-error';
|
|
164
|
+
cause: string;
|
|
165
|
+
} | {
|
|
166
|
+
kind: 'dead-child';
|
|
167
|
+
stderr: string;
|
|
168
|
+
} | {
|
|
169
|
+
kind: 'empty-answer';
|
|
170
|
+
};
|
|
171
|
+
export declare function classifyWorkerAnswer(r: WorkerAnswerInput): WorkerNoAnswer | undefined;
|
|
172
|
+
/** The one wording of a model error, so a caller can recognise it back (`isModelErrorMessage`). */
|
|
173
|
+
export declare function describeNoAnswer(f: WorkerNoAnswer): string;
|
|
174
|
+
export declare function isModelErrorMessage(msg: string): boolean;
|
|
@@ -138,3 +138,32 @@ export function classifyWorkerFailure(r) {
|
|
|
138
138
|
}
|
|
139
139
|
return undefined;
|
|
140
140
|
}
|
|
141
|
+
export function classifyWorkerAnswer(r) {
|
|
142
|
+
if (r.modelError !== undefined && r.modelError.length > 0) {
|
|
143
|
+
return { kind: 'model-error', cause: r.modelError };
|
|
144
|
+
}
|
|
145
|
+
if (r.text.trim().length > 0)
|
|
146
|
+
return undefined;
|
|
147
|
+
if (r.sawOutput === false)
|
|
148
|
+
return { kind: 'dead-child', stderr: r.stderr ?? '' };
|
|
149
|
+
return { kind: 'empty-answer' };
|
|
150
|
+
}
|
|
151
|
+
const MODEL_ERROR_PREFIX = 'model error — ';
|
|
152
|
+
const CAUSE_MAX = 200;
|
|
153
|
+
const STDERR_TAIL = 300;
|
|
154
|
+
/** The one wording of a model error, so a caller can recognise it back (`isModelErrorMessage`). */
|
|
155
|
+
export function describeNoAnswer(f) {
|
|
156
|
+
switch (f.kind) {
|
|
157
|
+
case 'model-error':
|
|
158
|
+
return `${MODEL_ERROR_PREFIX}${f.cause.slice(0, CAUSE_MAX)}`;
|
|
159
|
+
case 'dead-child':
|
|
160
|
+
return ('produced no output — the child never wrote a single byte, so it died '
|
|
161
|
+
+ 'before it could answer'
|
|
162
|
+
+ (f.stderr ? `: ${f.stderr.slice(-STDERR_TAIL)}` : ''));
|
|
163
|
+
case 'empty-answer':
|
|
164
|
+
return 'produced no output';
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
export function isModelErrorMessage(msg) {
|
|
168
|
+
return msg.startsWith(MODEL_ERROR_PREFIX);
|
|
169
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.40.
|
|
3
|
+
"version": "0.40.42",
|
|
4
4
|
"description": "Deterministic task planning and spec-orchestration for local models — crash-safe /task pipelines with verify/enforce gates, a real-time remote web view, and web/docs/fetch/worker subagent tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|