@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.
- package/README.md +182 -135
- package/package.json +1 -1
- package/src/manager/changelog.mjs +195 -0
- package/src/manager/config.mjs +1 -0
- package/src/manager/github.mjs +842 -0
- package/src/manager/main.mjs +10 -3
- package/src/manager/news.mjs +5 -5
- package/src/manager/server.mjs +70 -8
- package/src/manager/session-manager.mjs +32 -3
- package/src/manager/session.mjs +4 -1
- package/web/app.js +1005 -11
- package/web/art/art.css +374 -159
- package/web/art/characters/anthropic/idle.avif +0 -0
- package/web/art/characters/anthropic/idle.webp +0 -0
- package/web/art/characters/anthropic/locked.avif +0 -0
- package/web/art/characters/anthropic/locked.webp +0 -0
- package/web/art/characters/anthropic/working.avif +0 -0
- package/web/art/characters/anthropic/working.webp +0 -0
- package/web/art/characters/google/idle.avif +0 -0
- package/web/art/characters/google/idle.webp +0 -0
- package/web/art/characters/google/locked.avif +0 -0
- package/web/art/characters/google/locked.webp +0 -0
- package/web/art/characters/google/working.avif +0 -0
- package/web/art/characters/google/working.webp +0 -0
- package/web/art/characters/openai/idle.avif +0 -0
- package/web/art/characters/openai/idle.webp +0 -0
- package/web/art/characters/openai/locked.avif +0 -0
- package/web/art/characters/openai/locked.webp +0 -0
- package/web/art/characters/openai/working.avif +0 -0
- package/web/art/characters/openai/working.webp +0 -0
- package/web/art/characters/shell/idle.avif +0 -0
- package/web/art/characters/shell/idle.webp +0 -0
- package/web/art/characters/shell/locked.avif +0 -0
- package/web/art/characters/shell/locked.webp +0 -0
- package/web/art/characters/shell/working.avif +0 -0
- package/web/art/characters/shell/working.webp +0 -0
- package/web/art/characters/xai/idle.avif +0 -0
- package/web/art/characters/xai/idle.webp +0 -0
- package/web/art/characters/xai/locked.avif +0 -0
- package/web/art/characters/xai/locked.webp +0 -0
- package/web/art/characters/xai/working.avif +0 -0
- package/web/art/characters/xai/working.webp +0 -0
- package/web/art/icons/anthropic.avif +0 -0
- package/web/art/icons/anthropic.webp +0 -0
- package/web/art/icons/google.avif +0 -0
- package/web/art/icons/google.webp +0 -0
- package/web/art/icons/openai.avif +0 -0
- package/web/art/icons/openai.webp +0 -0
- package/web/art/icons/shell.avif +0 -0
- package/web/art/icons/shell.webp +0 -0
- package/web/art/icons/xai.avif +0 -0
- package/web/art/icons/xai.webp +0 -0
- package/web/art/page-light.avif +0 -0
- package/web/art/page-light.webp +0 -0
- package/web/art/page.avif +0 -0
- package/web/art/page.webp +0 -0
- package/web/art/ui/empty-state.avif +0 -0
- package/web/art/ui/empty-state.webp +0 -0
- package/web/art/ui/favicon.png +0 -0
- package/web/art/ui/frame-ornate.png +0 -0
- package/web/art/ui/guild-crest.png +0 -0
- package/web/fonts/OFL.txt +93 -0
- package/web/fonts/cinzel.woff2 +0 -0
- package/web/index.html +101 -15
- package/web/styles.css +240 -71
- package/web/art/characters/anthropic/idle.png +0 -0
- package/web/art/characters/anthropic/locked.png +0 -0
- package/web/art/characters/anthropic/working.png +0 -0
- package/web/art/characters/google/idle.png +0 -0
- package/web/art/characters/google/locked.png +0 -0
- package/web/art/characters/google/working.png +0 -0
- package/web/art/characters/openai/idle.png +0 -0
- package/web/art/characters/openai/locked.png +0 -0
- package/web/art/characters/openai/working.png +0 -0
- package/web/art/characters/shell/idle.png +0 -0
- package/web/art/characters/shell/locked.png +0 -0
- package/web/art/characters/shell/working.png +0 -0
- package/web/art/characters/xai/idle.png +0 -0
- package/web/art/characters/xai/locked.png +0 -0
- package/web/art/characters/xai/working.png +0 -0
- package/web/art/icons/anthropic.png +0 -0
- package/web/art/icons/google.png +0 -0
- package/web/art/icons/openai.png +0 -0
- package/web/art/icons/shell.png +0 -0
- package/web/art/icons/xai.png +0 -0
- package/web/art/page.png +0 -0
package/src/manager/main.mjs
CHANGED
|
@@ -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 `
|
|
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
|
|
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,
|
package/src/manager/news.mjs
CHANGED
|
@@ -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';
|
package/src/manager/server.mjs
CHANGED
|
@@ -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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
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');
|
package/src/manager/session.mjs
CHANGED
|
@@ -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,
|