@agent-relay/dashboard-server 2.0.66-beta.1 → 2.0.67

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 (85) hide show
  1. package/dist/mocks/fixtures.d.ts +66 -2
  2. package/dist/mocks/fixtures.d.ts.map +1 -1
  3. package/dist/mocks/fixtures.js +82 -2
  4. package/dist/mocks/fixtures.js.map +1 -1
  5. package/dist/mocks/routes.d.ts.map +1 -1
  6. package/dist/mocks/routes.js +311 -77
  7. package/dist/mocks/routes.js.map +1 -1
  8. package/dist/server.d.ts.map +1 -1
  9. package/dist/server.js +108 -24
  10. package/dist/server.js.map +1 -1
  11. package/out/404.html +1 -1
  12. package/out/_next/static/chunks/118-4c8241b0218335de.js +1 -0
  13. package/out/_next/static/chunks/{202-6cfbf8339f05e5ef.js → 202-fc0763dd7488e58f.js} +1 -1
  14. package/out/_next/static/chunks/259-b560f20df53128e5.js +1 -0
  15. package/out/_next/static/chunks/285-1efcdae8bd9b3272.js +1 -0
  16. package/out/_next/static/chunks/722-85011b58b9caf88b.js +1 -0
  17. package/out/_next/static/chunks/994-0ce5f1d759089504.js +1 -0
  18. package/out/_next/static/chunks/app/app/[[...slug]]/page-589620c567f85400.js +1 -0
  19. package/out/_next/static/chunks/app/{page-ba281b017e148cd6.js → page-5c60a00d938ac40a.js} +1 -1
  20. package/out/_next/static/chunks/app/providers/page-394875a22b5ba7ce.js +1 -0
  21. package/out/_next/static/chunks/app/providers/setup/[provider]/page-f058bf6696242d7b.js +1 -0
  22. package/out/_next/static/css/{fd373f99378195fc.css → 4c58d9cf493aa626.css} +1 -1
  23. package/out/about.html +2 -2
  24. package/out/about.txt +1 -1
  25. package/out/app/onboarding.html +1 -1
  26. package/out/app/onboarding.txt +1 -1
  27. package/out/app.html +1 -1
  28. package/out/app.txt +2 -2
  29. package/out/blog/go-to-bed-wake-up-to-a-finished-product.html +2 -2
  30. package/out/blog/go-to-bed-wake-up-to-a-finished-product.txt +1 -1
  31. package/out/blog/let-them-cook-multi-agent-orchestration.html +2 -2
  32. package/out/blog/let-them-cook-multi-agent-orchestration.txt +1 -1
  33. package/out/blog.html +2 -2
  34. package/out/blog.txt +1 -1
  35. package/out/careers.html +2 -2
  36. package/out/careers.txt +1 -1
  37. package/out/changelog.html +2 -2
  38. package/out/changelog.txt +1 -1
  39. package/out/cloud/link.html +1 -1
  40. package/out/cloud/link.txt +2 -2
  41. package/out/complete-profile.html +2 -2
  42. package/out/complete-profile.txt +1 -1
  43. package/out/connect-repos.html +1 -1
  44. package/out/connect-repos.txt +2 -2
  45. package/out/contact.html +2 -2
  46. package/out/contact.txt +1 -1
  47. package/out/docs.html +2 -2
  48. package/out/docs.txt +1 -1
  49. package/out/history.html +1 -1
  50. package/out/history.txt +2 -2
  51. package/out/index.html +1 -1
  52. package/out/index.txt +2 -2
  53. package/out/login.html +2 -2
  54. package/out/login.txt +2 -2
  55. package/out/metrics.html +1 -1
  56. package/out/metrics.txt +2 -2
  57. package/out/pricing.html +2 -2
  58. package/out/pricing.txt +1 -1
  59. package/out/privacy.html +2 -2
  60. package/out/privacy.txt +1 -1
  61. package/out/providers/setup/claude.html +1 -1
  62. package/out/providers/setup/claude.txt +2 -2
  63. package/out/providers/setup/codex.html +1 -1
  64. package/out/providers/setup/codex.txt +2 -2
  65. package/out/providers/setup/cursor.html +1 -1
  66. package/out/providers/setup/cursor.txt +2 -2
  67. package/out/providers.html +1 -1
  68. package/out/providers.txt +2 -2
  69. package/out/security.html +2 -2
  70. package/out/security.txt +1 -1
  71. package/out/signup.html +2 -2
  72. package/out/signup.txt +2 -2
  73. package/out/terms.html +2 -2
  74. package/out/terms.txt +1 -1
  75. package/package.json +10 -10
  76. package/out/_next/static/chunks/118-b821e49d30a9f6af.js +0 -1
  77. package/out/_next/static/chunks/259-141fb39611979082.js +0 -1
  78. package/out/_next/static/chunks/535-cecb00f34c2ed9ba.js +0 -1
  79. package/out/_next/static/chunks/722-af84568996237c02.js +0 -1
  80. package/out/_next/static/chunks/994-e927457424324a78.js +0 -1
  81. package/out/_next/static/chunks/app/app/[[...slug]]/page-bba8842697e6192c.js +0 -1
  82. package/out/_next/static/chunks/app/providers/page-59e92abba4f8d895.js +0 -1
  83. package/out/_next/static/chunks/app/providers/setup/[provider]/page-c667546c4902f1b0.js +0 -1
  84. /package/out/_next/static/{g8CQjGCOpPwFcSToizDmH → UFF2wGBwSkKsayPv0zus_}/_buildManifest.js +0 -0
  85. /package/out/_next/static/{g8CQjGCOpPwFcSToizDmH → UFF2wGBwSkKsayPv0zus_}/_ssgManifest.js +0 -0
package/dist/server.js CHANGED
@@ -5,7 +5,7 @@ import path from 'path';
5
5
  import fs from 'fs';
6
6
  import os from 'os';
7
7
  import crypto from 'crypto';
8
- import { exec } from 'child_process';
8
+ import { exec, execFile } from 'child_process';
9
9
  import { fileURLToPath } from 'url';
10
10
  import { createStorageAdapter } from '@agent-relay/storage/adapter';
11
11
  import { RelayClient } from '@agent-relay/sdk';
@@ -642,6 +642,9 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
642
642
  clearInterval(bridgePingInterval);
643
643
  });
644
644
  const onlineUsers = new Map();
645
+ // Track cwd per spawned agent (name -> cwd)
646
+ // This is set when /api/spawn is called and included in /api/spawned responses
647
+ const agentCwdMap = new Map();
645
648
  // Validation helpers for presence
646
649
  const isValidUsername = (username) => {
647
650
  if (typeof username !== 'string')
@@ -1834,7 +1837,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
1834
1837
  // Ignore errors reading processing state - it's optional
1835
1838
  }
1836
1839
  }
1837
- // Mark spawned agents with isSpawned flag, team, and model
1840
+ // Mark spawned agents with isSpawned flag, team, model, and cwd
1838
1841
  if (spawnReader) {
1839
1842
  const activeWorkers = spawnReader.getActiveWorkers();
1840
1843
  for (const worker of activeWorkers) {
@@ -1844,6 +1847,12 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
1844
1847
  if (worker.team) {
1845
1848
  agent.team = worker.team;
1846
1849
  }
1850
+ // Inject cwd from agentCwdMap (set during /api/spawn) or from worker info
1851
+ // (set by SpawnManager for relay-protocol spawns that bypass /api/spawn)
1852
+ const workerCwd = agentCwdMap.get(worker.name) || worker.cwd;
1853
+ if (workerCwd) {
1854
+ agent.cwd = workerCwd;
1855
+ }
1847
1856
  // Extract model from spawn command (e.g., "codex --model gpt-5.2-codex" → "gpt-5.2-codex")
1848
1857
  if (worker.cli) {
1849
1858
  // Support both `--model foo` and `--model=foo`
@@ -1855,6 +1864,14 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
1855
1864
  }
1856
1865
  }
1857
1866
  }
1867
+ // Inject cwd from agentCwdMap for agents not in spawner's active workers
1868
+ // (e.g., agents that connected before spawner tracked them, or after restarts)
1869
+ for (const [name, cwd] of agentCwdMap) {
1870
+ const agent = agentsMap.get(name);
1871
+ if (agent && !agent.cwd) {
1872
+ agent.cwd = cwd;
1873
+ }
1874
+ }
1858
1875
  // Also check workers.json for externally-spawned workers (e.g., from agentswarm)
1859
1876
  // These workers have log files but weren't spawned by the dashboard's spawner
1860
1877
  const workersJsonPath = path.join(teamDir, 'workers.json');
@@ -1872,6 +1889,21 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
1872
1889
  // Ignore errors reading workers.json
1873
1890
  }
1874
1891
  }
1892
+ // Mark relay-protocol spawned agents (spawned by other agents, not via dashboard /api/spawn)
1893
+ // These agents have log files in the team directory but aren't tracked by agentCwdMap
1894
+ if (spawnReader) {
1895
+ for (const [name, agent] of agentsMap) {
1896
+ if (agent.isSpawned)
1897
+ continue;
1898
+ if (onlineUsers.has(name) || name === 'Dashboard')
1899
+ continue;
1900
+ // Check if there's a log file for this agent (indicates it was spawned)
1901
+ const logPath = path.join(teamDir, `${name}.log`);
1902
+ if (fs.existsSync(logPath)) {
1903
+ agent.isSpawned = true;
1904
+ }
1905
+ }
1906
+ }
1875
1907
  // Set team from teams.json for agents that don't have a team yet
1876
1908
  // This ensures agents defined in teams.json are associated with their team
1877
1909
  // even if they weren't spawned via auto-spawn
@@ -4167,26 +4199,9 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
4167
4199
  });
4168
4200
  }
4169
4201
  }
4170
- // Also check agents.json for registered agents that may not be spawned
4171
- const agentsPath = path.join(teamDir, 'agents.json');
4172
- if (fs.existsSync(agentsPath)) {
4173
- const data = JSON.parse(fs.readFileSync(agentsPath, 'utf-8'));
4174
- const registeredAgents = data.agents || [];
4175
- for (const agent of registeredAgents) {
4176
- if (!agents.find(a => a.name === agent.name)) {
4177
- // Check if recently active (within 30 seconds)
4178
- const lastSeen = agent.lastSeen ? new Date(agent.lastSeen).getTime() : 0;
4179
- const isActive = Date.now() - lastSeen < 30000;
4180
- if (isActive) {
4181
- agents.push({
4182
- name: agent.name,
4183
- status: 'active',
4184
- alertLevel: 'normal',
4185
- });
4186
- }
4187
- }
4188
- }
4189
- }
4202
+ // Note: We only show spawned agents with actual PIDs in memory metrics.
4203
+ // Human users and non-process entries from agents.json are excluded since
4204
+ // they don't have memory usage to track.
4190
4205
  res.json({
4191
4206
  agents,
4192
4207
  system: {
@@ -4664,6 +4679,20 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
4664
4679
  const online = isAgentOnline(name);
4665
4680
  res.json({ name, online });
4666
4681
  });
4682
+ /**
4683
+ * PUT /api/agents/:name/cwd - Register an agent's working directory
4684
+ * Used by relay-pty-orchestrator after daemon socket spawns (which bypass /api/spawn).
4685
+ */
4686
+ app.put('/api/agents/:name/cwd', (req, res) => {
4687
+ const { name } = req.params;
4688
+ const { cwd } = req.body || {};
4689
+ if (!cwd || typeof cwd !== 'string') {
4690
+ return res.status(400).json({ error: 'Missing required field: cwd' });
4691
+ }
4692
+ agentCwdMap.set(name, cwd);
4693
+ broadcastData().catch(() => { });
4694
+ res.json({ success: true, name, cwd });
4695
+ });
4667
4696
  // ===== Agent Spawn API =====
4668
4697
  /**
4669
4698
  * POST /api/spawn - Spawn a new agent
@@ -4683,6 +4712,8 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
4683
4712
  error: 'Missing required field: name',
4684
4713
  });
4685
4714
  }
4715
+ // Inherit spawner's cwd if no explicit cwd provided (for nested/agent-to-agent spawns)
4716
+ const effectiveCwd = cwd || (spawnerName ? agentCwdMap.get(spawnerName) : undefined);
4686
4717
  try {
4687
4718
  let result;
4688
4719
  if (useExternalSpawnManager) {
@@ -4702,7 +4733,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
4702
4733
  cli,
4703
4734
  task,
4704
4735
  team: team || undefined,
4705
- cwd: cwd || undefined,
4736
+ cwd: effectiveCwd || undefined,
4706
4737
  interactive,
4707
4738
  shadowMode,
4708
4739
  shadowAgent,
@@ -4722,7 +4753,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
4722
4753
  task,
4723
4754
  team: team || undefined,
4724
4755
  spawnerName: spawnerName || undefined,
4725
- cwd: cwd || undefined,
4756
+ cwd: effectiveCwd || undefined,
4726
4757
  interactive,
4727
4758
  shadowMode,
4728
4759
  shadowAgent,
@@ -4735,6 +4766,10 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
4735
4766
  result = await spawner.spawn(request);
4736
4767
  }
4737
4768
  if (result.success) {
4769
+ // Track cwd for this agent so /api/spawned can return it
4770
+ if (effectiveCwd) {
4771
+ agentCwdMap.set(name, effectiveCwd);
4772
+ }
4738
4773
  // Broadcast update to WebSocket clients
4739
4774
  broadcastData().catch(() => { });
4740
4775
  // Broadcast agent_spawned event to activity feed
@@ -4758,6 +4793,53 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
4758
4793
  });
4759
4794
  }
4760
4795
  });
4796
+ /**
4797
+ * POST /api/repos/clone - Clone a repo into the workspace directory
4798
+ * Body: { fullName: "Owner/RepoName" }
4799
+ * Used by cloud API to hot-clone repos added to a running workspace.
4800
+ */
4801
+ app.post('/api/repos/clone', async (req, res) => {
4802
+ const { fullName } = req.body;
4803
+ if (!fullName || typeof fullName !== 'string' || !fullName.includes('/')) {
4804
+ return res.status(400).json({ success: false, error: 'fullName is required (e.g., "Owner/RepoName")' });
4805
+ }
4806
+ // Validate format: "Owner/RepoName" with safe characters only
4807
+ if (!/^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/.test(fullName)) {
4808
+ return res.status(400).json({ success: false, error: 'Invalid repository name format' });
4809
+ }
4810
+ const repoName = fullName.split('/').pop();
4811
+ const workspaceDir = process.env.WORKSPACE_DIR || path.dirname(projectRoot || dataDir);
4812
+ const targetDir = path.join(workspaceDir, repoName);
4813
+ // Idempotent: skip if already cloned
4814
+ if (fs.existsSync(targetDir)) {
4815
+ return res.json({ success: true, message: 'Already cloned', path: targetDir });
4816
+ }
4817
+ // Use plain HTTPS URL - git credential helper handles authentication.
4818
+ // The credential helper (git-credential-relay) fetches per-repo tokens from
4819
+ // the cloud API, which correctly resolves installation tokens for private repos.
4820
+ const cloneUrl = `https://github.com/${fullName}.git`;
4821
+ try {
4822
+ // Use execFile to avoid shell injection
4823
+ await new Promise((resolve, reject) => {
4824
+ execFile('git', ['clone', cloneUrl, targetDir], { timeout: 120000 }, (error, _stdout, stderr) => {
4825
+ if (error) {
4826
+ reject(new Error(stderr || error.message));
4827
+ }
4828
+ else {
4829
+ resolve();
4830
+ }
4831
+ });
4832
+ });
4833
+ // Mark directory as safe for git
4834
+ execFile('git', ['config', '--global', '--add', 'safe.directory', targetDir], () => { });
4835
+ res.json({ success: true, path: targetDir });
4836
+ }
4837
+ catch (err) {
4838
+ const safeMessage = (err.message || 'Clone failed');
4839
+ console.error('[api/repos/clone] Clone failed:', safeMessage);
4840
+ res.status(500).json({ success: false, error: safeMessage });
4841
+ }
4842
+ });
4761
4843
  /**
4762
4844
  * POST /api/spawn/architect - Spawn an Architect agent for bridge mode
4763
4845
  * Body: { cli?: string }
@@ -4908,6 +4990,7 @@ Start by greeting the project leads and asking for status updates.`;
4908
4990
  spawnedAt: worker.spawnedAt,
4909
4991
  task: worker.task,
4910
4992
  team: worker.team,
4993
+ cwd: agentCwdMap.get(worker.name) || worker.cwd,
4911
4994
  source: 'spawner',
4912
4995
  });
4913
4996
  }
@@ -4978,6 +5061,7 @@ Start by greeting the project leads and asking for status updates.`;
4978
5061
  released = await spawner.release(name);
4979
5062
  }
4980
5063
  if (released) {
5064
+ agentCwdMap.delete(name);
4981
5065
  broadcastData().catch(() => { });
4982
5066
  // Broadcast agent_released event to activity feed
4983
5067
  broadcastPresence({