ccpool-server 0.0.1 → 0.0.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.
Files changed (2) hide show
  1. package/dist/index.js +204 -108
  2. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -20,7 +20,7 @@ function isValidName(name) {
20
20
  }
21
21
 
22
22
  // ../../packages/core/dist/storage/storage.js
23
- var SCHEMA_VERSION = 1;
23
+ var SCHEMA_VERSION = 2;
24
24
  var DEFAULT_GROUP_ID = "default";
25
25
  function isEmptyBatch(b) {
26
26
  return b.samples.length === 0 && b.resets.length === 0 && b.messages.length === 0 && b.markers.length === 0;
@@ -57,19 +57,42 @@ function attributeShares(samples, messages, now = Date.now(), resets = [], marke
57
57
  const capSamples = samples.filter((s) => s.cap === cap).map((s) => ({ t: Date.parse(s.capturedAt), pct: s.pct })).filter((s) => Number.isFinite(s.t)).sort((a, b) => a.t - b.t);
58
58
  if (capSamples.length === 0)
59
59
  continue;
60
- const resetTimes = resets.filter((r) => r.cap === cap).map((r) => Date.parse(r.at)).filter((t) => Number.isFinite(t) && t <= now);
61
- out.push(...attributeCap(cap, capSamples, msgs, mks, now, resetTimes));
60
+ const capResets = resets.filter((r) => r.cap === cap).map((r) => ({ t: Date.parse(r.at), previousPct: r.previousPct })).filter((r) => Number.isFinite(r.t) && r.t <= now);
61
+ out.push(...attributeCap(cap, capSamples, msgs, mks, now, capResets));
62
62
  }
63
63
  return out;
64
64
  }
65
- function attributeCap(cap, capSamples, msgs, markers, now, resetTimes) {
65
+ var RESET_DROP_MIN_PCT = 2;
66
+ function resolveResetBoundary(capSamples, resetEvents) {
67
+ let boundary = -Infinity;
68
+ for (const e of resetEvents) {
69
+ if (e.t <= boundary)
70
+ continue;
71
+ let before = null;
72
+ let after = null;
73
+ for (const s of capSamples) {
74
+ if (s.t < e.t)
75
+ before = s.pct;
76
+ else {
77
+ after = s.pct;
78
+ break;
79
+ }
80
+ }
81
+ const beforeLevel = before ?? e.previousPct;
82
+ const corroborated = after === null || beforeLevel - after > RESET_DROP_MIN_PCT;
83
+ if (corroborated)
84
+ boundary = e.t;
85
+ }
86
+ return boundary;
87
+ }
88
+ function attributeCap(cap, capSamples, msgs, markers, now, resetEvents) {
66
89
  const cutoff = now - CAP_WINDOW_MS[cap];
67
90
  let start = 0;
68
91
  for (let i = 1; i < capSamples.length; i++) {
69
92
  if (capSamples[i].t < cutoff)
70
93
  start = i;
71
94
  }
72
- const lastReset = resetTimes.length ? Math.max(...resetTimes) : -Infinity;
95
+ const lastReset = resolveResetBoundary(capSamples, resetEvents);
73
96
  if (lastReset > -Infinity) {
74
97
  let firstAfter = capSamples.length - 1;
75
98
  for (let i = 0; i < capSamples.length; i++) {
@@ -226,10 +249,104 @@ var AccountConflictError = class extends Error {
226
249
  // ../../packages/core/dist/remote/api.js
227
250
  var MIN_PASSWORD_LENGTH = 8;
228
251
 
229
- // ../../packages/core/dist/backend/storage.js
230
- var DEFAULT_PRUNE_INTERVAL_MS = 6 * 36e5;
252
+ // ../../packages/core/dist/backend/finalizer.js
231
253
  var DEFAULT_GRACE_MS = 30 * 6e4;
232
254
  var DEFAULT_FINALIZE_INTERVAL_MS = 6e4;
255
+ var HistoryFinalizer = class {
256
+ storage;
257
+ graceMs;
258
+ finalizeIntervalMs;
259
+ retentionMs;
260
+ now;
261
+ lastFinalizeMs = 0;
262
+ /** `${cap} ${windowStart}` of windows already frozen — skip recompute. */
263
+ frozen = /* @__PURE__ */ new Set();
264
+ constructor(storage, opts = {}) {
265
+ this.storage = storage;
266
+ this.graceMs = opts.graceMs ?? DEFAULT_GRACE_MS;
267
+ this.finalizeIntervalMs = opts.finalizeIntervalMs ?? DEFAULT_FINALIZE_INTERVAL_MS;
268
+ this.retentionMs = opts.retentionMs ?? RETENTION_MS;
269
+ this.now = opts.now ?? Date.now;
270
+ }
271
+ /**
272
+ * Freeze any window whose closing reset is now past the grace period. Throttled.
273
+ * A window spans two consecutive resets of the cap (the first opens at the
274
+ * earliest retained sample); shares come from the same `attributeShares` the live
275
+ * view uses.
276
+ */
277
+ async maybeFinalize(now = this.now()) {
278
+ if (now - this.lastFinalizeMs < this.finalizeIntervalMs)
279
+ return;
280
+ this.lastFinalizeMs = now;
281
+ const since = new Date(now - this.retentionMs).toISOString();
282
+ const resets = await this.storage.getResetsSince(since);
283
+ if (resets.length === 0)
284
+ return;
285
+ const capResets = /* @__PURE__ */ new Map();
286
+ for (const r of resets) {
287
+ const t = Date.parse(r.at);
288
+ if (!Number.isFinite(t))
289
+ continue;
290
+ const arr = capResets.get(r.cap);
291
+ if (arr)
292
+ arr.push(t);
293
+ else
294
+ capResets.set(r.cap, [t]);
295
+ }
296
+ let samples = null;
297
+ let messages = null;
298
+ let markers = null;
299
+ for (const [cap, times] of capResets) {
300
+ times.sort((a, b) => a - b);
301
+ for (let i = 0; i < times.length; i++) {
302
+ const endMs = times[i];
303
+ if (endMs + this.graceMs > now)
304
+ continue;
305
+ if (samples === null) {
306
+ [samples, messages, markers] = await Promise.all([
307
+ this.storage.getUsageSamplesSince(since),
308
+ this.storage.getMessageUsageSince(since),
309
+ this.storage.getUsageMarkersSince(since).catch(() => [])
310
+ ]);
311
+ }
312
+ const capSamples = samples.filter((s) => s.cap === cap);
313
+ const startMs = i > 0 ? times[i - 1] : Math.min(...capSamples.map((s) => Date.parse(s.capturedAt)).filter(Number.isFinite));
314
+ if (!Number.isFinite(startMs) || startMs >= endMs)
315
+ continue;
316
+ const windowStart = new Date(startMs).toISOString();
317
+ const key = `${cap} ${windowStart}`;
318
+ if (this.frozen.has(key))
319
+ continue;
320
+ const windowEnd = new Date(endMs).toISOString();
321
+ const inWin = (iso) => {
322
+ const t = Date.parse(iso);
323
+ return t >= startMs && t < endMs;
324
+ };
325
+ const winSamples = capSamples.filter((s) => inWin(s.capturedAt));
326
+ if (winSamples.length === 0) {
327
+ this.frozen.add(key);
328
+ continue;
329
+ }
330
+ const winMsgs = messages.filter((m) => inWin(m.timestamp));
331
+ const winMarkers = markers.filter((m) => inWin(m.at));
332
+ const openingReset = [{ cap, at: windowStart, previousPct: 0 }];
333
+ const shares = attributeShares(winSamples, winMsgs, endMs, openingReset, winMarkers).filter((sh) => sh.cap === cap);
334
+ const overall = Math.max(...winSamples.map((s) => s.pct));
335
+ const historyShares = shares.map((sh) => ({
336
+ cap,
337
+ windowStart,
338
+ user: sh.user,
339
+ pct: sh.pct
340
+ }));
341
+ await this.storage.recordHistoryWindow({ cap, windowStart, windowEnd, overall, closedAt: new Date(now).toISOString() }, historyShares);
342
+ this.frozen.add(key);
343
+ }
344
+ }
345
+ }
346
+ };
347
+
348
+ // ../../packages/core/dist/backend/storage.js
349
+ var DEFAULT_PRUNE_INTERVAL_MS = 6 * 36e5;
233
350
  var EnvelopeFilter = class {
234
351
  envMax = /* @__PURE__ */ new Map();
235
352
  winStart = /* @__PURE__ */ new Map();
@@ -269,22 +386,22 @@ var StorageIngestSink = class {
269
386
  lastPruneMs;
270
387
  retentionMs;
271
388
  pruneIntervalMs;
272
- graceMs;
273
- finalizeIntervalMs;
274
- lastFinalizeMs = 0;
275
- /** `${cap} ${windowStart}` of windows already frozen — skip recompute. */
276
- frozen = /* @__PURE__ */ new Set();
277
389
  now;
278
390
  window;
391
+ finalizer;
279
392
  envelope = new EnvelopeFilter();
280
393
  constructor(storage, opts = {}) {
281
394
  this.storage = storage;
282
395
  this.retentionMs = opts.retentionMs ?? RETENTION_MS;
283
396
  this.pruneIntervalMs = opts.pruneIntervalMs ?? DEFAULT_PRUNE_INTERVAL_MS;
284
- this.graceMs = opts.graceMs ?? DEFAULT_GRACE_MS;
285
- this.finalizeIntervalMs = opts.finalizeIntervalMs ?? DEFAULT_FINALIZE_INTERVAL_MS;
286
397
  this.now = opts.now ?? Date.now;
287
398
  this.window = opts.window;
399
+ this.finalizer = opts.finalizer ?? new HistoryFinalizer(storage, {
400
+ graceMs: opts.graceMs,
401
+ finalizeIntervalMs: opts.finalizeIntervalMs,
402
+ retentionMs: opts.retentionMs,
403
+ now: opts.now
404
+ });
288
405
  this.lastPruneMs = this.now();
289
406
  }
290
407
  /**
@@ -316,7 +433,7 @@ var StorageIngestSink = class {
316
433
  this.window?.append(reduced);
317
434
  }
318
435
  const now = this.now();
319
- await this.maybeFinalize(now);
436
+ await this.finalizer.maybeFinalize(now);
320
437
  if (now - this.lastPruneMs >= this.pruneIntervalMs) {
321
438
  this.lastPruneMs = now;
322
439
  const before = new Date(now - this.retentionMs).toISOString();
@@ -324,82 +441,6 @@ var StorageIngestSink = class {
324
441
  this.window?.applyPrune(before);
325
442
  }
326
443
  }
327
- /**
328
- * Freeze any window whose closing reset is now past the grace period into an
329
- * immutable {@link HistoryWindow} + shares (ADR-0002/0008). Throttled (grace is 30
330
- * min). A window spans two consecutive resets of the cap (the first opens at the
331
- * earliest retained sample); shares come from the same `attributeShares` the live
332
- * view uses. `recordHistoryWindow` is idempotent and `frozen` skips recompute.
333
- */
334
- async maybeFinalize(now) {
335
- if (now - this.lastFinalizeMs < this.finalizeIntervalMs)
336
- return;
337
- this.lastFinalizeMs = now;
338
- const since = new Date(now - this.retentionMs).toISOString();
339
- const resets = await this.storage.getResetsSince(since);
340
- if (resets.length === 0)
341
- return;
342
- const capResets = /* @__PURE__ */ new Map();
343
- for (const r of resets) {
344
- const t = Date.parse(r.at);
345
- if (!Number.isFinite(t))
346
- continue;
347
- const arr = capResets.get(r.cap);
348
- if (arr)
349
- arr.push(t);
350
- else
351
- capResets.set(r.cap, [t]);
352
- }
353
- let samples = null;
354
- let messages = null;
355
- let markers = null;
356
- for (const [cap, times] of capResets) {
357
- times.sort((a, b) => a - b);
358
- for (let i = 0; i < times.length; i++) {
359
- const endMs = times[i];
360
- if (endMs + this.graceMs > now)
361
- continue;
362
- if (samples === null) {
363
- [samples, messages, markers] = await Promise.all([
364
- this.storage.getUsageSamplesSince(since),
365
- this.storage.getMessageUsageSince(since),
366
- this.storage.getUsageMarkersSince(since).catch(() => [])
367
- ]);
368
- }
369
- const capSamples = samples.filter((s) => s.cap === cap);
370
- const startMs = i > 0 ? times[i - 1] : Math.min(...capSamples.map((s) => Date.parse(s.capturedAt)).filter(Number.isFinite));
371
- if (!Number.isFinite(startMs) || startMs >= endMs)
372
- continue;
373
- const windowStart = new Date(startMs).toISOString();
374
- const key = `${cap} ${windowStart}`;
375
- if (this.frozen.has(key))
376
- continue;
377
- const windowEnd = new Date(endMs).toISOString();
378
- const inWin = (iso) => {
379
- const t = Date.parse(iso);
380
- return t >= startMs && t < endMs;
381
- };
382
- const winSamples = capSamples.filter((s) => inWin(s.capturedAt));
383
- if (winSamples.length === 0) {
384
- this.frozen.add(key);
385
- continue;
386
- }
387
- const winMsgs = messages.filter((m) => inWin(m.timestamp));
388
- const winMarkers = markers.filter((m) => inWin(m.at));
389
- const openingReset = [{ cap, at: windowStart, previousPct: 0 }];
390
- const shares = attributeShares(winSamples, winMsgs, endMs, openingReset, winMarkers).filter((sh) => sh.cap === cap);
391
- const overall = Math.max(...winSamples.map((s) => s.pct));
392
- const historyShares = shares.map((sh) => ({
393
- cap,
394
- windowStart,
395
- user: sh.user,
396
- pct: sh.pct
397
- }));
398
- await this.storage.recordHistoryWindow({ cap, windowStart, windowEnd, overall, closedAt: new Date(now).toISOString() }, historyShares);
399
- this.frozen.add(key);
400
- }
401
- }
402
- }
403
444
  async close() {
404
445
  await this.storage.close();
405
446
  }
@@ -422,16 +463,22 @@ var StorageViewSource = class {
422
463
  before: query.before,
423
464
  limit
424
465
  });
425
- const view = await Promise.all(windows.map(async (w) => ({
466
+ const shares = await this.storage.getHistorySharesForWindows(query.cap, windows.map((w) => w.windowStart));
467
+ const byWindow = /* @__PURE__ */ new Map();
468
+ for (const s of shares) {
469
+ const arr = byWindow.get(s.windowStart);
470
+ if (arr)
471
+ arr.push({ user: s.user, pct: s.pct });
472
+ else
473
+ byWindow.set(s.windowStart, [{ user: s.user, pct: s.pct }]);
474
+ }
475
+ const view = windows.map((w) => ({
426
476
  cap: w.cap,
427
477
  windowStart: w.windowStart,
428
478
  windowEnd: w.windowEnd,
429
479
  overall: w.overall,
430
- shares: (await this.storage.getHistoryShares(query.cap, w.windowStart)).map((s) => ({
431
- user: s.user,
432
- pct: s.pct
433
- }))
434
- })));
480
+ shares: byWindow.get(w.windowStart) ?? []
481
+ }));
435
482
  const nextBefore = windows.length === limit && windows.length > 0 ? windows[windows.length - 1].windowStart : null;
436
483
  return { windows: view, nextBefore };
437
484
  }
@@ -868,6 +915,8 @@ var MAX_INGEST_BYTES = 1024 * 1024;
868
915
  var TOKEN_TOUCH_INTERVAL_MS = 6e4;
869
916
  var TOKEN_CACHE_TTL_MS = 6e4;
870
917
  var TOKEN_CACHE_MAX = 2e4;
918
+ var TOKEN_STALE_MS = 90 * 24 * 36e5;
919
+ var TOKEN_SWEEP_INTERVAL_MS = 6 * 36e5;
871
920
  function err(c, status, code, error) {
872
921
  return c.json({ error, code }, status);
873
922
  }
@@ -897,6 +946,7 @@ function makeApp(deps2) {
897
946
  const damper = new FailureDamper();
898
947
  const lastTouch = /* @__PURE__ */ new Map();
899
948
  const tokenCache = /* @__PURE__ */ new Map();
949
+ let lastTokenSweep = 0;
900
950
  const damperKey = (accountId) => accountId;
901
951
  app2.get("/healthz", (c) => c.json({ ok: true }));
902
952
  app2.use("/v1/ingest", bearer);
@@ -925,10 +975,21 @@ function makeApp(deps2) {
925
975
  tokenCache.set(tokenHash, hit);
926
976
  }
927
977
  if (now - (lastTouch.get(tokenHash) ?? 0) >= TOKEN_TOUCH_INTERVAL_MS) {
978
+ lastTouch.delete(tokenHash);
928
979
  lastTouch.set(tokenHash, now);
980
+ if (lastTouch.size > TOKEN_CACHE_MAX) {
981
+ const oldest = lastTouch.keys().next().value;
982
+ if (oldest !== void 0) lastTouch.delete(oldest);
983
+ }
929
984
  await registry.touchToken(tokenHash).catch(() => {
930
985
  });
931
986
  }
987
+ if (now - lastTokenSweep >= TOKEN_SWEEP_INTERVAL_MS) {
988
+ lastTokenSweep = now;
989
+ const before = new Date(now - TOKEN_STALE_MS).toISOString();
990
+ void registry.deleteStaleTokens(before).catch(() => {
991
+ });
992
+ }
932
993
  c.set("member", hit.member);
933
994
  c.set("group", hit.group);
934
995
  await next();
@@ -969,7 +1030,7 @@ function makeApp(deps2) {
969
1030
  if (e instanceof RegistryConflictError) {
970
1031
  return err(c, 409, "conflict", "a group for this Claude account already exists \u2014 join it");
971
1032
  }
972
- return err(c, 500, "invalid", `could not provision the group: ${e.message}`);
1033
+ return err(c, 500, "server", `could not provision the group: ${e.message}`);
973
1034
  }
974
1035
  });
975
1036
  app2.post("/v1/groups/join", async (c) => {
@@ -1064,7 +1125,7 @@ function makeApp(deps2) {
1064
1125
  if (e instanceof AccountConflictError) {
1065
1126
  return err(c, 409, "account-conflict", e.message);
1066
1127
  }
1067
- return err(c, 500, "invalid", e.message);
1128
+ return err(c, 500, "server", e.message);
1068
1129
  }
1069
1130
  return c.body(null, 204);
1070
1131
  });
@@ -1078,6 +1139,8 @@ function makeApp(deps2) {
1078
1139
  const group = c.get("group");
1079
1140
  const tenant = await tenants.get(group);
1080
1141
  const now = Date.now();
1142
+ await tenant.finalizer.maybeFinalize(now).catch(() => {
1143
+ });
1081
1144
  const etag = `"${await tenant.view.currentKey(now)}"`;
1082
1145
  if (c.req.header("if-none-match") === etag) {
1083
1146
  return c.body(null, 304, { ETag: etag });
@@ -1092,6 +1155,8 @@ function makeApp(deps2) {
1092
1155
  const limitRaw = Number(c.req.query("limit"));
1093
1156
  const limit = Number.isFinite(limitRaw) && limitRaw > 0 ? Math.min(200, Math.floor(limitRaw)) : 50;
1094
1157
  const tenant = await tenants.get(c.get("group"));
1158
+ await tenant.finalizer.maybeFinalize(Date.now()).catch(() => {
1159
+ });
1095
1160
  const page = await tenant.view.history({ cap: capQ, before, limit });
1096
1161
  return c.json(page);
1097
1162
  });
@@ -1157,7 +1222,7 @@ var LEDGER_DDL = [
1157
1222
  )`,
1158
1223
  `CREATE INDEX IF NOT EXISTS idx_reset_events_at ON reset_events (group_id, at)`,
1159
1224
  `CREATE UNIQUE INDEX IF NOT EXISTS idx_reset_events_uniq ON reset_events (group_id, cap, at)`,
1160
- // Immutable history of completed cap cycles (ADR-0002/0005). Retained unbounded.
1225
+ // Immutable history of completed cap cycles. Retained unbounded.
1161
1226
  `CREATE TABLE IF NOT EXISTS history_windows (
1162
1227
  group_id TEXT NOT NULL,
1163
1228
  cap TEXT NOT NULL,
@@ -1269,6 +1334,21 @@ var LibsqlRegistry = class {
1269
1334
  args: [(/* @__PURE__ */ new Date()).toISOString(), tokenHash]
1270
1335
  });
1271
1336
  }
1337
+ /**
1338
+ * Bearer retention: delete every token last used (or, if never used, created)
1339
+ * before `before` (ISO 8601), returning how many were removed. A live daemon
1340
+ * touches its token ~once a minute, so a token untouched for the whole window is
1341
+ * genuinely abandoned — this bounds the `tokens` table without logging anyone
1342
+ * active out. `COALESCE(lastUsedAt, createdAt)` covers a token minted but never
1343
+ * used (login/rejoin that was thrown away).
1344
+ */
1345
+ async deleteStaleTokens(before) {
1346
+ const { rowsAffected } = await this.client.execute({
1347
+ sql: `DELETE FROM tokens WHERE COALESCE(lastUsedAt, createdAt) < ?`,
1348
+ args: [before]
1349
+ });
1350
+ return rowsAffected;
1351
+ }
1272
1352
  async createGroupWithMember(input) {
1273
1353
  const now = (/* @__PURE__ */ new Date()).toISOString();
1274
1354
  const group = {
@@ -1663,12 +1743,18 @@ var LibsqlStorage = class {
1663
1743
  WHERE group_id = ? AND cap = ? AND windowStart = ?`,
1664
1744
  args: [this.groupId, cap, windowStart]
1665
1745
  });
1666
- return rows.map((r) => ({
1667
- cap: r.cap,
1668
- windowStart: String(r.windowStart),
1669
- user: String(r.user),
1670
- pct: Number(r.pct)
1671
- }));
1746
+ return rows.map(toHistoryShare);
1747
+ }
1748
+ async getHistorySharesForWindows(cap, windowStarts) {
1749
+ if (windowStarts.length === 0)
1750
+ return [];
1751
+ const placeholders = windowStarts.map(() => "?").join(", ");
1752
+ const { rows } = await this.client.execute({
1753
+ sql: `SELECT cap, windowStart, user, pct FROM history_shares
1754
+ WHERE group_id = ? AND cap = ? AND windowStart IN (${placeholders})`,
1755
+ args: [this.groupId, cap, ...windowStarts]
1756
+ });
1757
+ return rows.map(toHistoryShare);
1672
1758
  }
1673
1759
  /** Every write batch ends with this so one tick bumps this group's token once. */
1674
1760
  bumpWriteSeq() {
@@ -1678,6 +1764,14 @@ var LibsqlStorage = class {
1678
1764
  };
1679
1765
  }
1680
1766
  };
1767
+ function toHistoryShare(r) {
1768
+ return {
1769
+ cap: r.cap,
1770
+ windowStart: String(r.windowStart),
1771
+ user: String(r.user),
1772
+ pct: Number(r.pct)
1773
+ };
1774
+ }
1681
1775
 
1682
1776
  // ../../packages/storage-libsql/dist/url.js
1683
1777
  import { mkdirSync } from "fs";
@@ -1769,10 +1863,12 @@ var TenantCache = class {
1769
1863
  }
1770
1864
  const storage = this.db.forGroup(group.id);
1771
1865
  const window = new LedgerWindow(storage);
1866
+ const finalizer = new HistoryFinalizer(storage);
1772
1867
  const entry = {
1773
1868
  tenant: {
1774
- sink: new StorageIngestSink(storage, { window }),
1775
- view: new StorageViewSource(storage, { window })
1869
+ sink: new StorageIngestSink(storage, { window, finalizer }),
1870
+ view: new StorageViewSource(storage, { window }),
1871
+ finalizer
1776
1872
  }
1777
1873
  };
1778
1874
  this.tenants.set(group.id, entry);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ccpool-server",
3
3
  "type": "module",
4
- "version": "0.0.1",
4
+ "version": "0.0.2",
5
5
  "description": "Multi-tenant HTTP server for ccpool (Hono + libSQL) — the one path to the shared ledger. Self-host it to run your own group.",
6
6
  "keywords": [
7
7
  "claude",
@@ -48,9 +48,9 @@
48
48
  "tsup": "^8.3.5",
49
49
  "tsx": "^4.19.2",
50
50
  "typescript": "^5.7.2",
51
- "@ccpool/core": "0.0.1",
52
- "@ccpool/storage-libsql": "0.0.1",
53
- "@ccpool/daemon": "0.0.1"
51
+ "@ccpool/core": "0.0.2",
52
+ "@ccpool/daemon": "0.0.1",
53
+ "@ccpool/storage-libsql": "0.0.1"
54
54
  },
55
55
  "scripts": {
56
56
  "build": "tsup",