@screenpipe-ui/core 0.1.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/dist/index.d.ts +96 -0
- package/dist/index.js +350 -0
- package/package.json +28 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { ScreenpipeQueryParams, ScreenpipeResponse, ContentItem, PaginationInfo } from '@screenpipe/js';
|
|
2
|
+
export { AudioContent, ContentItem, ContentType, OCRContent, PaginationInfo, ScreenpipeQueryParams, ScreenpipeResponse, Speaker, UiContent } from '@screenpipe/js';
|
|
3
|
+
import * as zustand_vanilla from 'zustand/vanilla';
|
|
4
|
+
|
|
5
|
+
interface HealthCheckResponse {
|
|
6
|
+
status: string;
|
|
7
|
+
status_code: number;
|
|
8
|
+
last_frame_timestamp?: string;
|
|
9
|
+
last_audio_timestamp?: string;
|
|
10
|
+
frame_status: string;
|
|
11
|
+
audio_status: string;
|
|
12
|
+
message: string;
|
|
13
|
+
}
|
|
14
|
+
interface ScreenpipeClientConfig {
|
|
15
|
+
baseUrl?: string;
|
|
16
|
+
}
|
|
17
|
+
declare class ScreenpipeUIClient {
|
|
18
|
+
private baseUrl;
|
|
19
|
+
constructor(config?: ScreenpipeClientConfig);
|
|
20
|
+
private get;
|
|
21
|
+
search(params?: ScreenpipeQueryParams): Promise<ScreenpipeResponse>;
|
|
22
|
+
health(): Promise<HealthCheckResponse>;
|
|
23
|
+
getFrameUrl(frameId: number): string;
|
|
24
|
+
}
|
|
25
|
+
declare function createClient(config?: ScreenpipeClientConfig): ScreenpipeUIClient;
|
|
26
|
+
declare function getContentText(item: ContentItem): string;
|
|
27
|
+
declare function getContentTimestamp(item: ContentItem): string;
|
|
28
|
+
declare function getContentAppName(item: ContentItem): string;
|
|
29
|
+
|
|
30
|
+
interface TimeRange {
|
|
31
|
+
start: string;
|
|
32
|
+
end: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface SearchState {
|
|
36
|
+
query: string;
|
|
37
|
+
contentType: ScreenpipeQueryParams["contentType"];
|
|
38
|
+
results: ContentItem[];
|
|
39
|
+
pagination: PaginationInfo;
|
|
40
|
+
loading: boolean;
|
|
41
|
+
error: string | null;
|
|
42
|
+
setQuery: (q: string) => void;
|
|
43
|
+
setContentType: (t: ScreenpipeQueryParams["contentType"]) => void;
|
|
44
|
+
executeSearch: (client: ScreenpipeUIClient) => Promise<void>;
|
|
45
|
+
nextPage: (client: ScreenpipeUIClient) => Promise<void>;
|
|
46
|
+
prevPage: (client: ScreenpipeUIClient) => Promise<void>;
|
|
47
|
+
reset: () => void;
|
|
48
|
+
}
|
|
49
|
+
declare function createSearchStore(): zustand_vanilla.StoreApi<SearchState>;
|
|
50
|
+
|
|
51
|
+
interface TimelineState {
|
|
52
|
+
items: ContentItem[];
|
|
53
|
+
startTime: string;
|
|
54
|
+
endTime: string;
|
|
55
|
+
appFilter: string | undefined;
|
|
56
|
+
loading: boolean;
|
|
57
|
+
error: string | null;
|
|
58
|
+
setTimeRange: (start: string, end: string) => void;
|
|
59
|
+
setAppFilter: (app: string | undefined) => void;
|
|
60
|
+
loadTimeline: (client: ScreenpipeUIClient) => Promise<void>;
|
|
61
|
+
reset: () => void;
|
|
62
|
+
}
|
|
63
|
+
declare function createTimelineStore(): zustand_vanilla.StoreApi<TimelineState>;
|
|
64
|
+
|
|
65
|
+
interface HealthState {
|
|
66
|
+
health: HealthCheckResponse | null;
|
|
67
|
+
loading: boolean;
|
|
68
|
+
error: string | null;
|
|
69
|
+
checkHealth: (client: ScreenpipeUIClient) => Promise<void>;
|
|
70
|
+
}
|
|
71
|
+
declare function createHealthStore(): zustand_vanilla.StoreApi<HealthState>;
|
|
72
|
+
|
|
73
|
+
declare function timeAgo(timestamp: string): string;
|
|
74
|
+
declare function formatDuration(ms: number): string;
|
|
75
|
+
declare function formatTime(timestamp: string): string;
|
|
76
|
+
declare function formatDate(timestamp: string): string;
|
|
77
|
+
declare function formatDateTime(timestamp: string): string;
|
|
78
|
+
declare function todayRange(): {
|
|
79
|
+
start: string;
|
|
80
|
+
end: string;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
declare function truncate(text: string, maxLen?: number): string;
|
|
84
|
+
declare function contentTypeLabel(item: ContentItem): string;
|
|
85
|
+
declare function highlightMatch(text: string, query: string): string;
|
|
86
|
+
declare function stripAnsi(text: string): string;
|
|
87
|
+
declare function contentPreview(item: ContentItem, maxLen?: number): string;
|
|
88
|
+
|
|
89
|
+
interface Column {
|
|
90
|
+
header: string;
|
|
91
|
+
width: number;
|
|
92
|
+
align?: "left" | "right";
|
|
93
|
+
}
|
|
94
|
+
declare function formatTable(columns: Column[], rows: string[][]): string;
|
|
95
|
+
|
|
96
|
+
export { type Column, type HealthCheckResponse, type HealthState, type ScreenpipeClientConfig, ScreenpipeUIClient, type SearchState, type TimeRange, type TimelineState, contentPreview, contentTypeLabel, createClient, createHealthStore, createSearchStore, createTimelineStore, formatDate, formatDateTime, formatDuration, formatTable, formatTime, getContentAppName, getContentText, getContentTimestamp, highlightMatch, stripAnsi, timeAgo, todayRange, truncate };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
// src/client.ts
|
|
2
|
+
var DEFAULT_BASE_URL = "http://localhost:3030";
|
|
3
|
+
var ScreenpipeUIClient = class {
|
|
4
|
+
baseUrl;
|
|
5
|
+
constructor(config) {
|
|
6
|
+
this.baseUrl = (config?.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
7
|
+
}
|
|
8
|
+
async get(path, params) {
|
|
9
|
+
const qs = new URLSearchParams();
|
|
10
|
+
if (params) {
|
|
11
|
+
for (const [key, value] of Object.entries(params)) {
|
|
12
|
+
if (value === void 0 || value === null || value === "") continue;
|
|
13
|
+
const snakeKey = key.replace(/[A-Z]/g, (m) => `_${m.toLowerCase()}`);
|
|
14
|
+
if (Array.isArray(value)) {
|
|
15
|
+
if (value.length > 0) qs.append(snakeKey, value.join(","));
|
|
16
|
+
} else {
|
|
17
|
+
qs.append(snakeKey, String(value));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const url = `${this.baseUrl}${path}${qs.toString() ? `?${qs}` : ""}`;
|
|
22
|
+
const res = await fetch(url);
|
|
23
|
+
if (!res.ok) {
|
|
24
|
+
const body = await res.text().catch(() => "");
|
|
25
|
+
throw new Error(`GET ${path} failed (${res.status}): ${body}`);
|
|
26
|
+
}
|
|
27
|
+
return res.json();
|
|
28
|
+
}
|
|
29
|
+
async search(params = {}) {
|
|
30
|
+
return this.get("/search", params);
|
|
31
|
+
}
|
|
32
|
+
async health() {
|
|
33
|
+
return this.get("/health");
|
|
34
|
+
}
|
|
35
|
+
getFrameUrl(frameId) {
|
|
36
|
+
return `${this.baseUrl}/frames/${frameId}`;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
function createClient(config) {
|
|
40
|
+
return new ScreenpipeUIClient(config);
|
|
41
|
+
}
|
|
42
|
+
function getContentText(item) {
|
|
43
|
+
switch (item.type) {
|
|
44
|
+
case "OCR":
|
|
45
|
+
return item.content.text;
|
|
46
|
+
case "Audio":
|
|
47
|
+
return item.content.transcription;
|
|
48
|
+
case "UI":
|
|
49
|
+
return item.content.text;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function getContentTimestamp(item) {
|
|
53
|
+
return item.content.timestamp;
|
|
54
|
+
}
|
|
55
|
+
function getContentAppName(item) {
|
|
56
|
+
switch (item.type) {
|
|
57
|
+
case "OCR":
|
|
58
|
+
return item.content.appName;
|
|
59
|
+
case "Audio":
|
|
60
|
+
return item.content.deviceName;
|
|
61
|
+
case "UI":
|
|
62
|
+
return item.content.appName;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/stores/search-store.ts
|
|
67
|
+
import { createStore } from "zustand/vanilla";
|
|
68
|
+
var DEFAULT_LIMIT = 20;
|
|
69
|
+
var initialState = {
|
|
70
|
+
query: "",
|
|
71
|
+
contentType: "all",
|
|
72
|
+
results: [],
|
|
73
|
+
pagination: { limit: DEFAULT_LIMIT, offset: 0, total: 0 },
|
|
74
|
+
loading: false,
|
|
75
|
+
error: null
|
|
76
|
+
};
|
|
77
|
+
function createSearchStore() {
|
|
78
|
+
return createStore((set, get) => ({
|
|
79
|
+
...initialState,
|
|
80
|
+
setQuery: (q) => set({ query: q }),
|
|
81
|
+
setContentType: (t) => set({ contentType: t }),
|
|
82
|
+
executeSearch: async (client) => {
|
|
83
|
+
const { query, contentType } = get();
|
|
84
|
+
set({ loading: true, error: null });
|
|
85
|
+
try {
|
|
86
|
+
const res = await client.search({
|
|
87
|
+
q: query || void 0,
|
|
88
|
+
contentType,
|
|
89
|
+
limit: DEFAULT_LIMIT,
|
|
90
|
+
offset: 0
|
|
91
|
+
});
|
|
92
|
+
set({
|
|
93
|
+
results: res.data,
|
|
94
|
+
pagination: res.pagination,
|
|
95
|
+
loading: false
|
|
96
|
+
});
|
|
97
|
+
} catch (e) {
|
|
98
|
+
set({
|
|
99
|
+
error: e instanceof Error ? e.message : String(e),
|
|
100
|
+
loading: false
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
nextPage: async (client) => {
|
|
105
|
+
const { query, contentType, pagination } = get();
|
|
106
|
+
const newOffset = pagination.offset + pagination.limit;
|
|
107
|
+
if (newOffset >= pagination.total) return;
|
|
108
|
+
set({ loading: true, error: null });
|
|
109
|
+
try {
|
|
110
|
+
const res = await client.search({
|
|
111
|
+
q: query || void 0,
|
|
112
|
+
contentType,
|
|
113
|
+
limit: pagination.limit,
|
|
114
|
+
offset: newOffset
|
|
115
|
+
});
|
|
116
|
+
set({
|
|
117
|
+
results: res.data,
|
|
118
|
+
pagination: res.pagination,
|
|
119
|
+
loading: false
|
|
120
|
+
});
|
|
121
|
+
} catch (e) {
|
|
122
|
+
set({
|
|
123
|
+
error: e instanceof Error ? e.message : String(e),
|
|
124
|
+
loading: false
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
prevPage: async (client) => {
|
|
129
|
+
const { query, contentType, pagination } = get();
|
|
130
|
+
const newOffset = Math.max(0, pagination.offset - pagination.limit);
|
|
131
|
+
if (newOffset === pagination.offset) return;
|
|
132
|
+
set({ loading: true, error: null });
|
|
133
|
+
try {
|
|
134
|
+
const res = await client.search({
|
|
135
|
+
q: query || void 0,
|
|
136
|
+
contentType,
|
|
137
|
+
limit: pagination.limit,
|
|
138
|
+
offset: newOffset
|
|
139
|
+
});
|
|
140
|
+
set({
|
|
141
|
+
results: res.data,
|
|
142
|
+
pagination: res.pagination,
|
|
143
|
+
loading: false
|
|
144
|
+
});
|
|
145
|
+
} catch (e) {
|
|
146
|
+
set({
|
|
147
|
+
error: e instanceof Error ? e.message : String(e),
|
|
148
|
+
loading: false
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
reset: () => set(initialState)
|
|
153
|
+
}));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/stores/timeline-store.ts
|
|
157
|
+
import { createStore as createStore2 } from "zustand/vanilla";
|
|
158
|
+
|
|
159
|
+
// src/formatters/time.ts
|
|
160
|
+
var MINUTE = 60 * 1e3;
|
|
161
|
+
var HOUR = 60 * MINUTE;
|
|
162
|
+
var DAY = 24 * HOUR;
|
|
163
|
+
function timeAgo(timestamp) {
|
|
164
|
+
const diff = Date.now() - new Date(timestamp).getTime();
|
|
165
|
+
if (diff < MINUTE) return "just now";
|
|
166
|
+
if (diff < HOUR) {
|
|
167
|
+
const mins = Math.floor(diff / MINUTE);
|
|
168
|
+
return `${mins}m ago`;
|
|
169
|
+
}
|
|
170
|
+
if (diff < DAY) {
|
|
171
|
+
const hours = Math.floor(diff / HOUR);
|
|
172
|
+
return `${hours}h ago`;
|
|
173
|
+
}
|
|
174
|
+
const days = Math.floor(diff / DAY);
|
|
175
|
+
return `${days}d ago`;
|
|
176
|
+
}
|
|
177
|
+
function formatDuration(ms) {
|
|
178
|
+
if (ms < MINUTE) return `${Math.round(ms / 1e3)}s`;
|
|
179
|
+
if (ms < HOUR) return `${Math.floor(ms / MINUTE)}m`;
|
|
180
|
+
const h = Math.floor(ms / HOUR);
|
|
181
|
+
const m = Math.floor(ms % HOUR / MINUTE);
|
|
182
|
+
return m > 0 ? `${h}h ${m}m` : `${h}h`;
|
|
183
|
+
}
|
|
184
|
+
function formatTime(timestamp) {
|
|
185
|
+
return new Date(timestamp).toLocaleTimeString(void 0, {
|
|
186
|
+
hour: "2-digit",
|
|
187
|
+
minute: "2-digit"
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
function formatDate(timestamp) {
|
|
191
|
+
return new Date(timestamp).toLocaleDateString(void 0, {
|
|
192
|
+
month: "short",
|
|
193
|
+
day: "numeric",
|
|
194
|
+
year: "numeric"
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
function formatDateTime(timestamp) {
|
|
198
|
+
return `${formatDate(timestamp)} ${formatTime(timestamp)}`;
|
|
199
|
+
}
|
|
200
|
+
function todayRange() {
|
|
201
|
+
const now = /* @__PURE__ */ new Date();
|
|
202
|
+
const start = new Date(now);
|
|
203
|
+
start.setHours(0, 0, 0, 0);
|
|
204
|
+
return {
|
|
205
|
+
start: start.toISOString(),
|
|
206
|
+
end: now.toISOString()
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// src/stores/timeline-store.ts
|
|
211
|
+
function createTimelineStore() {
|
|
212
|
+
const { start, end } = todayRange();
|
|
213
|
+
return createStore2((set, get) => ({
|
|
214
|
+
items: [],
|
|
215
|
+
startTime: start,
|
|
216
|
+
endTime: end,
|
|
217
|
+
appFilter: void 0,
|
|
218
|
+
loading: false,
|
|
219
|
+
error: null,
|
|
220
|
+
setTimeRange: (start2, end2) => set({ startTime: start2, endTime: end2 }),
|
|
221
|
+
setAppFilter: (app) => set({ appFilter: app }),
|
|
222
|
+
loadTimeline: async (client) => {
|
|
223
|
+
const { startTime, endTime, appFilter } = get();
|
|
224
|
+
set({ loading: true, error: null });
|
|
225
|
+
try {
|
|
226
|
+
const res = await client.search({
|
|
227
|
+
startTime,
|
|
228
|
+
endTime,
|
|
229
|
+
appName: appFilter,
|
|
230
|
+
limit: 100,
|
|
231
|
+
contentType: "all"
|
|
232
|
+
});
|
|
233
|
+
set({ items: res.data, loading: false });
|
|
234
|
+
} catch (e) {
|
|
235
|
+
set({
|
|
236
|
+
error: e instanceof Error ? e.message : String(e),
|
|
237
|
+
loading: false
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
reset: () => {
|
|
242
|
+
const { start: start2, end: end2 } = todayRange();
|
|
243
|
+
set({
|
|
244
|
+
items: [],
|
|
245
|
+
startTime: start2,
|
|
246
|
+
endTime: end2,
|
|
247
|
+
appFilter: void 0,
|
|
248
|
+
loading: false,
|
|
249
|
+
error: null
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/stores/health-store.ts
|
|
256
|
+
import { createStore as createStore3 } from "zustand/vanilla";
|
|
257
|
+
function createHealthStore() {
|
|
258
|
+
return createStore3((set) => ({
|
|
259
|
+
health: null,
|
|
260
|
+
loading: false,
|
|
261
|
+
error: null,
|
|
262
|
+
checkHealth: async (client) => {
|
|
263
|
+
set({ loading: true, error: null });
|
|
264
|
+
try {
|
|
265
|
+
const health = await client.health();
|
|
266
|
+
set({ health, loading: false });
|
|
267
|
+
} catch (e) {
|
|
268
|
+
set({
|
|
269
|
+
error: e instanceof Error ? e.message : String(e),
|
|
270
|
+
loading: false
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// src/formatters/content.ts
|
|
278
|
+
function truncate(text, maxLen = 80) {
|
|
279
|
+
if (text.length <= maxLen) return text;
|
|
280
|
+
return text.slice(0, maxLen - 1) + "\u2026";
|
|
281
|
+
}
|
|
282
|
+
function contentTypeLabel(item) {
|
|
283
|
+
switch (item.type) {
|
|
284
|
+
case "OCR":
|
|
285
|
+
return "screen";
|
|
286
|
+
case "Audio":
|
|
287
|
+
return "audio";
|
|
288
|
+
case "UI":
|
|
289
|
+
return "ui";
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function highlightMatch(text, query) {
|
|
293
|
+
if (!query) return text;
|
|
294
|
+
const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
295
|
+
const re = new RegExp(`(${escaped})`, "gi");
|
|
296
|
+
return text.replace(re, "\x1B[1;33m$1\x1B[0m");
|
|
297
|
+
}
|
|
298
|
+
function stripAnsi(text) {
|
|
299
|
+
return text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
300
|
+
}
|
|
301
|
+
function contentPreview(item, maxLen = 80) {
|
|
302
|
+
switch (item.type) {
|
|
303
|
+
case "OCR":
|
|
304
|
+
return truncate(item.content.text.replace(/\n/g, " "), maxLen);
|
|
305
|
+
case "Audio":
|
|
306
|
+
return truncate(item.content.transcription.replace(/\n/g, " "), maxLen);
|
|
307
|
+
case "UI":
|
|
308
|
+
return truncate(item.content.text.replace(/\n/g, " "), maxLen);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// src/formatters/table.ts
|
|
313
|
+
function formatTable(columns, rows) {
|
|
314
|
+
const divider = columns.map((c) => "\u2500".repeat(c.width)).join("\u2500\u252C\u2500");
|
|
315
|
+
const headerRow = columns.map((c) => pad(c.header, c.width, c.align)).join(" \u2502 ");
|
|
316
|
+
const dataRows = rows.map(
|
|
317
|
+
(row) => columns.map((c, i) => pad(row[i] ?? "", c.width, c.align)).join(" \u2502 ")
|
|
318
|
+
);
|
|
319
|
+
return [headerRow, divider, ...dataRows].join("\n");
|
|
320
|
+
}
|
|
321
|
+
function pad(text, width, align = "left") {
|
|
322
|
+
const visible = text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
323
|
+
const padding = Math.max(0, width - visible.length);
|
|
324
|
+
if (align === "right") {
|
|
325
|
+
return " ".repeat(padding) + text;
|
|
326
|
+
}
|
|
327
|
+
return text + " ".repeat(padding);
|
|
328
|
+
}
|
|
329
|
+
export {
|
|
330
|
+
ScreenpipeUIClient,
|
|
331
|
+
contentPreview,
|
|
332
|
+
contentTypeLabel,
|
|
333
|
+
createClient,
|
|
334
|
+
createHealthStore,
|
|
335
|
+
createSearchStore,
|
|
336
|
+
createTimelineStore,
|
|
337
|
+
formatDate,
|
|
338
|
+
formatDateTime,
|
|
339
|
+
formatDuration,
|
|
340
|
+
formatTable,
|
|
341
|
+
formatTime,
|
|
342
|
+
getContentAppName,
|
|
343
|
+
getContentText,
|
|
344
|
+
getContentTimestamp,
|
|
345
|
+
highlightMatch,
|
|
346
|
+
stripAnsi,
|
|
347
|
+
timeAgo,
|
|
348
|
+
todayRange,
|
|
349
|
+
truncate
|
|
350
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@screenpipe-ui/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": ["dist"],
|
|
14
|
+
"publishConfig": { "access": "public" },
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "bun test",
|
|
17
|
+
"build": "tsup src/index.ts --format esm --dts --clean"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@screenpipe/js": "^1.0.21",
|
|
21
|
+
"zustand": "^5.0.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/bun": "^1.1.14",
|
|
25
|
+
"tsup": "^8.0.0",
|
|
26
|
+
"typescript": "^5.3.3"
|
|
27
|
+
}
|
|
28
|
+
}
|