dsh-my-observability 0.3.0 → 0.3.2

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/lib/store.js CHANGED
@@ -13,291 +13,294 @@
13
13
  *
14
14
  * 磁盘 I/O 与格式规整在 store-persist.js。
15
15
  */
16
- import {
17
- jsonlFile,
18
- legacyFile,
19
- loadPersisted,
20
- writeSnapshot,
21
- appendLines,
22
- removeLegacyAfterMigration,
23
- normalizeLoaded,
24
- MAX_EVENTS_PER_SESSION,
25
- MAX_TOTAL_EVENTS,
26
- COMPACT_LINES,
27
- } from './store-persist.js'
28
-
29
- const FLUSH_INTERVAL_MS = 500
30
- const COMPACT_DEBOUNCE_MS = 300
31
- const PREFIX = '[dsh-my-observability]'
32
-
16
+ import { jsonlFile, legacyFile, loadPersisted, writeSnapshot, appendLines, removeLegacyAfterMigration, normalizeLoaded, MAX_EVENTS_PER_SESSION, MAX_TOTAL_EVENTS, COMPACT_LINES, } from './store-persist.js';
17
+ const FLUSH_INTERVAL_MS = 500;
18
+ const COMPACT_DEBOUNCE_MS = 300;
19
+ const PREFIX = '[dsh-my-observability]';
33
20
  /** 初始空状态。 */
34
21
  function createState() {
35
- return { version: 1, bySession: {} }
22
+ return { version: 1, bySession: {} };
36
23
  }
37
-
38
24
  /** 创建审计存储:{ record, events, sessions, count, dispose }。
39
25
  * record 在状态加载完成前缓冲(不丢事件);dispose 冲刷未落盘数据。 */
40
26
  export function createStore(ctx) {
41
- const store = { state: createState() }
42
- const handle = {
43
- ctx,
44
- file: jsonlFile(),
45
- legacy: legacyFile(),
46
- store,
47
- pending: [],
48
- ready: false,
49
- lineQueue: [],
50
- queuedLines: 0,
51
- total: 0,
52
- flushTimer: null,
53
- compactTimer: null,
54
- compacting: false,
55
- migrated: false,
56
- persistEnabled: true,
57
- dirtyChain: Promise.resolve(),
58
- }
59
- store.record = (event) => record(handle, event)
60
- store.events = (sessionId, type, limit) => eventsOf(handle, sessionId, type, limit)
61
- store.sessions = () => sessionsOf(handle)
62
- store.count = () => countOf(handle)
63
- store.setPersistEnabled = (enabled) => setPersistEnabled(handle, enabled)
64
- store.dispose = () => dispose(handle)
65
- void loadPersisted(handle.file, handle.legacy).then((result) => onLoaded(handle, result))
66
- return store
27
+ const store = { state: createState() };
28
+ let markReady = () => { };
29
+ const readyPromise = new Promise((resolve) => {
30
+ markReady = resolve;
31
+ });
32
+ const handle = {
33
+ ctx,
34
+ file: jsonlFile(),
35
+ legacy: legacyFile(),
36
+ store,
37
+ pending: [],
38
+ ready: false,
39
+ readyPromise,
40
+ markReady,
41
+ lineQueue: [],
42
+ queuedLines: 0,
43
+ total: 0,
44
+ flushTimer: null,
45
+ compactTimer: null,
46
+ compacting: false,
47
+ migrated: false,
48
+ persistEnabled: true,
49
+ dirtyChain: Promise.resolve(),
50
+ };
51
+ store.record = (event) => record(handle, event);
52
+ store.events = (sessionId, type, limit) => eventsOf(handle, sessionId, type, limit);
53
+ store.sessions = () => sessionsOf(handle);
54
+ store.count = () => countOf(handle);
55
+ store.setPersistEnabled = (enabled) => setPersistEnabled(handle, enabled);
56
+ store.whenReady = () => handle.readyPromise;
57
+ store.dispose = () => dispose(handle);
58
+ void loadPersisted(handle.file, handle.legacy)
59
+ .then((result) => onLoaded(handle, result))
60
+ // 兜底:加载链上的任何异常都不能让 whenReady 永不 resolve——那会让所有
61
+ // API 请求永久挂起(比「读到空 state」严重得多)。loadPersisted 自身已
62
+ // 逐层 catch,这里是防未来改动引入 reject 的护栏。
63
+ .catch(() => onLoaded(handle, { state: createState(), migrated: false, lines: 0 }));
64
+ return store;
67
65
  }
68
-
69
66
  /** 追加一条审计事件(自动分配 id/时间戳);未就绪时缓冲。 */
70
67
  function record(handle, event) {
71
- const item = { id: nextId(handle), time: Date.now(), ...event }
72
- if (!handle.ready) {
73
- handle.pending.push(item)
74
- return item
75
- }
76
- enqueueRecord(handle, item)
77
- scheduleFlush(handle)
78
- return item
68
+ const item = { id: nextId(handle), time: Date.now(), ...event };
69
+ if (!handle.ready) {
70
+ handle.pending.push(item);
71
+ return item;
72
+ }
73
+ enqueueRecord(handle, item);
74
+ scheduleFlush(handle);
75
+ return item;
79
76
  }
80
-
81
77
  /** 事件入内存桶 + 排队待落盘行(回放与运行时共用,保证回放也落盘)。
82
78
  * 降级(persistEnabled=false)时事件照常入内存桶(有 FIFO 上限保护),
83
79
  * 但不排队落盘行、不触发 flush——写盘完全停止,恢复时全量快照补齐。 */
84
80
  function enqueueRecord(handle, item) {
85
- appendEvent(handle, item)
86
- if (!handle.persistEnabled) return
87
- handle.lineQueue.push(JSON.stringify(item))
88
- handle.queuedLines += 1
89
- if (handle.queuedLines >= COMPACT_LINES) scheduleCompact(handle)
81
+ appendEvent(handle, item);
82
+ if (!handle.persistEnabled)
83
+ return;
84
+ handle.lineQueue.push(JSON.stringify(item));
85
+ handle.queuedLines += 1;
86
+ if (handle.queuedLines >= COMPACT_LINES)
87
+ scheduleCompact(handle);
90
88
  }
91
-
92
89
  /**
93
90
  * 落盘开关(资源看门狗降级用):
94
91
  * - false(降级):停止落盘——清空待写行(内存 state 仍在,不丢),停 flush。
95
92
  * - true(恢复):立即 compact 全量快照(内存=真相,补齐降级窗口事件),恢复增量。
96
93
  */
97
94
  function setPersistEnabled(handle, enabled) {
98
- if (handle.persistEnabled === enabled) return
99
- handle.persistEnabled = enabled
100
- if (handle.flushTimer !== null) {
101
- clearTimeout(handle.flushTimer)
102
- handle.flushTimer = null
103
- }
104
- handle.lineQueue = []
105
- handle.queuedLines = 0
106
- if (enabled) compactNow(handle)
95
+ if (handle.persistEnabled === enabled)
96
+ return;
97
+ handle.persistEnabled = enabled;
98
+ if (handle.flushTimer !== null) {
99
+ clearTimeout(handle.flushTimer);
100
+ handle.flushTimer = null;
101
+ }
102
+ handle.lineQueue = [];
103
+ handle.queuedLines = 0;
104
+ if (enabled)
105
+ compactNow(handle);
107
106
  }
108
-
109
107
  /** 全部会话事件:合并各会话并按时间正序(sessionId='*')。 */
110
108
  function eventsAllOf(handle, type, limit) {
111
- let all = []
112
- for (const bucket of Object.values(handle.store.state.bySession)) {
113
- if (Array.isArray(bucket.events)) all.push(...bucket.events)
114
- }
115
- all.sort((a, b) => a.time - b.time)
116
- if (type !== undefined && type !== null && type !== '') {
117
- all = all.filter((event) => event.type === type)
118
- }
119
- const capped = typeof limit === 'number' && limit > 0 ? all.slice(-limit) : all
120
- return capped.map((event) => ({ ...event }))
109
+ let all = [];
110
+ for (const bucket of Object.values(handle.store.state.bySession)) {
111
+ if (Array.isArray(bucket.events))
112
+ all.push(...bucket.events);
113
+ }
114
+ all.sort((a, b) => a.time - b.time);
115
+ if (type !== undefined && type !== null && type !== '') {
116
+ all = all.filter((event) => event.type === type);
117
+ }
118
+ const capped = typeof limit === 'number' && limit > 0 ? all.slice(-limit) : all;
119
+ return capped.map((event) => ({ ...event }));
121
120
  }
122
-
123
121
  /** 查询会话事件(正序时间轴;type 可选过滤;limit 限制条数)。
124
122
  * sessionId === '*' 表示全部会话;空字符串('')保持既有语义返回空数组。 */
125
123
  function eventsOf(handle, sessionId, type, limit) {
126
- if (sessionId === '*') return eventsAllOf(handle, type, limit)
127
- const bucket = handle.store.state.bySession[sessionId]?.events
128
- if (!Array.isArray(bucket)) return []
129
- let list = bucket
130
- if (type !== undefined && type !== null && type !== '') {
131
- list = list.filter((event) => event.type === type)
132
- }
133
- const capped = typeof limit === 'number' && limit > 0 ? list.slice(-limit) : list
134
- return capped.map((event) => ({ ...event }))
124
+ if (sessionId === '*')
125
+ return eventsAllOf(handle, type, limit);
126
+ const bucket = handle.store.state.bySession[sessionId]?.events;
127
+ if (!Array.isArray(bucket))
128
+ return [];
129
+ let list = bucket;
130
+ if (type !== undefined && type !== null && type !== '') {
131
+ list = list.filter((event) => event.type === type);
132
+ }
133
+ const capped = typeof limit === 'number' && limit > 0 ? list.slice(-limit) : list;
134
+ return capped.map((event) => ({ ...event }));
135
135
  }
136
-
137
136
  /** 有审计事件的会话列表(按最后活动时间倒序,含事件数与可读标题)。 */
138
137
  function sessionsOf(handle) {
139
- const entries = Object.entries(handle.store.state.bySession)
140
- const list = entries
141
- .map(([sessionId, bucket]) => ({
142
- sessionId,
143
- count: bucket.events.length,
144
- lastTime: bucket.events.length > 0 ? bucket.events[bucket.events.length - 1].time : 0,
145
- title: sessionTitleOf(bucket),
138
+ const entries = Object.entries(handle.store.state.bySession);
139
+ const list = entries
140
+ .map(([sessionId, bucket]) => ({
141
+ sessionId,
142
+ count: bucket.events.length,
143
+ lastTime: bucket.events.length > 0 ? bucket.events[bucket.events.length - 1].time : 0,
144
+ title: sessionTitleOf(bucket),
146
145
  }))
147
- .filter((entry) => entry.count > 0)
148
- list.sort((a, b) => b.lastTime - a.lastTime)
149
- return list
146
+ .filter((entry) => entry.count > 0);
147
+ list.sort((a, b) => b.lastTime - a.lastTime);
148
+ return list;
150
149
  }
151
-
152
150
  /** 会话标题:最早一条 user_message 事件的文本(无则空串,面板回退 UUID 短显)。 */
153
151
  function sessionTitleOf(bucket) {
154
- for (const event of bucket.events) {
155
- if (event.type === 'user_message' && typeof event.data?.text === 'string' && event.data.text !== '') {
156
- return event.data.text
152
+ for (const event of bucket.events) {
153
+ const text = event.data?.text;
154
+ if (event.type === 'user_message' && typeof text === 'string' && text !== '') {
155
+ return text;
156
+ }
157
157
  }
158
- }
159
- return ''
158
+ return '';
160
159
  }
161
-
162
160
  /** 全部会话事件总数(O(1) 计数)。 */
163
161
  function countOf(handle) {
164
- return handle.total
162
+ return handle.total;
165
163
  }
166
-
167
164
  /** 事件自增 id(跨会话单调)。 */
168
165
  function nextId(handle) {
169
- handle.seq = (handle.seq ?? 0) + 1
170
- return handle.seq
166
+ handle.seq = (handle.seq ?? 0) + 1;
167
+ return handle.seq;
171
168
  }
172
-
173
169
  /** 追加事件到会话桶:FIFO 淘汰 + 全局上限轮转淘汰(维护 O(1) 计数)。 */
174
170
  function appendEvent(handle, event) {
175
- const state = handle.store.state
176
- const bucket = state.bySession[event.sessionId] ?? (state.bySession[event.sessionId] = { events: [] })
177
- bucket.events.push(event)
178
- handle.total += 1
179
- if (bucket.events.length > MAX_EVENTS_PER_SESSION) {
180
- const removed = bucket.events.splice(0, bucket.events.length - MAX_EVENTS_PER_SESSION).length
181
- handle.total -= removed
182
- }
183
- if (handle.total > MAX_TOTAL_EVENTS) evictOldest(handle)
171
+ const state = handle.store.state;
172
+ const bucket = state.bySession[event.sessionId] ?? (state.bySession[event.sessionId] = { events: [] });
173
+ bucket.events.push(event);
174
+ handle.total += 1;
175
+ if (bucket.events.length > MAX_EVENTS_PER_SESSION) {
176
+ const removed = bucket.events.splice(0, bucket.events.length - MAX_EVENTS_PER_SESSION).length;
177
+ handle.total -= removed;
178
+ }
179
+ if (handle.total > MAX_TOTAL_EVENTS)
180
+ evictOldest(handle);
184
181
  }
185
-
186
182
  /** 全局超限:从最早活动的会话整桶淘汰,直到回到上限内。 */
187
183
  function evictOldest(handle) {
188
- const state = handle.store.state
189
- const sessions = Object.entries(state.bySession)
190
- .filter(([, bucket]) => bucket.events.length > 0)
191
- .sort((a, b) => firstTimeOf(a[1]) - firstTimeOf(b[1]))
192
- for (const [sessionId, bucket] of sessions) {
193
- if (handle.total <= MAX_TOTAL_EVENTS) break
194
- handle.total -= bucket.events.length
195
- delete state.bySession[sessionId]
196
- }
184
+ const state = handle.store.state;
185
+ const sessions = Object.entries(state.bySession)
186
+ .filter(([, bucket]) => bucket.events.length > 0)
187
+ .sort((a, b) => firstTimeOf(a[1]) - firstTimeOf(b[1]));
188
+ for (const [sessionId, bucket] of sessions) {
189
+ if (handle.total <= MAX_TOTAL_EVENTS)
190
+ break;
191
+ handle.total -= bucket.events.length;
192
+ delete state.bySession[sessionId];
193
+ }
197
194
  }
198
-
199
195
  function firstTimeOf(bucket) {
200
- return bucket.events.length > 0 ? bucket.events[0].time : 0
196
+ return bucket.events.length > 0 ? bucket.events[0].time : 0;
201
197
  }
202
-
203
198
  /** 状态加载完成:合并本进程已产生的事件(防 dispose 回放被覆盖)+ 回放缓冲 + 迁移/紧凑调度。 */
204
199
  function onLoaded(handle, result) {
205
- const parsed = result.state
206
- mergeCurrentEvents(handle.store.state, parsed)
207
- const normalized = normalizeLoaded(parsed.bySession)
208
- handle.store.state = normalized
209
- handle.total = countOfState(normalized)
210
- handle.ready = true
211
- const pending = handle.pending.splice(0)
212
- for (const item of pending) enqueueRecord(handle, item)
213
- if (pending.length > 0) scheduleFlush(handle)
214
- handle.migrated = result.migrated
215
- if (result.migrated || result.lines >= COMPACT_LINES) scheduleCompact(handle)
200
+ const parsed = result.state;
201
+ mergeCurrentEvents(handle.store.state, parsed);
202
+ const normalized = normalizeLoaded(parsed.bySession);
203
+ handle.store.state = normalized;
204
+ handle.total = countOfState(normalized);
205
+ handle.ready = true;
206
+ const pending = handle.pending.splice(0);
207
+ for (const item of pending)
208
+ enqueueRecord(handle, item);
209
+ if (pending.length > 0)
210
+ scheduleFlush(handle);
211
+ handle.migrated = result.migrated;
212
+ if (result.migrated || result.lines >= COMPACT_LINES)
213
+ scheduleCompact(handle);
214
+ // markReady 放在合并/回放**之后**:whenReady 返回即保证缓冲事件已并入
215
+ // state,查询结果与「加载耗时」无关(不再需要调用方猜 sleep 时长)。
216
+ handle.markReady();
216
217
  }
217
-
218
218
  /** 把 load 完成前(或 dispose 回放时)已进入内存的事件合并进加载状态:后到的事件追加于桶尾。 */
219
219
  function mergeCurrentEvents(current, parsed) {
220
- for (const [sessionId, bucket] of Object.entries(current.bySession ?? {})) {
221
- if (!Array.isArray(bucket.events) || bucket.events.length === 0) continue
222
- const target = parsed.bySession[sessionId] ?? (parsed.bySession[sessionId] = { events: [] })
223
- target.events.push(...bucket.events)
224
- }
220
+ for (const [sessionId, bucket] of Object.entries(current.bySession ?? {})) {
221
+ if (!Array.isArray(bucket.events) || bucket.events.length === 0)
222
+ continue;
223
+ const target = parsed.bySession[sessionId] ?? (parsed.bySession[sessionId] = { events: [] });
224
+ target.events.push(...bucket.events);
225
+ }
225
226
  }
226
-
227
227
  function countOfState(state) {
228
- let total = 0
229
- for (const bucket of Object.values(state.bySession ?? {})) {
230
- if (Array.isArray(bucket?.events)) total += bucket.events.length
231
- }
232
- return total
228
+ let total = 0;
229
+ for (const bucket of Object.values(state.bySession ?? {})) {
230
+ if (Array.isArray(bucket?.events))
231
+ total += bucket.events.length;
232
+ }
233
+ return total;
233
234
  }
234
-
235
235
  /** 防抖批量追加落盘(一次 append 全部待写行)。 */
236
236
  function scheduleFlush(handle) {
237
- if (handle.flushTimer !== null) return
238
- handle.flushTimer = setTimeout(() => {
239
- handle.flushTimer = null
240
- flushNow(handle)
241
- }, FLUSH_INTERVAL_MS)
237
+ if (handle.flushTimer !== null)
238
+ return;
239
+ handle.flushTimer = setTimeout(() => {
240
+ handle.flushTimer = null;
241
+ flushNow(handle);
242
+ }, FLUSH_INTERVAL_MS);
242
243
  }
243
-
244
244
  function flushNow(handle) {
245
- if (handle.lineQueue.length === 0 || !handle.persistEnabled) return
246
- const text = `${handle.lineQueue.join('\n')}\n`
247
- handle.lineQueue = []
248
- handle.dirtyChain = handle.dirtyChain.then(() => appendLines(handle.file, text, handle.ctx.logger, PREFIX))
245
+ if (handle.lineQueue.length === 0 || !handle.persistEnabled)
246
+ return;
247
+ const text = `${handle.lineQueue.join('\n')}\n`;
248
+ handle.lineQueue = [];
249
+ handle.dirtyChain = handle.dirtyChain.then(() => appendLines(handle.file, text, handle.ctx.logger, PREFIX));
249
250
  }
250
-
251
251
  /** 防抖紧凑调度(合并短时间多次触发)。 */
252
252
  function scheduleCompact(handle) {
253
- if (handle.compactTimer !== null || handle.compacting) return
254
- handle.compactTimer = setTimeout(() => {
255
- handle.compactTimer = null
256
- compactNow(handle)
257
- }, COMPACT_DEBOUNCE_MS)
253
+ if (handle.compactTimer !== null || handle.compacting)
254
+ return;
255
+ handle.compactTimer = setTimeout(() => {
256
+ handle.compactTimer = null;
257
+ compactNow(handle);
258
+ }, COMPACT_DEBOUNCE_MS);
258
259
  }
259
-
260
260
  /** 原子快照重写 jsonl(内存 state 全量;含未 flush 行 → 排队行清空防重复)。 */
261
261
  function compactNow(handle) {
262
- if (handle.compacting) return
263
- handle.compacting = true
264
- if (handle.flushTimer !== null) {
265
- clearTimeout(handle.flushTimer)
266
- handle.flushTimer = null
267
- }
268
- handle.lineQueue = []
269
- handle.queuedLines = 0
270
- const migrated = handle.migrated
271
- handle.migrated = false
272
- const file = handle.file
273
- const legacy = handle.legacy
274
- handle.dirtyChain = handle.dirtyChain
275
- .then(async () => {
276
- await writeSnapshot(file, handle.store.state, handle.ctx.logger, PREFIX)
277
- if (migrated) await removeLegacyAfterMigration(legacy)
278
- })
279
- .finally(() => {
280
- handle.compacting = false
262
+ if (handle.compacting)
263
+ return;
264
+ handle.compacting = true;
265
+ if (handle.flushTimer !== null) {
266
+ clearTimeout(handle.flushTimer);
267
+ handle.flushTimer = null;
268
+ }
269
+ handle.lineQueue = [];
270
+ handle.queuedLines = 0;
271
+ const migrated = handle.migrated;
272
+ handle.migrated = false;
273
+ const file = handle.file;
274
+ const legacy = handle.legacy;
275
+ handle.dirtyChain = handle.dirtyChain
276
+ .then(async () => {
277
+ await writeSnapshot(file, handle.store.state, handle.ctx.logger, PREFIX);
278
+ if (migrated)
279
+ await removeLegacyAfterMigration(legacy);
281
280
  })
281
+ .finally(() => {
282
+ handle.compacting = false;
283
+ });
282
284
  }
283
-
284
285
  /** 卸载冲刷:清定时器 + 回放未就绪缓冲 + 立即落盘(迁移兜底)。
285
286
  * 返回落盘链 promise:调用方 await 可保证冲刷完成(卸载/退出时事件
286
287
  * 不丢);fire-and-forget 调用亦兼容(触发后异步落盘)。 */
287
288
  function dispose(handle) {
288
- if (handle.flushTimer !== null) {
289
- clearTimeout(handle.flushTimer)
290
- handle.flushTimer = null
291
- }
292
- if (handle.compactTimer !== null) {
293
- clearTimeout(handle.compactTimer)
294
- handle.compactTimer = null
295
- }
296
- if (!handle.ready) {
297
- const pending = handle.pending.splice(0)
298
- for (const item of pending) enqueueRecord(handle, item)
299
- }
300
- flushNow(handle)
301
- if (handle.migrated || handle.queuedLines >= COMPACT_LINES) compactNow(handle)
302
- return handle.dirtyChain
289
+ if (handle.flushTimer !== null) {
290
+ clearTimeout(handle.flushTimer);
291
+ handle.flushTimer = null;
292
+ }
293
+ if (handle.compactTimer !== null) {
294
+ clearTimeout(handle.compactTimer);
295
+ handle.compactTimer = null;
296
+ }
297
+ if (!handle.ready) {
298
+ const pending = handle.pending.splice(0);
299
+ for (const item of pending)
300
+ enqueueRecord(handle, item);
301
+ }
302
+ flushNow(handle);
303
+ if (handle.migrated || handle.queuedLines >= COMPACT_LINES)
304
+ compactNow(handle);
305
+ return handle.dirtyChain;
303
306
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-my-observability",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "DSH 可观测性 + Git 工程工具插件:会话轨迹回放(时间轴)、事件审计(agent 行为记录)、结构化 Git 提交(Conventional Commits)、增量 diff 审查(提交前审查)。DSH web plugin: trajectory replay timeline, event audit log, structured git commits, pre-commit incremental diff review.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -12,8 +12,9 @@
12
12
  "node": ">=22"
13
13
  },
14
14
  "scripts": {
15
- "build": "node scripts/build.mjs",
16
- "test": "npx vitest run --coverage && cucumber-js test/features --import 'test/features/steps/*.mjs'"
15
+ "build": "npx tsc -p tsconfig.json && node scripts/build.mjs",
16
+ "typecheck": "npx tsc --noEmit -p tsconfig.json && npx tsc --noEmit -p tsconfig.client.json",
17
+ "test": "npx vitest run --coverage && cucumber-js test/features --import \"test/features/steps/*.mjs\""
17
18
  },
18
19
  "exports": {
19
20
  ".": {
@@ -41,9 +42,9 @@
41
42
  }
42
43
  },
43
44
  "peerDependencies": {
44
- "cordis": "^4.0.0-rc.8",
45
- "react": "^18.2.0 || ^19.2.0",
46
- "dsh-shared": "^0.1.0"
45
+ "cordis": "^4.0.0-rc.10",
46
+ "dsh-shared": "^0.1.4",
47
+ "react": "^18.2.0 || ^19.3.0"
47
48
  },
48
49
  "peerDependenciesMeta": {
49
50
  "cordis": {
@@ -53,5 +54,12 @@
53
54
  "optional": true
54
55
  }
55
56
  },
56
- "license": "MIT"
57
+ "devDependencies": {
58
+ "@types/node": "^26.5.1",
59
+ "typescript": "^7.0.2"
60
+ },
61
+ "license": "MIT",
62
+ "dependencies": {
63
+ "tmp": "^0.2.7"
64
+ }
57
65
  }