@evomap/evolver-webui 2.0.0-beta.1 → 2.0.0-beta.10

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/server.js CHANGED
@@ -1,22 +1,124 @@
1
1
  import { createServer } from 'node:http';
2
2
  import { randomBytes, timingSafeEqual } from 'node:crypto';
3
- import { events as ev, assetstore, mailbox as mb, ops } from '@evomap/evolver-core';
3
+ import { events as ev, assetstore, mailbox as mb, ops, util } from '@evomap/evolver-core';
4
4
  import { CONSOLE_HTML } from './console.js';
5
+ import { EventSnapshotCache, fileEventSnapshotSource } from './eventSnapshot.js';
6
+ import { listLineageAssets, loadAssetLineage } from './assetLineage.js';
7
+ import { eventListRelations } from './observabilityRelations.js';
8
+ import { redactDiagnosticText, sanitizeDiagnosticValue } from './diagnosticSanitize.js';
5
9
  const LOOPBACK = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']);
10
+ const DASHBOARD_COOKIE = 'evolver_dashboard';
6
11
  /** The empty value summary (zero entries) — the shape /api/value returns when no provider is wired, so the card
7
12
  * always gets a valid ValueSummary to render. Derived from core's aggregator to stay shape-identical. */
8
13
  const EMPTY_VALUE_SUMMARY = ops.valueSummary([]);
14
+ const MEMORY_GRAPH_REASON_PATTERN = /scoped memory-graph outcome ([+-]\d+\.\d{3}) \(boost=([+-]?\d+\.\d{2})\)/;
15
+ const MEMORY_GRAPH_RECOVERY_STATES = new Set(['healthy', 'degraded', 'recovered', 'empty']);
16
+ function boundedMemoryGraphCount(value) {
17
+ const count = Number(value);
18
+ return Number.isFinite(count) && count > 0 ? Math.min(1_000_000, Math.floor(count)) : 0;
19
+ }
20
+ function sanitizeMemoryGraphReason(value) {
21
+ if (typeof value !== 'string')
22
+ return undefined;
23
+ const match = MEMORY_GRAPH_REASON_PATTERN.exec(value);
24
+ if (!match?.[1] || !match[2])
25
+ return undefined;
26
+ const outcome = Number(match[1]);
27
+ const boost = Number(match[2]);
28
+ if (!Number.isFinite(outcome) || !Number.isFinite(boost) || Math.abs(outcome) > 1 || Math.abs(boost) > 1)
29
+ return undefined;
30
+ return `scoped memory-graph outcome ${match[1]} (boost=${match[2]})`;
31
+ }
32
+ function sanitizeMemoryGraphStatus(value) {
33
+ const raw = value;
34
+ const recovery = MEMORY_GRAPH_RECOVERY_STATES.has(raw['recovery'])
35
+ ? raw['recovery']
36
+ : 'degraded';
37
+ const selectionReason = sanitizeMemoryGraphReason(raw['selectionReason']);
38
+ return {
39
+ recovery,
40
+ compactedRecords: boundedMemoryGraphCount(raw['compactedRecords']),
41
+ activeRecords: boundedMemoryGraphCount(raw['activeRecords']),
42
+ corruptLines: boundedMemoryGraphCount(raw['corruptLines']),
43
+ oversizedLines: boundedMemoryGraphCount(raw['oversizedLines']),
44
+ oversizedFiles: boundedMemoryGraphCount(raw['oversizedFiles']),
45
+ archives: boundedMemoryGraphCount(raw['archives']),
46
+ ...(selectionReason ? { selectionReason } : {}),
47
+ };
48
+ }
9
49
  /** Constant-time token compare (avoids leaking the token via timing). */
10
50
  function tokenEq(a, b) {
11
51
  const ba = Buffer.from(a), bb = Buffer.from(b);
12
52
  return ba.length === bb.length && timingSafeEqual(ba, bb);
13
53
  }
54
+ function cookieValue(header, name) {
55
+ for (const part of (header ?? '').split(';')) {
56
+ const at = part.indexOf('=');
57
+ if (at < 0 || part.slice(0, at).trim() !== name)
58
+ continue;
59
+ return part.slice(at + 1).trim();
60
+ }
61
+ return '';
62
+ }
14
63
  function positiveIntParam(value) {
15
64
  if (value === null || value.trim() === '')
16
65
  return undefined;
17
66
  const n = Number(value);
18
67
  return Number.isFinite(n) && n > 0 ? Math.floor(n) : undefined;
19
68
  }
69
+ function requireGet(req, res) {
70
+ if (req.method === 'GET')
71
+ return true;
72
+ res.writeHead(405, { allow: 'GET', 'content-type': 'application/json' });
73
+ res.end(JSON.stringify({ error: 'method_not_allowed' }));
74
+ return false;
75
+ }
76
+ const STABLE_WORKFLOW_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
77
+ function boundedString(value, maxLength = 256) {
78
+ return typeof value === 'string' ? value.slice(0, maxLength) : '';
79
+ }
80
+ function safeRunSummary(value) {
81
+ const runId = boundedString(value.runId, 128);
82
+ const workflowId = boundedString(value.workflowId, 128);
83
+ if (!STABLE_WORKFLOW_ID.test(runId) || !STABLE_WORKFLOW_ID.test(workflowId))
84
+ return null;
85
+ const currentStep = value.currentStep === null ? null : boundedString(value.currentStep);
86
+ const completedAt = value.completedAt === null ? null : boundedString(value.completedAt, 64);
87
+ return {
88
+ runId,
89
+ workflowId,
90
+ status: boundedString(value.status, 64),
91
+ currentStep,
92
+ createdAt: boundedString(value.createdAt, 64),
93
+ updatedAt: boundedString(value.updatedAt, 64),
94
+ completedAt,
95
+ };
96
+ }
97
+ function safeHistoryEntry(value) {
98
+ if (!Number.isSafeInteger(value.sequence) || value.sequence < 0)
99
+ return null;
100
+ const optional = (candidate, maxLength = 256) => {
101
+ if (candidate === undefined)
102
+ return undefined;
103
+ if (candidate === null)
104
+ return null;
105
+ return boundedString(candidate, maxLength);
106
+ };
107
+ return {
108
+ sequence: value.sequence,
109
+ timestamp: boundedString(value.timestamp, 64),
110
+ type: boundedString(value.type, 128),
111
+ ...(value.status !== undefined ? { status: optional(value.status, 64) } : {}),
112
+ ...(value.stepId !== undefined ? { stepId: optional(value.stepId) } : {}),
113
+ ...(value.executionId !== undefined ? { executionId: optional(value.executionId) } : {}),
114
+ ...(value.gateId !== undefined ? { gateId: optional(value.gateId) } : {}),
115
+ ...(value.attempt !== undefined && (value.attempt === null || (Number.isSafeInteger(value.attempt) && value.attempt >= 0))
116
+ ? { attempt: value.attempt }
117
+ : {}),
118
+ ...(value.actorId !== undefined ? { actorId: optional(value.actorId, 128) } : {}),
119
+ ...(value.errorClass !== undefined ? { errorClass: optional(value.errorClass, 64) } : {}),
120
+ };
121
+ }
20
122
  const HUMAN_ACTIONS = {
21
123
  observe: 'actor.human.observe', nudge: 'actor.human.nudge', intervene: 'actor.human.intervene', teach: 'actor.human.teach',
22
124
  };
@@ -33,62 +135,229 @@ export class WebUIServer {
33
135
  actorId;
34
136
  /** Review ledger backing the human-review queue. Undefined when no LocalJsonlProvider store is available. */
35
137
  review;
36
- /** Token guarding /api/*; printed by the launcher, supplied by the browser via ?token= or Bearer. */
138
+ provenance;
139
+ /** Token guarding /api/*; supplied by the browser via Bearer, with ?token= retained for compatibility. */
37
140
  token;
141
+ launchTicket;
142
+ launchTicketAvailable = true;
143
+ eventSnapshots;
38
144
  constructor(deps) {
39
145
  this.deps = deps;
40
146
  this.host = deps.host ?? '127.0.0.1';
41
147
  this.now = deps.now ?? (() => Date.now());
42
148
  this.actorId = deps.actorId ?? 'console';
43
149
  this.token = deps.token ?? randomBytes(16).toString('hex');
150
+ this.launchTicket = deps.launchTicket ?? randomBytes(16).toString('hex');
151
+ this.eventSnapshots = new EventSnapshotCache(deps.eventSource ?? fileEventSnapshotSource(deps.eventsPath));
44
152
  this.ingestor = deps.ingestor ?? new ev.Ingestor({ path: deps.eventsPath });
45
153
  // Co-locate the review ledger with the store so the queue reads the same review.jsonl the CLI writes.
46
154
  this.review = deps.review ?? (deps.store instanceof assetstore.LocalJsonlProvider ? new assetstore.ReviewLedger(deps.store.baseDir) : undefined);
155
+ this.provenance = deps.provenance ?? (deps.store instanceof assetstore.LocalJsonlProvider ? new assetstore.ProvenanceStore(deps.store.baseDir) : undefined);
47
156
  this.server = createServer((req, res) => { void this.handle(req, res); });
48
157
  }
49
- listen(port = 0) {
50
- return new Promise((resolve) => this.server.listen(port, this.host, () => {
51
- const a = this.server.address();
52
- resolve(a && typeof a === 'object' ? a.port : port);
53
- }));
158
+ async listen(port = 0) {
159
+ if (port !== 0 && util.isFetchForbiddenPort(port))
160
+ throw new Error('webui_port_blocked');
161
+ for (let attempt = 0; attempt < 5; attempt += 1) {
162
+ const assigned = await this.listenOnce(port);
163
+ if (port !== 0 || !util.isFetchForbiddenPort(assigned))
164
+ return assigned;
165
+ await this.close();
166
+ }
167
+ throw new Error('webui_safe_port_unavailable');
168
+ }
169
+ listenOnce(port) {
170
+ return new Promise((resolve, reject) => {
171
+ const onError = (error) => reject(error);
172
+ this.server.once('error', onError);
173
+ this.server.listen(port, this.host, () => {
174
+ this.server.removeListener('error', onError);
175
+ const a = this.server.address();
176
+ resolve(a && typeof a === 'object' ? a.port : port);
177
+ });
178
+ });
179
+ }
180
+ close() {
181
+ return new Promise((resolve, reject) => {
182
+ this.server.close((error) => (error ? reject(error) : resolve()));
183
+ this.server.closeAllConnections();
184
+ });
54
185
  }
55
- close() { return new Promise((res, rej) => this.server.close((e) => (e ? rej(e) : res()))); }
56
186
  async handle(req, res) {
57
187
  try {
58
188
  if (!LOOPBACK.has(req.socket.remoteAddress ?? ''))
59
189
  return this.send(res, 403, 'text/plain', 'non-loopback');
60
190
  const url = new URL(req.url ?? '/', 'http://localhost');
61
191
  const p = url.pathname;
192
+ if (p === '/launch') {
193
+ if (req.method !== 'GET') {
194
+ res.writeHead(405, { allow: 'GET', 'content-type': 'text/plain' });
195
+ res.end('method not allowed');
196
+ return;
197
+ }
198
+ const ticket = url.searchParams.get('ticket') ?? '';
199
+ if (!this.launchTicketAvailable || !tokenEq(ticket, this.launchTicket)) {
200
+ return this.send(res, 401, 'text/plain', 'unauthorized');
201
+ }
202
+ this.launchTicketAvailable = false;
203
+ res.writeHead(302, {
204
+ location: '/',
205
+ 'set-cookie': `${DASHBOARD_COOKIE}=${this.token}; HttpOnly; SameSite=Strict; Path=/`,
206
+ 'cache-control': 'no-store',
207
+ 'referrer-policy': 'no-referrer',
208
+ });
209
+ res.end();
210
+ return;
211
+ }
62
212
  // Root HTML is a static shell (no data) → served freely; it reads ?token= and authenticates the /api calls.
63
213
  if (p === '/' || p === '/index.html')
64
214
  return this.send(res, 200, 'text/html; charset=utf-8', CONSOLE_HTML);
65
215
  // Everything else (all /api/*) requires the token — accepted via Bearer header or ?token= (browser convenience).
66
216
  const auth = req.headers['authorization'] ?? '';
67
- const supplied = auth.startsWith('Bearer ') ? auth.slice(7) : (url.searchParams.get('token') ?? '');
68
- if (!tokenEq(supplied, this.token))
217
+ const bearer = auth.startsWith('Bearer ') ? auth.slice(7) : '';
218
+ const queryToken = url.searchParams.get('token') ?? '';
219
+ const cookieToken = cookieValue(req.headers.cookie, DASHBOARD_COOKIE);
220
+ const bearerValid = tokenEq(bearer, this.token);
221
+ const queryValid = tokenEq(queryToken, this.token);
222
+ const cookieValid = tokenEq(cookieToken, this.token);
223
+ if (!bearerValid && !queryValid && !cookieValid) {
69
224
  return this.send(res, 401, 'application/json', JSON.stringify({ error: 'unauthorized' }));
225
+ }
226
+ const stateChanging = req.method !== 'GET' && req.method !== 'HEAD';
227
+ if (stateChanging && cookieValid && !bearerValid && !queryValid) {
228
+ const expectedOrigin = req.headers.host ? `http://${req.headers.host}` : '';
229
+ if (!expectedOrigin || req.headers.origin !== expectedOrigin) {
230
+ return this.send(res, 403, 'application/json', JSON.stringify({ error: 'same_origin_required' }));
231
+ }
232
+ if (!(req.headers['content-type'] ?? '').toLowerCase().startsWith('application/json')) {
233
+ return this.send(res, 415, 'application/json', JSON.stringify({ error: 'json_required' }));
234
+ }
235
+ }
70
236
  if (p === '/api/status')
71
- return this.json(res, ev.statusReport(ev.readEvents(this.deps.eventsPath)));
237
+ return this.json(res, ev.statusReport(await this.eventSnapshots.read()));
72
238
  if (p === '/api/cycles')
73
- return this.json(res, ev.listCycles(ev.readEvents(this.deps.eventsPath)));
74
- if (p === '/api/cycle')
75
- return this.json(res, ev.showCycle(ev.readEvents(this.deps.eventsPath), url.searchParams.get('id') ?? ''));
239
+ return this.json(res, ev.listCycles(await this.eventSnapshots.read()));
240
+ if (p === '/api/cycle') {
241
+ const cycle = ev.showCycle(await this.eventSnapshots.read(), url.searchParams.get('id') ?? '');
242
+ const safeTimeline = cycle.timeline.map((event) => ({
243
+ ...event,
244
+ title: redactDiagnosticText(event.title),
245
+ ...(event.why ? { why: redactDiagnosticText(event.why) } : {}),
246
+ ...(event.payload ? { payload: sanitizeDiagnosticValue(event.payload) } : {}),
247
+ }));
248
+ const relationEvents = cycle.timeline.map((event) => ({
249
+ seq: event.seq, type: event.type, ts: event.ts, payload: event.payload,
250
+ human: { title: event.title, ...(event.why ? { why: event.why } : {}) },
251
+ }));
252
+ return this.json(res, { ...cycle, timeline: safeTimeline, relations: eventListRelations(relationEvents) });
253
+ }
76
254
  if (p === '/api/narrative') {
77
- return this.json(res, ev.buildNarrativeSnapshot(ev.readEvents(this.deps.eventsPath), { limit: positiveIntParam(url.searchParams.get('limit')) }));
255
+ return this.json(res, ev.buildNarrativeSnapshot(await this.eventSnapshots.read(), { limit: positiveIntParam(url.searchParams.get('limit')) }));
78
256
  }
79
257
  if (p === '/api/triggers')
80
- return this.json(res, ev.listTriggers(ev.readEvents(this.deps.eventsPath)));
258
+ return this.json(res, ev.listTriggers(await this.eventSnapshots.read()));
81
259
  if (p === '/api/daily-summary') {
82
260
  const day = url.searchParams.get('day') ?? new Date(this.now()).toISOString().slice(0, 10);
83
- return this.json(res, ev.dailySummary(ev.readEvents(this.deps.eventsPath), day));
261
+ return this.json(res, ev.dailySummary(await this.eventSnapshots.read(), day));
84
262
  }
85
263
  if (p === '/api/value') {
86
264
  // Thin pass-through: the provider (composition layer) owns prices + traces; the server only scopes the
87
265
  // window and serializes. No provider wired → an empty summary so the card renders "no savings yet".
88
266
  const window = ops.windowFromSpec(url.searchParams.get('window') ?? undefined, this.now());
89
- const summary = this.deps.valueSummary ? this.deps.valueSummary(window) : EMPTY_VALUE_SUMMARY;
267
+ const summary = this.deps.valueSummary ? this.deps.valueSummary(window, await this.eventSnapshots.read()) : EMPTY_VALUE_SUMMARY;
90
268
  return this.json(res, summary);
91
269
  }
270
+ if (p === '/api/personality') {
271
+ if (!requireGet(req, res))
272
+ return;
273
+ return this.json(res, this.deps.personalityDiagnostics
274
+ ? await this.deps.personalityDiagnostics()
275
+ : { available: false, error: 'personality_unavailable' });
276
+ }
277
+ if (p === '/api/memory-graph') {
278
+ if (!requireGet(req, res))
279
+ return;
280
+ if (!this.deps.memoryGraphStatus)
281
+ return this.json(res, { available: false });
282
+ try {
283
+ const status = sanitizeMemoryGraphStatus(this.deps.memoryGraphStatus());
284
+ return this.json(res, { available: true, ...status });
285
+ }
286
+ catch {
287
+ return this.send(res, 503, 'application/json', JSON.stringify({
288
+ available: false,
289
+ error: 'memory_graph_unavailable',
290
+ }));
291
+ }
292
+ }
293
+ if (p === '/api/logs') {
294
+ if (!requireGet(req, res))
295
+ return;
296
+ return this.json(res, this.deps.logDiagnostics
297
+ ? await this.deps.logDiagnostics()
298
+ : { available: false, error: 'logs_unavailable' });
299
+ }
300
+ if (p === '/api/github-prs') {
301
+ if (!requireGet(req, res))
302
+ return;
303
+ return this.json(res, this.deps.githubPrDiagnostics
304
+ ? await this.deps.githubPrDiagnostics()
305
+ : { available: false, error: 'github_prs_unavailable' });
306
+ }
307
+ if (p === '/api/retention') {
308
+ if (!this.deps.retentionReport)
309
+ return this.json(res, { available: false });
310
+ try {
311
+ return this.json(res, { ...this.deps.retentionReport(), available: true });
312
+ }
313
+ catch {
314
+ return this.send(res, 503, 'application/json', JSON.stringify({
315
+ available: false,
316
+ error: 'retention_unavailable',
317
+ }));
318
+ }
319
+ }
320
+ if (p === '/api/workflows') {
321
+ if (req.method !== 'GET')
322
+ return this.methodNotAllowed(res, 'GET');
323
+ if (!this.deps.workflow)
324
+ return this.json(res, { workflows: [] });
325
+ try {
326
+ const workflows = (await this.deps.workflow.listRuns())
327
+ .map(safeRunSummary)
328
+ .filter((run) => run !== null);
329
+ return this.json(res, { workflows });
330
+ }
331
+ catch {
332
+ return this.apiError(res, 503, 'workflow_unavailable');
333
+ }
334
+ }
335
+ if (p === '/api/workflow' || p === '/api/workflow/history') {
336
+ if (req.method !== 'GET')
337
+ return this.methodNotAllowed(res, 'GET');
338
+ const runId = url.searchParams.get('id') ?? '';
339
+ if (!STABLE_WORKFLOW_ID.test(runId))
340
+ return this.apiError(res, 400, 'invalid_workflow_id');
341
+ if (!this.deps.workflow)
342
+ return this.apiError(res, 503, 'workflow_unavailable');
343
+ try {
344
+ if (p === '/api/workflow') {
345
+ const candidate = await this.deps.workflow.getRun(runId);
346
+ const run = candidate === null ? null : safeRunSummary(candidate);
347
+ return run ? this.json(res, { workflow: run }) : this.apiError(res, 404, 'workflow_not_found');
348
+ }
349
+ const history = await this.deps.workflow.getHistory(runId);
350
+ if (history === null)
351
+ return this.apiError(res, 404, 'workflow_not_found');
352
+ return this.json(res, {
353
+ runId,
354
+ history: history.map(safeHistoryEntry).filter((entry) => entry !== null),
355
+ });
356
+ }
357
+ catch {
358
+ return this.apiError(res, 503, 'workflow_unavailable');
359
+ }
360
+ }
92
361
  if (p === '/api/mailbox') {
93
362
  if (!this.deps.mailbox)
94
363
  return this.json(res, { pending: 0, dlq: 0, messages: [] });
@@ -104,6 +373,31 @@ export class WebUIServer {
104
373
  const kind = url.searchParams.get('kind');
105
374
  return this.json(res, (await this.deps.store.list(kind ?? undefined, 100)).map((a) => ({ asset_id: a.asset_id, type: a.type, summary: a.summary })));
106
375
  }
376
+ if (p === '/api/asset-lineage/assets') {
377
+ if (!requireGet(req, res))
378
+ return;
379
+ return this.json(res, await listLineageAssets(this.deps.store, {
380
+ page: positiveIntParam(url.searchParams.get('page')),
381
+ pageSize: positiveIntParam(url.searchParams.get('pageSize')),
382
+ }));
383
+ }
384
+ if (p === '/api/asset-lineage') {
385
+ if (!requireGet(req, res))
386
+ return;
387
+ return this.json(res, await loadAssetLineage({
388
+ store: this.deps.store,
389
+ events: () => this.eventSnapshots.read(),
390
+ review: this.review,
391
+ provenance: this.provenance,
392
+ }, url.searchParams.get('id') ?? '', {
393
+ page: positiveIntParam(url.searchParams.get('page')),
394
+ pageSize: positiveIntParam(url.searchParams.get('pageSize')),
395
+ capsulePage: positiveIntParam(url.searchParams.get('capsulePage')),
396
+ capsulePageSize: positiveIntParam(url.searchParams.get('capsulePageSize')),
397
+ eventPage: positiveIntParam(url.searchParams.get('eventPage')),
398
+ eventPageSize: positiveIntParam(url.searchParams.get('eventPageSize')),
399
+ }));
400
+ }
107
401
  if (p === '/api/review' && req.method === 'POST') {
108
402
  // Approve/reject a quarantined draft from the console: the SAME gate the CLI uses — flip the ReviewLedger
109
403
  // state + record an audited actor.human.review.* event. Loopback + bearer already guard the route; actor is
@@ -143,7 +437,7 @@ export class WebUIServer {
143
437
  // record) are appended as context from a bounded list. Pending (quarantined) sorts first. Read-only here.
144
438
  // Match the CLI's semantics exactly: only the unattended distill-observer counts as "auto-drafted"
145
439
  // (manual `ingest --distill` / skill2gep emit gene.distilled with a different source and are NOT auto).
146
- const autoDrafted = new Set(this.ingestor.readAll()
440
+ const autoDrafted = new Set((await this.eventSnapshots.read())
147
441
  .filter((e) => e.type === 'gene.distilled' && e.payload?.['source'] === 'distill-observer')
148
442
  .map((e) => String(e.payload?.['assetId'] ?? ''))
149
443
  .filter(Boolean));
@@ -178,8 +472,8 @@ export class WebUIServer {
178
472
  }
179
473
  return this.send(res, 404, 'application/json', JSON.stringify({ error: 'not found' }));
180
474
  }
181
- catch (e) {
182
- return this.send(res, 500, 'application/json', JSON.stringify({ error: e instanceof Error ? e.message : String(e) }));
475
+ catch {
476
+ return this.send(res, 500, 'application/json', JSON.stringify({ error: 'dashboard_request_failed' }));
183
477
  }
184
478
  }
185
479
  readJson(req) {
@@ -200,5 +494,10 @@ export class WebUIServer {
200
494
  });
201
495
  }
202
496
  json(res, body) { this.send(res, 200, 'application/json', JSON.stringify(body)); }
497
+ apiError(res, code, error) { this.send(res, code, 'application/json', JSON.stringify({ error })); }
498
+ methodNotAllowed(res, allow) {
499
+ res.writeHead(405, { allow, 'content-type': 'application/json' });
500
+ res.end(JSON.stringify({ error: 'method_not_allowed' }));
501
+ }
203
502
  send(res, code, ct, body) { res.writeHead(code, { 'content-type': ct }); res.end(body); }
204
503
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evomap/evolver-webui",
3
- "version": "2.0.0-beta.1",
3
+ "version": "2.0.0-beta.10",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "保活/可视化 WebUI",
@@ -13,7 +13,11 @@
13
13
  }
14
14
  },
15
15
  "dependencies": {
16
- "@evomap/evolver-core": "2.0.0-beta.1"
16
+ "@evomap/evolver-core": "2.0.0-beta.10"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/EvoMap/evolver.git"
17
21
  },
18
22
  "publishConfig": {
19
23
  "access": "public",
@@ -21,6 +25,7 @@
21
25
  },
22
26
  "files": [
23
27
  "dist/",
28
+ "assets/",
24
29
  "README.md",
25
30
  "package.json"
26
31
  ]