@oddessentials/agent-guild 0.5.0 → 0.7.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.
Files changed (86) hide show
  1. package/README.md +182 -135
  2. package/package.json +1 -1
  3. package/src/manager/changelog.mjs +195 -0
  4. package/src/manager/config.mjs +1 -0
  5. package/src/manager/github.mjs +842 -0
  6. package/src/manager/main.mjs +10 -3
  7. package/src/manager/news.mjs +5 -5
  8. package/src/manager/server.mjs +70 -8
  9. package/src/manager/session-manager.mjs +32 -3
  10. package/src/manager/session.mjs +4 -1
  11. package/web/app.js +1005 -11
  12. package/web/art/art.css +374 -159
  13. package/web/art/characters/anthropic/idle.avif +0 -0
  14. package/web/art/characters/anthropic/idle.webp +0 -0
  15. package/web/art/characters/anthropic/locked.avif +0 -0
  16. package/web/art/characters/anthropic/locked.webp +0 -0
  17. package/web/art/characters/anthropic/working.avif +0 -0
  18. package/web/art/characters/anthropic/working.webp +0 -0
  19. package/web/art/characters/google/idle.avif +0 -0
  20. package/web/art/characters/google/idle.webp +0 -0
  21. package/web/art/characters/google/locked.avif +0 -0
  22. package/web/art/characters/google/locked.webp +0 -0
  23. package/web/art/characters/google/working.avif +0 -0
  24. package/web/art/characters/google/working.webp +0 -0
  25. package/web/art/characters/openai/idle.avif +0 -0
  26. package/web/art/characters/openai/idle.webp +0 -0
  27. package/web/art/characters/openai/locked.avif +0 -0
  28. package/web/art/characters/openai/locked.webp +0 -0
  29. package/web/art/characters/openai/working.avif +0 -0
  30. package/web/art/characters/openai/working.webp +0 -0
  31. package/web/art/characters/shell/idle.avif +0 -0
  32. package/web/art/characters/shell/idle.webp +0 -0
  33. package/web/art/characters/shell/locked.avif +0 -0
  34. package/web/art/characters/shell/locked.webp +0 -0
  35. package/web/art/characters/shell/working.avif +0 -0
  36. package/web/art/characters/shell/working.webp +0 -0
  37. package/web/art/characters/xai/idle.avif +0 -0
  38. package/web/art/characters/xai/idle.webp +0 -0
  39. package/web/art/characters/xai/locked.avif +0 -0
  40. package/web/art/characters/xai/locked.webp +0 -0
  41. package/web/art/characters/xai/working.avif +0 -0
  42. package/web/art/characters/xai/working.webp +0 -0
  43. package/web/art/icons/anthropic.avif +0 -0
  44. package/web/art/icons/anthropic.webp +0 -0
  45. package/web/art/icons/google.avif +0 -0
  46. package/web/art/icons/google.webp +0 -0
  47. package/web/art/icons/openai.avif +0 -0
  48. package/web/art/icons/openai.webp +0 -0
  49. package/web/art/icons/shell.avif +0 -0
  50. package/web/art/icons/shell.webp +0 -0
  51. package/web/art/icons/xai.avif +0 -0
  52. package/web/art/icons/xai.webp +0 -0
  53. package/web/art/page-light.avif +0 -0
  54. package/web/art/page-light.webp +0 -0
  55. package/web/art/page.avif +0 -0
  56. package/web/art/page.webp +0 -0
  57. package/web/art/ui/empty-state.avif +0 -0
  58. package/web/art/ui/empty-state.webp +0 -0
  59. package/web/art/ui/favicon.png +0 -0
  60. package/web/art/ui/frame-ornate.png +0 -0
  61. package/web/art/ui/guild-crest.png +0 -0
  62. package/web/fonts/OFL.txt +93 -0
  63. package/web/fonts/cinzel.woff2 +0 -0
  64. package/web/index.html +101 -15
  65. package/web/styles.css +240 -71
  66. package/web/art/characters/anthropic/idle.png +0 -0
  67. package/web/art/characters/anthropic/locked.png +0 -0
  68. package/web/art/characters/anthropic/working.png +0 -0
  69. package/web/art/characters/google/idle.png +0 -0
  70. package/web/art/characters/google/locked.png +0 -0
  71. package/web/art/characters/google/working.png +0 -0
  72. package/web/art/characters/openai/idle.png +0 -0
  73. package/web/art/characters/openai/locked.png +0 -0
  74. package/web/art/characters/openai/working.png +0 -0
  75. package/web/art/characters/shell/idle.png +0 -0
  76. package/web/art/characters/shell/locked.png +0 -0
  77. package/web/art/characters/shell/working.png +0 -0
  78. package/web/art/characters/xai/idle.png +0 -0
  79. package/web/art/characters/xai/locked.png +0 -0
  80. package/web/art/characters/xai/working.png +0 -0
  81. package/web/art/icons/anthropic.png +0 -0
  82. package/web/art/icons/google.png +0 -0
  83. package/web/art/icons/openai.png +0 -0
  84. package/web/art/icons/shell.png +0 -0
  85. package/web/art/icons/xai.png +0 -0
  86. package/web/art/page.png +0 -0
@@ -10,6 +10,8 @@ import { UsageMonitor } from './usage.mjs';
10
10
  import { SessionHistory } from './session-history.mjs';
11
11
  import { ModelStats } from './model-stats.mjs';
12
12
  import { NewsFeed } from './news.mjs';
13
+ import { Changelog } from './changelog.mjs';
14
+ import { GitHub } from './github.mjs';
13
15
  import { createManagerServer } from './server.mjs';
14
16
  import { SelfUpdate } from './self-update.mjs';
15
17
  import { resolveBaseEnv, pathReader } from './shell-env.mjs';
@@ -33,8 +35,8 @@ const rootDir = path.resolve(here, '../..');
33
35
 
34
36
  const VERSION_REFRESH_MS = 60 * 60 * 1000;
35
37
 
36
- /** `version` and `packageFile` stand in for the real ones in tests. */
37
- export async function startManager({ port = resolvePort(), host = DEFAULT_HOST, sessionDefaults, version = VERSION, packageFile = PACKAGE_FILE } = {}) {
38
+ /** `version`, `packageFile` and `github` (GitHub's URLs and client id) stand in for the real ones in tests. */
39
+ export async function startManager({ port = resolvePort(), host = DEFAULT_HOST, sessionDefaults, version = VERSION, packageFile = PACKAGE_FILE, github: githubOptions = {} } = {}) {
38
40
  ensureDataDir();
39
41
  const token = loadOrCreateToken();
40
42
  const baseEnv = resolveBaseEnv();
@@ -59,11 +61,13 @@ export async function startManager({ port = resolvePort(), host = DEFAULT_HOST,
59
61
 
60
62
  let api;
61
63
  const selfUpdate = new SelfUpdate({ pkg: PACKAGE_NAME, version, packageFile, registry });
62
- const manager = new SessionManager({ registry, baseEnv, getApiUrl: () => api.url, sessionDefaults, shimDir, selfUpdate });
64
+ const github = new GitHub({ dir: paths.github, registry, ...githubOptions });
65
+ const manager = new SessionManager({ registry, baseEnv, getApiUrl: () => api.url, sessionDefaults, shimDir, selfUpdate, github });
63
66
  const usage = new UsageMonitor({ registry, env: baseEnv });
64
67
  const history = new SessionHistory({ registry, env: baseEnv });
65
68
  const modelStats = new ModelStats({ registry });
66
69
  const news = new NewsFeed({ registry });
70
+ const changelog = new Changelog({ latest: () => selfUpdate.latest });
67
71
  let closing = null;
68
72
 
69
73
  const refreshVersions = () => {
@@ -82,6 +86,7 @@ export async function startManager({ port = resolvePort(), host = DEFAULT_HOST,
82
86
  if (closing) return closing;
83
87
  console.log(`[manager] ${restart ? 'restarting' : 'stopping'} (${reason}); ending ${manager.sessions.size} session(s)`);
84
88
  clearInterval(versionTimer);
89
+ github.close();
85
90
  removeRuntimeFile();
86
91
  // Sessions end before the API closes, and the last event says whether
87
92
  // every process confirmed its exit, so a client can tell a clean stop
@@ -113,6 +118,8 @@ export async function startManager({ port = resolvePort(), host = DEFAULT_HOST,
113
118
  history,
114
119
  modelStats,
115
120
  news,
121
+ changelog,
122
+ github,
116
123
  token,
117
124
  host,
118
125
  port,
@@ -14,7 +14,7 @@ const LIMITS = { news: 20, releases: 5, research: 10 };
14
14
  const TITLE_MAX = 300;
15
15
  const SUMMARY_MAX = 240;
16
16
  const RAW_MAX = 8000;
17
- const USER_AGENT = `agent-guild/${VERSION} (+https://github.com/oddessentials/agent-guild)`;
17
+ export const USER_AGENT = `agent-guild/${VERSION} (+https://github.com/oddessentials/agent-guild)`;
18
18
  const FEED_ACCEPT = 'application/rss+xml, application/atom+xml, application/xml;q=0.9, text/xml;q=0.8, */*;q=0.5';
19
19
 
20
20
  const latestRelease = (id, name, repo, provider) => ({
@@ -64,7 +64,7 @@ const NAMED = {
64
64
  uacute: 'ú', uuml: 'ü', ccedil: 'ç', ntilde: 'ñ', szlig: 'ß', Auml: 'Ä', Ouml: 'Ö', Uuml: 'Ü',
65
65
  };
66
66
 
67
- class FeedError extends Error {}
67
+ export class FeedError extends Error {}
68
68
 
69
69
  const fields = new Map();
70
70
  const attrs = new Map();
@@ -329,7 +329,7 @@ function merge(previous, fresh, limit, now) {
329
329
  return [...byKey.values()].sort((a, b) => b.time - a.time).slice(0, limit);
330
330
  }
331
331
 
332
- async function readBody(res, maxBytes) {
332
+ export async function readBody(res, maxBytes) {
333
333
  const tooLarge = () => new FeedError(`sent more than ${maxBytes / 1024 / 1024} MB`);
334
334
  if (Number(res.headers.get('content-length')) > maxBytes) throw tooLarge();
335
335
  const chunks = [];
@@ -349,12 +349,12 @@ async function readBody(res, maxBytes) {
349
349
  }
350
350
  }
351
351
 
352
- function refusal(res) {
352
+ export function refusal(res) {
353
353
  if ((res.status === 403 || res.status === 429) && res.headers.get('x-ratelimit-remaining') === '0') return 'GitHub API rate limit exceeded';
354
354
  return `HTTP ${res.status}`;
355
355
  }
356
356
 
357
- function failure(err, timeoutMs) {
357
+ export function failure(err, timeoutMs) {
358
358
  if (err instanceof FeedError) return err.message;
359
359
  if (err?.name === 'TimeoutError') return `did not answer within ${timeoutMs / 1000} seconds`;
360
360
  if (err instanceof SyntaxError) return 'sent data that could not be read';
@@ -21,6 +21,9 @@ const MIME = {
21
21
  '.css': 'text/css; charset=utf-8',
22
22
  '.svg': 'image/svg+xml',
23
23
  '.png': 'image/png',
24
+ '.avif': 'image/avif',
25
+ '.webp': 'image/webp',
26
+ '.woff2': 'font/woff2',
24
27
  '.ico': 'image/x-icon',
25
28
  '.json': 'application/json; charset=utf-8',
26
29
  };
@@ -103,6 +106,8 @@ export function createManagerServer({
103
106
  history,
104
107
  modelStats,
105
108
  news = null,
109
+ changelog = null,
110
+ github = null,
106
111
  token,
107
112
  host = '127.0.0.1',
108
113
  port = 0,
@@ -148,16 +153,27 @@ export function createManagerServer({
148
153
  }
149
154
  }
150
155
 
151
- function serveFile(res, file, { cache = false } = {}) {
152
- fs.readFile(file, (err, data) => {
153
- if (err) return sendJson(res, 404, { error: { code: 'not_found', message: 'not found' } });
154
- res.writeHead(200, {
156
+ function serveFile(req, res, file, { cache = false } = {}) {
157
+ const notFound = () => sendJson(res, 404, { error: { code: 'not_found', message: 'not found' } });
158
+ fs.stat(file, (statErr, stat) => {
159
+ if (statErr || !stat.isFile()) return notFound();
160
+ const etag = `W/"${stat.size.toString(16)}-${Math.floor(stat.mtimeMs).toString(16)}"`;
161
+ const headers = {
155
162
  ...SECURITY_HEADERS,
156
163
  'Content-Type': MIME[path.extname(file).toLowerCase()] || 'application/octet-stream',
157
164
  'Cache-Control': cache ? 'public, max-age=3600' : 'no-cache',
158
- 'Content-Length': data.length,
165
+ ETag: etag,
166
+ };
167
+ const known = String(req.headers['if-none-match'] || '').split(',').map((tag) => tag.trim());
168
+ if (known.includes(etag) || known.includes('*')) {
169
+ res.writeHead(304, headers);
170
+ return res.end();
171
+ }
172
+ fs.readFile(file, (err, data) => {
173
+ if (err) return notFound();
174
+ res.writeHead(200, { ...headers, 'Content-Length': data.length });
175
+ res.end(data);
159
176
  });
160
- res.end(data);
161
177
  });
162
178
  }
163
179
 
@@ -165,7 +181,7 @@ export function createManagerServer({
165
181
  if (req.method !== 'GET' && req.method !== 'HEAD') {
166
182
  throw new HttpError(405, 'method not allowed', 'method_not_allowed');
167
183
  }
168
- if (vendor[pathname]) return serveFile(res, vendor[pathname], { cache: true });
184
+ if (vendor[pathname]) return serveFile(req, res, vendor[pathname], { cache: true });
169
185
  let rel;
170
186
  try {
171
187
  rel = pathname === '/' ? 'index.html' : decodeURIComponent(pathname).replace(/^\/+/, '');
@@ -177,7 +193,7 @@ export function createManagerServer({
177
193
  if (file !== webDir && !file.startsWith(webDir + path.sep)) {
178
194
  throw new HttpError(404, 'not found', 'not_found');
179
195
  }
180
- return serveFile(res, file);
196
+ return serveFile(req, res, file);
181
197
  }
182
198
 
183
199
  async function handleApi(req, res, url) {
@@ -241,6 +257,10 @@ export function createManagerServer({
241
257
  if (route === '/news' && method === 'GET' && news) {
242
258
  return sendJson(res, 200, news.snapshot());
243
259
  }
260
+ if (route === '/changelog' && method === 'GET' && changelog) {
261
+ return sendJson(res, 200, changelog.snapshot());
262
+ }
263
+ if (route.startsWith('/github') && github) return handleGitHub(req, res, url, route, method);
244
264
  const historyMatch = route.match(/^\/providers\/([a-z0-9][a-z0-9_-]{0,31})\/history$/);
245
265
  if (historyMatch && method === 'GET') {
246
266
  const provider = registry.get(historyMatch[1]);
@@ -310,6 +330,45 @@ export function createManagerServer({
310
330
  throw new HttpError(404, `no route for ${method} ${url.pathname}`, 'not_found');
311
331
  }
312
332
 
333
+ async function handleGitHub(req, res, url, route, method) {
334
+ const snapshot = () => ({ github: github.snapshot() });
335
+ if (route === '/github' && method === 'GET') return sendJson(res, 200, snapshot());
336
+ if (route === '/github/sign-in' && method === 'POST') {
337
+ await github.startSignIn();
338
+ return sendJson(res, 202, snapshot());
339
+ }
340
+ if (route === '/github/sign-in' && method === 'DELETE') {
341
+ github.cancelSignIn();
342
+ return sendJson(res, 200, snapshot());
343
+ }
344
+ if (route === '/github/clone' && method === 'POST') {
345
+ const body = await readJsonBody(req);
346
+ const session = manager.clone({ account: body.account, repo: body.repo, parent: body.parent });
347
+ return sendJson(res, 201, { session: session.toJSON() });
348
+ }
349
+ const match = route.match(/^\/github\/accounts\/([^/]+)(\/repos|\/ssh)?$/);
350
+ if (match) {
351
+ const [, id, action] = match;
352
+ if (!action && method === 'DELETE') {
353
+ github.signOut(id);
354
+ return sendJson(res, 200, snapshot());
355
+ }
356
+ if (action === '/repos' && method === 'POST') {
357
+ const body = await readJsonBody(req);
358
+ return sendJson(res, 201, { repo: await github.createRepo(id, body) });
359
+ }
360
+ if (action === '/repos' && method === 'GET') {
361
+ const raw = url.searchParams.get('parent');
362
+ const parent = raw && raw.trim() ? manager.resolveCwd(raw) : null;
363
+ return sendJson(res, 200, { repos: await github.repos(id, { parent, refresh: url.searchParams.get('refresh') === '1' }) });
364
+ }
365
+ if (action === '/ssh' && method === 'POST') {
366
+ return sendJson(res, 200, { account: await github.setupSsh(id) });
367
+ }
368
+ }
369
+ throw new HttpError(404, `no route for ${method} ${url.pathname}`, 'not_found');
370
+ }
371
+
313
372
  const server = http.createServer(async (req, res) => {
314
373
  let url;
315
374
  try {
@@ -323,6 +382,7 @@ export function createManagerServer({
323
382
  if (!res.headersSent) {
324
383
  const error = { code: err.code || 'error', message: err.message };
325
384
  if (err.running !== undefined) error.running = err.running;
385
+ if (err.target !== undefined) error.target = err.target;
326
386
  sendJson(res, status, { error });
327
387
  }
328
388
  }
@@ -364,6 +424,8 @@ export function createManagerServer({
364
424
  registry.on('updated', () => broadcast({ type: 'providers.updated', providers: registry.list() }));
365
425
  selfUpdate?.on('updated', () => broadcast({ type: 'manager.upgrade', upgrade: upgradeInfo() }));
366
426
  news?.on('updated', () => broadcast({ type: 'news.updated' }));
427
+ changelog?.on('updated', () => broadcast({ type: 'changelog.updated' }));
428
+ github?.on('updated', () => broadcast({ type: 'github.updated' }));
367
429
 
368
430
  function handleEvents(ws) {
369
431
  eventClients.add(ws);
@@ -11,6 +11,7 @@ import { Session, newId, clampDimension, cleanName } from './session.mjs';
11
11
  import { prependPath } from './report-shims.mjs';
12
12
  import { CHANNEL_LABELS } from './install-channels.mjs';
13
13
  import { SELF_PROVIDER } from './self-update.mjs';
14
+ import { GITHUB_PROVIDER, dropsFromCloneEnv, parseRepo } from './github.mjs';
14
15
 
15
16
  export const MAX_SESSIONS = 32;
16
17
  const examplesDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../examples');
@@ -50,8 +51,9 @@ export class SessionManager extends EventEmitter {
50
51
  * @param {object} [opts.sessionDefaults] passed through to Session
51
52
  * @param {string|null} [opts.shimDir] folder with the agent-guild-report launchers, put first on PATH
52
53
  * @param {import('./self-update.mjs').SelfUpdate|null} [opts.selfUpdate] the manager's own upgrade
54
+ * @param {import('./github.mjs').GitHub|null} [opts.github]
53
55
  */
54
- constructor({ registry, baseEnv, getApiUrl, sessionDefaults = {}, shimDir = null, selfUpdate = null }) {
56
+ constructor({ registry, baseEnv, getApiUrl, sessionDefaults = {}, shimDir = null, selfUpdate = null, github = null }) {
55
57
  super();
56
58
  this.registry = registry;
57
59
  this.baseEnv = baseEnv;
@@ -59,6 +61,7 @@ export class SessionManager extends EventEmitter {
59
61
  this.sessionDefaults = sessionDefaults;
60
62
  this.shimDir = shimDir;
61
63
  this.selfUpdate = selfUpdate;
64
+ this.github = github;
62
65
  this.sessions = new Map();
63
66
  /** Removed sessions whose process has not exited yet. */
64
67
  this.exiting = new Set();
@@ -191,6 +194,26 @@ export class SessionManager extends EventEmitter {
191
194
  return session;
192
195
  }
193
196
 
197
+ /** Clone a GitHub repository into a folder under `parent`, in a visible session. */
198
+ clone({ account, repo, parent } = {}) {
199
+ if (!this.github) throw httpError(400, 'this manager has no GitHub integration', 'github_unavailable');
200
+ if (this.closing) throw httpError(503, 'the session manager is stopping', 'manager_stopping');
201
+ if (parent === undefined || parent === null || String(parent).trim() === '') throw httpError(400, 'parent must name the folder to clone into', 'bad_cwd');
202
+ const dir = this.resolveCwd(parent);
203
+ const target = path.join(dir, parseRepo(repo).name);
204
+ for (const s of this.sessions.values()) {
205
+ if (s.status === 'running' && s.task === 'clone' && s.clone?.path === target) {
206
+ throw httpError(409, `${s.clone.repo} is already being cloned into ${target}`, 'clone_in_progress');
207
+ }
208
+ }
209
+ const spec = this.github.cloneSpec({ accountId: account, repo, parent: dir });
210
+ return this._spawn({
211
+ provider: GITHUB_PROVIDER, description: GITHUB_PROVIDER, spawnSpec: spec.spawnSpec, cwd: dir,
212
+ name: `Clone ${spec.fullName}`, task: 'clone', extraEnv: spec.env, dropEnv: dropsFromCloneEnv,
213
+ clone: { repo: spec.fullName, path: spec.target, accountId: spec.account.id },
214
+ });
215
+ }
216
+
194
217
  runningFor(providerId) {
195
218
  let n = 0;
196
219
  for (const s of this.sessions.values()) if (s.status === 'running' && s.task === null && s.provider.id === providerId) n++;
@@ -204,7 +227,10 @@ export class SessionManager extends EventEmitter {
204
227
  return n;
205
228
  }
206
229
 
207
- _spawn({ provider, description = this.registry.describe(provider), spawnSpec, cwd, cols, rows, name, resume = null, task = null, installKind = null, account = null }) {
230
+ _spawn({
231
+ provider, description = this.registry.describe(provider), spawnSpec, cwd, cols, rows, name, resume = null, task = null, installKind = null, account = null,
232
+ extraEnv = null, dropEnv = null, clone = null,
233
+ }) {
208
234
  if (this.closing) throw httpError(503, 'the session manager is stopping', 'manager_stopping');
209
235
  if (this.sessions.size >= MAX_SESSIONS) {
210
236
  throw httpError(429, `session limit reached (${MAX_SESSIONS}); remove finished sessions first`, 'too_many_sessions');
@@ -214,7 +240,7 @@ export class SessionManager extends EventEmitter {
214
240
 
215
241
  // The tool's hooks run `agent-guild-report` by name, so the launchers
216
242
  // go first on PATH, after any provider PATH override.
217
- const env = prependPath(mergeEnv([this.baseEnv, provider.env, account?.env, {
243
+ let env = prependPath(mergeEnv([this.baseEnv, provider.env, account?.env, {
218
244
  TERM: 'xterm-256color',
219
245
  COLORTERM: 'truecolor',
220
246
  AGENT_GUILD_SESSION_ID: id,
@@ -228,6 +254,8 @@ export class SessionManager extends EventEmitter {
228
254
  // agent-team panes in that tmux window, outside the page, and tools
229
255
  // would tune their output to a terminal program that is not there.
230
256
  for (const key of ['TMUX', 'TMUX_PANE', 'STY', 'TERM_PROGRAM', 'TERM_PROGRAM_VERSION', 'ZELLIJ', 'ZELLIJ_SESSION_NAME', 'ZELLIJ_PANE_ID']) delete env[key];
257
+ if (dropEnv) for (const key of Object.keys(env)) if (dropEnv(key)) delete env[key];
258
+ if (extraEnv) env = mergeEnv([env, extraEnv]);
231
259
 
232
260
  let session;
233
261
  try {
@@ -245,6 +273,7 @@ export class SessionManager extends EventEmitter {
245
273
  resume,
246
274
  task,
247
275
  account: account ? { id: account.id, label: account.label } : null,
276
+ clone,
248
277
  });
249
278
  } catch (err) {
250
279
  throw httpError(500, `could not start ${provider.tool}: ${err.message}`, 'spawn_failed');
@@ -55,8 +55,9 @@ export class Session extends EventEmitter {
55
55
  * @param {number} opts.rows
56
56
  * @param {string} [opts.name]
57
57
  * @param {string|null} [opts.resume] id of the tool's own session being resumed
58
- * @param {string|null} [opts.task] "install" for a package install, "upgrade" for the manager's own, else null
58
+ * @param {string|null} [opts.task] "install" for a package install, "upgrade" for the manager's own, "clone" for a GitHub clone, else null
59
59
  * @param {{id: string, label: string}|null} [opts.account] the tool sign-in the session runs under
60
+ * @param {{repo: string, path: string, accountId: number}|null} [opts.clone] what a clone session clones, and where
60
61
  * @param {string} opts.reportToken
61
62
  * @param {number} [opts.scrollback]
62
63
  * @param {number} [opts.activityIdleMs]
@@ -71,6 +72,7 @@ export class Session extends EventEmitter {
71
72
  this.resume = opts.resume ?? null;
72
73
  this.task = opts.task ?? null;
73
74
  this.account = opts.account ?? null;
75
+ this.clone = opts.clone ?? null;
74
76
  this.cwd = opts.cwd;
75
77
  this.cols = opts.cols;
76
78
  this.rows = opts.rows;
@@ -523,6 +525,7 @@ export class Session extends EventEmitter {
523
525
  resume: this.resume,
524
526
  task: this.task,
525
527
  account: this.account,
528
+ clone: this.clone,
526
529
  pid: this.pid,
527
530
  status: this.status,
528
531
  exitCode: this.exitCode,