@agent-relay/dashboard-server 2.0.68 → 2.0.70

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 (58) hide show
  1. package/dist/server.js +62 -7
  2. package/dist/server.js.map +1 -1
  3. package/out/404.html +1 -1
  4. package/out/about.html +1 -1
  5. package/out/about.txt +1 -1
  6. package/out/app/onboarding.html +1 -1
  7. package/out/app/onboarding.txt +1 -1
  8. package/out/app.html +1 -1
  9. package/out/app.txt +1 -1
  10. package/out/blog/go-to-bed-wake-up-to-a-finished-product.html +1 -1
  11. package/out/blog/go-to-bed-wake-up-to-a-finished-product.txt +1 -1
  12. package/out/blog/let-them-cook-multi-agent-orchestration.html +1 -1
  13. package/out/blog/let-them-cook-multi-agent-orchestration.txt +1 -1
  14. package/out/blog.html +1 -1
  15. package/out/blog.txt +1 -1
  16. package/out/careers.html +1 -1
  17. package/out/careers.txt +1 -1
  18. package/out/changelog.html +1 -1
  19. package/out/changelog.txt +1 -1
  20. package/out/cloud/link.html +1 -1
  21. package/out/cloud/link.txt +1 -1
  22. package/out/complete-profile.html +1 -1
  23. package/out/complete-profile.txt +1 -1
  24. package/out/connect-repos.html +1 -1
  25. package/out/connect-repos.txt +1 -1
  26. package/out/contact.html +1 -1
  27. package/out/contact.txt +1 -1
  28. package/out/docs.html +1 -1
  29. package/out/docs.txt +1 -1
  30. package/out/history.html +1 -1
  31. package/out/history.txt +1 -1
  32. package/out/index.html +1 -1
  33. package/out/index.txt +1 -1
  34. package/out/login.html +1 -1
  35. package/out/login.txt +1 -1
  36. package/out/metrics.html +1 -1
  37. package/out/metrics.txt +1 -1
  38. package/out/pricing.html +1 -1
  39. package/out/pricing.txt +1 -1
  40. package/out/privacy.html +1 -1
  41. package/out/privacy.txt +1 -1
  42. package/out/providers/setup/claude.html +1 -1
  43. package/out/providers/setup/claude.txt +1 -1
  44. package/out/providers/setup/codex.html +1 -1
  45. package/out/providers/setup/codex.txt +1 -1
  46. package/out/providers/setup/cursor.html +1 -1
  47. package/out/providers/setup/cursor.txt +1 -1
  48. package/out/providers.html +1 -1
  49. package/out/providers.txt +1 -1
  50. package/out/security.html +1 -1
  51. package/out/security.txt +1 -1
  52. package/out/signup.html +1 -1
  53. package/out/signup.txt +1 -1
  54. package/out/terms.html +1 -1
  55. package/out/terms.txt +1 -1
  56. package/package.json +1 -1
  57. /package/out/_next/static/{pIFM3z2Q4_ebl06dD0vjJ → -BPTMieIVPN3UZGwSczoP}/_buildManifest.js +0 -0
  58. /package/out/_next/static/{pIFM3z2Q4_ebl06dD0vjJ → -BPTMieIVPN3UZGwSczoP}/_ssgManifest.js +0 -0
package/dist/server.js CHANGED
@@ -1756,15 +1756,17 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
1756
1756
  existing.avatarUrl = user.avatarUrl;
1757
1757
  }
1758
1758
  else {
1759
- const now = new Date().toISOString();
1759
+ // Use stable timestamps from the user/file data, not new Date(),
1760
+ // so getAllData() produces deterministic output for dedup comparison
1761
+ const stableTimestamp = user.lastSeen || user.connectedAt || new Date(remoteData.updatedAt).toISOString();
1760
1762
  agentsMap.set(user.name, {
1761
1763
  name: user.name,
1762
1764
  role: 'User',
1763
1765
  cli: 'dashboard',
1764
1766
  messageCount: 0,
1765
1767
  status: 'online',
1766
- lastSeen: now,
1767
- lastActive: now,
1768
+ lastSeen: stableTimestamp,
1769
+ lastActive: stableTimestamp,
1768
1770
  needsAttention: false,
1769
1771
  avatarUrl: user.avatarUrl,
1770
1772
  });
@@ -1954,18 +1956,21 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
1954
1956
  return true;
1955
1957
  });
1956
1958
  // Separate AI agents from human users
1959
+ // Sort by name for deterministic JSON serialization (enables dedup comparison)
1957
1960
  const filteredAgents = validEntries
1958
1961
  .filter(agent => agent.cli !== 'dashboard')
1959
1962
  .map(agent => ({
1960
1963
  ...agent,
1961
1964
  isHuman: false,
1962
- }));
1965
+ }))
1966
+ .sort((a, b) => a.name.localeCompare(b.name));
1963
1967
  const humanUsers = validEntries
1964
1968
  .filter(agent => agent.cli === 'dashboard')
1965
1969
  .map(agent => ({
1966
1970
  ...agent,
1967
1971
  isHuman: true,
1968
- }));
1972
+ }))
1973
+ .sort((a, b) => a.name.localeCompare(b.name));
1969
1974
  return {
1970
1975
  agents: filteredAgents,
1971
1976
  users: humanUsers,
@@ -1978,6 +1983,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
1978
1983
  // Track clients that are still initializing (haven't received first data yet)
1979
1984
  // This prevents race conditions where broadcastData sends before initial data is sent
1980
1985
  const initializingClients = new WeakSet();
1986
+ let lastBroadcastPayload = '';
1981
1987
  const broadcastData = async () => {
1982
1988
  try {
1983
1989
  const data = await getAllData();
@@ -1987,6 +1993,11 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
1987
1993
  console.warn('[dashboard] Skipping broadcast - empty payload');
1988
1994
  return;
1989
1995
  }
1996
+ // Skip broadcast if data hasn't changed since last send
1997
+ if (rawPayload === lastBroadcastPayload) {
1998
+ return;
1999
+ }
2000
+ lastBroadcastPayload = rawPayload;
1990
2001
  // Push into buffer and wrap with sequence ID for replay support
1991
2002
  const seq = mainMessageBuffer.push('data', rawPayload);
1992
2003
  const payload = JSON.stringify({ seq, ...data });
@@ -2066,6 +2077,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
2066
2077
  }
2067
2078
  return { projects: [], messages: [], connected: false };
2068
2079
  };
2080
+ let lastBridgeBroadcastPayload = '';
2069
2081
  const broadcastBridgeData = async () => {
2070
2082
  try {
2071
2083
  const data = await getBridgeData();
@@ -2075,6 +2087,11 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
2075
2087
  console.warn('[dashboard] Skipping bridge broadcast - empty payload');
2076
2088
  return;
2077
2089
  }
2090
+ // Skip broadcast if data hasn't changed since last send
2091
+ if (payload === lastBridgeBroadcastPayload) {
2092
+ return;
2093
+ }
2094
+ lastBridgeBroadcastPayload = payload;
2078
2095
  wssBridge.clients.forEach(client => {
2079
2096
  if (client.readyState === WebSocket.OPEN) {
2080
2097
  try {
@@ -4840,6 +4857,41 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
4840
4857
  res.status(500).json({ success: false, error: safeMessage });
4841
4858
  }
4842
4859
  });
4860
+ /**
4861
+ * POST /api/repos/remove - Remove a cloned repo directory from the workspace
4862
+ * Body: { fullName: "Owner/RepoName" }
4863
+ * Used by cloud API when a user removes a repo from their workspace settings.
4864
+ */
4865
+ app.post('/api/repos/remove', async (req, res) => {
4866
+ const { fullName } = req.body;
4867
+ if (!fullName || typeof fullName !== 'string' || !fullName.includes('/')) {
4868
+ return res.status(400).json({ success: false, error: 'fullName is required (e.g., "Owner/RepoName")' });
4869
+ }
4870
+ if (!/^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/.test(fullName)) {
4871
+ return res.status(400).json({ success: false, error: 'Invalid repository name format' });
4872
+ }
4873
+ const repoName = fullName.split('/').pop();
4874
+ const workspaceDir = process.env.WORKSPACE_DIR || path.dirname(projectRoot || dataDir);
4875
+ const targetDir = path.join(workspaceDir, repoName);
4876
+ // Verify the directory is inside the workspace dir (prevent path traversal)
4877
+ const resolvedTarget = path.resolve(targetDir);
4878
+ const resolvedWorkspace = path.resolve(workspaceDir);
4879
+ if (!resolvedTarget.startsWith(resolvedWorkspace + path.sep)) {
4880
+ return res.status(400).json({ success: false, error: 'Invalid path' });
4881
+ }
4882
+ if (!fs.existsSync(targetDir)) {
4883
+ return res.json({ success: true, message: 'Directory does not exist', path: targetDir });
4884
+ }
4885
+ try {
4886
+ fs.rmSync(targetDir, { recursive: true, force: true });
4887
+ console.log(`[api/repos/remove] Removed directory: ${targetDir}`);
4888
+ res.json({ success: true, path: targetDir });
4889
+ }
4890
+ catch (err) {
4891
+ console.error('[api/repos/remove] Remove failed:', err.message);
4892
+ res.status(500).json({ success: false, error: err.message || 'Remove failed' });
4893
+ }
4894
+ });
4843
4895
  /**
4844
4896
  * POST /api/spawn/architect - Spawn an Architect agent for bridge mode
4845
4897
  * Body: { cli?: string }
@@ -5781,12 +5833,15 @@ Start by greeting the project leads and asking for status updates.`;
5781
5833
  }
5782
5834
  return {};
5783
5835
  }
5784
- // Watch for changes
5836
+ // Watch for changes - poll as a safety net for DB-backed storage mode.
5837
+ // Real-time updates are already handled by explicit broadcastData() calls
5838
+ // at every data mutation point (message send, spawn, release, cwd update, etc.).
5839
+ // This interval only catches external/indirect changes (presence, DB edits).
5785
5840
  if (storage) {
5786
5841
  setInterval(() => {
5787
5842
  broadcastData().catch((err) => console.error('Broadcast failed', err));
5788
5843
  broadcastBridgeData().catch((err) => console.error('Bridge broadcast failed', err));
5789
- }, 1000);
5844
+ }, 5000);
5790
5845
  }
5791
5846
  else {
5792
5847
  let fsWait = null;