ai-agentmanager 1.1.0 → 1.3.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/cli.js CHANGED
@@ -451,6 +451,332 @@ export async function main(argv) {
451
451
  console.log(`${res.name}: P${res.priority}`);
452
452
  });
453
453
 
454
+ program
455
+ .command('edit')
456
+ .description("Change an agent's name, request, project, priority, permission mode or model")
457
+ .argument('<agent>')
458
+ .option('-n, --name <name>', 'display name')
459
+ .option('-i, --intent <text>', 'the request as recorded (add --tell to send it to the session too)')
460
+ .option('--tell', 'with --intent: also tell the agent about the changed request')
461
+ .option('--project <project>', 'move under this project (shortcut or name); "" for its own repo or folder')
462
+ .option('--priority <P0-P3>', 'priority')
463
+ .option('-p, --permission-mode <mode>', 'permission mode, from the next launch ("" for the default)')
464
+ .option('-m, --model <model>', 'model, from the next launch ("" for the default)')
465
+ .option('--budget <usd>', 'its spending limit in US dollars ("" for the default)')
466
+ .action(async (ref, opts) => {
467
+ await daemon();
468
+ const a = await resolveAgent(ref);
469
+ const patch = Object.fromEntries(
470
+ [['name', opts.name], ['intent', opts.intent], ['project', opts.project], ['priority', opts.priority], ['permissionMode', opts.permissionMode], ['model', opts.model], ['budgetUsd', opts.budget === undefined ? undefined : opts.budget === '' ? null : Number(opts.budget)]].filter(([, v]) => v !== undefined),
471
+ );
472
+ if (!Object.keys(patch).length) throw new Error('Nothing to change: give --name, --intent, --project, --priority, --permission-mode, --model or --budget');
473
+ const res = await api('PATCH', `/agents/${a.id}`, patch);
474
+ if (opts.tell && opts.intent) await api('POST', `/agents/${a.id}/tell`, { text: `The request changed. It is now:\n${opts.intent}` });
475
+ console.log(`${green('✓')} ${bold(res.name)} ${dim(res.id)} ${res.projectInfo?.name ?? ''} P${res.priority}${res.permissionMode ? ` ${res.permissionMode}` : ''}${res.model ? ` ${res.model}` : ''}`);
476
+ if (patch.permissionMode !== undefined || patch.model !== undefined) console.log(dim('Permission mode and model apply from its next launch.'));
477
+ });
478
+
479
+ program
480
+ .command('move')
481
+ .description('Move an agent under another project (shortcut or name); "" moves it back to its own')
482
+ .argument('<agent>')
483
+ .argument('<project>')
484
+ .action(async (ref, project) => {
485
+ await daemon();
486
+ const a = await resolveAgent(ref);
487
+ const res = await api('PATCH', `/agents/${a.id}`, { project });
488
+ console.log(`${green('✓')} ${bold(res.name)} is now under ${bold(res.projectInfo?.full || res.projectInfo?.name)}`);
489
+ });
490
+
491
+ program
492
+ .command('changes')
493
+ .description("An agent's changes since its branch started: files, commits and the diff")
494
+ .argument('<agent>')
495
+ .option('--stat', 'files and counts only, no diff')
496
+ .action(async (ref, opts) => {
497
+ await daemon();
498
+ const a = await resolveAgent(ref);
499
+ const c = await api('GET', `/agents/${a.id}/changes`);
500
+ console.log(`${bold(c.name)} ${c.branch}${c.base ? ` ← ${c.base.ref}` : ''} ${c.commits.length} commit(s)${c.uncommitted ? yellow(` ${c.uncommitted} uncommitted`) : ''}${c.unpushed ? yellow(` ${c.unpushed} not pushed`) : ''}`);
501
+ for (const f of c.files) console.log(` ${green(`+${f.added ?? '?'}`)} ${red(`-${f.removed ?? '?'}`)} ${f.path}${f.status !== 'modified' ? dim(` (${f.status})`) : ''}`);
502
+ if (!c.files.length) console.log(dim(' No changes yet.'));
503
+ if (!opts.stat && c.diff) console.log(`\n${c.diff}`);
504
+ });
505
+
506
+ program
507
+ .command('comment')
508
+ .description('Send review comments to an agent as one message: --at file:line=text (repeatable) and -m')
509
+ .argument('<agent>')
510
+ .option('--at <file:line=text>', 'a line comment, e.g. "src/a.js:42=Use const here"', (v, all) => [...all, v], [])
511
+ .option('-m, --message <text>', 'an overall comment')
512
+ .action(async (ref, opts) => {
513
+ await daemon();
514
+ const a = await resolveAgent(ref);
515
+ const comments = opts.at.map((x) => {
516
+ const m = /^(.+?):(\d+)=(.+)$/s.exec(x);
517
+ if (!m) throw new Error(`--at wants file:line=text, got "${x}"`);
518
+ return { file: m[1], line: Number(m[2]), side: 'new', text: m[3] };
519
+ });
520
+ const r = await api('POST', `/agents/${a.id}/changes/comments`, { comments, note: opts.message ?? '' });
521
+ console.log(`${green('✓')} sent ${r.sent} comment(s) to ${bold(a.name)}`);
522
+ });
523
+
524
+ program
525
+ .command('commit')
526
+ .description("Commit everything in an agent's worktree, new files included, as you")
527
+ .argument('<agent>')
528
+ .requiredOption('-m, --message <text>', 'commit message')
529
+ .action(async (ref, opts) => {
530
+ await daemon();
531
+ const a = await resolveAgent(ref);
532
+ const r = await api('POST', `/agents/${a.id}/changes/commit`, { message: opts.message });
533
+ console.log(`${green('✓')} ${r.sha} ${r.files} file(s)`);
534
+ });
535
+
536
+ program
537
+ .command('pr')
538
+ .description("Push an agent's branch and open a pull request with gh (or push to the one already open)")
539
+ .argument('<agent>')
540
+ .option('-t, --title <title>', 'title (default: from the commits)')
541
+ .option('-b, --body <text>', 'description')
542
+ .option('--draft', 'open it as a draft')
543
+ .action(async (ref, opts) => {
544
+ await daemon();
545
+ const a = await resolveAgent(ref);
546
+ const r = await api('POST', `/agents/${a.id}/changes/pr`, { title: opts.title ?? '', body: opts.body ?? '', draft: Boolean(opts.draft) });
547
+ console.log(`${green('✓')} ${r.existing ? 'pushed to' : 'opened'} ${r.url ?? '(no URL printed by gh)'}`);
548
+ });
549
+
550
+ program
551
+ .command('stats')
552
+ .description('Results across agents: merge rate, time to a first PR and to merge, cost per merged PR, by engine and model')
553
+ .option('-d, --days <n>', 'agents created in the last n days (0: all)', '30')
554
+ .option('--json', 'print JSON')
555
+ .action(async (opts) => {
556
+ await daemon();
557
+ const st = await api('GET', `/stats?days=${Number(opts.days) || 0}`);
558
+ if (opts.json) return console.log(JSON.stringify(st, null, 2));
559
+ const t = st.total;
560
+ const pct = (r) => (r == null ? '—' : `${Math.round(r * 100)}%`);
561
+ const hrs = (h) => (h == null ? '—' : `${h}h`);
562
+ const m = (n) => (n == null ? '—' : `$${n.toFixed(2)}`);
563
+ console.log(`${bold(`Last ${st.days || 'all'} days`)}: ${t.agents} agents, ${t.withPr} opened a PR, ${t.merged} merged, ${t.closed} closed, ${t.open} open`);
564
+ console.log(`merge rate ${bold(pct(t.mergeRate))} · median to first PR ${hrs(t.medianHoursToPr)} · to merge ${hrs(t.medianHoursToMerge)} · ${m(t.costPerMerged)} per merged PR (${m(t.cost)} in all)`);
565
+ for (const [title, rows] of [['By engine and model', st.byEngine], ['By source', st.bySource]]) {
566
+ console.log(`\n${bold(title)}`);
567
+ for (const r of rows) console.log(` ${r.key.padEnd(24)} ${String(r.agents).padStart(4)} agents ${String(r.merged).padStart(3)} merged ${pct(r.mergeRate).padStart(4)} ${m(r.costPerMerged)}/merged`);
568
+ }
569
+ });
570
+
571
+ program
572
+ .command('spend')
573
+ .description('What agents cost today and lately, and spending limits')
574
+ .option('--per-agent <usd>', 'limit for each agent ("" for none)')
575
+ .option('--per-day <usd>', 'limit for all agents a day ("" for none)')
576
+ .action(async (opts) => {
577
+ await daemon();
578
+ const patch = {};
579
+ if (opts.perAgent !== undefined) patch.perAgentUsd = opts.perAgent === '' ? null : Number(opts.perAgent);
580
+ if (opts.perDay !== undefined) patch.perDayUsd = opts.perDay === '' ? null : Number(opts.perDay);
581
+ const b = Object.keys(patch).length ? await api('PUT', '/budgets', patch) : await api('GET', '/budgets');
582
+ const m = (n) => `$${Number(n ?? 0).toFixed(2)}`;
583
+ console.log(`${bold('Today')}: ${m(b.today.total)}${b.perDayUsd ? ` of ${m(b.perDayUsd)}` : ''} ${dim(`each agent at most ${b.perAgentUsd ? m(b.perAgentUsd) : 'no limit'}`)}`);
584
+ for (const d of b.days.slice(1, 8)) console.log(dim(` ${d.day} ${m(d.total)}`));
585
+ for (const a of b.agents.slice(0, 15)) console.log(` ${m(a.cost).padStart(8)} ${bold(a.name)}${a.limit ? dim(` budget ${m(a.limit)}`) : ''}${a.hit ? red(' reached') : ''}`);
586
+ console.log(dim(b.note));
587
+ });
588
+
589
+ program
590
+ .command('learn')
591
+ .description("Add one line to the shared memory of an agent's repo (agents run this themselves)")
592
+ .argument('<text>', 'the learning, one short line')
593
+ .option('-a, --agent <agent>', 'which agent (default: the agent this runs in)')
594
+ .action(async (text, o) => {
595
+ await daemon();
596
+ const ref = o.agent ?? process.env.AGENTMGR_AGENT_ID;
597
+ if (!ref) throw new Error('Say which agent with --agent (outside an agent there is no default)');
598
+ const a = await resolveAgent(ref);
599
+ const r = await api('POST', `/agents/${a.id}/learn`, { text });
600
+ console.log(r.added ? `${green('✓')} remembered for ${r.key} (${r.entries} learnings)` : dim('Already in the memory.'));
601
+ });
602
+
603
+ program
604
+ .command('memory')
605
+ .description("Show the shared memory of an agent's repo (what agents learned there)")
606
+ .argument('<agent>')
607
+ .option('--clear', 'empty it')
608
+ .action(async (ref, o) => {
609
+ await daemon();
610
+ const a = await resolveAgent(ref);
611
+ const r = o.clear ? await api('PUT', `/agents/${a.id}/memory`, { text: '' }) : await api('GET', `/agents/${a.id}/memory`);
612
+ console.log(dim(`${r.file} · ${r.entries} learnings · ${r.bytes} of ${r.max} bytes`));
613
+ if (r.text) console.log(r.text.trimEnd());
614
+ });
615
+
616
+ const pb = program.command('playbook').description('Saved requests you run again, in several projects, by hand or on a schedule');
617
+ pb.command('ls', { isDefault: true })
618
+ .description('List playbooks')
619
+ .action(async () => {
620
+ await daemon();
621
+ const list = await api('GET', '/playbooks');
622
+ for (const p of list) {
623
+ const sched = p.schedule ? `${p.schedule.every} at ${p.schedule.at}${p.schedule.day ? ` (${p.schedule.day})` : ''}${p.enabled ? '' : ' (off)'}` : 'by hand';
624
+ console.log(`${dim(p.id)} ${bold(p.name)} ${p.projects.map((x) => `@${x}`).join(' ')} ${dim(sched)}${p.nextRunAt ? dim(` next ${p.nextRunAt.slice(0, 16).replace('T', ' ')}`) : ''}`);
625
+ }
626
+ if (!list.length) console.log(dim('No playbooks yet: agentmgr playbook add'));
627
+ });
628
+ pb.command('add')
629
+ .description('Save a playbook: a request ({{name}} for values filled in at run time) and the projects to run it in')
630
+ .argument('<name>')
631
+ .argument('<request>')
632
+ .requiredOption('--projects <list>', 'project shortcuts, comma separated (one agent each)')
633
+ .option('-m, --model <model>', 'model')
634
+ .option('-p, --permission-mode <mode>', 'permission mode')
635
+ .option('--every <when>', 'schedule: hour, day, weekday or week')
636
+ .option('--at <HH:MM>', 'schedule time (minute past the hour for "hour")', '09:00')
637
+ .option('--day <day>', 'for "week": mon..sun', 'mon')
638
+ .option('--default <name=value>', 'default value for a {{placeholder}} (repeatable)', (v, all) => [...all, v], [])
639
+ .action(async (name, intent, o) => {
640
+ await daemon();
641
+ const defaults = Object.fromEntries(o.default.map((x) => x.split(/=(.*)/s).slice(0, 2)));
642
+ const p = await api('POST', '/playbooks', { name, intent, projects: o.projects, model: o.model, permissionMode: o.permissionMode, defaults, schedule: o.every ? { every: o.every, at: o.at, day: o.day } : null });
643
+ console.log(`${green('✓')} ${bold(p.name)} ${dim(p.id)}${p.nextRunAt ? ` next run ${p.nextRunAt}` : ''}`);
644
+ });
645
+ pb.command('run')
646
+ .description('Run a playbook now')
647
+ .argument('<playbook>', 'id or name')
648
+ .option('--var <name=value>', 'a {{placeholder}} value (repeatable)', (v, all) => [...all, v], [])
649
+ .action(async (ref, o) => {
650
+ await daemon();
651
+ const vars = Object.fromEntries(o.var.map((x) => x.split(/=(.*)/s).slice(0, 2)));
652
+ const r = await api('POST', `/playbooks/${encodeURIComponent(ref)}/run`, { vars });
653
+ for (const a of r.agents) console.log(`${green('✓')} ${bold(a.name)} ${dim(a.id)} @${a.project}`);
654
+ for (const x of r.failed) console.log(`${red('!')} @${x.project}: ${x.reason}`);
655
+ });
656
+ pb.command('rm')
657
+ .description('Delete a playbook (its agents stay)')
658
+ .argument('<playbook>', 'id or name')
659
+ .action(async (ref) => {
660
+ await daemon();
661
+ await api('DELETE', `/playbooks/${encodeURIComponent(ref)}`);
662
+ console.log(`${green('✓')} deleted`);
663
+ });
664
+
665
+ const trig = program.command('triggers').description('Start agents from GitHub labels, /agent PR comments and a webhook (no tokens spent watching)');
666
+ trig
667
+ .command('status', { isDefault: true })
668
+ .description('What is on, and what triggers started lately')
669
+ .action(async () => {
670
+ await daemon();
671
+ const t = await api('GET', '/triggers');
672
+ console.log(`${bold('GitHub label')}: ${t.github.enabled ? green('on') : dim('off')} "${t.github.label}" in ${[...t.github.repos, ...t.github.owners.map((o) => `${o}/*`)].join(', ') || dim('(no repos)')} every ${t.github.everyMin} min`);
673
+ console.log(`${bold('PR comments')}: ${t.prCommands.enabled ? green('on') : dim('off')} "${t.prCommands.prefix} …" from owners, members, collaborators`);
674
+ console.log(`${bold('Webhook')}: ${t.webhook.enabled ? green('on') : dim('off')} POST /api/hooks/agents${t.webhook.hasToken ? '' : dim(' (no token: agentmgr triggers set --new-token)')}`);
675
+ console.log(dim(`${t.startedToday} started in the last day (at most ${t.maxPerDay}); last check ${t.lastPoll?.at ?? 'never'}${t.lastError ? `; error: ${t.lastError.message}` : ''}`));
676
+ for (const r of t.recent.slice(0, 10)) console.log(` ${r.at.slice(0, 16).replace('T', ' ')} ${bold(r.name)} ${r.source}${r.url ? ` ${dim(r.url)}` : ''}`);
677
+ });
678
+ trig
679
+ .command('poll')
680
+ .description('Check GitHub now')
681
+ .action(async () => {
682
+ await daemon();
683
+ const r = await api('POST', '/triggers/poll');
684
+ console.log(`${r.issues} agent(s) started, ${r.commands} comment(s) passed on`);
685
+ });
686
+ trig
687
+ .command('set')
688
+ .description('Change triggers')
689
+ .option('--github <on|off>', 'start agents for labeled issues')
690
+ .option('--label <label>', 'the label (default agentmgr)')
691
+ .option('--repos <list>', 'repos to watch, owner/name, comma separated')
692
+ .option('--owners <list>', 'or every repo of these owners')
693
+ .option('--every <minutes>', 'how often to check')
694
+ .option('--pr-comments <on|off>', 'pass /agent comments on agent PRs to the agent')
695
+ .option('--webhook <on|off>', 'accept POST /api/hooks/agents with the token')
696
+ .option('--new-token', 'make a new webhook token (printed once)')
697
+ .option('--max-per-day <n>', 'agents triggers may start a day')
698
+ .action(async (o) => {
699
+ await daemon();
700
+ const onOff = (v) => (v === undefined ? undefined : ['on', 'true', 'yes', '1'].includes(String(v).toLowerCase()));
701
+ const clean = (x) => Object.fromEntries(Object.entries(x).filter(([, v]) => v !== undefined));
702
+ const patch = clean({
703
+ github: clean({ enabled: onOff(o.github), label: o.label, repos: o.repos, owners: o.owners, everyMin: o.every === undefined ? undefined : Number(o.every) }),
704
+ prCommands: clean({ enabled: onOff(o.prComments) }),
705
+ webhook: clean({ enabled: o.newToken ? true : onOff(o.webhook), newToken: o.newToken ? true : undefined }),
706
+ maxPerDay: o.maxPerDay === undefined ? undefined : Number(o.maxPerDay),
707
+ });
708
+ const r = await api('PUT', '/triggers', patch);
709
+ if (r.token) console.log(`Webhook token (shown only now): ${r.token}\nPOST /api/hooks/agents with "Authorization: Bearer <token>" and {"intent": "…", "project": "<shortcut>"} or {"repo": "<git URL>"}`);
710
+ console.log(`${green('✓')} saved`);
711
+ });
712
+
713
+ const land = program.command('land').description("Agents' open pull requests: list them, update, ask to fix, merge");
714
+ land
715
+ .command('list', { isDefault: true })
716
+ .description('Open pull requests by what they need: ready, fix, behind, waiting, draft')
717
+ .action(async () => {
718
+ await daemon();
719
+ const { rows, settings } = await api('GET', '/landing');
720
+ const color = { ready: green, fix: red, behind: yellow, waiting: dim, draft: dim };
721
+ for (const r of rows) {
722
+ const c = r.pr.checks;
723
+ const checks = c ? { failed: red(`✗ ${c.failed.join(', ')}`), running: `◔ ${c.running}`, passed: green('✓'), none: '' }[c.state] : '';
724
+ console.log(`${(color[r.land] ?? dim)(r.land.padEnd(7))} P${r.agent.priority} ${bold(r.agent.name)} #${r.pr.number ?? ''} ${r.pr.title ?? r.pr.url} ${checks}${r.pr.review ? ` ${r.pr.review.toLowerCase().replace(/_/g, ' ')}` : ''}${r.fixes ? dim(` sent back ${r.fixes}x`) : ''}`);
725
+ }
726
+ if (!rows.length) console.log(dim('No open pull requests from agents.'));
727
+ console.log(dim(`Failing checks ${settings.fixChecks ? 'are' : 'are not'} sent to agents, requested changes ${settings.fixReviews ? 'are' : 'are not'}, at most ${settings.maxFixes} times per PR; merges use ${settings.mergeMethod}.`));
728
+ });
729
+ land
730
+ .command('update')
731
+ .description('Merge the base branch into the agent branch and push (asks the agent when busy, dirty or conflicting)')
732
+ .argument('<agent>')
733
+ .option('--agent', 'ask the agent to do it')
734
+ .action(async (ref, opts) => {
735
+ await daemon();
736
+ const a = await resolveAgent(ref);
737
+ const r = await api('POST', `/agents/${a.id}/land/update`, { via: opts.agent ? 'agent' : 'auto' });
738
+ console.log(r.upToDate ? `Up to date with ${r.target}` : r.merged ? `${green('✓')} merged ${r.merged} commit(s) from ${r.target}${r.pushed ? ' and pushed' : ''}` : `Asked ${bold(a.name)} to update (${r.why})`);
739
+ });
740
+ land
741
+ .command('fix')
742
+ .description("Send the PR's failing checks, requested changes or conflicts to the agent")
743
+ .argument('<agent>')
744
+ .action(async (ref) => {
745
+ await daemon();
746
+ const a = await resolveAgent(ref);
747
+ await api('POST', `/agents/${a.id}/land/fix`, {});
748
+ console.log(`${green('✓')} sent to ${bold(a.name)}`);
749
+ });
750
+ land
751
+ .command('merge')
752
+ .description("Merge the agent's open pull request with gh")
753
+ .argument('<agent>')
754
+ .option('--method <m>', 'squash, merge or rebase (default from settings)')
755
+ .option('--auto', 'turn on auto-merge instead: GitHub merges once checks and reviews allow')
756
+ .action(async (ref, opts) => {
757
+ await daemon();
758
+ const a = await resolveAgent(ref);
759
+ const r = await api('POST', `/agents/${a.id}/land/merge`, { method: opts.method, auto: Boolean(opts.auto) });
760
+ console.log(`${green('✓')} ${r.auto ? 'auto-merge on for' : 'merged'} ${r.url}`);
761
+ });
762
+ land
763
+ .command('set')
764
+ .description('Landing settings')
765
+ .option('--fix-checks <on|off>', 'send failing checks to the agent')
766
+ .option('--fix-reviews <on|off>', 'send requested changes to the agent')
767
+ .option('--max-fixes <n>', 'at most this many times per pull request')
768
+ .option('--method <m>', 'merge method: squash, merge or rebase')
769
+ .option('--delete-branch <on|off>', 'delete the branch on GitHub after merging')
770
+ .action(async (opts) => {
771
+ await daemon();
772
+ const onOff = (v) => (v === undefined ? undefined : ['on', 'true', 'yes', '1'].includes(String(v).toLowerCase()));
773
+ const patch = Object.fromEntries(
774
+ [['fixChecks', onOff(opts.fixChecks)], ['fixReviews', onOff(opts.fixReviews)], ['maxFixes', opts.maxFixes === undefined ? undefined : Number(opts.maxFixes)], ['mergeMethod', opts.method], ['deleteBranch', onOff(opts.deleteBranch)]].filter(([, v]) => v !== undefined),
775
+ );
776
+ const { settings } = await api('PUT', '/landing', patch);
777
+ console.log(JSON.stringify(settings, null, 2));
778
+ });
779
+
454
780
  program
455
781
  .command('plan')
456
782
  .description('While the workspace is still being prepared, plan the task with Claude and attach')
@@ -1043,6 +1369,68 @@ export async function main(argv) {
1043
1369
  if (c.lastRun) console.log(dim(`\nLast run ${new Date(c.lastRun.at).toLocaleString()} (${c.lastRun.by}): closed ${c.lastRun.closed.length}, deleted ${c.lastRun.deleted.length}`));
1044
1370
  });
1045
1371
 
1372
+ const asCmd = program.command('assistant').description('An always-on session that finds new work (Jira, GitHub, Slack) and starts agents for it');
1373
+ const showAssistant = (a) => {
1374
+ const state = !a.agentId ? dim('not set up (agentmgr assistant start)') : a.on ? green(a.busy ? 'on, checking now' : 'on') : yellow('paused');
1375
+ console.log(`${bold('Assistant')}: ${state}${a.agentId ? dim(` agent ${a.agentId}`) : ''}`);
1376
+ console.log(`checks every ${a.everyMin} min · watches ${a.sources.join(', ') || 'nothing'} · at most ${a.maxPerDay} new agents a day, ${a.maxRunning} running`);
1377
+ if (a.lastCheckAt) console.log(dim(`last check ${new Date(a.lastCheckAt).toLocaleString()}${a.nextAt ? ` · next ${new Date(a.nextAt).toLocaleTimeString()}` : ''} · started ${a.startedToday} today, ${a.startedTotal} in all`));
1378
+ for (const x of a.started.slice(0, 10)) console.log(` ${green('+')} ${bold(x.name)} ${dim(x.id)} ${truncate(x.intent, 70)}`);
1379
+ };
1380
+ asCmd
1381
+ .command('status', { isDefault: true })
1382
+ .description('Show whether it is on, its settings, and the agents it started')
1383
+ .option('--activity', 'also print its activity log')
1384
+ .action(async (opts) => {
1385
+ await daemon();
1386
+ const a = await api('GET', '/assistant');
1387
+ showAssistant(a);
1388
+ if (opts.activity) console.log(`\n${a.activity || dim('No activity yet.')}`);
1389
+ });
1390
+ asCmd
1391
+ .command('start')
1392
+ .description('Set it up (first time) or turn it back on')
1393
+ .action(async () => {
1394
+ await daemon();
1395
+ showAssistant(await api('POST', '/assistant/start'));
1396
+ });
1397
+ asCmd
1398
+ .command('pause')
1399
+ .description('Stop checking and close its session (start resumes it with its memory)')
1400
+ .action(async () => {
1401
+ await daemon();
1402
+ showAssistant(await api('POST', '/assistant/pause'));
1403
+ });
1404
+ asCmd
1405
+ .command('check')
1406
+ .description('Check for new work now')
1407
+ .action(async () => {
1408
+ await daemon();
1409
+ await api('POST', '/assistant/check');
1410
+ console.log(`${green('✓')} asked the assistant to check now`);
1411
+ });
1412
+ asCmd
1413
+ .command('set')
1414
+ .description('Change its settings (they apply from its next launch; pause and start to apply now)')
1415
+ .option('--every <minutes>', 'check this often')
1416
+ .option('--sources <list>', 'comma-separated: jira,comments,reviews,ci,slack')
1417
+ .option('--max-per-day <n>', 'new agents a day, at most')
1418
+ .option('--max-running <n>', 'of its agents running at once, at most')
1419
+ .option('--permission-mode <mode>', 'its own permission mode (default auto, so it can run agentmgr and gh without asking)')
1420
+ .option('--notes <text>', 'standing notes, e.g. which Jira project goes to which repo')
1421
+ .action(async (opts) => {
1422
+ await daemon();
1423
+ const patch = {
1424
+ ...(opts.every ? { everyMin: Number(opts.every) } : {}),
1425
+ ...(opts.sources ? { sources: opts.sources.split(',').map((x) => x.trim()).filter(Boolean) } : {}),
1426
+ ...(opts.maxPerDay ? { maxPerDay: Number(opts.maxPerDay) } : {}),
1427
+ ...(opts.maxRunning ? { maxRunning: Number(opts.maxRunning) } : {}),
1428
+ ...(opts.permissionMode ? { permissionMode: opts.permissionMode } : {}),
1429
+ ...(opts.notes !== undefined ? { notes: opts.notes } : {}),
1430
+ };
1431
+ showAssistant(await api('PUT', '/assistant', patch));
1432
+ });
1433
+
1046
1434
  program
1047
1435
  .command('doctor')
1048
1436
  .description('Check what AI Agent Manager needs on this machine (Node.js, Claude Code, git, gh, the daemon) and how to fix it')
@@ -1459,7 +1847,7 @@ export async function main(argv) {
1459
1847
  i === 0 || proj(agents[i - 1]) !== proj(a) ? bold(truncate(proj(a), 30)) : '',
1460
1848
  a.priority === 0 ? red(bold('P0')) : a.priority === 1 ? red('P1') : a.priority === 3 ? dim('P3') : 'P2',
1461
1849
  a.id,
1462
- truncate(a.kind === 'general' ? `${a.name} (general)` : a.name, 28),
1850
+ truncate(a.kind === 'general' ? `${a.name} (general)` : a.startedBy ? `${a.name} (assistant)` : a.name, 28),
1463
1851
  statusLabel(a),
1464
1852
  a.git?.branch ?? '',
1465
1853
  String(a.prs.length || ''),
package/src/client.js CHANGED
@@ -10,7 +10,8 @@ const BIN = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'bin',
10
10
  export async function api(method, route, body) {
11
11
  const res = await fetch(`${BASE}/api${route}`, {
12
12
  method,
13
- headers: { 'content-type': 'application/json', 'x-agentmgr': '1' },
13
+ // From inside an agent's session, say which agent is asking (the assistant's limits use it).
14
+ headers: { 'content-type': 'application/json', 'x-agentmgr': '1', ...(process.env.AGENTMGR_AGENT_ID ? { 'x-agentmgr-agent': process.env.AGENTMGR_AGENT_ID } : {}) },
14
15
  body: body === undefined ? undefined : JSON.stringify(body),
15
16
  });
16
17
  const data = await res.json().catch(() => ({}));
@@ -18,12 +18,14 @@ export default {
18
18
  extraArgs: (config) => [...(config.engines?.claude?.args ?? config.claudeArgs ?? [])],
19
19
 
20
20
  /** Options for every launch: permissions, model, a folder it may write to, extra instructions. */
21
- optionArgs({ permissionMode, model, writableDir, instructions, agents }) {
21
+ optionArgs({ permissionMode, model, writableDir, instructions, agents, allowedTools }) {
22
22
  const args = [];
23
23
  if (permissionMode && permissionMode !== 'default') args.push('--permission-mode', permissionMode);
24
24
  if (model) args.push('--model', model);
25
25
  const dirs = [writableDir ?? []].flat();
26
- if (dirs.length) args.push('--add-dir', ...dirs, '--allowedTools', ...dirs.flatMap((d) => [`Write(/${d}/**)`, `Edit(/${d}/**)`]));
26
+ if (dirs.length) args.push('--add-dir', ...dirs);
27
+ const allowed = [...dirs.flatMap((d) => [`Write(/${d}/**)`, `Edit(/${d}/**)`]), ...(allowedTools ?? [])];
28
+ if (allowed.length) args.push('--allowedTools', ...allowed);
27
29
  if (instructions) args.push('--append-system-prompt', instructions);
28
30
  // Sub-agents for this session only (a team lead's roles).
29
31
  if (agents) args.push('--agents', JSON.stringify(agents));
@@ -31,8 +33,8 @@ export default {
31
33
  },
32
34
 
33
35
  /** Full arguments for a launch: options, then the session. */
34
- launchArgs({ extra = [], permissionMode, model, writableDir, instructions, agents, sessionId, resume, name, prompt }) {
35
- return [...extra, ...this.optionArgs({ permissionMode, model, writableDir, instructions, agents }), ...this.sessionArgs({ sessionId, resume, name, prompt })];
36
+ launchArgs({ extra = [], permissionMode, model, writableDir, instructions, agents, allowedTools, sessionId, resume, name, prompt }) {
37
+ return [...extra, ...this.optionArgs({ permissionMode, model, writableDir, instructions, agents, allowedTools }), ...this.sessionArgs({ sessionId, resume, name, prompt })];
36
38
  },
37
39
 
38
40
  /** A new session under the ID we chose, or a resumed one; prompt is the first (or next) message. */
package/src/git.js CHANGED
@@ -3,13 +3,13 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
 
5
5
  // Never wait on a username/password prompt nobody can see; fail instead.
6
- const ENV = { ...process.env, GIT_TERMINAL_PROMPT: '0' };
6
+ const baseEnv = () => ({ ...process.env, GIT_TERMINAL_PROMPT: '0' });
7
7
 
8
8
  const cancelled = () => Object.assign(new Error('Cancelled'), { cancelled: true });
9
9
 
10
10
  export function run(cmd, args, { cwd, timeout = 60_000, signal, env } = {}) {
11
11
  return new Promise((resolve, reject) => {
12
- execFile(cmd, args, { cwd, timeout, signal, env: env ? { ...ENV, ...env } : ENV, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
12
+ execFile(cmd, args, { cwd, timeout, signal, env: { ...baseEnv(), ...env }, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
13
13
  if (err) {
14
14
  if (signal?.aborted) return reject(cancelled());
15
15
  err.message = `${cmd} ${args.join(' ')} failed: ${(stderr || err.message).trim()}`;
@@ -57,7 +57,7 @@ function gitProgress(args, { cwd, signal, onProgress } = {}) {
57
57
  return new Promise((resolve, reject) => {
58
58
  // Own process group, so cancelling also kills git's helpers (git-remote-http etc.).
59
59
  // Killing only git leaves them holding stderr open, and 'close' would never come.
60
- const child = spawn('git', args, { cwd, env: ENV, stdio: ['ignore', 'ignore', 'pipe'], detached: true });
60
+ const child = spawn('git', args, { cwd, env: baseEnv(), stdio: ['ignore', 'ignore', 'pipe'], detached: true });
61
61
  const kill = () => {
62
62
  try {
63
63
  process.kill(-child.pid, 'SIGKILL');
@@ -332,7 +332,7 @@ export async function fullDiff(dir, baseSha, max = 400_000) {
332
332
  if (out.length > max) break;
333
333
  // --no-index exits 1 when the files differ, which is always here.
334
334
  const patch = await new Promise((resolve) =>
335
- execFile('git', ['diff', '--no-color', '--no-index', '--', '/dev/null', f], { ...opts, env: ENV, maxBuffer: 10 * 1024 * 1024 }, (_e, stdout) => resolve(stdout ?? '')),
335
+ execFile('git', ['diff', '--no-color', '--no-index', '--', '/dev/null', f], { ...opts, env: baseEnv(), maxBuffer: 10 * 1024 * 1024 }, (_e, stdout) => resolve(stdout ?? '')),
336
336
  );
337
337
  out += `\n${patch}`;
338
338
  }