@codemem/server 0.30.0 → 0.31.1

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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,KAAK,eAAe,EAAiB,OAAO,EAAE,MAAM,eAAe,CAAC;AAE1F,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,EAEN,KAAK,0BAA0B,EAC/B,MAAM,yBAAyB,CAAC;AAQjC,OAAO,EAAE,OAAO,EAAE,CAAC;AAKnB,uFAAuF;AACvF,wBAAgB,QAAQ,IAAI,WAAW,CAKtC;AAED,mDAAmD;AACnD,wBAAgB,UAAU,IAAI,IAAI,CAGjC;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IAC1B,YAAY,CAAC,EAAE,MAAM,WAAW,CAAC;IACjC,OAAO,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC;IACjC,oBAAoB,CAAC,EAAE;QACtB,OAAO,CAAC,EAAE,0BAA0B,CAAC;QACrC,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,wBAAwB,CAAC,EAAE,MAAM,CAAC;QAClC,4BAA4B,CAAC,EAAE,MAAM,CAAC;KACtC,CAAC;IACF,oBAAoB,CAAC,EAAE,MAAM;QAC5B,KAAK,EACF,UAAU,GACV,SAAS,GACT,UAAU,GACV,OAAO,GACP,UAAU,GACV,iBAAiB,GACjB,iBAAiB,GACjB,IAAI,CAAC;QACR,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KACvB,GAAG,IAAI,CAAC;CACT;AAED,wBAAgB,SAAS,CAAC,IAAI,CAAC,EAAE,UAAU,8EAuD1C;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,IAAI,CAAC,EAAE,UAAU,8EA8B9C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,KAAK,eAAe,EAAiB,OAAO,EAAE,MAAM,eAAe,CAAC;AAE1F,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,EAEN,KAAK,0BAA0B,EAC/B,MAAM,yBAAyB,CAAC;AAQjC,OAAO,EAAE,OAAO,EAAE,CAAC;AAKnB,uFAAuF;AACvF,wBAAgB,QAAQ,IAAI,WAAW,CAKtC;AAED,mDAAmD;AACnD,wBAAgB,UAAU,IAAI,IAAI,CAGjC;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IAC1B,YAAY,CAAC,EAAE,MAAM,WAAW,CAAC;IACjC,OAAO,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC;IACjC,oBAAoB,CAAC,EAAE;QACtB,OAAO,CAAC,EAAE,0BAA0B,CAAC;QACrC,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,wBAAwB,CAAC,EAAE,MAAM,CAAC;QAClC,4BAA4B,CAAC,EAAE,MAAM,CAAC;KACtC,CAAC;IACF,oBAAoB,CAAC,EAAE,MAAM;QAC5B,KAAK,EACF,UAAU,GACV,SAAS,GACT,UAAU,GACV,OAAO,GACP,UAAU,GACV,iBAAiB,GACjB,iBAAiB,GACjB,IAAI,CAAC;QACR,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KACvB,GAAG,IAAI,CAAC;CACT;AAED,wBAAgB,SAAS,CAAC,IAAI,CAAC,EAAE,UAAU,8EA6D1C;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,IAAI,CAAC,EAAE,UAAU,8EA8B9C"}
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
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, LEGACY_SHARED_REVIEW_SCOPE_ID, LOCAL_DEFAULT_SCOPE_ID, LOCAL_SYNC_CAPABILITY, MemoryStore, SYNC_SCOPE_QUERY_PARAM, VERSION, VERSION as VERSION$1, addSyncScopeToBoundary, analyzeProjectScopeMappingChangeGuardrails, applyReplicationOps, buildBaseUrl, buildFilterClausesWithContext, buildRawEventEnvelopeFromHook, canonicalMemoryKind, cleanupNonces, coordinatorArchiveGroupAction, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorCreateScopeAction, coordinatorDisableDeviceAction, coordinatorEnableDeviceAction, coordinatorGrantScopeMembershipAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorListScopeMembershipsAction, coordinatorListScopesAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorRenameGroupAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorRevokeScopeMembershipAction, coordinatorStatusSnapshot, coordinatorUnarchiveGroupAction, coordinatorUpdateScopeAction, createCoordinatorReciprocalApproval, deleteProjectScopeSettingsMapping, ensureDeviceIdentity, extractReplicationOps, filterReplicationOpsForSyncWithStatus, fingerprintPublicKey, fromJson, getCodememConfigPath, getCodememEnvOverrides, getCoordinatorGroupPreference, getSemanticIndexDiagnostics, getSyncResetState, isSummaryLikeMemory, listCoordinatorJoinRequests, listInboundScopeRejections, listMaintenanceJobs, listObserverProviderOptions, listProjectScopeCandidates, listProjectScopeSettingsMappings, listSharingDomainSettingsScopes, loadMemorySnapshotPageForPeer, loadReplicationOpsForPeer, lookupCoordinatorPeers, mergeAddresses, negotiateSyncCapability, normalizeSyncCapability, parseStrictInteger, parseSyncScopeRequest, personalScopeGrantStatusForPeer, probeAvailableCredentials, readCodememConfigFile, readCoordinatorSyncConfig, recordNonce, rejectInboundScopeFailures, requestJson, resolveDbPath, runSyncPass, schema, stripJsonComments, stripPrivateObj, stripTrailingCommas, summarizeInboundScopeRejections, syncScopeResetRequiredPayload, updatePeerAddresses, upsertCoordinatorGroupPreference, upsertProjectScopeSettingsMapping, verifySignature, writeCodememConfigFile } from "@codemem/core";
3
+ import { CODEMEM_CONFIG_ENV_OVERRIDES, DEFAULT_TIME_WINDOW_S, LEGACY_SHARED_REVIEW_SCOPE_ID, LOCAL_DEFAULT_SCOPE_ID, LOCAL_SYNC_CAPABILITY, MemoryStore, SYNC_SCOPE_QUERY_PARAM, VERSION, VERSION as VERSION$1, addSyncScopeToBoundary, analyzeProjectScopeMappingChangeGuardrails, applyReplicationOps, buildBaseUrl, buildFilterClausesWithContext, buildRawEventEnvelopeFromHook, canonicalMemoryKind, canonicalWorkspaceIdentity, cleanupNonces, coordinatorArchiveGroupAction, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorCreateScopeAction, coordinatorDisableDeviceAction, coordinatorEnableDeviceAction, coordinatorGrantScopeMembershipAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorListScopeMembershipsAction, coordinatorListScopesAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorRenameGroupAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorRevokeScopeMembershipAction, coordinatorStatusSnapshot, coordinatorUnarchiveGroupAction, coordinatorUpdateScopeAction, createCoordinatorReciprocalApproval, deleteProjectScopeSettingsMapping, ensureDeviceIdentity, extractReplicationOps, filterReplicationOpsForSyncWithStatus, fingerprintPublicKey, fromJson, getCodememConfigPath, getCodememEnvOverrides, getCoordinatorGroupPreference, getSemanticIndexDiagnostics, getSyncResetState, isSummaryLikeMemory, listCoordinatorJoinRequests, listInboundScopeRejections, listMaintenanceJobs, listObserverProviderOptions, listProjectScopeCandidates, listProjectScopeInventory, listProjectScopeSettingsMappings, listSharingDomainSettingsScopes, loadMemorySnapshotPageForPeer, loadReplicationOpsForPeer, lookupCoordinatorPeers, mergeAddresses, negotiateSyncCapability, normalizeSyncCapability, parseStrictInteger, parseSyncScopeRequest, personalScopeGrantStatusForPeer, probeAvailableCredentials, readCodememConfigFile, readCoordinatorSyncConfig, reassignProjectScopeInventoryProject, recordNonce, rejectInboundScopeFailures, requestJson, resolveDbPath, runSyncPass, schema, stripJsonComments, stripPrivateObj, stripTrailingCommas, summarizeInboundScopeRejections, syncScopeResetRequiredPayload, updatePeerAddresses, upsertCoordinatorGroupPreference, upsertProjectScopeSettingsMapping, 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";
@@ -1771,6 +1771,20 @@ function missingGuardrailConfirmations(warnings, confirmedTokens) {
1771
1771
  const confirmed = new Set(confirmedTokens);
1772
1772
  return warnings.filter((warning) => warning.requires_confirmation && (!warning.confirmation_token || !confirmed.has(warning.confirmation_token)));
1773
1773
  }
1774
+ function parseViewerProjectMappingInput(body) {
1775
+ const id = optionalViewerInteger(body, "id");
1776
+ const priority = optionalViewerInteger(body, "priority");
1777
+ if (Number.isNaN(id)) throw new Error("id must be an integer");
1778
+ if (Number.isNaN(priority)) throw new Error("priority must be an integer");
1779
+ return {
1780
+ id,
1781
+ workspace_identity: optionalViewerStrictString(body, "workspace_identity"),
1782
+ project_pattern: optionalViewerStrictString(body, "project_pattern"),
1783
+ scope_id: optionalViewerStrictString(body, "scope_id") ?? "",
1784
+ priority,
1785
+ source: optionalViewerStrictString(body, "source") ?? "user"
1786
+ };
1787
+ }
1774
1788
  function coordinatorAdminMutationStatus(message) {
1775
1789
  if (message.includes("scope_not_found") || message.includes("membership_not_found") || message.includes("group_not_found") || message.includes("Group not found") || message.includes("Scope not found") || message.includes("device_not_enrolled_for_scope_group") || message.includes("device must be enrolled")) return 404;
1776
1790
  if (message.includes("not active") || message.includes("scope_not_active") || message.includes("group_archived") || message.includes("Group is archived")) return 409;
@@ -2454,18 +2468,228 @@ function recentScopeRejectionsByPeer(store) {
2454
2468
  return map;
2455
2469
  }
2456
2470
  function legacySharedReviewSummary(store) {
2457
- const row = store.db.prepare(`SELECT COUNT(*) AS memory_count,
2458
- MAX(updated_at) AS last_updated_at
2459
- FROM memory_items
2460
- WHERE scope_id = ?
2461
- AND active = 1
2462
- AND deleted_at IS NULL`).get(LEGACY_SHARED_REVIEW_SCOPE_ID);
2463
- const memoryCount = Number(row?.memory_count ?? 0);
2471
+ const rows = legacySharedReviewRows(store);
2472
+ const memoryCount = rows.length;
2473
+ const lastUpdatedAt = rows.reduce((current, row) => row.updated_at && (!current || row.updated_at > current) ? row.updated_at : current, null);
2474
+ const ownedBySelf = store.buildOwnershipPredicate();
2475
+ const candidatesByIdentity = new Map(listProjectScopeCandidates(store.db, { limit: null }).map((candidate) => [candidate.workspace_identity, candidate]));
2476
+ const groupsByIdentity = /* @__PURE__ */ new Map();
2477
+ for (const row of rows) {
2478
+ const identity = canonicalWorkspaceIdentity({
2479
+ cwd: row.cwd,
2480
+ gitBranch: row.git_branch,
2481
+ gitRemote: row.git_remote,
2482
+ project: row.project,
2483
+ workspaceId: row.workspace_id
2484
+ });
2485
+ const candidate = candidatesByIdentity.get(identity.value);
2486
+ const resolvedScope = candidate?.resolved_scope_id ?? null;
2487
+ const suggestedScope = resolvedScope && resolvedScope !== LOCAL_DEFAULT_SCOPE_ID && resolvedScope !== LEGACY_SHARED_REVIEW_SCOPE_ID ? resolvedScope : candidate?.suggested_scope_id;
2488
+ const existing = groupsByIdentity.get(identity.value);
2489
+ const isOwnedBySelf = ownedBySelf(row);
2490
+ const rowUpdatedAt = row.updated_at ?? null;
2491
+ if (existing) {
2492
+ existing.memory_count += 1;
2493
+ if (isOwnedBySelf) existing.reassignable_memory_count += 1;
2494
+ else existing.peer_owned_memory_count += 1;
2495
+ if (rowUpdatedAt && (!existing.last_updated_at || rowUpdatedAt > existing.last_updated_at)) existing.last_updated_at = rowUpdatedAt;
2496
+ continue;
2497
+ }
2498
+ groupsByIdentity.set(identity.value, {
2499
+ workspace_identity: identity.value,
2500
+ identity_source: identity.source,
2501
+ display_project: identity.displayProject ?? row.project ?? row.cwd ?? identity.value,
2502
+ memory_count: 1,
2503
+ reassignable_memory_count: isOwnedBySelf ? 1 : 0,
2504
+ peer_owned_memory_count: isOwnedBySelf ? 0 : 1,
2505
+ last_updated_at: rowUpdatedAt,
2506
+ suggested_scope_id: suggestedScope ?? null,
2507
+ suggestion_reason: suggestedScope ? candidate?.suggestion_reason ?? "Existing project mapping can be reviewed as a destination, but legacy data is not promoted automatically." : null
2508
+ });
2509
+ }
2510
+ const totalGroupCount = groupsByIdentity.size;
2511
+ const groups = [...groupsByIdentity.values()].sort((left, right) => right.memory_count - left.memory_count || (right.last_updated_at ?? "").localeCompare(left.last_updated_at ?? "") || left.workspace_identity.localeCompare(right.workspace_identity));
2512
+ const targetScopes = listSharingDomainSettingsScopes(store.db).filter((scope) => scope.scope_id !== LOCAL_DEFAULT_SCOPE_ID && scope.scope_id !== LEGACY_SHARED_REVIEW_SCOPE_ID).map((scope) => ({
2513
+ authority_type: scope.authority_type,
2514
+ label: scope.label || scope.scope_id,
2515
+ scope_id: scope.scope_id
2516
+ }));
2464
2517
  return {
2465
2518
  scope_id: LEGACY_SHARED_REVIEW_SCOPE_ID,
2466
2519
  memory_count: memoryCount,
2467
2520
  has_data: memoryCount > 0,
2468
- last_updated_at: row?.last_updated_at ?? null
2521
+ last_updated_at: lastUpdatedAt,
2522
+ groups,
2523
+ total_group_count: totalGroupCount,
2524
+ target_scopes: targetScopes
2525
+ };
2526
+ }
2527
+ function legacySharedReviewRows(store) {
2528
+ return store.db.prepare(`SELECT m.id,
2529
+ m.rev,
2530
+ m.active,
2531
+ m.deleted_at,
2532
+ m.updated_at,
2533
+ m.scope_id,
2534
+ s.project,
2535
+ s.cwd,
2536
+ s.git_remote,
2537
+ s.git_branch,
2538
+ m.workspace_id,
2539
+ m.actor_id,
2540
+ m.origin_device_id,
2541
+ m.metadata_json
2542
+ FROM memory_items m
2543
+ LEFT JOIN sessions s ON s.id = m.session_id
2544
+ WHERE m.scope_id = ?
2545
+ AND m.active = 1
2546
+ AND m.deleted_at IS NULL`).all(LEGACY_SHARED_REVIEW_SCOPE_ID);
2547
+ }
2548
+ function legacySharedReviewConfirmationToken(input) {
2549
+ const ownedBySelf = input.store.buildOwnershipPredicate();
2550
+ const payload = input.rows.map((row) => {
2551
+ const ownership = ownedBySelf(row) ? "local" : "peer";
2552
+ return `${row.id}:${row.rev ?? 0}:${row.scope_id ?? ""}:${ownership}`;
2553
+ }).sort().join(",");
2554
+ return Buffer.from(`${input.workspaceIdentity}|${input.scopeId}|${payload}`, "utf8").toString("base64url");
2555
+ }
2556
+ function projectInventoryRowsForWorkspace(store, workspaceIdentity) {
2557
+ return store.db.prepare(`SELECT m.id,
2558
+ m.rev,
2559
+ s.project,
2560
+ s.cwd,
2561
+ s.git_remote,
2562
+ s.git_branch,
2563
+ m.workspace_id,
2564
+ m.actor_id,
2565
+ m.origin_device_id,
2566
+ m.metadata_json
2567
+ FROM memory_items m
2568
+ LEFT JOIN sessions s ON s.id = m.session_id
2569
+ WHERE m.active = 1
2570
+ AND m.deleted_at IS NULL`).all().filter((row) => {
2571
+ return canonicalWorkspaceIdentity({
2572
+ cwd: row.cwd,
2573
+ gitBranch: row.git_branch,
2574
+ gitRemote: row.git_remote,
2575
+ project: row.project,
2576
+ workspaceId: row.workspace_id
2577
+ }).value === workspaceIdentity;
2578
+ });
2579
+ }
2580
+ function projectForgetConfirmationToken(store, rows) {
2581
+ const ownedBySelf = store.buildOwnershipPredicate();
2582
+ const payload = rows.map((row) => {
2583
+ const ownership = ownedBySelf(row) ? "local" : "peer";
2584
+ return `${row.id}:${row.rev ?? 0}:${ownership}`;
2585
+ }).sort().join(",");
2586
+ return Buffer.from(payload, "utf8").toString("base64url");
2587
+ }
2588
+ function forgetProjectInventoryLocalMemories(store, input) {
2589
+ if (!input.workspaceIdentity.trim()) throw new Error("workspace_identity must be a non-empty string");
2590
+ const rows = projectInventoryRowsForWorkspace(store, input.workspaceIdentity);
2591
+ if (rows.length === 0) throw new Error("project identity not found");
2592
+ const ownedBySelf = store.buildOwnershipPredicate();
2593
+ const localRows = rows.filter((row) => ownedBySelf(row));
2594
+ const confirmationToken = projectForgetConfirmationToken(store, rows);
2595
+ const preview = {
2596
+ confirmation_token: confirmationToken,
2597
+ local_owned_memory_count: localRows.length,
2598
+ peer_owned_memory_count: rows.length - localRows.length,
2599
+ workspace_identity: input.workspaceIdentity
2600
+ };
2601
+ if (!input.confirmed) return {
2602
+ confirmed: false,
2603
+ ...preview
2604
+ };
2605
+ if (input.confirmationToken !== confirmationToken) throw new Error("project memories changed before cleanup; refresh and try again");
2606
+ store.db.transaction(() => {
2607
+ for (const row of localRows) store.forget(row.id);
2608
+ })();
2609
+ return {
2610
+ confirmed: true,
2611
+ forgotten_memory_count: localRows.length,
2612
+ ...preview
2613
+ };
2614
+ }
2615
+ function assertLocalDeviceScopeMembership(store, scopeId) {
2616
+ if (scopeId === LOCAL_DEFAULT_SCOPE_ID) return;
2617
+ if (!store.db.prepare(`SELECT 1
2618
+ FROM scope_memberships sm
2619
+ JOIN replication_scopes rs ON rs.scope_id = sm.scope_id
2620
+ WHERE sm.scope_id = ?
2621
+ AND sm.device_id = ?
2622
+ AND sm.status = 'active'
2623
+ AND rs.status = 'active'
2624
+ AND sm.membership_epoch >= rs.membership_epoch
2625
+ LIMIT 1`).get(scopeId, store.deviceId)) throw new Error(`local device is not a member of Sharing domain ${scopeId}`);
2626
+ }
2627
+ function legacySharedReviewRowsForWorkspace(store, workspaceIdentity) {
2628
+ return legacySharedReviewRows(store).filter((row) => {
2629
+ return canonicalWorkspaceIdentity({
2630
+ cwd: row.cwd,
2631
+ gitBranch: row.git_branch,
2632
+ gitRemote: row.git_remote,
2633
+ project: row.project,
2634
+ workspaceId: row.workspace_id
2635
+ }).value === workspaceIdentity;
2636
+ });
2637
+ }
2638
+ function reassignableLegacySharedReviewRows(store, rows) {
2639
+ const reassignableRows = rows.filter((row) => store.memoryOwnedBySelf(row));
2640
+ if (reassignableRows.length === 0) throw new Error("This legacy review group contains only peer-owned memories; this device cannot reassign them to a Sharing domain.");
2641
+ const currentRows = store.db.prepare(`SELECT id, scope_id, active, deleted_at
2642
+ FROM memory_items
2643
+ WHERE id IN (${reassignableRows.map(() => "?").join(",")})`).all(...reassignableRows.map((row) => row.id));
2644
+ const currentById = new Map(currentRows.map((row) => [Number(row.id), row]));
2645
+ for (const row of reassignableRows) {
2646
+ const current = currentById.get(row.id);
2647
+ if (current?.scope_id !== LEGACY_SHARED_REVIEW_SCOPE_ID || current.active !== 1 || current.deleted_at) throw new Error("legacy shared review group changed before reassignment; refresh and try again");
2648
+ }
2649
+ return reassignableRows;
2650
+ }
2651
+ function legacySharedReviewReassignmentPreview(store, input) {
2652
+ if (!input.workspaceIdentity.trim()) throw new Error("workspace_identity must be a non-empty string");
2653
+ if (!input.scopeId.trim()) throw new Error("scope_id must be a non-empty string");
2654
+ if (input.scopeId === LOCAL_DEFAULT_SCOPE_ID) throw new Error("local-default is not a valid target for legacy shared review reassignment");
2655
+ if (input.scopeId === LEGACY_SHARED_REVIEW_SCOPE_ID) throw new Error("legacy-shared-review is a review bucket, not an assignable Sharing domain");
2656
+ const targetScope = listSharingDomainSettingsScopes(store.db).find((scope) => scope.scope_id === input.scopeId);
2657
+ if (!targetScope) throw new Error(`scope_id ${input.scopeId} is not an active Sharing domain`);
2658
+ assertLocalDeviceScopeMembership(store, input.scopeId);
2659
+ const rows = legacySharedReviewRowsForWorkspace(store, input.workspaceIdentity);
2660
+ if (rows.length === 0) throw new Error("legacy shared review group not found");
2661
+ const reassignableRows = reassignableLegacySharedReviewRows(store, rows);
2662
+ const peerDeviceIds = [...new Set(rows.map((row) => String(row.origin_device_id ?? "").trim()).filter((deviceId) => deviceId && deviceId !== store.deviceId))].sort();
2663
+ return {
2664
+ affected_peer_device_count: peerDeviceIds.length,
2665
+ affected_peer_device_ids: peerDeviceIds.slice(0, 5),
2666
+ confirmation_token: legacySharedReviewConfirmationToken({
2667
+ rows,
2668
+ scopeId: input.scopeId,
2669
+ store,
2670
+ workspaceIdentity: input.workspaceIdentity
2671
+ }),
2672
+ memory_count: rows.length,
2673
+ reassignable_memory_count: reassignableRows.length,
2674
+ scope_id: input.scopeId,
2675
+ skipped_memory_count: rows.length - reassignableRows.length,
2676
+ target_scope_label: targetScope.label || targetScope.scope_id,
2677
+ warning: "This changes future sync authorization for reassignable local memories. It does not erase data already copied to peers under legacy-shared-review.",
2678
+ workspace_identity: input.workspaceIdentity
2679
+ };
2680
+ }
2681
+ function reassignLegacySharedReviewGroup(store, input) {
2682
+ const preview = legacySharedReviewReassignmentPreview(store, input);
2683
+ if (input.confirmationToken !== preview.confirmation_token) throw new Error("legacy shared review group changed before reassignment; refresh and try again");
2684
+ const rows = reassignableLegacySharedReviewRows(store, legacySharedReviewRowsForWorkspace(store, input.workspaceIdentity));
2685
+ store.db.transaction(() => {
2686
+ for (const row of rows) store.reassignMemoryScope(row.id, input.scopeId);
2687
+ })();
2688
+ return {
2689
+ ok: true,
2690
+ ...preview,
2691
+ reassigned_memory_count: rows.length,
2692
+ legacy_shared_review: legacySharedReviewSummary(store)
2469
2693
  };
2470
2694
  }
2471
2695
  var SYNC_DIRECTION_WINDOW_SECONDS = 1440 * 60;
@@ -3162,32 +3386,104 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
3162
3386
  local_default_scope_id: LOCAL_DEFAULT_SCOPE_ID
3163
3387
  });
3164
3388
  });
3165
- app.put("/api/sync/sharing-domains/project-mappings", async (c) => {
3389
+ app.get("/api/sync/projects", (c) => {
3390
+ const store = getStore();
3391
+ const limit = Math.max(1, queryInt(c.req.query("limit"), 50));
3392
+ const offset = Math.max(0, queryInt(c.req.query("offset"), 0));
3393
+ return c.json(listProjectScopeInventory(store.db, {
3394
+ identitySource: c.req.query("identity_source"),
3395
+ limit,
3396
+ offset,
3397
+ query: c.req.query("q"),
3398
+ scopeId: c.req.query("scope_id"),
3399
+ status: c.req.query("status")
3400
+ }));
3401
+ });
3402
+ app.post("/api/sync/projects/reassign-project", async (c) => {
3403
+ const store = getStore();
3404
+ const body = await parseViewerJsonBody(c);
3405
+ if (!body) return c.json({ error: "invalid json" }, 400);
3406
+ try {
3407
+ const workspaceIdentity = optionalViewerStrictString(body, "workspace_identity") ?? "";
3408
+ const project = optionalViewerStrictString(body, "project") ?? "";
3409
+ const result = reassignProjectScopeInventoryProject(store.db, {
3410
+ project,
3411
+ workspaceIdentity
3412
+ });
3413
+ return c.json({
3414
+ ok: true,
3415
+ ...result
3416
+ });
3417
+ } catch (error) {
3418
+ const message = error instanceof Error ? error.message : String(error);
3419
+ return c.json({ error: message }, message.includes("not found") ? 404 : 400);
3420
+ }
3421
+ });
3422
+ app.post("/api/sync/projects/forget", async (c) => {
3423
+ const store = getStore();
3424
+ const body = await parseViewerJsonBody(c);
3425
+ if (!body) return c.json({ error: "invalid json" }, 400);
3426
+ try {
3427
+ const workspaceIdentity = optionalViewerStrictString(body, "workspace_identity") ?? "";
3428
+ const confirmationToken = optionalViewerStrictString(body, "confirmation_token");
3429
+ const confirmed = body.confirmed === true;
3430
+ const result = forgetProjectInventoryLocalMemories(store, {
3431
+ confirmationToken,
3432
+ confirmed,
3433
+ workspaceIdentity
3434
+ });
3435
+ if (!confirmed) return c.json({
3436
+ error: "project_forget_confirmation_required",
3437
+ preview: result
3438
+ }, 409);
3439
+ return c.json({
3440
+ ok: true,
3441
+ ...result
3442
+ });
3443
+ } catch (error) {
3444
+ const message = error instanceof Error ? error.message : String(error);
3445
+ return c.json({ error: message }, message.includes("not found") ? 404 : 400);
3446
+ }
3447
+ });
3448
+ app.post("/api/sync/legacy-shared-review/reassign", async (c) => {
3166
3449
  const store = getStore();
3167
3450
  const body = await parseViewerJsonBody(c);
3168
3451
  if (!body) return c.json({ error: "invalid json" }, 400);
3169
3452
  try {
3170
- const id = optionalViewerInteger(body, "id");
3171
- const priority = optionalViewerInteger(body, "priority");
3172
- if (Number.isNaN(id)) return c.json({ error: "id must be an integer" }, 400);
3173
- if (Number.isNaN(priority)) return c.json({ error: "priority must be an integer" }, 400);
3174
- const workspaceIdentity = optionalViewerStrictString(body, "workspace_identity");
3175
- const projectPattern = optionalViewerStrictString(body, "project_pattern");
3453
+ const workspaceIdentity = optionalViewerStrictString(body, "workspace_identity") ?? "";
3176
3454
  const scopeId = optionalViewerStrictString(body, "scope_id") ?? "";
3177
- const source = optionalViewerStrictString(body, "source") ?? "user";
3455
+ const confirmationToken = optionalViewerStrictString(body, "confirmation_token");
3456
+ const confirmedOldCopies = body.confirmed_old_copies === true;
3457
+ const preview = legacySharedReviewReassignmentPreview(store, {
3458
+ scopeId,
3459
+ workspaceIdentity
3460
+ });
3461
+ if (!confirmedOldCopies) return c.json({
3462
+ error: "legacy_review_confirmation_required",
3463
+ message: preview.warning,
3464
+ preview
3465
+ }, 409);
3466
+ return c.json(reassignLegacySharedReviewGroup(store, {
3467
+ confirmationToken,
3468
+ scopeId,
3469
+ workspaceIdentity
3470
+ }));
3471
+ } catch (error) {
3472
+ const message = error instanceof Error ? error.message : String(error);
3473
+ return c.json({ error: message }, message.includes("not found") ? 404 : 400);
3474
+ }
3475
+ });
3476
+ app.put("/api/sync/sharing-domains/project-mappings", async (c) => {
3477
+ const store = getStore();
3478
+ const body = await parseViewerJsonBody(c);
3479
+ if (!body) return c.json({ error: "invalid json" }, 400);
3480
+ try {
3178
3481
  const confirmedGuardrailTokens = optionalViewerStringList(body, "confirmed_guardrail_tokens");
3179
- const mappingInput = {
3180
- id,
3181
- workspace_identity: workspaceIdentity,
3182
- project_pattern: projectPattern,
3183
- scope_id: scopeId,
3184
- priority,
3185
- source
3186
- };
3482
+ const mappingInput = parseViewerProjectMappingInput(body);
3187
3483
  const analysis = analyzeProjectScopeMappingChangeGuardrails(store.db, mappingInput);
3188
- if (analysis.requested_workspace_identity?.startsWith("unmapped:") && scopeId !== LOCAL_DEFAULT_SCOPE_ID) return c.json({
3484
+ if (analysis.requested_workspace_identity?.startsWith("unmapped:")) return c.json({
3189
3485
  error: "unmapped_project_local_only",
3190
- message: "Unmapped projects need a stable path, git remote, or workspace id before assigning a non-local Sharing domain.",
3486
+ message: "Unmapped projects need a stable path, git remote, or workspace id before assigning a Sharing domain.",
3191
3487
  guardrail_warnings: analysis.warnings
3192
3488
  }, 400);
3193
3489
  const missingConfirmations = missingGuardrailConfirmations(analysis.warnings, confirmedGuardrailTokens);
@@ -3211,6 +3507,45 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
3211
3507
  return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
3212
3508
  }
3213
3509
  });
3510
+ app.put("/api/sync/sharing-domains/project-mappings/bulk", async (c) => {
3511
+ const store = getStore();
3512
+ const body = await parseViewerJsonBody(c);
3513
+ if (!body) return c.json({ error: "invalid json" }, 400);
3514
+ try {
3515
+ const rawMappings = body.mappings;
3516
+ if (!Array.isArray(rawMappings) || rawMappings.length === 0) return c.json({ error: "mappings must be a non-empty array" }, 400);
3517
+ const mappingInputs = rawMappings.map((raw) => {
3518
+ if (raw == null || typeof raw !== "object" || Array.isArray(raw)) throw new Error("each mapping must be an object");
3519
+ return parseViewerProjectMappingInput(raw);
3520
+ });
3521
+ const analyses = mappingInputs.map((mappingInput) => analyzeProjectScopeMappingChangeGuardrails(store.db, mappingInput));
3522
+ const unmapped = analyses.find((analysis) => analysis.requested_workspace_identity?.startsWith("unmapped:"));
3523
+ if (unmapped) return c.json({
3524
+ error: "unmapped_project_local_only",
3525
+ message: "Unmapped projects need a stable path, git remote, or workspace id before assigning a Sharing domain.",
3526
+ guardrail_warnings: unmapped.warnings
3527
+ }, 400);
3528
+ const missingConfirmations = analyses.flatMap((analysis) => missingGuardrailConfirmations(analysis.warnings, []));
3529
+ if (missingConfirmations.length > 0) {
3530
+ const missingCodes = [...new Set(missingConfirmations.map((warning) => warning.code))];
3531
+ const missingTokens = [...new Set(missingConfirmations.map((warning) => warning.confirmation_token))].filter((token) => typeof token === "string" && token.length > 0);
3532
+ return c.json({
3533
+ error: "guardrail_confirmation_required",
3534
+ required_guardrails: missingCodes,
3535
+ required_guardrail_tokens: missingTokens,
3536
+ guardrail_warnings: analyses.flatMap((analysis) => analysis.warnings)
3537
+ }, 409);
3538
+ }
3539
+ const saveMappings = store.db.transaction(() => mappingInputs.map((mappingInput) => upsertProjectScopeSettingsMapping(store.db, mappingInput)));
3540
+ return c.json({
3541
+ ok: true,
3542
+ mappings: saveMappings(),
3543
+ guardrail_warnings: analyses.flatMap((analysis) => analysis.warnings)
3544
+ });
3545
+ } catch (error) {
3546
+ return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
3547
+ }
3548
+ });
3214
3549
  app.delete("/api/sync/sharing-domains/project-mappings/:id", (c) => {
3215
3550
  const store = getStore();
3216
3551
  const id = Number(c.req.param("id"));
@@ -4706,6 +5041,10 @@ function createApp(opts) {
4706
5041
  app.route("/", rawEventsRoutes(storeFactory, sweeper));
4707
5042
  app.route("/", syncRoutes(storeFactory, getSyncRuntimeStatus));
4708
5043
  const staticRoot = process.env.CODEMEM_VIEWER_STATIC_DIR ?? join(import.meta.dirname ?? ".", "../static");
5044
+ app.use("/assets/*", async (c, next) => {
5045
+ c.header("Cache-Control", "no-cache");
5046
+ await next();
5047
+ });
4709
5048
  app.use("/assets/*", serveStatic({
4710
5049
  root: staticRoot,
4711
5050
  rewriteRequestPath: (path) => path.replace(/^\/assets/, "")
@@ -4715,6 +5054,7 @@ function createApp(opts) {
4715
5054
  const indexHtml = readFileSync(indexPath, "utf-8");
4716
5055
  app.get("*", (c) => {
4717
5056
  if (c.req.path.startsWith("/api/")) return c.json({ error: "not found" }, 404);
5057
+ c.header("Cache-Control", "no-store");
4718
5058
  return c.html(indexHtml);
4719
5059
  });
4720
5060
  return app;