@echofiles/echo-pdf 0.11.0 → 0.11.4

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/README.md CHANGED
@@ -187,12 +187,12 @@ Published docs site:
187
187
  ## Development
188
188
 
189
189
  ```bash
190
- npm ci
191
- npm run build
192
- npm run typecheck
193
- npm run test:unit
194
- npm run test:acceptance
195
- npm run test:integration
190
+ bun install --frozen-lockfile
191
+ bun run build
192
+ bun run typecheck
193
+ bun run test:unit
194
+ bun run test:acceptance
195
+ bun run test:integration
196
196
  ```
197
197
 
198
198
  For source-checkout CLI development and repo-local workflows, see [docs/DEVELOPMENT.md](./docs/DEVELOPMENT.md).
package/bin/echo-pdf.js CHANGED
@@ -203,13 +203,13 @@ const loadLocalDocumentApi = async () => {
203
203
  }
204
204
  throw new Error(
205
205
  "Internal source-checkout CLI dev mode requires Bun and src/local/index.ts. " +
206
- "Use `npm run cli:dev -- <primitive> ...` only from a source checkout."
206
+ "Use `bun run cli:dev -- <primitive> ...` only from a source checkout."
207
207
  )
208
208
  }
209
209
  if (!fs.existsSync(LOCAL_DOCUMENT_DIST_PATH)) {
210
210
  throw new Error(
211
211
  "Local primitive commands require built artifacts in a source checkout. " +
212
- "Run `npm run build` first, use the internal `npm run cli:dev -- <primitive> ...` path in this repo, or install the published package."
212
+ "Run `bun run build` first, use the internal `bun run cli:dev -- <primitive> ...` path in this repo, or install the published package."
213
213
  )
214
214
  }
215
215
  return import(LOCAL_DOCUMENT_DIST_ENTRY.href)
@@ -5,7 +5,7 @@ import { resolveModelForProvider, resolveProviderAlias } from "../provider-defau
5
5
  import { toDataUrl } from "../file-utils.js";
6
6
  import { generateText, visionRecognize } from "../provider-client.js";
7
7
  import { ensureRenderArtifact, indexDocumentInternal } from "./document.js";
8
- import { fileExists, matchesSourceSnapshot, matchesStrategyKey, pageLabel, parseJsonObject, readJson, resolveConfig, resolveEnv, writeJson, } from "./shared.js";
8
+ import { fileExists, matchesSourceSnapshot, matchesStrategyKey, pageLabel, parseJsonObject, parseJsonObjectWithRepair, readJson, resolveConfig, resolveEnv, writeJson, } from "./shared.js";
9
9
  import { normalizeFigureItems, normalizeUnderstandingFormulas, normalizeUnderstandingTables } from "./understanding.js";
10
10
  const resolveSemanticExtractionBudget = (input) => ({
11
11
  pageSelection: "all",
@@ -134,6 +134,15 @@ const resolveSemanticAgentContext = (config, request) => {
134
134
  }
135
135
  return { provider, model };
136
136
  };
137
+ class SemanticAggregationModelOutputError extends Error {
138
+ detail;
139
+ code = "SEMANTIC_AGGREGATION_INVALID_JSON";
140
+ constructor(message, detail) {
141
+ super(message);
142
+ this.detail = detail;
143
+ this.name = "SemanticAggregationModelOutputError";
144
+ }
145
+ }
137
146
  const extractCombinedPageData = async (input) => {
138
147
  const renderArtifact = await ensureRenderArtifact({
139
148
  pdfPath: input.request.pdfPath,
@@ -170,6 +179,55 @@ const extractCombinedPageData = async (input) => {
170
179
  },
171
180
  };
172
181
  };
182
+ const buildSemanticAggregationRetryPrompt = (record, candidates) => {
183
+ return [
184
+ buildSemanticAggregationPrompt(record, candidates),
185
+ "",
186
+ "Your previous response was not strict JSON.",
187
+ "Return the same semantic structure again, but this time produce strict RFC 8259 JSON only.",
188
+ "Do not wrap in markdown fences.",
189
+ "Do not use invalid backslash escapes such as \\(, \\), \\_, or \\- inside JSON strings.",
190
+ ].join("\n");
191
+ };
192
+ const parseSemanticAggregationResponse = async (input) => {
193
+ try {
194
+ const parsed = parseJsonObjectWithRepair(input.aggregated);
195
+ return {
196
+ sections: parsed.parsed?.sections,
197
+ repaired: parsed.repaired,
198
+ retried: false,
199
+ };
200
+ }
201
+ catch (firstError) {
202
+ const causeMessage = firstError instanceof Error ? firstError.message : String(firstError);
203
+ const retried = await generateText({
204
+ config: input.config,
205
+ env: input.env,
206
+ providerAlias: input.provider,
207
+ model: input.model,
208
+ prompt: buildSemanticAggregationRetryPrompt(input.record, input.candidates),
209
+ runtimeApiKeys: input.runtimeApiKeys,
210
+ });
211
+ try {
212
+ const parsed = parseJsonObjectWithRepair(retried);
213
+ return {
214
+ sections: parsed.parsed?.sections,
215
+ repaired: parsed.repaired,
216
+ retried: true,
217
+ };
218
+ }
219
+ catch (retryError) {
220
+ const retryCauseMessage = retryError instanceof Error ? retryError.message : String(retryError);
221
+ throw new SemanticAggregationModelOutputError("semantic aggregation returned invalid JSON after repair and retry", {
222
+ provider: input.provider,
223
+ model: input.model,
224
+ repaired: false,
225
+ retried: true,
226
+ causeMessage: `${causeMessage}; retry=${retryCauseMessage}`,
227
+ });
228
+ }
229
+ }
230
+ };
173
231
  const mergeCrossPageTables = (understandings) => {
174
232
  const merged = [];
175
233
  let nextId = 1;
@@ -304,8 +362,17 @@ const ensureSemanticStructureArtifact = async (request) => {
304
362
  prompt: buildSemanticAggregationPrompt(record, [...candidateMap.values()]),
305
363
  runtimeApiKeys: request.providerApiKeys,
306
364
  });
307
- const parsed = parseJsonObject(aggregated);
308
- const sections = toSemanticTree(parsed?.sections, pageArtifactPaths);
365
+ const parsed = await parseSemanticAggregationResponse({
366
+ aggregated,
367
+ record,
368
+ candidates: [...candidateMap.values()],
369
+ config,
370
+ env,
371
+ provider,
372
+ model,
373
+ runtimeApiKeys: request.providerApiKeys,
374
+ });
375
+ const sections = toSemanticTree(parsed.sections, pageArtifactPaths);
309
376
  const mergedTables = mergeCrossPageTables(pageElements);
310
377
  const mergedFormulas = mergeCrossPageFormulas(pageElements);
311
378
  const mergedFigures = mergeCrossPageFigures(pageElements);
@@ -19,6 +19,10 @@ export declare const createPreview: (text: string) => string;
19
19
  export declare const createPageTitle: (pageNumber: number, text: string) => string;
20
20
  export declare const stripCodeFences: (value: string) => string;
21
21
  export declare const parseJsonObject: (value: string) => unknown;
22
+ export declare const parseJsonObjectWithRepair: (value: string) => {
23
+ parsed: unknown;
24
+ repaired: boolean;
25
+ };
22
26
  export declare const normalizeTableItems: (value: unknown) => LocalTableArtifactItem[];
23
27
  export declare const normalizeFormulaItems: (value: unknown) => LocalFormulaArtifactItem[];
24
28
  export declare const resolveEnv: (env?: Env) => Env;
@@ -77,6 +77,96 @@ export const parseJsonObject = (value) => {
77
77
  throw new Error("model output was not valid JSON");
78
78
  }
79
79
  };
80
+ const validJsonEscape = (value) => /["\\/bfnrt]/.test(value);
81
+ const repairInvalidJsonEscapes = (value) => {
82
+ let repaired = false;
83
+ let inString = false;
84
+ let escaping = false;
85
+ let unicodeDigitsRemaining = 0;
86
+ let output = "";
87
+ for (let index = 0; index < value.length; index += 1) {
88
+ const char = value[index] ?? "";
89
+ if (!inString) {
90
+ output += char;
91
+ if (char === "\"")
92
+ inString = true;
93
+ continue;
94
+ }
95
+ if (unicodeDigitsRemaining > 0) {
96
+ output += char;
97
+ if (/^[0-9a-fA-F]$/.test(char)) {
98
+ unicodeDigitsRemaining -= 1;
99
+ }
100
+ else {
101
+ repaired = true;
102
+ unicodeDigitsRemaining = 0;
103
+ }
104
+ continue;
105
+ }
106
+ if (escaping) {
107
+ if (validJsonEscape(char)) {
108
+ output += char;
109
+ }
110
+ else if (char === "u") {
111
+ output += char;
112
+ unicodeDigitsRemaining = 4;
113
+ }
114
+ else {
115
+ output += `\\${char}`;
116
+ repaired = true;
117
+ }
118
+ escaping = false;
119
+ continue;
120
+ }
121
+ if (char === "\\") {
122
+ output += char;
123
+ escaping = true;
124
+ continue;
125
+ }
126
+ output += char;
127
+ if (char === "\"")
128
+ inString = false;
129
+ }
130
+ if (escaping) {
131
+ output += "\\";
132
+ repaired = true;
133
+ }
134
+ return { repairedText: output, repaired };
135
+ };
136
+ export const parseJsonObjectWithRepair = (value) => {
137
+ const trimmed = stripCodeFences(value).trim();
138
+ if (!trimmed)
139
+ return { parsed: null, repaired: false };
140
+ const candidates = [trimmed];
141
+ const start = trimmed.indexOf("{");
142
+ const end = trimmed.lastIndexOf("}");
143
+ if (start >= 0 && end > start) {
144
+ const sliced = trimmed.slice(start, end + 1);
145
+ if (sliced !== trimmed)
146
+ candidates.push(sliced);
147
+ }
148
+ let lastError = null;
149
+ for (const candidate of candidates) {
150
+ try {
151
+ return { parsed: JSON.parse(candidate), repaired: false };
152
+ }
153
+ catch (error) {
154
+ lastError = error instanceof Error ? error : new Error(String(error));
155
+ }
156
+ const repairedCandidate = repairInvalidJsonEscapes(candidate);
157
+ if (!repairedCandidate.repaired)
158
+ continue;
159
+ try {
160
+ return { parsed: JSON.parse(repairedCandidate.repairedText), repaired: true };
161
+ }
162
+ catch (error) {
163
+ lastError = error instanceof Error ? error : new Error(String(error));
164
+ }
165
+ }
166
+ if (lastError)
167
+ throw lastError;
168
+ throw new Error("model output was not valid JSON");
169
+ };
80
170
  export const normalizeTableItems = (value) => {
81
171
  if (!Array.isArray(value))
82
172
  return [];
@@ -1,4 +1,28 @@
1
1
  import { resolveProviderApiKey } from "./provider-keys.js";
2
+ class ProviderRequestError extends Error {
3
+ code;
4
+ detail;
5
+ constructor(code, detail) {
6
+ const parts = [
7
+ `${detail.operation} failed`,
8
+ `code=${code}`,
9
+ `url=${detail.url}`,
10
+ `attempt=${detail.attempt}/${detail.maxAttempts}`,
11
+ ];
12
+ if (typeof detail.status === "number")
13
+ parts.push(`http=${detail.status}`);
14
+ if (detail.contentType)
15
+ parts.push(`contentType=${detail.contentType}`);
16
+ if (detail.causeMessage)
17
+ parts.push(`cause=${detail.causeMessage}`);
18
+ if (detail.responsePreview)
19
+ parts.push(`preview=${JSON.stringify(detail.responsePreview)}`);
20
+ super(parts.join(" "));
21
+ this.code = code;
22
+ this.detail = detail;
23
+ this.name = "ProviderRequestError";
24
+ }
25
+ }
2
26
  const defaultBaseUrl = (provider) => {
3
27
  if (provider.baseUrl)
4
28
  return provider.baseUrl;
@@ -32,33 +56,197 @@ const toAuthHeader = (config, providerAlias, provider, env, runtimeApiKeys) => {
32
56
  });
33
57
  return token ? { Authorization: `Bearer ${token}` } : {};
34
58
  };
35
- const withTimeout = async (url, init, timeoutMs) => {
59
+ const isPrivateIpv4 = (hostname) => /^10\./.test(hostname) ||
60
+ /^127\./.test(hostname) ||
61
+ /^192\.168\./.test(hostname) ||
62
+ /^172\.(1[6-9]|2\d|3[0-1])\./.test(hostname);
63
+ const isLocalOpenAiCompatible = (provider, url) => {
64
+ try {
65
+ const parsed = new URL(url);
66
+ const hostname = parsed.hostname.toLowerCase();
67
+ return (provider.type === "openai-compatible" &&
68
+ (hostname === "localhost" || hostname === "::1" || isPrivateIpv4(hostname)));
69
+ }
70
+ catch {
71
+ return false;
72
+ }
73
+ };
74
+ const maxAttemptsFor = (provider, url, method, operation) => {
75
+ if (method !== "POST")
76
+ return 1;
77
+ if (operation === "Model list request")
78
+ return 1;
79
+ return isLocalOpenAiCompatible(provider, url) ? 2 : 1;
80
+ };
81
+ const truncate = (value, length = 800) => value.slice(0, length);
82
+ const withTimeout = async (input) => {
36
83
  const ctrl = new AbortController();
37
- const timer = setTimeout(() => ctrl.abort("timeout"), timeoutMs);
84
+ const timer = setTimeout(() => ctrl.abort("timeout"), input.timeoutMs);
38
85
  try {
39
- return await fetch(url, { ...init, signal: ctrl.signal });
86
+ return await fetch(input.url, { ...input.init, signal: ctrl.signal });
40
87
  }
41
88
  catch (error) {
42
89
  if (error instanceof Error && error.name === "AbortError") {
43
- throw new Error(`Request timeout after ${timeoutMs}ms for ${url}`);
90
+ throw new ProviderRequestError("PROVIDER_REQUEST_TIMEOUT", {
91
+ operation: input.operation,
92
+ providerAlias: input.providerAlias,
93
+ url: input.url,
94
+ attempt: input.attempt,
95
+ maxAttempts: input.maxAttempts,
96
+ causeMessage: `timeout after ${input.timeoutMs}ms`,
97
+ });
44
98
  }
45
- throw error;
99
+ throw new ProviderRequestError("PROVIDER_REQUEST_SEND_FAILED", {
100
+ operation: input.operation,
101
+ providerAlias: input.providerAlias,
102
+ url: input.url,
103
+ attempt: input.attempt,
104
+ maxAttempts: input.maxAttempts,
105
+ causeMessage: error instanceof Error ? error.message : String(error),
106
+ });
46
107
  }
47
108
  finally {
48
109
  clearTimeout(timer);
49
110
  }
50
111
  };
51
- const responseDetail = async (response) => {
52
- const contentType = response.headers.get("content-type") ?? "";
112
+ const readResponseText = async (input) => {
113
+ const reader = input.response.body?.getReader();
114
+ if (!reader) {
115
+ try {
116
+ return await input.response.text();
117
+ }
118
+ catch (error) {
119
+ throw new ProviderRequestError("PROVIDER_RESPONSE_BODY_READ_FAILED", {
120
+ operation: input.operation,
121
+ providerAlias: input.providerAlias,
122
+ url: input.url,
123
+ status: input.response.status,
124
+ contentType: input.response.headers.get("content-type") ?? "",
125
+ attempt: input.attempt,
126
+ maxAttempts: input.maxAttempts,
127
+ causeMessage: error instanceof Error ? error.message : String(error),
128
+ });
129
+ }
130
+ }
131
+ const chunks = [];
132
+ let total = 0;
53
133
  try {
54
- if (contentType.includes("application/json")) {
55
- return JSON.stringify(await response.json()).slice(0, 800);
134
+ while (true) {
135
+ const { done, value } = await reader.read();
136
+ if (done)
137
+ break;
138
+ if (!value || value.length === 0)
139
+ continue;
140
+ chunks.push(value);
141
+ total += value.length;
56
142
  }
57
- return (await response.text()).slice(0, 800);
58
143
  }
59
- catch {
60
- return "<unable to parse response payload>";
144
+ catch (error) {
145
+ const combined = new Uint8Array(total);
146
+ let offset = 0;
147
+ for (const chunk of chunks) {
148
+ combined.set(chunk, offset);
149
+ offset += chunk.length;
150
+ }
151
+ throw new ProviderRequestError("PROVIDER_RESPONSE_BODY_READ_FAILED", {
152
+ operation: input.operation,
153
+ providerAlias: input.providerAlias,
154
+ url: input.url,
155
+ status: input.response.status,
156
+ contentType: input.response.headers.get("content-type") ?? "",
157
+ attempt: input.attempt,
158
+ maxAttempts: input.maxAttempts,
159
+ causeMessage: error instanceof Error ? error.message : String(error),
160
+ responsePreview: truncate(new TextDecoder().decode(combined)),
161
+ });
162
+ }
163
+ const combined = new Uint8Array(total);
164
+ let offset = 0;
165
+ for (const chunk of chunks) {
166
+ combined.set(chunk, offset);
167
+ offset += chunk.length;
61
168
  }
169
+ return new TextDecoder().decode(combined);
170
+ };
171
+ const parseJsonResponse = (input) => {
172
+ try {
173
+ return JSON.parse(input.rawText);
174
+ }
175
+ catch (error) {
176
+ throw new ProviderRequestError("PROVIDER_RESPONSE_JSON_PARSE_FAILED", {
177
+ operation: input.operation,
178
+ providerAlias: input.providerAlias,
179
+ url: input.url,
180
+ status: input.response.status,
181
+ contentType: input.response.headers.get("content-type") ?? "",
182
+ attempt: input.attempt,
183
+ maxAttempts: input.maxAttempts,
184
+ causeMessage: error instanceof Error ? error.message : String(error),
185
+ responsePreview: truncate(input.rawText),
186
+ });
187
+ }
188
+ };
189
+ const requestJson = async (input) => {
190
+ const maxAttempts = maxAttemptsFor(input.provider, input.url, input.method, input.operation);
191
+ let lastError = null;
192
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
193
+ try {
194
+ const response = await withTimeout({
195
+ operation: input.operation,
196
+ providerAlias: input.providerAlias,
197
+ url: input.url,
198
+ init: {
199
+ method: input.method,
200
+ headers: input.headers,
201
+ ...(typeof input.body === "string" ? { body: input.body } : {}),
202
+ },
203
+ timeoutMs: input.provider.timeoutMs ?? 30000,
204
+ attempt,
205
+ maxAttempts,
206
+ });
207
+ const rawText = await readResponseText({
208
+ response,
209
+ operation: input.operation,
210
+ providerAlias: input.providerAlias,
211
+ url: input.url,
212
+ attempt,
213
+ maxAttempts,
214
+ });
215
+ if (!response.ok) {
216
+ throw new ProviderRequestError("PROVIDER_HTTP_ERROR", {
217
+ operation: input.operation,
218
+ providerAlias: input.providerAlias,
219
+ url: input.url,
220
+ status: response.status,
221
+ contentType: response.headers.get("content-type") ?? "",
222
+ attempt,
223
+ maxAttempts,
224
+ responsePreview: truncate(rawText),
225
+ });
226
+ }
227
+ return parseJsonResponse({
228
+ rawText,
229
+ response,
230
+ operation: input.operation,
231
+ providerAlias: input.providerAlias,
232
+ url: input.url,
233
+ attempt,
234
+ maxAttempts,
235
+ });
236
+ }
237
+ catch (error) {
238
+ lastError = error instanceof Error ? error : new Error(String(error));
239
+ if (attempt < maxAttempts &&
240
+ lastError instanceof ProviderRequestError &&
241
+ (lastError.code === "PROVIDER_REQUEST_SEND_FAILED" ||
242
+ lastError.code === "PROVIDER_RESPONSE_BODY_READ_FAILED" ||
243
+ lastError.code === "PROVIDER_RESPONSE_JSON_PARSE_FAILED")) {
244
+ continue;
245
+ }
246
+ throw lastError;
247
+ }
248
+ }
249
+ throw lastError ?? new Error(`${input.operation} failed`);
62
250
  };
63
251
  const getProvider = (config, alias) => {
64
252
  const provider = config.providers[alias];
@@ -70,18 +258,20 @@ const getProvider = (config, alias) => {
70
258
  export const listProviderModels = async (config, env, alias, runtimeApiKeys) => {
71
259
  const provider = getProvider(config, alias);
72
260
  const url = resolveEndpoint(provider, "modelsPath");
73
- const response = await withTimeout(url, {
261
+ const payload = await requestJson({
262
+ operation: "Model list request",
263
+ config,
264
+ env,
265
+ providerAlias: alias,
266
+ provider,
267
+ url,
74
268
  method: "GET",
75
269
  headers: {
76
270
  Accept: "application/json",
77
271
  ...toAuthHeader(config, alias, provider, env, runtimeApiKeys),
78
272
  ...(provider.headers ?? {}),
79
273
  },
80
- }, provider.timeoutMs ?? 30000);
81
- if (!response.ok) {
82
- throw new Error(`Model list request failed: HTTP ${response.status} url=${url} detail=${await responseDetail(response)}`);
83
- }
84
- const payload = await response.json();
274
+ });
85
275
  const data = payload.data;
86
276
  if (!Array.isArray(data))
87
277
  return [];
@@ -93,7 +283,13 @@ export const listProviderModels = async (config, env, alias, runtimeApiKeys) =>
93
283
  export const visionRecognize = async (input) => {
94
284
  const provider = getProvider(input.config, input.providerAlias);
95
285
  const url = resolveEndpoint(provider, "chatCompletionsPath");
96
- const response = await withTimeout(url, {
286
+ const payload = await requestJson({
287
+ operation: "Vision request",
288
+ config: input.config,
289
+ env: input.env,
290
+ providerAlias: input.providerAlias,
291
+ provider,
292
+ url,
97
293
  method: "POST",
98
294
  headers: {
99
295
  "Content-Type": "application/json",
@@ -112,11 +308,7 @@ export const visionRecognize = async (input) => {
112
308
  },
113
309
  ],
114
310
  }),
115
- }, provider.timeoutMs ?? 30000);
116
- if (!response.ok) {
117
- throw new Error(`Vision request failed: HTTP ${response.status} url=${url} detail=${await responseDetail(response)}`);
118
- }
119
- const payload = await response.json();
311
+ });
120
312
  const message = payload.choices?.[0]?.message;
121
313
  if (!message)
122
314
  return "";
@@ -135,7 +327,13 @@ export const visionRecognize = async (input) => {
135
327
  export const generateText = async (input) => {
136
328
  const provider = getProvider(input.config, input.providerAlias);
137
329
  const url = resolveEndpoint(provider, "chatCompletionsPath");
138
- const response = await withTimeout(url, {
330
+ const payload = await requestJson({
331
+ operation: "Text generation request",
332
+ config: input.config,
333
+ env: input.env,
334
+ providerAlias: input.providerAlias,
335
+ provider,
336
+ url,
139
337
  method: "POST",
140
338
  headers: {
141
339
  "Content-Type": "application/json",
@@ -151,11 +349,7 @@ export const generateText = async (input) => {
151
349
  },
152
350
  ],
153
351
  }),
154
- }, provider.timeoutMs ?? 30000);
155
- if (!response.ok) {
156
- throw new Error(`Text generation request failed: HTTP ${response.status} url=${url} detail=${await responseDetail(response)}`);
157
- }
158
- const payload = await response.json();
352
+ });
159
353
  const message = payload.choices?.[0]?.message;
160
354
  if (!message)
161
355
  return "";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@echofiles/echo-pdf",
3
3
  "description": "Local-first PDF document component core with CLI, workspace artifacts, and reusable page primitives.",
4
- "version": "0.11.0",
4
+ "version": "0.11.4",
5
5
  "type": "module",
6
6
  "homepage": "https://pdf.echofile.ai/",
7
7
  "repository": {
@@ -18,6 +18,7 @@
18
18
  "cli",
19
19
  "vision-language"
20
20
  ],
21
+ "packageManager": "bun@1.3.5",
21
22
  "publishConfig": {
22
23
  "access": "public"
23
24
  },
@@ -53,14 +54,14 @@
53
54
  "eval:stress": "node ./eval/run-local.mjs --suite stress",
54
55
  "eval:known-bad": "node ./eval/run-local.mjs --suite known-bad",
55
56
  "eval:fetch-public-samples": "node ./eval/fetch-public-samples.mjs",
56
- "typecheck": "npm run check:runtime && tsc --noEmit",
57
- "test:unit": "npm run check:runtime && vitest run tests/unit",
58
- "test:acceptance": "npm run check:runtime && npm run build && vitest run tests/acceptance",
59
- "test:import-smoke": "npm run check:runtime && npm run build && vitest run tests/integration/npm-pack-import.integration.test.ts tests/integration/ts-nodenext-consumer.integration.test.ts",
60
- "test:integration": "npm run check:runtime && npm run build && vitest run tests/integration/local-document-cli.integration.test.ts tests/integration/local-document.integration.test.ts tests/integration/local-semantic-structure.integration.test.ts tests/integration/npm-pack-import.integration.test.ts tests/integration/ts-nodenext-consumer.integration.test.ts",
61
- "test": "npm run test:unit && npm run test:acceptance && npm run test:integration",
57
+ "typecheck": "bun run check:runtime && tsc --noEmit",
58
+ "test:unit": "bun run check:runtime && vitest run tests/unit",
59
+ "test:acceptance": "bun run check:runtime && bun run build && vitest run tests/acceptance",
60
+ "test:import-smoke": "bun run check:runtime && bun run build && vitest run tests/integration/npm-pack-import.integration.test.ts tests/integration/ts-nodenext-consumer.integration.test.ts",
61
+ "test:integration": "bun run check:runtime && bun run build && vitest run tests/integration/local-document-cli.integration.test.ts tests/integration/local-document.integration.test.ts tests/integration/local-provider-stability.integration.test.ts tests/integration/local-semantic-structure.integration.test.ts tests/integration/npm-pack-import.integration.test.ts tests/integration/ts-nodenext-consumer.integration.test.ts",
62
+ "test": "bun run test:unit && bun run test:acceptance && bun run test:integration",
62
63
  "smoke": "bash ./scripts/smoke.sh",
63
- "prepublishOnly": "npm run build && npm run typecheck && npm run test"
64
+ "prepublishOnly": "bun run build && bun run typecheck && bun run test"
64
65
  },
65
66
  "engines": {
66
67
  "node": ">=20.0.0"
@@ -9,7 +9,7 @@ if [[ -z "${current_node_major}" ]] || (( current_node_major < required_node_maj
9
9
  exit 1
10
10
  fi
11
11
 
12
- for cmd in npm curl grep sed; do
12
+ for cmd in bun curl grep sed; do
13
13
  if ! command -v "${cmd}" >/dev/null 2>&1; then
14
14
  echo "Missing required command: ${cmd}"
15
15
  exit 1
@@ -23,4 +23,4 @@ if [[ "${CHECK_LLM_KEYS:-0}" == "1" ]]; then
23
23
  fi
24
24
  fi
25
25
 
26
- echo "runtime check passed: node=$(node -v), npm=$(npm -v)"
26
+ echo "runtime check passed: node=$(node -v), bun=$(bun -v)"
package/scripts/smoke.sh CHANGED
@@ -11,4 +11,4 @@ bash "${SCRIPT_DIR}/check-runtime.sh"
11
11
  # - SMOKE_LLM_PROVIDER
12
12
  # - SMOKE_LLM_MODEL
13
13
  # - TESTCASE_DIR
14
- npm run test:integration
14
+ bun run test:integration