agent-relay 1.2.3 → 1.3.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.
Files changed (189) hide show
  1. package/.trajectories/agent-relay-322-324.md +17 -0
  2. package/.trajectories/completed/2026-01/traj_03zupyv1s7b9.json +49 -0
  3. package/.trajectories/completed/2026-01/traj_03zupyv1s7b9.md +31 -0
  4. package/.trajectories/completed/2026-01/traj_0zacdjl1g4ht.json +125 -0
  5. package/.trajectories/completed/2026-01/traj_0zacdjl1g4ht.md +62 -0
  6. package/.trajectories/completed/2026-01/traj_33iuy72sezbk.json +49 -0
  7. package/.trajectories/completed/2026-01/traj_33iuy72sezbk.md +31 -0
  8. package/.trajectories/completed/2026-01/traj_5ammh5qtvklq.json +77 -0
  9. package/.trajectories/completed/2026-01/traj_5ammh5qtvklq.md +42 -0
  10. package/.trajectories/completed/2026-01/traj_6mieijqyvaag.json +77 -0
  11. package/.trajectories/completed/2026-01/traj_6mieijqyvaag.md +42 -0
  12. package/.trajectories/completed/2026-01/traj_78ffm31jn3uk.json +77 -0
  13. package/.trajectories/completed/2026-01/traj_78ffm31jn3uk.md +42 -0
  14. package/.trajectories/completed/2026-01/traj_94gnp3k30goq.json +66 -0
  15. package/.trajectories/completed/2026-01/traj_94gnp3k30goq.md +36 -0
  16. package/.trajectories/completed/2026-01/traj_avqeghu6pz5a.json +40 -0
  17. package/.trajectories/completed/2026-01/traj_avqeghu6pz5a.md +22 -0
  18. package/.trajectories/completed/2026-01/traj_dcsp9s8y01ra.json +121 -0
  19. package/.trajectories/completed/2026-01/traj_dcsp9s8y01ra.md +29 -0
  20. package/.trajectories/completed/2026-01/traj_fhx9irlckht6.json +53 -0
  21. package/.trajectories/completed/2026-01/traj_fhx9irlckht6.md +32 -0
  22. package/.trajectories/completed/2026-01/traj_fqduidx3xbtp.json +101 -0
  23. package/.trajectories/completed/2026-01/traj_fqduidx3xbtp.md +52 -0
  24. package/.trajectories/completed/2026-01/traj_hf81ey93uz6t.json +49 -0
  25. package/.trajectories/completed/2026-01/traj_hf81ey93uz6t.md +31 -0
  26. package/.trajectories/completed/2026-01/traj_hfmki2jr9d4r.json +65 -0
  27. package/.trajectories/completed/2026-01/traj_hfmki2jr9d4r.md +37 -0
  28. package/.trajectories/completed/2026-01/traj_lq450ly148uw.json +49 -0
  29. package/.trajectories/completed/2026-01/traj_lq450ly148uw.md +31 -0
  30. package/.trajectories/completed/2026-01/traj_multi_server_arch.md +101 -0
  31. package/.trajectories/completed/2026-01/traj_psd9ob0j2ru3.json +27 -0
  32. package/.trajectories/completed/2026-01/traj_psd9ob0j2ru3.md +14 -0
  33. package/.trajectories/completed/2026-01/traj_ub8csuv3lcv4.json +53 -0
  34. package/.trajectories/completed/2026-01/traj_ub8csuv3lcv4.md +32 -0
  35. package/.trajectories/completed/2026-01/traj_uc29tlso8i9s.json +186 -0
  36. package/.trajectories/completed/2026-01/traj_uc29tlso8i9s.md +86 -0
  37. package/.trajectories/completed/2026-01/traj_ui9b4tqxoa7j.json +77 -0
  38. package/.trajectories/completed/2026-01/traj_ui9b4tqxoa7j.md +42 -0
  39. package/.trajectories/completed/2026-01/traj_v9dkdoxylyid.json +89 -0
  40. package/.trajectories/completed/2026-01/traj_v9dkdoxylyid.md +47 -0
  41. package/.trajectories/completed/2026-01/traj_xy9vifpqet80.json +65 -0
  42. package/.trajectories/completed/2026-01/traj_xy9vifpqet80.md +37 -0
  43. package/.trajectories/completed/2026-01/traj_y7aiwijyfmmv.json +49 -0
  44. package/.trajectories/completed/2026-01/traj_y7aiwijyfmmv.md +31 -0
  45. package/.trajectories/consolidate-settings-panel.md +24 -0
  46. package/.trajectories/gh-cli-user-token.md +26 -0
  47. package/.trajectories/index.json +155 -1
  48. package/deploy/workspace/codex.config.toml +15 -0
  49. package/deploy/workspace/entrypoint.sh +167 -7
  50. package/deploy/workspace/git-credential-relay +17 -2
  51. package/dist/bridge/spawner.d.ts +7 -0
  52. package/dist/bridge/spawner.js +40 -9
  53. package/dist/bridge/types.d.ts +2 -0
  54. package/dist/cli/index.js +210 -168
  55. package/dist/cloud/api/admin.d.ts +8 -0
  56. package/dist/cloud/api/admin.js +212 -0
  57. package/dist/cloud/api/auth.js +8 -0
  58. package/dist/cloud/api/billing.d.ts +0 -10
  59. package/dist/cloud/api/billing.js +248 -58
  60. package/dist/cloud/api/codex-auth-helper.d.ts +10 -4
  61. package/dist/cloud/api/codex-auth-helper.js +215 -8
  62. package/dist/cloud/api/coordinators.js +402 -0
  63. package/dist/cloud/api/daemons.js +15 -11
  64. package/dist/cloud/api/git.js +104 -17
  65. package/dist/cloud/api/github-app.js +42 -8
  66. package/dist/cloud/api/nango-auth.js +297 -16
  67. package/dist/cloud/api/onboarding.js +97 -33
  68. package/dist/cloud/api/providers.js +12 -16
  69. package/dist/cloud/api/repos.js +200 -124
  70. package/dist/cloud/api/test-helpers.js +40 -0
  71. package/dist/cloud/api/usage.js +13 -0
  72. package/dist/cloud/api/webhooks.js +1 -1
  73. package/dist/cloud/api/workspaces.d.ts +18 -0
  74. package/dist/cloud/api/workspaces.js +945 -15
  75. package/dist/cloud/config.d.ts +8 -0
  76. package/dist/cloud/config.js +15 -0
  77. package/dist/cloud/db/drizzle.d.ts +5 -2
  78. package/dist/cloud/db/drizzle.js +27 -20
  79. package/dist/cloud/db/schema.d.ts +19 -51
  80. package/dist/cloud/db/schema.js +5 -4
  81. package/dist/cloud/index.d.ts +0 -1
  82. package/dist/cloud/index.js +0 -1
  83. package/dist/cloud/provisioner/index.d.ts +93 -1
  84. package/dist/cloud/provisioner/index.js +608 -63
  85. package/dist/cloud/server.js +156 -16
  86. package/dist/cloud/services/compute-enforcement.d.ts +57 -0
  87. package/dist/cloud/services/compute-enforcement.js +175 -0
  88. package/dist/cloud/services/index.d.ts +2 -0
  89. package/dist/cloud/services/index.js +4 -0
  90. package/dist/cloud/services/intro-expiration.d.ts +55 -0
  91. package/dist/cloud/services/intro-expiration.js +211 -0
  92. package/dist/cloud/services/nango.d.ts +14 -0
  93. package/dist/cloud/services/nango.js +74 -14
  94. package/dist/cloud/services/ssh-security.d.ts +31 -0
  95. package/dist/cloud/services/ssh-security.js +63 -0
  96. package/dist/continuity/manager.d.ts +5 -0
  97. package/dist/continuity/manager.js +56 -2
  98. package/dist/daemon/api.d.ts +2 -0
  99. package/dist/daemon/api.js +214 -5
  100. package/dist/daemon/cli-auth.d.ts +13 -1
  101. package/dist/daemon/cli-auth.js +166 -47
  102. package/dist/daemon/connection.d.ts +7 -1
  103. package/dist/daemon/connection.js +15 -0
  104. package/dist/daemon/orchestrator.d.ts +2 -0
  105. package/dist/daemon/orchestrator.js +26 -0
  106. package/dist/daemon/repo-manager.d.ts +116 -0
  107. package/dist/daemon/repo-manager.js +384 -0
  108. package/dist/daemon/router.d.ts +60 -1
  109. package/dist/daemon/router.js +281 -20
  110. package/dist/daemon/user-directory.d.ts +111 -0
  111. package/dist/daemon/user-directory.js +233 -0
  112. package/dist/dashboard/out/404.html +1 -1
  113. package/dist/dashboard/out/_next/static/chunks/532-bace199897eeab37.js +9 -0
  114. package/dist/dashboard/out/_next/static/chunks/766-b54f0853794b78c3.js +1 -0
  115. package/dist/dashboard/out/_next/static/chunks/83-b51836037078006c.js +1 -0
  116. package/dist/dashboard/out/_next/static/chunks/891-6cd50de1224f70bb.js +1 -0
  117. package/dist/dashboard/out/_next/static/chunks/899-fc02ed79e3de4302.js +1 -0
  118. package/dist/dashboard/out/_next/static/chunks/app/app/onboarding/{page-3fdfa60e53f2810d.js → page-8553743baca53a00.js} +1 -1
  119. package/dist/dashboard/out/_next/static/chunks/app/app/page-c617745b81344f4f.js +1 -0
  120. package/dist/dashboard/out/_next/static/chunks/app/metrics/page-f829604fb75a831a.js +1 -0
  121. package/dist/dashboard/out/_next/static/chunks/app/{page-77e9c65420a06cfb.js → page-dc786c183425c2ac.js} +1 -1
  122. package/dist/dashboard/out/_next/static/chunks/app/providers/page-84322991d7244499.js +1 -0
  123. package/dist/dashboard/out/_next/static/chunks/app/providers/setup/[provider]/page-05606941a8e2be83.js +1 -0
  124. package/dist/dashboard/out/_next/static/chunks/{main-ed4e1fb6f29c34cf.js → main-2ee6beb2ae96d210.js} +1 -1
  125. package/dist/dashboard/out/_next/static/css/48a8fbe3e659080e.css +1 -0
  126. package/dist/dashboard/out/_next/static/css/fe4b28883eeff359.css +1 -0
  127. package/dist/dashboard/out/_next/static/sDcbGRTYLcpPvyTs_rsNb/_ssgManifest.js +1 -0
  128. package/dist/dashboard/out/app/onboarding.html +1 -1
  129. package/dist/dashboard/out/app/onboarding.txt +3 -3
  130. package/dist/dashboard/out/app.html +1 -1
  131. package/dist/dashboard/out/app.txt +3 -3
  132. package/dist/dashboard/out/apple-icon.png +0 -0
  133. package/dist/dashboard/out/connect-repos.html +1 -1
  134. package/dist/dashboard/out/connect-repos.txt +2 -2
  135. package/dist/dashboard/out/history.html +1 -1
  136. package/dist/dashboard/out/history.txt +2 -2
  137. package/dist/dashboard/out/index.html +1 -1
  138. package/dist/dashboard/out/index.txt +3 -3
  139. package/dist/dashboard/out/login.html +2 -2
  140. package/dist/dashboard/out/login.txt +2 -2
  141. package/dist/dashboard/out/metrics.html +1 -1
  142. package/dist/dashboard/out/metrics.txt +3 -3
  143. package/dist/dashboard/out/pricing.html +2 -2
  144. package/dist/dashboard/out/pricing.txt +3 -3
  145. package/dist/dashboard/out/providers/setup/claude.html +1 -0
  146. package/dist/dashboard/out/providers/setup/claude.txt +8 -0
  147. package/dist/dashboard/out/providers/setup/codex.html +1 -0
  148. package/dist/dashboard/out/providers/setup/codex.txt +8 -0
  149. package/dist/dashboard/out/providers.html +1 -1
  150. package/dist/dashboard/out/providers.txt +3 -3
  151. package/dist/dashboard/out/signup.html +2 -2
  152. package/dist/dashboard/out/signup.txt +2 -2
  153. package/dist/dashboard-server/server.js +316 -12
  154. package/dist/dashboard-server/user-bridge.d.ts +103 -0
  155. package/dist/dashboard-server/user-bridge.js +189 -0
  156. package/dist/protocol/channels.d.ts +205 -0
  157. package/dist/protocol/channels.js +154 -0
  158. package/dist/protocol/types.d.ts +13 -1
  159. package/dist/resiliency/provider-context.js +2 -0
  160. package/dist/shared/cli-auth-config.d.ts +19 -0
  161. package/dist/shared/cli-auth-config.js +58 -2
  162. package/dist/utils/agent-config.js +1 -1
  163. package/dist/wrapper/auth-detection.d.ts +49 -0
  164. package/dist/wrapper/auth-detection.js +192 -0
  165. package/dist/wrapper/base-wrapper.d.ts +153 -0
  166. package/dist/wrapper/base-wrapper.js +393 -0
  167. package/dist/wrapper/client.d.ts +7 -1
  168. package/dist/wrapper/client.js +3 -0
  169. package/dist/wrapper/index.d.ts +1 -0
  170. package/dist/wrapper/index.js +4 -3
  171. package/dist/wrapper/pty-wrapper.d.ts +62 -84
  172. package/dist/wrapper/pty-wrapper.js +154 -180
  173. package/dist/wrapper/tmux-wrapper.d.ts +41 -66
  174. package/dist/wrapper/tmux-wrapper.js +90 -134
  175. package/package.json +4 -2
  176. package/scripts/postinstall.js +11 -155
  177. package/scripts/test-interactive-terminal.sh +248 -0
  178. package/dist/cloud/vault/index.d.ts +0 -76
  179. package/dist/cloud/vault/index.js +0 -219
  180. package/dist/dashboard/out/_next/static/chunks/699-3b1cd6618a45d259.js +0 -1
  181. package/dist/dashboard/out/_next/static/chunks/724-2dae7627550ab88f.js +0 -9
  182. package/dist/dashboard/out/_next/static/chunks/766-1f2dd8cb7f766b0b.js +0 -1
  183. package/dist/dashboard/out/_next/static/chunks/app/app/page-e6381e5a6e1fbcfd.js +0 -1
  184. package/dist/dashboard/out/_next/static/chunks/app/metrics/page-67a3e98d9a43a6ed.js +0 -1
  185. package/dist/dashboard/out/_next/static/chunks/app/providers/page-e88bc117ef7671c3.js +0 -1
  186. package/dist/dashboard/out/_next/static/css/29852f26181969a0.css +0 -1
  187. package/dist/dashboard/out/_next/static/css/7c3ae9e8617d42a5.css +0 -1
  188. package/dist/dashboard/out/_next/static/wPgKJtcOmTFLpUncDg16A/_ssgManifest.js +0 -1
  189. /package/dist/dashboard/out/_next/static/{wPgKJtcOmTFLpUncDg16A → sDcbGRTYLcpPvyTs_rsNb}/_buildManifest.js +0 -0
@@ -23,11 +23,14 @@ export class Connection {
23
23
  config;
24
24
  _state = 'CONNECTING';
25
25
  _agentName;
26
+ _entityType;
26
27
  _cli;
27
28
  _program;
28
29
  _model;
29
30
  _task;
30
31
  _workingDirectory;
32
+ _displayName;
33
+ _avatarUrl;
31
34
  _sessionId;
32
35
  _resumeToken;
33
36
  _isResumed = false;
@@ -58,6 +61,9 @@ export class Connection {
58
61
  get agentName() {
59
62
  return this._agentName;
60
63
  }
64
+ get entityType() {
65
+ return this._entityType;
66
+ }
61
67
  get cli() {
62
68
  return this._cli;
63
69
  }
@@ -73,6 +79,12 @@ export class Connection {
73
79
  get workingDirectory() {
74
80
  return this._workingDirectory;
75
81
  }
82
+ get displayName() {
83
+ return this._displayName;
84
+ }
85
+ get avatarUrl() {
86
+ return this._avatarUrl;
87
+ }
76
88
  get sessionId() {
77
89
  return this._sessionId;
78
90
  }
@@ -133,11 +145,14 @@ export class Connection {
133
145
  return;
134
146
  }
135
147
  this._agentName = envelope.payload.agent;
148
+ this._entityType = envelope.payload.entityType;
136
149
  this._cli = envelope.payload.cli;
137
150
  this._program = envelope.payload.program;
138
151
  this._model = envelope.payload.model;
139
152
  this._task = envelope.payload.task;
140
153
  this._workingDirectory = envelope.payload.workingDirectory;
154
+ this._displayName = envelope.payload.displayName;
155
+ this._avatarUrl = envelope.payload.avatarUrl;
141
156
  // Check for session resume
142
157
  const resumeToken = envelope.payload.session?.resume_token;
143
158
  if (resumeToken) {
@@ -26,6 +26,8 @@ export declare class Orchestrator extends EventEmitter {
26
26
  private sessions;
27
27
  private supervisor;
28
28
  private workspacesFile;
29
+ private clientAlive;
30
+ private pingInterval?;
29
31
  constructor(config?: Partial<OrchestratorConfig>);
30
32
  /**
31
33
  * Start the orchestrator
@@ -39,6 +39,9 @@ export class Orchestrator extends EventEmitter {
39
39
  contextPersistence: { enabled: true, autoInjectOnRestart: true },
40
40
  });
41
41
  workspacesFile;
42
+ // Track alive status for ping/pong keepalive
43
+ clientAlive = new WeakMap();
44
+ pingInterval;
42
45
  constructor(config = {}) {
43
46
  super();
44
47
  this.config = { ...DEFAULT_CONFIG, ...config };
@@ -73,6 +76,18 @@ export class Orchestrator extends EventEmitter {
73
76
  // Setup WebSocket
74
77
  this.wss = new WebSocketServer({ server: this.server });
75
78
  this.wss.on('connection', (ws, req) => this.handleWebSocket(ws, req));
79
+ // Setup ping/pong keepalive (30 second interval)
80
+ this.pingInterval = setInterval(() => {
81
+ this.wss?.clients.forEach((ws) => {
82
+ if (this.clientAlive.get(ws) === false) {
83
+ logger.info('WebSocket client unresponsive, closing');
84
+ ws.terminate();
85
+ return;
86
+ }
87
+ this.clientAlive.set(ws, false);
88
+ ws.ping();
89
+ });
90
+ }, 30000);
76
91
  return new Promise((resolve) => {
77
92
  this.server.listen(this.config.port, this.config.host, () => {
78
93
  logger.info('Orchestrator started', {
@@ -87,6 +102,11 @@ export class Orchestrator extends EventEmitter {
87
102
  */
88
103
  async stop() {
89
104
  logger.info('Stopping orchestrator');
105
+ // Clear ping interval
106
+ if (this.pingInterval) {
107
+ clearInterval(this.pingInterval);
108
+ this.pingInterval = undefined;
109
+ }
90
110
  // Stop all workspace daemons
91
111
  for (const [id] of this.workspaces) {
92
112
  await this.stopWorkspaceDaemon(id);
@@ -501,6 +521,12 @@ export class Orchestrator extends EventEmitter {
501
521
  */
502
522
  handleWebSocket(ws, _req) {
503
523
  logger.info('WebSocket client connected');
524
+ // Mark client as alive for ping/pong keepalive
525
+ this.clientAlive.set(ws, true);
526
+ // Handle pong responses
527
+ ws.on('pong', () => {
528
+ this.clientAlive.set(ws, true);
529
+ });
504
530
  const session = {
505
531
  userId: 'anonymous',
506
532
  githubUsername: 'anonymous',
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Workspace Repository Manager
3
+ *
4
+ * Manages repository cloning, updating, and removal for workspace containers.
5
+ * Uses a file-based tracking system (repos.json) to persist state across restarts.
6
+ *
7
+ * This replaces the static REPOSITORIES env var approach, allowing dynamic
8
+ * repo management without workspace restart.
9
+ */
10
+ import { EventEmitter } from 'events';
11
+ export interface RepoInfo {
12
+ /** Full GitHub repo name (e.g., "owner/repo") */
13
+ fullName: string;
14
+ /** Local directory name */
15
+ localName: string;
16
+ /** Absolute path to the cloned repo */
17
+ path: string;
18
+ /** Current status */
19
+ status: 'cloned' | 'cloning' | 'error' | 'removed';
20
+ /** Last sync timestamp */
21
+ lastSynced?: string;
22
+ /** Default branch */
23
+ defaultBranch?: string;
24
+ /** Error message if status is 'error' */
25
+ error?: string;
26
+ /** When the repo was added */
27
+ addedAt: string;
28
+ }
29
+ export interface ReposConfig {
30
+ version: number;
31
+ workspaceDir: string;
32
+ repos: Record<string, RepoInfo>;
33
+ lastUpdated: string;
34
+ }
35
+ export interface SyncResult {
36
+ success: boolean;
37
+ repo: string;
38
+ action: 'cloned' | 'updated' | 'already_synced' | 'error';
39
+ path?: string;
40
+ error?: string;
41
+ }
42
+ export interface RepoManagerConfig {
43
+ workspaceDir: string;
44
+ configFile?: string;
45
+ }
46
+ export declare class RepoManager extends EventEmitter {
47
+ private workspaceDir;
48
+ private configPath;
49
+ private config;
50
+ constructor(options: RepoManagerConfig);
51
+ /**
52
+ * Load or initialize the repos config file
53
+ */
54
+ private loadConfig;
55
+ /**
56
+ * Save the config to disk
57
+ */
58
+ private saveConfig;
59
+ /**
60
+ * Get all tracked repos
61
+ */
62
+ getRepos(): RepoInfo[];
63
+ /**
64
+ * Get a specific repo by full name
65
+ */
66
+ getRepo(fullName: string): RepoInfo | null;
67
+ /**
68
+ * Sync a repository (clone if new, pull if exists)
69
+ */
70
+ syncRepo(fullName: string): Promise<SyncResult>;
71
+ /**
72
+ * Remove a repository
73
+ */
74
+ removeRepo(fullName: string, deleteFiles?: boolean): Promise<boolean>;
75
+ /**
76
+ * Sync multiple repos (e.g., from initial REPOSITORIES env var)
77
+ */
78
+ syncRepos(fullNames: string[]): Promise<SyncResult[]>;
79
+ /**
80
+ * Initialize from REPOSITORIES env var (backward compatibility)
81
+ */
82
+ initFromEnv(): Promise<SyncResult[]>;
83
+ /**
84
+ * Scan workspace directory for existing repos and register them
85
+ * This handles repos that were cloned by entrypoint.sh before daemon started
86
+ */
87
+ scanExistingRepos(): void;
88
+ /**
89
+ * Clone a repository
90
+ */
91
+ private gitClone;
92
+ /**
93
+ * Pull updates for a repository
94
+ */
95
+ private gitPull;
96
+ /**
97
+ * Get the default branch of a repo
98
+ */
99
+ private getDefaultBranch;
100
+ /**
101
+ * Mark directory as safe for git (prevents "dubious ownership" errors)
102
+ */
103
+ private markSafeDirectory;
104
+ }
105
+ /**
106
+ * Get or create the repo manager instance
107
+ */
108
+ export declare function getRepoManager(workspaceDir?: string): RepoManager;
109
+ /**
110
+ * Initialize repo manager (call at startup)
111
+ *
112
+ * 1. Scans workspace for existing repos (handles entrypoint.sh clones)
113
+ * 2. Syncs any repos from REPOSITORIES env var that aren't already cloned
114
+ */
115
+ export declare function initRepoManager(workspaceDir?: string): Promise<RepoManager>;
116
+ //# sourceMappingURL=repo-manager.d.ts.map
@@ -0,0 +1,384 @@
1
+ /**
2
+ * Workspace Repository Manager
3
+ *
4
+ * Manages repository cloning, updating, and removal for workspace containers.
5
+ * Uses a file-based tracking system (repos.json) to persist state across restarts.
6
+ *
7
+ * This replaces the static REPOSITORIES env var approach, allowing dynamic
8
+ * repo management without workspace restart.
9
+ */
10
+ import * as fs from 'fs';
11
+ import * as path from 'path';
12
+ import { execSync, spawn } from 'child_process';
13
+ import { EventEmitter } from 'events';
14
+ import { createLogger } from '../resiliency/logger.js';
15
+ const logger = createLogger('repo-manager');
16
+ const DEFAULT_CONFIG_FILE = 'repos.json';
17
+ export class RepoManager extends EventEmitter {
18
+ workspaceDir;
19
+ configPath;
20
+ config;
21
+ constructor(options) {
22
+ super();
23
+ this.workspaceDir = options.workspaceDir;
24
+ this.configPath = path.join(this.workspaceDir, options.configFile || DEFAULT_CONFIG_FILE);
25
+ this.config = this.loadConfig();
26
+ }
27
+ /**
28
+ * Load or initialize the repos config file
29
+ */
30
+ loadConfig() {
31
+ try {
32
+ if (fs.existsSync(this.configPath)) {
33
+ const data = fs.readFileSync(this.configPath, 'utf-8');
34
+ const config = JSON.parse(data);
35
+ logger.info('Loaded repo config', { repoCount: Object.keys(config.repos).length });
36
+ return config;
37
+ }
38
+ }
39
+ catch (err) {
40
+ logger.warn('Failed to load repo config, starting fresh', { error: String(err) });
41
+ }
42
+ // Initialize new config
43
+ return {
44
+ version: 1,
45
+ workspaceDir: this.workspaceDir,
46
+ repos: {},
47
+ lastUpdated: new Date().toISOString(),
48
+ };
49
+ }
50
+ /**
51
+ * Save the config to disk
52
+ */
53
+ saveConfig() {
54
+ this.config.lastUpdated = new Date().toISOString();
55
+ fs.writeFileSync(this.configPath, JSON.stringify(this.config, null, 2));
56
+ }
57
+ /**
58
+ * Get all tracked repos
59
+ */
60
+ getRepos() {
61
+ return Object.values(this.config.repos).filter(r => r.status !== 'removed');
62
+ }
63
+ /**
64
+ * Get a specific repo by full name
65
+ */
66
+ getRepo(fullName) {
67
+ const key = fullName.toLowerCase();
68
+ return this.config.repos[key] || null;
69
+ }
70
+ /**
71
+ * Sync a repository (clone if new, pull if exists)
72
+ */
73
+ async syncRepo(fullName) {
74
+ const key = fullName.toLowerCase();
75
+ const localName = path.basename(fullName);
76
+ const repoPath = path.join(this.workspaceDir, localName);
77
+ logger.info('Syncing repo', { fullName, repoPath });
78
+ // Update status to cloning
79
+ this.config.repos[key] = {
80
+ fullName,
81
+ localName,
82
+ path: repoPath,
83
+ status: 'cloning',
84
+ addedAt: this.config.repos[key]?.addedAt || new Date().toISOString(),
85
+ };
86
+ this.saveConfig();
87
+ this.emit('repo:syncing', { fullName });
88
+ try {
89
+ const gitDir = path.join(repoPath, '.git');
90
+ const exists = fs.existsSync(gitDir);
91
+ if (exists) {
92
+ // Pull existing repo
93
+ await this.gitPull(repoPath, fullName);
94
+ this.config.repos[key] = {
95
+ ...this.config.repos[key],
96
+ status: 'cloned',
97
+ lastSynced: new Date().toISOString(),
98
+ defaultBranch: this.getDefaultBranch(repoPath),
99
+ error: undefined,
100
+ };
101
+ this.saveConfig();
102
+ this.emit('repo:synced', { fullName, action: 'updated' });
103
+ logger.info('Repo updated', { fullName });
104
+ return { success: true, repo: fullName, action: 'updated', path: repoPath };
105
+ }
106
+ else {
107
+ // Clone new repo
108
+ await this.gitClone(fullName, repoPath);
109
+ this.config.repos[key] = {
110
+ ...this.config.repos[key],
111
+ status: 'cloned',
112
+ lastSynced: new Date().toISOString(),
113
+ defaultBranch: this.getDefaultBranch(repoPath),
114
+ error: undefined,
115
+ };
116
+ this.saveConfig();
117
+ // Mark directory as safe for git
118
+ this.markSafeDirectory(repoPath);
119
+ this.emit('repo:synced', { fullName, action: 'cloned' });
120
+ logger.info('Repo cloned', { fullName });
121
+ return { success: true, repo: fullName, action: 'cloned', path: repoPath };
122
+ }
123
+ }
124
+ catch (err) {
125
+ const errorMsg = err instanceof Error ? err.message : String(err);
126
+ this.config.repos[key] = {
127
+ ...this.config.repos[key],
128
+ status: 'error',
129
+ error: errorMsg,
130
+ };
131
+ this.saveConfig();
132
+ this.emit('repo:error', { fullName, error: errorMsg });
133
+ logger.error('Repo sync failed', { fullName, error: errorMsg });
134
+ return { success: false, repo: fullName, action: 'error', error: errorMsg };
135
+ }
136
+ }
137
+ /**
138
+ * Remove a repository
139
+ */
140
+ async removeRepo(fullName, deleteFiles = false) {
141
+ const key = fullName.toLowerCase();
142
+ const repo = this.config.repos[key];
143
+ if (!repo) {
144
+ logger.warn('Repo not found for removal', { fullName });
145
+ return false;
146
+ }
147
+ logger.info('Removing repo', { fullName, deleteFiles });
148
+ if (deleteFiles && fs.existsSync(repo.path)) {
149
+ try {
150
+ fs.rmSync(repo.path, { recursive: true, force: true });
151
+ logger.info('Deleted repo files', { fullName, path: repo.path });
152
+ }
153
+ catch (err) {
154
+ logger.error('Failed to delete repo files', { fullName, error: String(err) });
155
+ // Continue anyway - mark as removed in config
156
+ }
157
+ }
158
+ // Mark as removed (or delete from config entirely)
159
+ if (deleteFiles) {
160
+ delete this.config.repos[key];
161
+ }
162
+ else {
163
+ this.config.repos[key] = {
164
+ ...repo,
165
+ status: 'removed',
166
+ };
167
+ }
168
+ this.saveConfig();
169
+ this.emit('repo:removed', { fullName });
170
+ return true;
171
+ }
172
+ /**
173
+ * Sync multiple repos (e.g., from initial REPOSITORIES env var)
174
+ */
175
+ async syncRepos(fullNames) {
176
+ const results = [];
177
+ for (const fullName of fullNames) {
178
+ if (!fullName.trim())
179
+ continue;
180
+ const result = await this.syncRepo(fullName.trim());
181
+ results.push(result);
182
+ }
183
+ return results;
184
+ }
185
+ /**
186
+ * Initialize from REPOSITORIES env var (backward compatibility)
187
+ */
188
+ async initFromEnv() {
189
+ const repoList = process.env.REPOSITORIES || '';
190
+ if (!repoList.trim()) {
191
+ logger.info('No REPOSITORIES env var set, skipping initial sync');
192
+ return [];
193
+ }
194
+ const repos = repoList.split(',').map(r => r.trim()).filter(Boolean);
195
+ logger.info('Initializing repos from env', { count: repos.length });
196
+ return this.syncRepos(repos);
197
+ }
198
+ /**
199
+ * Scan workspace directory for existing repos and register them
200
+ * This handles repos that were cloned by entrypoint.sh before daemon started
201
+ */
202
+ scanExistingRepos() {
203
+ try {
204
+ const entries = fs.readdirSync(this.workspaceDir, { withFileTypes: true });
205
+ for (const entry of entries) {
206
+ if (!entry.isDirectory())
207
+ continue;
208
+ if (entry.name === DEFAULT_CONFIG_FILE || entry.name.startsWith('.'))
209
+ continue;
210
+ const repoPath = path.join(this.workspaceDir, entry.name);
211
+ const gitDir = path.join(repoPath, '.git');
212
+ if (!fs.existsSync(gitDir))
213
+ continue;
214
+ // Try to get the remote URL to determine full repo name
215
+ let fullName = entry.name; // Default to directory name
216
+ try {
217
+ const remoteUrl = execSync('git config --get remote.origin.url', {
218
+ cwd: repoPath,
219
+ encoding: 'utf-8',
220
+ stdio: ['ignore', 'pipe', 'ignore'],
221
+ }).trim();
222
+ // Parse GitHub URL: https://github.com/owner/repo.git or git@github.com:owner/repo.git
223
+ const match = remoteUrl.match(/github\.com[/:]([a-zA-Z0-9_-]+)\/([a-zA-Z0-9_.-]+?)(?:\.git)?$/);
224
+ if (match) {
225
+ fullName = `${match[1]}/${match[2]}`;
226
+ }
227
+ }
228
+ catch {
229
+ // Couldn't get remote, use directory name
230
+ }
231
+ const key = fullName.toLowerCase();
232
+ // Only register if not already tracked
233
+ if (!this.config.repos[key]) {
234
+ this.config.repos[key] = {
235
+ fullName,
236
+ localName: entry.name,
237
+ path: repoPath,
238
+ status: 'cloned',
239
+ lastSynced: new Date().toISOString(),
240
+ defaultBranch: this.getDefaultBranch(repoPath),
241
+ addedAt: new Date().toISOString(),
242
+ };
243
+ logger.info('Registered existing repo', { fullName, path: repoPath });
244
+ }
245
+ }
246
+ this.saveConfig();
247
+ }
248
+ catch (err) {
249
+ logger.warn('Failed to scan for existing repos', { error: String(err) });
250
+ }
251
+ }
252
+ /**
253
+ * Clone a repository
254
+ */
255
+ gitClone(fullName, targetPath) {
256
+ return new Promise((resolve, reject) => {
257
+ const url = `https://github.com/${fullName}.git`;
258
+ logger.info('Cloning', { url, targetPath });
259
+ const proc = spawn('git', ['clone', url, targetPath], {
260
+ stdio: ['ignore', 'pipe', 'pipe'],
261
+ env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
262
+ });
263
+ let stderr = '';
264
+ proc.stderr?.on('data', (data) => {
265
+ stderr += data.toString();
266
+ });
267
+ proc.on('close', (code) => {
268
+ if (code === 0) {
269
+ resolve();
270
+ }
271
+ else {
272
+ reject(new Error(`git clone failed (code ${code}): ${stderr}`));
273
+ }
274
+ });
275
+ proc.on('error', (err) => {
276
+ reject(err);
277
+ });
278
+ });
279
+ }
280
+ /**
281
+ * Pull updates for a repository
282
+ */
283
+ gitPull(repoPath, fullName) {
284
+ return new Promise((resolve, reject) => {
285
+ logger.info('Pulling', { repoPath });
286
+ // First update remote URL in case it changed
287
+ try {
288
+ const url = `https://github.com/${fullName}.git`;
289
+ execSync(`git remote set-url origin "${url}"`, {
290
+ cwd: repoPath,
291
+ stdio: 'ignore',
292
+ });
293
+ }
294
+ catch {
295
+ // Ignore - remote might not exist yet
296
+ }
297
+ // Fetch and pull
298
+ const proc = spawn('git', ['pull', '--ff-only'], {
299
+ cwd: repoPath,
300
+ stdio: ['ignore', 'pipe', 'pipe'],
301
+ env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
302
+ });
303
+ let stderr = '';
304
+ proc.stderr?.on('data', (data) => {
305
+ stderr += data.toString();
306
+ });
307
+ proc.on('close', (code) => {
308
+ if (code === 0) {
309
+ resolve();
310
+ }
311
+ else {
312
+ // Try fetch --all as fallback (handles diverged branches better)
313
+ try {
314
+ execSync('git fetch --all --prune', { cwd: repoPath, stdio: 'ignore' });
315
+ resolve();
316
+ }
317
+ catch {
318
+ reject(new Error(`git pull failed (code ${code}): ${stderr}`));
319
+ }
320
+ }
321
+ });
322
+ proc.on('error', (err) => {
323
+ reject(err);
324
+ });
325
+ });
326
+ }
327
+ /**
328
+ * Get the default branch of a repo
329
+ */
330
+ getDefaultBranch(repoPath) {
331
+ try {
332
+ const result = execSync('git symbolic-ref --short HEAD', {
333
+ cwd: repoPath,
334
+ encoding: 'utf-8',
335
+ stdio: ['ignore', 'pipe', 'ignore'],
336
+ });
337
+ return result.trim();
338
+ }
339
+ catch {
340
+ return 'main';
341
+ }
342
+ }
343
+ /**
344
+ * Mark directory as safe for git (prevents "dubious ownership" errors)
345
+ */
346
+ markSafeDirectory(repoPath) {
347
+ try {
348
+ execSync(`git config --global --add safe.directory "${repoPath}"`, {
349
+ stdio: 'ignore',
350
+ });
351
+ }
352
+ catch {
353
+ // Ignore errors
354
+ }
355
+ }
356
+ }
357
+ // Singleton instance
358
+ let repoManagerInstance = null;
359
+ /**
360
+ * Get or create the repo manager instance
361
+ */
362
+ export function getRepoManager(workspaceDir) {
363
+ if (!repoManagerInstance) {
364
+ const dir = workspaceDir || process.env.WORKSPACE_DIR || '/workspace';
365
+ repoManagerInstance = new RepoManager({ workspaceDir: dir });
366
+ }
367
+ return repoManagerInstance;
368
+ }
369
+ /**
370
+ * Initialize repo manager (call at startup)
371
+ *
372
+ * 1. Scans workspace for existing repos (handles entrypoint.sh clones)
373
+ * 2. Syncs any repos from REPOSITORIES env var that aren't already cloned
374
+ */
375
+ export async function initRepoManager(workspaceDir) {
376
+ const manager = getRepoManager(workspaceDir);
377
+ // First, scan for repos already cloned by entrypoint.sh
378
+ manager.scanExistingRepos();
379
+ // Then sync any additional repos from env var
380
+ // (syncRepos skips repos that are already cloned and up-to-date)
381
+ await manager.initFromEnv();
382
+ return manager;
383
+ }
384
+ //# sourceMappingURL=repo-manager.js.map