ai-agentmanager 1.4.0 → 1.6.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/src/manager.js CHANGED
@@ -8,6 +8,12 @@ import { expandHome, paths } from './config.js';
8
8
  import { DEFAULT_ENGINE, ENGINES, engineOf, installed, notInstalled } from './engines/index.js';
9
9
  import {
10
10
  DOC_KINDS,
11
+ diagramsOnly,
12
+ diagramsPrompt,
13
+ walkthroughFrom,
14
+ walkthroughHunks,
15
+ walkthroughMarkdown,
16
+ walkthroughPrompt,
11
17
  briefDir,
12
18
  briefPath,
13
19
  briefPrompt,
@@ -33,13 +39,60 @@ const SUMMARIES = {
33
39
  insights: { field: 'insights', prompt: insightsPrompt },
34
40
  ...Object.fromEntries(Object.keys(DOC_KINDS).map((k) => [k, { doc: k, prompt: (ctx) => docPrompt(k, ctx) }])),
35
41
  humanai: { doc: 'humanai', run: 'runHumanAi' },
42
+ diagrams: { doc: 'diagrams', run: 'runDiagrams' },
43
+ walkthrough: { doc: 'walkthrough', run: 'runWalkthrough' },
36
44
  };
37
45
  /** Read / write where a summary of `kind` is kept on the agent record. */
38
46
  const getSummary = (agent, kind) => (SUMMARIES[kind].doc ? agent.docs?.[kind] : agent[SUMMARIES[kind].field]);
39
47
  const summaryPatch = (agent, kind, value) =>
40
48
  SUMMARIES[kind].doc ? { docs: { ...agent.docs, [kind]: value } } : { [SUMMARIES[kind].field]: value };
49
+
50
+ const stagedDir = () => path.join(paths.uploads, 'staged');
51
+ const safeName = (name) => path.basename(String(name || 'file')).replace(/[^\w.@+-]+/g, '_').slice(-120) || 'file';
52
+
53
+ /** Staged uploads named in a request, checked to be files in the staging folder. */
54
+ function stagedFiles(list) {
55
+ if (list == null) return [];
56
+ if (!Array.isArray(list)) throw Object.assign(new Error('attachments must be a list of uploaded files'), { status: 400 });
57
+ const root = path.resolve(stagedDir()) + path.sep;
58
+ return list.slice(0, 20).map((p) => {
59
+ const file = path.resolve(String(p ?? ''));
60
+ if (!file.startsWith(root) || !fs.existsSync(file) || !fs.statSync(file).isFile()) {
61
+ throw Object.assign(new Error(`Not an uploaded file: ${path.basename(file)} (attach it again)`), { status: 400 });
62
+ }
63
+ return file;
64
+ });
65
+ }
66
+
67
+ /** Copy staged files into an agent's uploads folder (a copy each: one upload can serve several agents). */
68
+ function copyAttachments(files, agentId) {
69
+ const dir = path.join(paths.uploads, agentId);
70
+ fs.mkdirSync(dir, { recursive: true });
71
+ return files.map((f) => {
72
+ // Drop the staging prefix (time and a random tag) to get the name it was attached with.
73
+ const dest = path.join(dir, path.basename(f).replace(/^\d{4}-\d\d-\d\dT[\d-]+Z-[0-9a-f]{6}-/, ''));
74
+ fs.copyFileSync(f, dest);
75
+ return dest;
76
+ });
77
+ }
78
+
79
+ /** The part of the request that lists attached files. */
80
+ export const attachmentNote = (files) => `Attached files (part of this request; read them):\n${files.map((f) => `- ${f}`).join('\n')}`;
81
+
82
+ /** The folders of a repo with how many files each holds, two levels deep: a map for drawing it. */
83
+ async function repoLayout(dir) {
84
+ const files = (await git.run('git', ['ls-files'], { cwd: dir, timeout: 20_000 })).split('\n').filter(Boolean);
85
+ const counts = new Map();
86
+ for (const f of files) {
87
+ const parts = f.split('/');
88
+ const key = parts.length > 2 ? `${parts[0]}/${parts[1]}/` : parts.length === 2 ? `${parts[0]}/` : f;
89
+ counts.set(key, (counts.get(key) ?? 0) + 1);
90
+ }
91
+ return [...counts].slice(0, 150).map(([k, n]) => (k.endsWith('/') ? `${k} (${n} files)` : k)).join('\n');
92
+ }
41
93
  import { spawn } from 'node:child_process';
42
94
  import * as git from './git.js';
95
+ import { parseDiff } from '../public/review.js';
43
96
  import { conflictMessage, findConflicts, related, repoKey, summarize as summarizeConflicts } from './conflicts.js';
44
97
  import { answerMessage, loadQuestions, questionsPath, questionsPrompt } from './inbox.js';
45
98
  import { STUCK_DEFAULTS, detectStuck } from './stuck.js';
@@ -501,9 +554,9 @@ export class AgentManager extends EventEmitter {
501
554
  );
502
555
  }
503
556
 
504
- /** A general Claude session with no repo or folder; the question (if any) is its first prompt. */
505
- createGeneral({ question, name, permissionMode, model } = {}) {
506
- return this.create({ kind: 'general', intent: question, name, permissionMode, model });
557
+ /** A general session with no repo or folder, on any engine; the question (if any) is its first prompt. */
558
+ createGeneral({ question, name, permissionMode, model, engine, attachments } = {}) {
559
+ return this.create({ kind: 'general', intent: question, name, permissionMode, model, engine, attachments });
507
560
  }
508
561
 
509
562
  create({
@@ -529,8 +582,10 @@ export class AgentManager extends EventEmitter {
529
582
  imported,
530
583
  startedBy,
531
584
  trigger,
585
+ attachments,
532
586
  }) {
533
587
  intent = (intent ?? '').trim();
588
+ const staged = stagedFiles(attachments);
534
589
  const engineDef = engineOf(engine || this.config.engine || DEFAULT_ENGINE);
535
590
  const engineId = engineDef.id;
536
591
  this.plugins.check('agent.create', {
@@ -554,7 +609,7 @@ export class AgentManager extends EventEmitter {
554
609
  base ??= entry.base ?? undefined;
555
610
  project = entry.key.startsWith('custom:') ? entry.name : null;
556
611
  }
557
- if (!intent && !general) throw Object.assign(new Error('Intent is required'), { status: 400 });
612
+ if (!intent && !general && !staged.length) throw Object.assign(new Error('Intent is required'), { status: 400 });
558
613
  const id = crypto.randomBytes(3).toString('hex');
559
614
  // An imported session keeps its ID, so the first start resumes it.
560
615
  const sessionId = adopt || crypto.randomUUID();
@@ -562,6 +617,8 @@ export class AgentManager extends EventEmitter {
562
617
  const jiraKey = !name?.trim() && intent ? leadingJiraKey(intent) : null;
563
618
  const slug = name || intent ? slugify(name || jiraKey || intent) : `general-${id}`;
564
619
  const refs = extractRefs(intent, this.config.bugPatterns);
620
+ // Attached files (a screenshot, a log) go in the agent's own uploads folder, listed in its request.
621
+ if (staged.length) intent = [intent, attachmentNote(copyAttachments(staged, id))].filter(Boolean).join('\n\n');
565
622
 
566
623
  const agent = {
567
624
  id,
@@ -847,15 +904,15 @@ export class AgentManager extends EventEmitter {
847
904
  resume = true;
848
905
  }
849
906
  }
850
- const args = engine.launchArgs({
851
- ...this.launchOptions(agent),
852
- sessionId: sid,
853
- resume,
854
- name: agent.name,
855
- // A team member's first message is its first step, not just the problem.
856
- prompt: resume ? (handoff ? handoffMessage(agent) : null) : (agent.kickoff ?? agent.intent),
857
- });
907
+ // A team member's first message is its first step, not just the problem.
908
+ const prompt = resume ? (handoff ? handoffMessage(agent) : null) : (agent.kickoff ?? agent.intent);
909
+ const args = engine.launchArgs({ ...this.launchOptions(agent), sessionId: sid, resume, name: agent.name, prompt });
858
910
  const proc = this.spawnAgent(agent, agent.workdir, args);
911
+ // Engines whose resume takes no message get it typed once the session is idle.
912
+ if (resume && prompt && engine.supports.resumePrompt === false) {
913
+ if (!this.outbox.has(agent.id)) this.outbox.set(agent.id, []);
914
+ this.outbox.get(agent.id).unshift({ text: prompt, source: 'system', at: now() });
915
+ }
859
916
 
860
917
  const session = agent.sessions.find((s) => s.id === sid);
861
918
  if (session) {
@@ -927,6 +984,12 @@ export class AgentManager extends EventEmitter {
927
984
  }
928
985
  }
929
986
  // Appended system prompts last only for the process they're passed to, so they go on every launch.
987
+ // Files attached for it (at creation or in its terminal): readable without a permission prompt.
988
+ const uploads = path.join(paths.uploads, agent.id);
989
+ if (fs.existsSync(uploads)) {
990
+ const dir = fs.realpathSync(uploads);
991
+ writableDir = writableDir ? [writableDir, dir].flat() : dir;
992
+ }
930
993
  return { extra: engine.extraArgs(this.config), permissionMode: agent.permissionMode, model: agent.model, writableDir, instructions: prompts.join('\n\n') || null, agents, allowedTools };
931
994
  }
932
995
 
@@ -1102,7 +1165,8 @@ export class AgentManager extends EventEmitter {
1102
1165
  done.dispose();
1103
1166
  resolve(this.view(this.store.get(agent.id) ?? agent));
1104
1167
  });
1105
- rt.pty.kill('SIGHUP');
1168
+ // A hangup, as when a terminal closes, unless the engine shuts down cleanly on another signal.
1169
+ rt.pty.kill(engineOf(agent).stopSignal ?? 'SIGHUP');
1106
1170
  rt.killTimer = setTimeout(() => rt.pty?.kill('SIGKILL'), timeoutMs);
1107
1171
  });
1108
1172
  }
@@ -1168,7 +1232,7 @@ export class AgentManager extends EventEmitter {
1168
1232
  }
1169
1233
 
1170
1234
  childEnv(agent) {
1171
- const env = { ...process.env, ...(agent ? { AGENTMGR_AGENT_ID: agent.id } : {}), TERM: 'xterm-256color', COLORTERM: 'truecolor' };
1235
+ const env = { ...process.env, ...(agent ? { AGENTMGR_AGENT_ID: agent.id } : {}), TERM: 'xterm-256color', COLORTERM: 'truecolor', AGENTMGR_TERMINAL: '1' };
1172
1236
  for (const key of STRIP_ENV) delete env[key];
1173
1237
  return env;
1174
1238
  }
@@ -1613,9 +1677,35 @@ export class AgentManager extends EventEmitter {
1613
1677
  }
1614
1678
 
1615
1679
  /** Save a file the user attached in the UI, so its path can be handed to Claude. */
1680
+ /**
1681
+ * A request with its attachments listed, for work that starts several agents from one request
1682
+ * (a team, a race): the files are copied once to a shared folder they all read.
1683
+ */
1684
+ withSharedAttachments(intent, attachments) {
1685
+ const staged = stagedFiles(attachments);
1686
+ if (!staged.length) return intent;
1687
+ const files = copyAttachments(staged, path.join('shared', crypto.randomBytes(4).toString('hex')));
1688
+ return [String(intent ?? '').trim(), attachmentNote(files)].filter(Boolean).join('\n\n');
1689
+ }
1690
+
1691
+ /** A file attached before its agent exists (New agents, Ask); create() copies it to the agent. */
1692
+ stageUpload(name, data) {
1693
+ fs.mkdirSync(stagedDir(), { recursive: true });
1694
+ // Staged files nobody used are dropped after 30 days (a backlog item may wait that long).
1695
+ const old = Date.now() - 30 * 86_400_000;
1696
+ for (const f of fs.readdirSync(stagedDir())) {
1697
+ try {
1698
+ if (fs.statSync(path.join(stagedDir(), f)).mtimeMs < old) fs.rmSync(path.join(stagedDir(), f), { force: true });
1699
+ } catch {}
1700
+ }
1701
+ const file = path.join(stagedDir(), `${new Date().toISOString().replace(/[:.]/g, '-')}-${crypto.randomBytes(3).toString('hex')}-${safeName(name)}`);
1702
+ fs.writeFileSync(file, data);
1703
+ return { path: file, name: safeName(name), size: data.length };
1704
+ }
1705
+
1616
1706
  saveUpload(ref, name, data) {
1617
1707
  const agent = this.mustGet(ref);
1618
- const safe = path.basename(String(name || 'file')).replace(/[^\w.@+-]+/g, '_').slice(-120) || 'file';
1708
+ const safe = safeName(name);
1619
1709
  const dir = path.join(paths.uploads, agent.id);
1620
1710
  fs.mkdirSync(dir, { recursive: true });
1621
1711
  const file = path.join(dir, `${new Date().toISOString().replace(/[:.]/g, '-')}-${safe}`);
@@ -1926,8 +2016,10 @@ export class AgentManager extends EventEmitter {
1926
2016
 
1927
2017
  /** One tool-less, read-only call to an engine; resolves to its text output. */
1928
2018
  oneShot(agent, text, { engine, model, timeoutMs = 300_000 }) {
1929
- // Some engines take the prompt as an argument instead of on stdin.
1930
- const args = [...engine.extraArgs(this.config), ...engine.oneShotArgs(this.config, { model, prompt: text })];
2019
+ // Some engines take the prompt as an argument instead of on stdin, and some need their extra
2020
+ // arguments after a subcommand (oneShotExtra: they place them).
2021
+ const extra = engine.extraArgs(this.config);
2022
+ const args = engine.oneShotExtra ? engine.oneShotArgs(this.config, { model, prompt: text, extra }) : [...extra, ...engine.oneShotArgs(this.config, { model, prompt: text })];
1931
2023
  return new Promise((resolve, reject) => {
1932
2024
  const child = spawn(engine.bin(this.config), args, { cwd: paths.home, env: this.childEnv(agent), stdio: ['pipe', 'pipe', 'pipe'] });
1933
2025
  let out = '';
@@ -1945,6 +2037,68 @@ export class AgentManager extends EventEmitter {
1945
2037
  });
1946
2038
  }
1947
2039
 
2040
+ /**
2041
+ * The agent's changes explained one by one for review. `changesSource` (set by the server) gives
2042
+ * the same diff the review screen shows; the result is keyed by file and hunk header.
2043
+ */
2044
+ async runWalkthrough(agent, ctx) {
2045
+ if (!this.changesSource) throw new Error('Changes are not available here');
2046
+ const data = await this.changesSource(agent.id);
2047
+ const { hunks, cut } = walkthroughHunks(parseDiff(data.diff));
2048
+ if (!hunks.length) throw Object.assign(new Error('No changes to explain yet'), { status: 400 });
2049
+ const writer = this.writerFor(agent);
2050
+ const result = walkthroughFrom(parseJsonReply(await this.oneShot(agent, walkthroughPrompt({ ...ctx, hunks }), writer)), hunks);
2051
+ const cur = this.store.get(agent.id);
2052
+ if (!cur) return null;
2053
+ return this.view(
2054
+ this.patch(
2055
+ agent.id,
2056
+ summaryPatch(cur, 'walkthrough', {
2057
+ data: result,
2058
+ cut,
2059
+ diffHash: hash(data.diff),
2060
+ markdown: walkthroughMarkdown(result, cur.name),
2061
+ writer: `${writer.engine.label}${writer.model ? ` (${writer.model})` : ''}`,
2062
+ at: now(),
2063
+ briefUpdatedAt: cur.brief?.updatedAt ?? null,
2064
+ pending: false,
2065
+ error: null,
2066
+ }),
2067
+ ),
2068
+ );
2069
+ }
2070
+
2071
+ /** Diagrams only: the problem and its solution drawn from the documents already written and the live context. */
2072
+ async runDiagrams(agent, ctx) {
2073
+ const cur0 = this.store.get(agent.id) ?? agent;
2074
+ const written = [
2075
+ ...['analysis', 'design', 'flow', 'proposal'].map((k) => [DOC_KINDS[k].title, cur0.docs?.[k]?.markdown]),
2076
+ ['Implementation plan', cur0.planReview?.markdown],
2077
+ ['Explanation', cur0.explanation?.markdown],
2078
+ ].filter(([, md]) => md);
2079
+ const documents = written.map(([title, md]) => `<document title="${title}">\n${md.slice(0, 12_000)}\n</document>`).join('\n');
2080
+ const repoMap = agent.workdir ? await repoLayout(agent.workdir).catch(() => '') : '';
2081
+ const writer = this.writerFor(agent);
2082
+ const markdown = diagramsOnly(await this.oneShot(agent, diagramsPrompt({ ...ctx, documents, repoMap }), writer));
2083
+ const cur = this.store.get(agent.id);
2084
+ if (!cur) return null;
2085
+ const from = written.map(([title]) => title);
2086
+ return this.view(
2087
+ this.patch(
2088
+ agent.id,
2089
+ summaryPatch(cur, 'diagrams', {
2090
+ markdown,
2091
+ from,
2092
+ writer: `${writer.engine.label}${writer.model ? ` (${writer.model})` : ''}`,
2093
+ at: now(),
2094
+ briefUpdatedAt: cur.brief?.updatedAt ?? null,
2095
+ pending: false,
2096
+ error: null,
2097
+ }),
2098
+ ),
2099
+ );
2100
+ }
2101
+
1948
2102
  /** Human vs AI: the writer splits and answers the open questions, then another model double-checks it. */
1949
2103
  async runHumanAi(agent, ctx) {
1950
2104
  const writer = this.writerFor(agent);
package/src/review.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // Review an agent's changes: the diff against where its branch started, line comments sent
2
2
  // back to the agent as one message, and commit / push / open a pull request from the UI.
3
3
  import * as git from './git.js';
4
+ import { hash } from './judge.js';
4
5
  import { NO_REPO } from './manager.js';
5
6
  import { ghBin } from './tracker.js';
6
7
 
@@ -135,6 +136,8 @@ export class Review {
135
136
  prs: Object.values(agent.prs ?? {}),
136
137
  busy: Boolean(rt?.pty && rt.busy),
137
138
  diff,
139
+ // Matches a walkthrough's diffHash while the changes are the ones it explained.
140
+ diffHash: hash(diff),
138
141
  truncated: diff.endsWith('… (diff truncated)'),
139
142
  };
140
143
  }
package/src/server.js CHANGED
@@ -32,6 +32,7 @@ import { Notifier, isWorkflowUrl, loadNotify, publicNotify, saveNotify, sendSlac
32
32
  import { Previews, parseUrl } from './preview.js';
33
33
  import { LiveBrowser } from './livebrowser.js';
34
34
  import * as remote from './remote.js';
35
+ import * as service from './service.js';
35
36
  import { Store } from './store.js';
36
37
  import { about, latestRelease } from './about.js';
37
38
 
@@ -59,7 +60,7 @@ function isAllowedOrigin(origin, port) {
59
60
  }
60
61
 
61
62
  const PUBLIC_REMOTE_API = new Set(['POST /api/login', 'GET /api/session']);
62
- const LOCAL_ONLY_API = /^\/api\/(remote(\/|$)|notify(\/|$)|hub(\/|$)|triggers(\/|$)|update(\/|$)|editors$|agents\/[\w-]+\/open-in$|shutdown$)/;
63
+ const LOCAL_ONLY_API = /^\/api\/(remote(\/|$)|notify(\/|$)|hub(\/|$)|triggers(\/|$)|update(\/|$)|service$|editors$|agents\/[\w-]+\/open-in$|shutdown$)/;
63
64
  const HOOK_PATH = '/api/hooks/agents';
64
65
  const NO_REPO_KINDS = new Set(['general', 'assistant']);
65
66
 
@@ -73,6 +74,8 @@ export async function startServer({ port = PORT, host = HOST } = {}) {
73
74
  const backlog = new Backlog({ manager, store, config });
74
75
  const races = new Races({ manager, store });
75
76
  const review = new Review({ manager });
77
+ // The walkthrough explains the same diff the review screen shows.
78
+ manager.changesSource = (id) => review.get(id);
76
79
  const studio = new Studio({ manager, review });
77
80
  const teams = new Teams({ manager, store, config });
78
81
  const cleanup = new Cleanup({ manager, store, config, teams });
@@ -259,6 +262,15 @@ export async function startServer({ port = PORT, host = HOST } = {}) {
259
262
  api.post('/agents/:id/shell/close', wrap((req) => manager.closeShell(req.params.id, req.body?.n ? Number(req.body.n) : undefined)));
260
263
  api.post('/agents/:id/shell/new', wrap((req) => ({ n: manager.nextShellNumber(manager.mustGet(req.params.id).id) })));
261
264
  api.post('/agents/:id/shell/drop', wrap((req) => manager.dropShell(req.params.id, Number(req.body?.n) || 1)));
265
+ // A file attached before its agent exists (New agents, Ask). Raw body, name in the query.
266
+ api.post(
267
+ '/uploads',
268
+ express.raw({ type: () => true, limit: '50mb' }),
269
+ wrap((req) => {
270
+ if (!Buffer.isBuffer(req.body) || !req.body.length) throw fail(400, 'Empty file');
271
+ return manager.stageUpload(req.query.name, req.body);
272
+ }),
273
+ );
262
274
  // Raw file body; the name comes in the query. Returns the saved path to paste into a terminal.
263
275
  api.post(
264
276
  '/agents/:id/upload',
@@ -466,7 +478,9 @@ export async function startServer({ port = PORT, host = HOST } = {}) {
466
478
  api.post('/agents/:id/fork', wrap((req) => manager.fork(req.params.id, req.body ?? {})));
467
479
 
468
480
  api.get('/races', wrap(() => races.list()));
469
- api.post('/races', wrap((req) => races.create(req.body ?? {})));
481
+ // Attachments: copied once to a shared folder and listed in the request every attempt gets.
482
+ const withFiles = ({ attachments, ...body } = {}) => ({ ...body, intent: manager.withSharedAttachments(body.intent, attachments) });
483
+ api.post('/races', wrap((req) => races.create(withFiles(req.body ?? {}))));
470
484
  api.get('/races/:id', wrap((req) => races.view(req.params.id)));
471
485
  api.post('/races/:id/tests', wrap((req) => races.runTests(req.params.id, req.body ?? {})));
472
486
  api.get('/races/:id/diff/:agent', wrap((req) => races.diff(req.params.id, req.params.agent)));
@@ -494,6 +508,20 @@ export async function startServer({ port = PORT, host = HOST } = {}) {
494
508
  api.get('/agents/:id/memory', wrap((req) => memory.read(req.params.id)));
495
509
  api.put('/agents/:id/memory', wrap((req) => memory.write(req.params.id, req.body?.text)));
496
510
  api.post('/agents/:id/learn', wrap((req) => memory.learn(req.params.id, req.body?.text, { by: req.headers['x-agentmgr-agent'] ? undefined : 'you' })));
511
+ // Start at login: the login service (launchd or systemd) that brings the daemon back after a reboot.
512
+ const serviceSupported = ['darwin', 'linux'].includes(process.platform);
513
+ const serviceView = async () => ({ supported: serviceSupported, ...(serviceSupported ? await service.status().catch(() => ({ installed: false, loaded: false })) : { installed: false, loaded: false }) });
514
+ api.get('/service', wrap(serviceView));
515
+ api.put(
516
+ '/service',
517
+ wrap(async (req) => {
518
+ if (!serviceSupported) throw fail(400, `Start at login isn't supported on ${process.platform}`);
519
+ // Installing loads it; its own copy finds this daemon running and exits cleanly (no restart),
520
+ // so it takes over at the next login.
521
+ const res = req.body?.enabled === true ? await service.install() : await service.uninstall();
522
+ return { ...(await serviceView()), note: res.note ?? null };
523
+ }),
524
+ );
497
525
  // The first-run checklist: what is set up, and what this machine is missing.
498
526
  api.get('/setup', wrap(async () => {
499
527
  const checks = (await doctor().catch(() => [])).filter((c) => c.status === 'fail' || c.status === 'warn').filter((c) => c.name !== 'Daemon');
@@ -506,6 +534,8 @@ export async function startServer({ port = PORT, host = HOST } = {}) {
506
534
  assistant: agentsNow.some((a) => a.kind === 'assistant') || Boolean(config.triggers?.github?.enabled || config.triggers?.webhook?.enabled),
507
535
  remote: Boolean(remote.loadSettings().enabled),
508
536
  budget: Boolean(config.budgets?.perAgentUsd || config.budgets?.perDayUsd),
537
+ // Only where it can be set up (macOS, Linux); elsewhere there's nothing to do.
538
+ service: serviceSupported ? Boolean((await service.status().catch(() => ({}))).installed) : true,
509
539
  },
510
540
  };
511
541
  }));
@@ -544,7 +574,7 @@ export async function startServer({ port = PORT, host = HOST } = {}) {
544
574
  return cleanup.run({ close: b.close !== false, delete: b.delete !== false, ids: Array.isArray(b.ids) ? b.ids : null });
545
575
  }));
546
576
  api.get('/teams', wrap(() => teams.list()));
547
- api.post('/teams', wrap((req) => teams.create(req.body ?? {})));
577
+ api.post('/teams', wrap((req) => teams.create(withFiles(req.body ?? {}))));
548
578
  api.get('/teams/:id', wrap((req) => teams.view(req.params.id)));
549
579
  api.get('/teams/:id/files/:name', wrap((req) => teams.file(req.params.id, req.params.name)));
550
580
  api.post('/teams/:id/resume', wrap((req) => teams.resume(req.params.id, req.body ?? {})));