@codemem/server 0.26.2 → 0.28.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.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
- import { CODEMEM_CONFIG_ENV_OVERRIDES, DEFAULT_TIME_WINDOW_S, MemoryStore, VERSION, VERSION as VERSION$1, applyReplicationOps, buildBaseUrl, buildFilterClausesWithContext, buildRawEventEnvelopeFromHook, canonicalMemoryKind, cleanupNonces, coordinatorArchiveGroupAction, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorDisableDeviceAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorRenameGroupAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorStatusSnapshot, coordinatorUnarchiveGroupAction, createCoordinatorReciprocalApproval, ensureDeviceIdentity, extractReplicationOps, fingerprintPublicKey, fromJson, getCodememConfigPath, getCodememEnvOverrides, getSemanticIndexDiagnostics, getSyncResetState, isSummaryLikeMemory, listCoordinatorJoinRequests, listMaintenanceJobs, listObserverProviderOptions, loadMemorySnapshotPageForPeer, loadReplicationOpsForPeer, lookupCoordinatorPeers, mergeAddresses, parseStrictInteger, probeAvailableCredentials, readCodememConfigFile, readCoordinatorSyncConfig, recordNonce, requestJson, resolveDbPath, runSyncPass, schema, stripJsonComments, stripPrivateObj, stripTrailingCommas, syncProjectAllowedByFilters, syncVisibilityAllowed, verifySignature, writeCodememConfigFile } from "@codemem/core";
3
+ import { CODEMEM_CONFIG_ENV_OVERRIDES, DEFAULT_TIME_WINDOW_S, MemoryStore, VERSION, VERSION as VERSION$1, applyReplicationOps, buildBaseUrl, buildFilterClausesWithContext, buildRawEventEnvelopeFromHook, canonicalMemoryKind, cleanupNonces, coordinatorArchiveGroupAction, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorDisableDeviceAction, coordinatorEnableDeviceAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorRenameGroupAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorStatusSnapshot, coordinatorUnarchiveGroupAction, createCoordinatorReciprocalApproval, ensureDeviceIdentity, extractReplicationOps, fingerprintPublicKey, fromJson, getCodememConfigPath, getCodememEnvOverrides, getSemanticIndexDiagnostics, getSyncResetState, isSummaryLikeMemory, listCoordinatorJoinRequests, listMaintenanceJobs, listObserverProviderOptions, loadMemorySnapshotPageForPeer, loadReplicationOpsForPeer, lookupCoordinatorPeers, mergeAddresses, parseStrictInteger, probeAvailableCredentials, readCodememConfigFile, readCoordinatorSyncConfig, recordNonce, requestJson, resolveDbPath, runSyncPass, schema, stripJsonComments, stripPrivateObj, stripTrailingCommas, syncProjectAllowedByFilters, syncVisibilityAllowed, verifySignature, writeCodememConfigFile } from "@codemem/core";
4
4
  import { serveStatic } from "@hono/node-server/serve-static";
5
5
  import { Hono } from "hono";
6
6
  import { createMiddleware } from "hono/factory";
7
7
  import { RateLimiter } from "limiter";
8
- import { homedir } from "node:os";
8
+ import { homedir, networkInterfaces } from "node:os";
9
9
  import { and, count, desc, eq, inArray, isNotNull, max, ne } from "drizzle-orm";
10
10
  import { drizzle } from "drizzle-orm/better-sqlite3";
11
11
  import { createHash, randomUUID } from "node:crypto";
@@ -1436,6 +1436,13 @@ function coordinatorAdminStatusPayload(config = readCoordinatorSyncConfig()) {
1436
1436
  has_groups: hasGroups
1437
1437
  };
1438
1438
  }
1439
+ function pairingAdvertiseAddresses(config = readCoordinatorSyncConfig()) {
1440
+ const advertise = String(config.syncAdvertise || "auto").trim().toLowerCase();
1441
+ if (advertise && advertise !== "auto" && advertise !== "default") return mergeAddresses([], String(config.syncAdvertise || "").split(",").map((item) => item.trim()).filter(Boolean));
1442
+ if (config.syncHost && config.syncHost !== "0.0.0.0") return [`${config.syncHost}:${config.syncPort}`];
1443
+ const addresses = Object.values(networkInterfaces()).flatMap((entries) => entries ?? []).filter((entry) => !entry.internal).map((entry) => entry.address).filter((address) => address && address !== "127.0.0.1" && address !== "::1").map((address) => `${address}:${config.syncPort}`);
1444
+ return [...new Set(addresses)];
1445
+ }
1439
1446
  function resolveCoordinatorAdminGroup(requestedGroup, status) {
1440
1447
  const explicit = String(requestedGroup ?? "").trim();
1441
1448
  if (explicit) return explicit;
@@ -1968,7 +1975,12 @@ function filterOpsForPeer(store, peerDeviceId, ops) {
1968
1975
  * When showDiag is false, sensitive fields (fingerprint, last_error, addresses)
1969
1976
  * are redacted.
1970
1977
  */
1971
- function mapPeerRow(store, row, showDiag) {
1978
+ function mapPeerRow(store, row, showDiag, recentOpsByPeer) {
1979
+ const peerId = String(row.peer_device_id ?? "");
1980
+ const recentOps = recentOpsByPeer?.get(peerId) ?? {
1981
+ in: 0,
1982
+ out: 0
1983
+ };
1972
1984
  return {
1973
1985
  peer_device_id: row.peer_device_id,
1974
1986
  name: row.name,
@@ -1982,9 +1994,33 @@ function mapPeerRow(store, row, showDiag) {
1982
1994
  claimed_local_actor: Boolean(row.claimed_local_actor),
1983
1995
  actor_id: row.actor_id ?? null,
1984
1996
  actor_display_name: row.actor_display_name ?? null,
1985
- project_scope: { ...currentProjectScope(row, readPeerProjectFilters(store, String(row.peer_device_id ?? ""))) }
1997
+ project_scope: { ...currentProjectScope(row, readPeerProjectFilters(store, String(row.peer_device_id ?? ""))) },
1998
+ recent_ops: {
1999
+ in: recentOps.in,
2000
+ out: recentOps.out
2001
+ }
1986
2002
  };
1987
2003
  }
2004
+ var SYNC_DIRECTION_WINDOW_SECONDS = 1440 * 60;
2005
+ function recentPeerOps(store) {
2006
+ const cutoff = (/* @__PURE__ */ new Date(Date.now() - SYNC_DIRECTION_WINDOW_SECONDS * 1e3)).toISOString();
2007
+ const rows = store.db.prepare(`SELECT peer_device_id,
2008
+ COALESCE(SUM(ops_in), 0) AS ops_in,
2009
+ COALESCE(SUM(ops_out), 0) AS ops_out
2010
+ FROM sync_attempts
2011
+ WHERE ok = 1 AND finished_at IS NOT NULL AND finished_at >= ?
2012
+ GROUP BY peer_device_id`).all(cutoff);
2013
+ const map = /* @__PURE__ */ new Map();
2014
+ for (const row of rows) {
2015
+ const id = String(row.peer_device_id ?? "").trim();
2016
+ if (!id) continue;
2017
+ map.set(id, {
2018
+ in: Number(row.ops_in ?? 0),
2019
+ out: Number(row.ops_out ?? 0)
2020
+ });
2021
+ }
2022
+ return map;
2023
+ }
1988
2024
  function isRecentIso(value, windowS = SYNC_STALE_AFTER_SECONDS) {
1989
2025
  const raw = String(value ?? "").trim();
1990
2026
  if (!raw) return false;
@@ -2354,8 +2390,10 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
2354
2390
  statusPayload.daemon_running = runtimeStatus.phase === "starting" || daemonRunning;
2355
2391
  statusPayload.daemon_detail = runtimeStatus.detail ?? daemonDetail;
2356
2392
  }
2357
- const peersItems = traceSync("peerRows", () => store.db.prepare(PEERS_QUERY).all()).map((row) => {
2358
- const peer = mapPeerRow(store, row, showDiag);
2393
+ const peerRows = traceSync("peerRows", () => store.db.prepare(PEERS_QUERY).all());
2394
+ const recentOpsByPeer = traceSync("recentPeerOps", () => recentPeerOps(store));
2395
+ const peersItems = peerRows.map((row) => {
2396
+ const peer = mapPeerRow(store, row, showDiag, recentOpsByPeer);
2359
2397
  peer.status = peerStatus(peer);
2360
2398
  return peer;
2361
2399
  });
@@ -2440,7 +2478,9 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
2440
2478
  const store = getStore();
2441
2479
  {
2442
2480
  const showDiag = queryBool(c.req.query("includeDiagnostics"));
2443
- const peers = store.db.prepare(PEERS_QUERY).all().map((row) => mapPeerRow(store, row, showDiag));
2481
+ const rows = store.db.prepare(PEERS_QUERY).all();
2482
+ const recentOpsByPeer = recentPeerOps(store);
2483
+ const peers = rows.map((row) => mapPeerRow(store, row, showDiag, recentOpsByPeer));
2444
2484
  return c.json({
2445
2485
  items: peers,
2446
2486
  redacted: !showDiag
@@ -2484,6 +2524,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
2484
2524
  redacted: true,
2485
2525
  pairing_filter_hint: PAIRING_FILTER_HINT
2486
2526
  });
2527
+ const config = readCoordinatorSyncConfig();
2487
2528
  const d = drizzle(store.db, { schema });
2488
2529
  const deviceRow = d.select({
2489
2530
  device_id: schema.syncDevice.device_id,
@@ -2511,7 +2552,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
2511
2552
  fingerprint,
2512
2553
  public_key: publicKey ?? null,
2513
2554
  pairing_filter_hint: PAIRING_FILTER_HINT,
2514
- addresses: []
2555
+ addresses: pairingAdvertiseAddresses(config)
2515
2556
  });
2516
2557
  }
2517
2558
  });
@@ -3373,6 +3414,49 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
3373
3414
  }, 400);
3374
3415
  }
3375
3416
  });
3417
+ app.post("/api/coordinator/admin/devices/:device_id/enable", async (c) => {
3418
+ const config = readCoordinatorSyncConfig();
3419
+ const status = coordinatorAdminStatusPayload(config);
3420
+ if (status.readiness === "not_configured") return c.json({
3421
+ error: "coordinator_not_configured",
3422
+ status
3423
+ }, 400);
3424
+ if (!status.has_admin_secret) return c.json({
3425
+ error: "coordinator_admin_secret_missing",
3426
+ status
3427
+ }, 400);
3428
+ const deviceId = String(c.req.param("device_id") ?? "").trim();
3429
+ if (!deviceId) return c.json({
3430
+ error: "device_id required",
3431
+ status
3432
+ }, 400);
3433
+ const groupId = resolveCoordinatorAdminGroup(c.req.query("group_id"), status);
3434
+ if (!groupId) return c.json({
3435
+ error: "group_id required",
3436
+ status
3437
+ }, 400);
3438
+ try {
3439
+ if (!await coordinatorEnableDeviceAction({
3440
+ groupId,
3441
+ deviceId,
3442
+ remoteUrl: config.syncCoordinatorUrl || null,
3443
+ adminSecret: config.syncCoordinatorAdminSecret || null
3444
+ })) return c.json({
3445
+ error: "device_not_found",
3446
+ status
3447
+ }, 404);
3448
+ return c.json({
3449
+ ok: true,
3450
+ device_id: deviceId,
3451
+ status
3452
+ });
3453
+ } catch (error) {
3454
+ return c.json({
3455
+ error: error instanceof Error ? error.message : String(error),
3456
+ status
3457
+ }, 400);
3458
+ }
3459
+ });
3376
3460
  app.post("/api/coordinator/admin/devices/:device_id/remove", async (c) => {
3377
3461
  const config = readCoordinatorSyncConfig();
3378
3462
  const status = coordinatorAdminStatusPayload(config);