ai-agentmanager 1.11.0 → 1.12.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/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.12.0 (2026-11-01)
4
+
5
+ - **agentkit (phase 1): agents you build and run.** An agent is a folder with `agent.yaml` (goal, instructions, engine, MCP tools, allow/approve lists, triggers, loop, memory, interfaces); `agentkit run <agent>` runs it continuously. Each input is a run of plan → act → check (acting again on what's left, up to `maxSteps`) → reflect into memory, through an engine's CLI (Claude Code, Codex, Muse Code or an ACP agent), self-contained (only its own MCP servers; none of your personal Claude settings). Triggers: `every`, `cron`, `webhook`, `watch`. Interfaces: a chat page and HTTP API, A2A (agent card and `message/send` / `tasks/get`), ACP on stdio (`agentkit acp`), and an MCP tool (`agentkit mcp`, one tool `ask_<name>`). `agentkit init`, `check`, `run --once`. See [docs/agentkit.md](docs/agentkit.md).
6
+ - **Agent builder:** a **Built** tab with **+ Build an agent**: describe a task or workflow (attachments welcome) and pick an engine; one call designs the agent: an analysis (problem, inputs, outputs, risks), its `agent.yaml` and instructions, suggested MCP tools and 3–5 test cases. Review and edit the spec (checked on save), **Run tests** (each through the real runtime, judged against its expectation), **Start** it on this computer (its chat page and API), **Download** its folder. Built agents live in `~/.agentmanager/built/<name>/`; `/api/builder` for scripts.
7
+ - **Single executables and background service for agents:** `agentkit package <agent> [--platform]` makes one file (macOS arm64/x64, Linux x64/arm64) that runs the agent with no Node or agentkit installed: a Node single executable with the agent's files embedded, built on the official Node binary (downloaded from nodejs.org, checksum-verified, cached). Linux ones build on a Mac. The executable unpacks to `~/.agentkit/<name>/` and takes the same commands (`--once`, `acp`, `mcp`, `check`). `service install|status|uninstall` keeps an agent running in the background (launchd or a systemd user service: started at login, restarted after a crash). In the Built tab: **Build executable** for a platform, with download links.
8
+ - **Approvals and monitoring for agents:** tools on an agent's `approve` list now ask a person instead of being unavailable (Claude Code engine, through `--permission-prompt-tool` and the agent's own approver), on its chat page and in AI Agent Manager's **Inbox**; the first answer decides, silence past `approveTimeout` is a no, and the approve list wins over `allow`. Agents report to AI Agent Manager (runs, cost, results, open approvals; every 30s, at once after a run): the **Built** tab lists them under *Reporting in*. Agents started here report automatically; others connect with `AGENTKIT_REPORT_URL`, `AGENTKIT_REPORT_TOKEN` and, over remote access, `AGENTKIT_REPORT_CERT` (certificate pinning).
9
+
10
+ ## 1.11.1 (2026-11-01)
11
+
12
+ - **Copy and paste in terminals on phones:** **Copy** and **Paste** in the terminal's key bar (and in the Terminal's bar). A phone can't select the terminal's text or paste into it directly, so Copy opens its recent output as plain text you can select (or **Copy all**), and Paste reads the clipboard (the phone asks first) or offers a box to paste into, then inserts it as one paste without pressing Enter.
13
+
3
14
  ## 1.11.0 (2026-11-01)
4
15
 
5
16
  - **Inbox:** permission menus that newer Claude Code draws as one run of text ("Do you want to proceed? ❯ 1. Yes 2. … 3. No") get their choices again, so they can be answered from the inbox.
package/README.md CHANGED
@@ -85,13 +85,13 @@ Runs on macOS and Linux; on Windows, use WSL2. Requires Node 20+, `git`, and the
85
85
 
86
86
  ```sh
87
87
  npm install -g ai-agentmanager # the latest release
88
- npm install -g ai-agentmanager@1.11.0 # or a specific one
88
+ npm install -g ai-agentmanager@1.11.1 # or a specific one
89
89
  ```
90
90
 
91
91
  Or from the repository, at a release tag:
92
92
 
93
93
  ```sh
94
- git clone --branch v1.11.0 --depth 1 https://github.com/jdmunta/ai-agentmanager-core.git
94
+ git clone --branch v1.11.1 --depth 1 https://github.com/jdmunta/ai-agentmanager-core.git
95
95
  cd ai-agentmanager-core
96
96
  npm install
97
97
  npm link # optional: puts `agentmgr` on your PATH
@@ -111,7 +111,7 @@ npm link
111
111
  | Installed with | Update |
112
112
  | --- | --- |
113
113
  | npm | `npm install -g ai-agentmanager@latest` |
114
- | A clone at a release tag | `git fetch --tags && git checkout v1.11.0 && npm install` (the tag you want) |
114
+ | A clone at a release tag | `git fetch --tags && git checkout v1.11.1 && npm install` (the tag you want) |
115
115
  | A clone of `main` | `git pull && npm install` |
116
116
 
117
117
  **Check your setup.** `agentmgr doctor` checks Node.js, Claude Code and its sign-in, git, `gh`, the data folder, the daemon's port and Chrome, and says how to fix anything missing.
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ // agentkit: run an agent described by agent.yaml (docs/agentkit.md).
3
+ // agentkit run <agent> [--once "<input>"] [--port N] triggers, chat page, API and A2A; or one run
4
+ // agentkit acp <agent> ACP on stdio
5
+ // agentkit mcp <agent> MCP on stdio (one tool: ask_<name>)
6
+ // agentkit check <agent> validate agent.yaml
7
+ // agentkit init <folder> a starter agent to edit
8
+ import { main } from '../src/agentkit/cli.js';
9
+
10
+ main(process.argv.slice(2));
@@ -0,0 +1,161 @@
1
+ # agentkit: agents you build, download and run
2
+
3
+ Status: the runtime, the builder (AI Agent Manager's **Built** tab), single executables, the background service, approvals and reporting to AI Agent Manager are in.
4
+
5
+ An **agentkit agent** is a folder (or one executable made from it) that does one job continuously: triage new
6
+ support tickets, keep a dependency report current, answer questions about a codebase, run a
7
+ release checklist. It is described by `agent.yaml`, runs its work through an engine's CLI (Claude
8
+ Code, Codex, Muse Code or any ACP agent), uses tools through MCP servers, and can be reached by
9
+ people (a chat page, an HTTP API), by other agents (A2A, MCP) and by editors (ACP).
10
+
11
+ ## agent.yaml
12
+
13
+ ```yaml
14
+ name: ticket-triage # lowercase, used for files and URLs
15
+ title: Support ticket triage
16
+ description: Reads new tickets, labels and routes them, drafts a first reply.
17
+ goal: |
18
+ Every new ticket is labelled, routed to the right team and has a drafted first reply
19
+ within 10 minutes, without closing or replying on its own.
20
+ instructions: instructions.md # or inline text: how to do the job, step by step
21
+ engine: claude # claude | codex | muse | acp:<name>
22
+ model: sonnet # optional, passed to the engine
23
+ workdir: ./work # where it works (a repo, a folder); default ./work
24
+
25
+ tools: # MCP servers the engine gets
26
+ jira:
27
+ command: npx
28
+ args: [-y, "@acme/jira-mcp"]
29
+ env: { JIRA_TOKEN: "${JIRA_TOKEN}" } # "${VAR}" (quoted) comes from the environment, never stored
30
+ allow: [Read, Grep, "mcp__jira__*"] # tools it may use without asking (engine names)
31
+ approve: ["mcp__jira__transition"] # tools that need a person's yes (asked in chat / Inbox)
32
+ approveTimeout: 30m # no answer by then counts as no
33
+ report: # report to AI Agent Manager (or set AGENTKIT_REPORT_*)
34
+ url: "${AGENTMGR_URL}"
35
+ token: "${AGENTMGR_REPORT_TOKEN}"
36
+
37
+ triggers:
38
+ - every: 10m # interval
39
+ - cron: "0 9 * * 1-5" # schedule (minute hour day month weekday)
40
+ - webhook: /hooks/ticket # POST here starts a run with the body as input
41
+ - watch: ./inbox # a new or changed file starts a run
42
+
43
+ loop:
44
+ plan: true # think and write a plan before acting
45
+ check: true # verify the result against the goal after acting
46
+ maxSteps: 6 # act/check rounds per run before stopping
47
+ budgetUsd: 0.50 # per run (when the engine reports cost)
48
+
49
+ memory: true # keep notes between runs (memory.md)
50
+
51
+ interfaces:
52
+ http: { port: 7700 } # chat page + API
53
+ a2a: true # agent card + tasks over HTTP
54
+ acp: true # `<agent> acp` speaks ACP on stdio
55
+ mcp: true # `<agent> mcp` exposes it as an MCP tool
56
+ ```
57
+
58
+ ## A run
59
+
60
+ 1. **Input** arrives: a trigger fires (with its payload), someone chats, an A2A task or an MCP
61
+ call comes in.
62
+ 2. **Plan** (when `loop.plan`): the engine reads the goal, instructions, memory and input and
63
+ writes a short plan.
64
+ 3. **Act**: the engine carries out the plan with its tools (MCP), in `workdir`.
65
+ 4. **Check** (when `loop.check`): the engine is asked whether the goal is met for this input,
66
+ and what's left. If not and steps remain, it acts again on what's left.
67
+ 5. **Reflect** (when `memory`): one or two lessons for next time are appended to memory.
68
+ 6. **Result**: the final answer goes back to whoever asked; the run (steps, output, cost,
69
+ duration) is logged in `runs/<id>.jsonl`.
70
+
71
+ The engine keeps one session per conversation (a chat, an A2A context), so follow-ups continue
72
+ it. Runs from triggers start fresh sessions.
73
+
74
+ ## Interfaces
75
+
76
+ - **HTTP**: `GET /` chat page; `POST /api/chat {message, conversation?}`; `POST /api/runs {input}`;
77
+ `GET /api/runs`, `GET /api/runs/:id`; `GET /api/health`.
78
+ - **A2A**: agent card at `GET /.well-known/agent.json`; JSON-RPC at `POST /a2a` with
79
+ `message/send` and `tasks/get`.
80
+ - **ACP**: `<agent> acp` runs an ACP agent on stdio (initialize, session/new, session/prompt,
81
+ streamed session/update), so editors (and AI Agent Manager's ACP engine) can drive it.
82
+ - **MCP**: `<agent> mcp` runs an MCP server on stdio with one tool, `ask_<name>`, so other
83
+ agents can delegate to it.
84
+
85
+ ## Running
86
+
87
+ `agentkit run ./ticket-triage` (or, packaged, `./ticket-triage`) starts it in the foreground:
88
+ triggers, the HTTP server, the loop. `agentkit run … --once "<input>"` does one run and prints
89
+ the result.
90
+
91
+ In the background: `agentkit service install ./ticket-triage` (packaged: `./ticket-triage service
92
+ install`) registers it with launchd (macOS) or systemd (Linux, a user service): it starts at login,
93
+ restarts after a crash, and writes to `agent.log` in its folder. It runs through your login shell,
94
+ so it finds the engine CLIs and tokens your terminal has. `service status` and `service uninstall`
95
+ do the rest. On Linux, `loginctl enable-linger $USER` keeps it running while you're logged out.
96
+
97
+ ## One executable
98
+
99
+ `agentkit package ./ticket-triage [--platform linux-x64] [-o dist]` makes one file that runs the
100
+ agent with no Node or agentkit installed: `ticket-triage-macos-arm64`, `-macos-x64`,
101
+ `-linux-x64` or `-linux-arm64` (about 110 MB; Windows isn't supported yet). It is a Node single
102
+ executable application: the runtime bundled, the agent's files (`agent.yaml`, instructions,
103
+ tests) embedded, injected into the official Node binary for that platform (downloaded once from
104
+ nodejs.org, checked against its published checksums, cached in `~/.agentmanager/node-dist`).
105
+ Linux executables build on any Mac or Linux machine; macOS ones are signed, so they build on a Mac.
106
+
107
+ On its first run, the executable unpacks its files to `~/.agentkit/<name>/` (or `AGENTKIT_HOME`),
108
+ where its memory and runs live too. Edits there stay until a newer build of the agent replaces the
109
+ files it shipped. Everything else works as with the folder: `./ticket-triage` (run),
110
+ `--once "<input>"`, `acp`, `mcp`, `check`, `service install`. Its engine's CLI (Claude Code, Codex
111
+ or Muse Code) must be installed and signed in where it runs. ACP engines aren't packaged yet.
112
+
113
+ In AI Agent Manager, a built agent's dialog has **Build executable** (pick the platform) and a
114
+ download link for each one built. Editing the agent drops the old executables.
115
+
116
+ ## Approvals
117
+
118
+ A tool on the `approve` list runs only after a person says yes. The approve list wins over
119
+ `allow`: with `allow: ["mcp__jira__*"]` and `approve: ["mcp__jira__transition"]`, every Jira tool
120
+ runs freely except transitions, which ask.
121
+
122
+ With Claude Code as the engine, the agent passes `--permission-prompt-tool` to point Claude at a
123
+ small MCP server of its own (the approver). Claude asks the approver before any tool it isn't
124
+ allowed outright. The approver hands the question to the running agent over a private local
125
+ address, and the agent asks whoever can answer:
126
+
127
+ - **its chat page**, which shows *Approve …?* with the tool's input and Approve / Deny buttons
128
+ (`GET /api/approvals`, `POST /api/approvals/<id>` with `{"allow": true}`);
129
+ - **AI Agent Manager**, when the agent reports there: the request shows up in the **Inbox** and the
130
+ **Built** tab, and the answer goes back with the agent's next check-in (every 2 seconds while
131
+ something waits).
132
+
133
+ The first answer decides. No answer within `approveTimeout` (default 30m) counts as no, and so does
134
+ having no one to ask (no chat page, not reporting). Tools on neither list are refused without
135
+ asking. Other engines can't ask mid-run, so for them approval tools aren't available at all. Test
136
+ runs in the builder never wait for anyone.
137
+
138
+ ## Reporting to AI Agent Manager
139
+
140
+ An agent that reports sends AI Agent Manager who it is (name, host, engine, triggers, chat
141
+ address), each run (status, source, cost, time, the start of its result) and the approvals it's
142
+ waiting on. It checks in every 30 seconds, and right away after a run or a new approval. The
143
+ **Built** tab lists these agents under *Reporting in*: online or last seen, idle or working, runs,
144
+ failures and total cost, recent runs, and approvals to answer. Runs are counted once; the list is
145
+ kept across restarts.
146
+
147
+ Agents started from the Built tab report automatically. For one running elsewhere (a packaged
148
+ executable on a server, a folder on another Mac), **Connect one from elsewhere** shows what to set
149
+ in its environment (or its service):
150
+
151
+ ```
152
+ AGENTKIT_REPORT_URL=https://192.168.1.20:4718 # AI Agent Manager's remote-access address
153
+ AGENTKIT_REPORT_TOKEN=… # the report token (made on first use)
154
+ AGENTKIT_REPORT_CERT=AB:CD:… # its certificate's SHA-256 fingerprint
155
+ ```
156
+
157
+ Remote access uses a self-signed certificate, so the agent trusts exactly that certificate (by
158
+ its fingerprint) and sends nothing until the fingerprint matches. The connection details are shown
159
+ only on the computer AI Agent Manager runs on. The report token lets a program report as an agent
160
+ and receive the decisions made for it, and nothing else in AI Agent Manager; keep it like a
161
+ password (give each computer the same one, or rotate it by deleting `~/.agentmanager/agentkit-reports.json`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-agentmanager",
3
- "version": "1.11.0",
3
+ "version": "1.12.0",
4
4
  "description": "Run background coding agents (Claude Code, Codex) per intent, and keep track of them from a web UI, your phone or the CLI",
5
5
  "keywords": [
6
6
  "claude",
@@ -23,7 +23,8 @@
23
23
  "license": "Apache-2.0",
24
24
  "type": "module",
25
25
  "bin": {
26
- "agentmgr": "bin/agentmgr.js"
26
+ "agentmgr": "bin/agentmgr.js",
27
+ "agentkit": "bin/agentkit.js"
27
28
  },
28
29
  "files": [
29
30
  "bin",
@@ -51,11 +52,14 @@
51
52
  "@xterm/xterm": "^6.0.0",
52
53
  "commander": "^15.0.0",
53
54
  "dompurify": "^3.4.17",
55
+ "esbuild": "^0.25.12",
54
56
  "express": "^5.2.1",
55
57
  "marked": "^18.0.14",
56
58
  "mermaid": "^12.2.0",
57
59
  "node-pty": "^1.1.0",
60
+ "postject": "^1.0.0-alpha.6",
58
61
  "qrcode": "^1.5.4",
59
- "ws": "^8.22.0"
62
+ "ws": "^8.22.0",
63
+ "yaml": "^2.9.1"
60
64
  }
61
65
  }
package/public/app.js CHANGED
@@ -6,6 +6,8 @@ import { touchScroll } from '/touchscroll.js';
6
6
  import { openStream, post, REMOTE } from '/transport.js';
7
7
  import { attachBox } from '/attach.js';
8
8
  import { initSide } from '/side.js';
9
+ import { openCopy, pasteInto } from '/termclip.js';
10
+ import { initBuilt } from '/built.js';
9
11
  import { addTools, download, htmlFile, initExport } from '/export.js';
10
12
  import { badgeHtml as scoreBadge, scorecardHtml, scorecardSpeech } from '/scorecard.js';
11
13
  import { bindListen, listenHtml, play, speakable, now as speaking, stop as stopSpeaking } from '/speak.js';
@@ -124,6 +126,8 @@ $('#new-playbook').onclick = () => {
124
126
  playbooksUi.open({ intent: f.intents.value.trim(), projects: f.project.value.trim() ? [f.project.value.trim()] : [], model: f.model.value.trim(), permissionMode: f.permissionMode.value });
125
127
  };
126
128
  $('#triggers-open').onclick = () => triggersUi.open();
129
+ let builtReports = []; // agents built with agentkit that report here (runs, approvals)
130
+ const builtUi = initBuilt({ api, esc, toast, $, isOpen: () => sideTab === 'built', setCount: (n) => ($('#built-count').textContent = n), reports: () => builtReports });
127
131
  const landUi = initLand({ api, esc, toast, agents, $, scoreBadge, select: (id) => (showTab('agents'), select(id)), openReview: (id) => reviewUi.open(id), isOpen: () => sideTab === 'land' });
128
132
 
129
133
  function statusOf(a) {
@@ -921,6 +925,8 @@ function showTab(tab) {
921
925
  $('#backlog-view').hidden = tab !== 'backlog';
922
926
  $('#inbox-view').hidden = tab !== 'inbox';
923
927
  $('#land-view').hidden = tab !== 'land';
928
+ $('#built-view').hidden = tab !== 'built';
929
+ if (tab === 'built') builtUi.load();
924
930
  if (tab === 'inbox') loadJudge();
925
931
  if (tab === 'land') landUi.load();
926
932
  renderBacklog();
@@ -1000,7 +1006,7 @@ function flags(a) {
1000
1006
  }
1001
1007
 
1002
1008
  const TRIAGED = { now: 'sent (AI: urgent)', digest: 'held for the digest', skip: 'dropped by the AI judge' };
1003
- const NEED_ORDER = { prompt: 0, waiting: 1, plan: 1, question: 1, team: 1, review: 2, stuck: 3, conflict: 4 };
1009
+ const NEED_ORDER = { approval: 0, prompt: 0, waiting: 1, plan: 1, question: 1, team: 1, review: 2, stuck: 3, conflict: 4 };
1004
1010
  /** What an agent needs from the user. `both`: overlaps on both agents (detail), else only the one that should rebase (inbox). */
1005
1011
  function needsFor(a, { both = false } = {}) {
1006
1012
  const items = [];
@@ -1020,23 +1026,34 @@ function teamNeeds() {
1020
1026
  .filter((t) => ['ready', 'blocked'].includes(t.stage) && agents.has(teamOwner(t)))
1021
1027
  .map((t) => ({ a: agents.get(teamOwner(t)), kind: 'team', key: `team:${t.id}`, t }));
1022
1028
  }
1029
+ /** Tools that built agents (agentkit) wait to run until someone approves. */
1030
+ function approvalNeeds() {
1031
+ return builtReports.flatMap((r) => r.pending.map((p) => ({ a: { id: r.key, name: r.title, priority: 1 }, kind: 'approval', key: `approval:${r.key}:${p.id}`, r, p })));
1032
+ }
1023
1033
  function inboxItems() {
1024
- return [...[...agents.values()].flatMap((a) => needsFor(a)), ...teamNeeds()]
1034
+ return [...approvalNeeds(), ...[...agents.values()].flatMap((a) => needsFor(a)), ...teamNeeds()]
1025
1035
  .sort((x, y) => NEED_ORDER[x.kind] - NEED_ORDER[y.kind] || (x.a.priority ?? 2) - (y.a.priority ?? 2));
1026
1036
  }
1027
1037
 
1028
- const NEED_LABEL = { team: 'Team', prompt: 'Needs input', waiting: 'Waiting on you', plan: 'Plan review', question: 'Question', review: 'AI review', stuck: 'Stuck?', conflict: 'Overlap' };
1038
+ const NEED_LABEL = { approval: 'Approval', team: 'Team', prompt: 'Needs input', waiting: 'Waiting on you', plan: 'Plan review', question: 'Question', review: 'AI review', stuck: 'Stuck?', conflict: 'Overlap' };
1029
1039
  /** Why the AI judge thinks so, as a small tag. */
1030
1040
  const aiTag = (confidence, what = 'AI') => `<span class="tag ai" title="Judged by a model from the agent's activity; your reactions tune later calls">🤖 ${what}${confidence != null ? ` ${Math.round(confidence * 100)}%` : ''}</span>`;
1031
1041
  function needCard(it, { inDetail = false } = {}) {
1032
1042
  const { a } = it;
1033
1043
  const who = inDetail
1034
1044
  ? ''
1045
+ : it.kind === 'approval'
1046
+ ? `<a href="#" class="need-agent" data-ib="built-open">⚙ ${esc(a.name)}</a> <span class="hint">${esc(it.r.host)}</span>`
1035
1047
  : `<a href="#${a.id}" class="need-agent" data-goto="${a.id}">${prioChip(a.priority ?? 2)}${esc(a.name)}</a>${a.projectInfo?.name ? ` <span class="hint">${esc(a.projectInfo.name)}</span>` : ''}`;
1036
1048
  const head = `<div class="need-head"><span class="need-kind ${it.kind}">${NEED_LABEL[it.kind]}</span>${who}</div>`;
1037
1049
  const draft = (k, fallback = '') => esc(needDrafts.get(`${it.key}:${k}`) ?? fallback);
1038
1050
  let body = '';
1039
- if (it.kind === 'prompt') {
1051
+ if (it.kind === 'approval') {
1052
+ const p = it.p;
1053
+ body = `<p class="need-title">Wants to run <code>${esc(p.tool)}</code> <span class="hint">asked ${ago(p.at)}</span></p>
1054
+ <pre class="need-input">${esc(JSON.stringify(p.input, null, 2).slice(0, 1500))}</pre>
1055
+ <div class="need-actions"><button class="small primary" data-ib="approve" data-allow="1">Approve</button><button class="small" data-ib="approve">Deny</button>${it.r.chat ? `<a class="button small ghost" href="${esc(it.r.chat)}" target="_blank" rel="noopener">Its chat ↗</a>` : ''}</div>`;
1056
+ } else if (it.kind === 'prompt') {
1040
1057
  const p = a.runtime.prompt;
1041
1058
  body = `<p class="need-title">${esc(p?.question || a.runtime.attention)}</p>
1042
1059
  <div class="need-actions">
@@ -1140,7 +1157,7 @@ function judgeLine() {
1140
1157
 
1141
1158
  function renderInbox() {
1142
1159
  const items = inboxItems();
1143
- const urgent = items.filter((i) => i.kind === 'prompt' || i.kind === 'question' || i.kind === 'waiting').length;
1160
+ const urgent = items.filter((i) => i.kind === 'approval' || i.kind === 'prompt' || i.kind === 'question' || i.kind === 'waiting').length;
1144
1161
  $('#inbox-count').textContent = items.length ? String(items.length) : '';
1145
1162
  $('#inbox-count').className = `badge ${urgent ? 'attention' : ''}`;
1146
1163
  if (sideTab !== 'inbox') return;
@@ -1174,6 +1191,14 @@ async function onNeed(e) {
1174
1191
  const id = card.dataset.agent;
1175
1192
  const key = card.dataset.need;
1176
1193
  switch (el.dataset.ib) {
1194
+ case 'built-open':
1195
+ return showTab('built');
1196
+ case 'approve': {
1197
+ const [, , pid] = key.match(/^approval:(.+):([^:]+)$/);
1198
+ return act(async () => {
1199
+ await api('POST', `/agentkit/agents/${encodeURIComponent(id)}/approvals/${encodeURIComponent(pid)}`, { allow: el.dataset.allow === '1' });
1200
+ }, el.dataset.allow === '1' ? 'Approved; the agent goes on' : 'Declined');
1201
+ }
1177
1202
  case 'key':
1178
1203
  return act(() => api('POST', `/agents/${id}/prompt-key`, { key: el.dataset.key }), 'Sent');
1179
1204
  case 'terminal':
@@ -3603,6 +3628,8 @@ function handleEvent(msg) {
3603
3628
  for (const r of msg.races ?? []) races.set(r.id, r);
3604
3629
  teams.clear();
3605
3630
  for (const t of msg.teams ?? []) teams.set(t.id, t);
3631
+ builtReports = msg.built ?? [];
3632
+ builtUi.render();
3606
3633
  msg.agents.forEach(watchDocs);
3607
3634
  if (selectedId && !agents.has(selectedId)) selectedId = null;
3608
3635
  if (openTermOnLoad && selectedId) {
@@ -3625,6 +3652,9 @@ function handleEvent(msg) {
3625
3652
  teams.set(msg.team.id, msg.team);
3626
3653
  } else if (msg.t === 'team-removed') {
3627
3654
  teams.delete(msg.id);
3655
+ } else if (msg.t === 'built') {
3656
+ builtReports = msg.reports;
3657
+ builtUi.render();
3628
3658
  } else if (msg.t === 'backlog') {
3629
3659
  backlogState = msg.backlog;
3630
3660
  } else if (msg.t === 'projects') {
@@ -3660,6 +3690,10 @@ const KEYS = {
3660
3690
  enter: '\r', ctrlc: '\x03', 1: '1', 2: '2', 3: '3', 4: '4',
3661
3691
  };
3662
3692
  $('#keybar').addEventListener('click', (e) => {
3693
+ // Copy / Paste: a phone can't select xterm's text or paste into it directly (termclip.js).
3694
+ const clip = e.target.closest('[data-clip-act]')?.dataset.clipAct;
3695
+ if (clip === 'copy') return openCopy(term.xterm, { esc, toast });
3696
+ if (clip === 'paste') return pasteInto(term.xterm, { toast });
3663
3697
  const key = e.target.closest('[data-key]')?.dataset.key;
3664
3698
  if (key) term.send({ t: 'i', d: KEYS[key] });
3665
3699
  });
@@ -0,0 +1,207 @@
1
+ // The Built tab: agents made with the builder (agentkit agents, docs/agentkit.md). Build one from
2
+ // a task; review its analysis, spec and tests; test it; run it here; open its chat; download it.
3
+ import { attachBox } from '/attach.js';
4
+
5
+ const STATUS = { designed: 'designed', 'needs-fixing': 'needs fixing', testing: 'testing…', tested: 'tests pass', failing: 'tests fail', running: 'running' };
6
+
7
+ const ago = (iso) => {
8
+ const s = Math.max(0, (Date.now() - Date.parse(iso)) / 1000);
9
+ return s < 60 ? 'just now' : s < 3600 ? `${Math.round(s / 60)} min ago` : s < 86400 ? `${Math.round(s / 3600)} h ago` : `${Math.round(s / 86400)} d ago`;
10
+ };
11
+
12
+ export function initBuilt({ api, esc, toast, $, isOpen, setCount, reports = () => [] }) {
13
+ const view = $('#built-view');
14
+ const dlg = $('#built-dialog');
15
+ const body = $('#built-body');
16
+ let list = [];
17
+ let open = null; // the built agent shown in the dialog
18
+ let poll = null;
19
+
20
+ async function load() {
21
+ try {
22
+ list = await api('GET', '/builder');
23
+ } catch {
24
+ list = [];
25
+ }
26
+ setCount(list.length || '');
27
+ if (isOpen()) render();
28
+ }
29
+
30
+ function render() {
31
+ view.innerHTML = `<div class="built-head"><button class="small primary" data-bt="new">+ Build an agent</button><span class="hint">An agent for a task or workflow: it plans, acts, checks itself and keeps working in the background.</span></div>
32
+ ${list.length ? `<ul class="built-list">${list.map((b) => `<li data-bt-open="${esc(b.name)}"><div class="line1"><b>${esc(b.title ?? b.name)}</b>${b.running ? ' <span class="dot-run" title="Running">●</span>' : ''}<span class="spacer"></span><span class="tag ${b.status === 'tested' ? 'open' : b.status === 'failing' || b.status === 'needs-fixing' ? 'fail' : ''}">${esc(b.pending ?? STATUS[b.status] ?? b.status)}</span></div>
33
+ <div class="hint">${esc(b.name)} · ${esc(b.engine ?? '')}${b.results?.total ? ` · tests ${b.results.passed}/${b.results.total}` : ''}${b.triggers.length ? ` · ${esc(b.triggers.join(', '))}` : ''}</div></li>`).join('')}</ul>` : '<p class="hint built-empty">No agents built yet.</p>'}
34
+ ${reporting()}`;
35
+ }
36
+
37
+ // ------------------------------------------------------------ agents reporting in (here or elsewhere)
38
+ function reporting() {
39
+ const rs = reports();
40
+ const head = `<div class="built-head reporting-head"><b>Reporting in</b><span class="hint">Agents running here or on other computers: their runs, cost and the tools they wait to have approved.</span><span class="spacer"></span><button class="small ghost" data-bt="connect">Connect one from elsewhere</button></div><div id="built-connect" ${connectHtml ? '' : 'hidden'}>${connectHtml ?? ''}</div>`;
41
+ if (!rs.length) return `${head}<p class="hint built-empty">None yet. Agents you start here report automatically.</p>`;
42
+ return `${head}<ul class="built-list reports">${rs.map((r) => `<li data-report="${esc(r.key)}">
43
+ <div class="line1"><span class="dot-run ${r.online ? '' : 'off'}" title="${r.online ? 'Online' : 'Not heard from'}">●</span> <b>${esc(r.title)}</b> <span class="hint">${esc(r.host)}</span><span class="spacer"></span>
44
+ ${r.chat && r.online ? `<a class="button small ghost" href="${esc(r.chat)}" target="_blank" rel="noopener">Chat ↗</a>` : ''}
45
+ <button class="small ghost" data-rp="forget" title="Remove it from this list (it shows up again when it reports)">Forget</button></div>
46
+ <div class="hint">${esc(r.engine)} · ${r.online ? (r.current ? `working on a run (${esc(r.current.source)})` : 'idle') : `last seen ${ago(r.lastSeen)}`} · ${r.totals.runs} run${r.totals.runs === 1 ? '' : 's'}${r.totals.failed ? `, ${r.totals.failed} failed` : ''}${r.totals.costUsd ? ` · $${r.totals.costUsd.toFixed(2)}` : ''}${r.triggers.length ? ` · ${esc(r.triggers.join(', '))}` : ''}</div>
47
+ ${r.pending.map((p) => `<div class="report-approval" data-ap="${esc(p.id)}"><b>Wants to run <code>${esc(p.tool)}</code></b> <span class="hint">${ago(p.at)}</span><pre>${esc(JSON.stringify(p.input, null, 2).slice(0, 1500))}</pre><button class="small primary" data-rp="approve">Approve</button> <button class="small" data-rp="deny">Deny</button></div>`).join('')}
48
+ ${r.runs.length ? `<details><summary class="hint">Recent runs</summary><ul class="built-runs">${r.runs.map((x) => `<li><span class="tag ${x.status === 'failed' ? 'fail' : x.status === 'done' ? 'open' : ''}">${esc(x.status)}</span> <span class="hint">${esc(x.source)} · ${x.at ? ago(x.at) : ''}${x.costUsd ? ` · $${x.costUsd.toFixed(3)}` : ''}${x.ms ? ` · ${Math.round(x.ms / 1000)}s` : ''}</span><div>${esc(x.result.slice(0, 400))}</div></li>`).join('')}</ul></details>` : ''}
49
+ </li>`).join('')}</ul>`;
50
+ }
51
+
52
+ let connectHtml = null; // the connection details, kept open across live re-renders
53
+ async function connect() {
54
+ const box = $('#built-connect');
55
+ if (connectHtml) return (connectHtml = null), (box.hidden = true);
56
+ try {
57
+ const c = await api('GET', '/agentkit/connect');
58
+ const lines = (url, cert) => [`AGENTKIT_REPORT_URL=${url}`, `AGENTKIT_REPORT_TOKEN=${c.token}`, ...(cert ? [`AGENTKIT_REPORT_CERT=${cert}`] : [])].join('\n');
59
+ box.innerHTML = `<p class="hint">Set these where the agent runs (in its environment, or in its service), then start it. It reports here and asks here for approvals.</p>
60
+ ${c.remote.length ? c.remote.map((u) => `<p><b>From another computer</b> <span class="hint">(over remote access)</span></p><pre class="code-area">${esc(lines(u.replace(/\/$/, ''), c.cert))}</pre>`).join('') : '<p class="hint">Turn on remote access (Settings) to connect agents on other computers.</p>'}
61
+ <p><b>On this computer</b></p><pre class="code-area">${esc(lines(c.local))}</pre>`;
62
+ } catch (err) {
63
+ box.innerHTML = `<p class="hint">${esc(err.message)}. Open AI Agent Manager on this computer to see the connection details.</p>`;
64
+ }
65
+ connectHtml = box.innerHTML;
66
+ box.hidden = false;
67
+ }
68
+
69
+ // ------------------------------------------------------------ build
70
+ const buildDlg = $('#build-dialog');
71
+ const files = attachBox({ slot: $('#build-attach'), form: $('#build-form'), paste: $('#build-form textarea'), esc, toast });
72
+ async function openBuild() {
73
+ const sel = $('#build-form').engine;
74
+ if (sel.options.length <= 1) {
75
+ const engines = await api('GET', '/engines').catch(() => []);
76
+ for (const e of engines.filter((x) => x.installed && x.id !== 'claude')) sel.append(Object.assign(document.createElement('option'), { value: e.id.startsWith('acp-') ? `acp:${e.id.slice(4)}` : e.id, textContent: e.label }));
77
+ }
78
+ $('#build-error').hidden = true;
79
+ buildDlg.showModal();
80
+ $('#build-form textarea').focus();
81
+ }
82
+ $('#build-form').addEventListener('submit', async (e) => {
83
+ e.preventDefault();
84
+ const f = e.target;
85
+ if (files.pending()) return toast('Still uploading the attached files');
86
+ const btn = f.querySelector('button[type=submit]');
87
+ btn.disabled = true;
88
+ btn.textContent = 'Designing… (about a minute)';
89
+ try {
90
+ const b = await api('POST', '/builder', { task: f.task.value, engine: f.engine.value, attachments: files.paths().length ? files.paths() : undefined });
91
+ buildDlg.close();
92
+ f.reset();
93
+ files.clear();
94
+ await load();
95
+ show(b);
96
+ } catch (err) {
97
+ $('#build-error').textContent = err.message;
98
+ $('#build-error').hidden = false;
99
+ } finally {
100
+ btn.disabled = false;
101
+ btn.textContent = 'Design the agent';
102
+ }
103
+ });
104
+ $('#build-cancel').onclick = () => buildDlg.close();
105
+
106
+ // ------------------------------------------------------------ one agent
107
+ function show(b) {
108
+ open = b;
109
+ $('#built-title').textContent = b.title ?? b.name;
110
+ const a = b.analysis ?? {};
111
+ const li = (xs) => (xs?.length ? `<ul>${xs.map((x) => `<li>${esc(x)}</li>`).join('')}</ul>` : '<p class="hint">—</p>');
112
+ const r = b.results;
113
+ body.innerHTML = `
114
+ <div class="built-actions">
115
+ <span class="tag ${b.status === 'tested' ? 'open' : b.status === 'failing' || b.status === 'needs-fixing' ? 'fail' : ''}">${esc(b.pending ?? STATUS[b.status] ?? b.status)}</span>
116
+ ${b.running ? `<span class="hint">running since ${new Date(b.running.since).toLocaleTimeString()}</span>${b.running.url ? ` <a href="${esc(b.running.url)}" target="_blank" rel="noopener">Open its chat ↗</a>` : ''}` : ''}
117
+ <span class="spacer"></span>
118
+ <button class="small" data-bt="test" ${b.pending ? 'disabled' : ''} title="Run each test through the real engine and judge the results">${b.pending === 'testing' ? 'Testing…' : 'Run tests'}</button>
119
+ <button class="small ${b.running ? '' : 'primary'}" data-bt="${b.running ? 'stop' : 'start'}" ${b.specError ? 'disabled' : ''} title="${b.running ? 'Stop it' : 'Run it continuously on this computer: its triggers, chat page and API'}">${b.running ? 'Stop' : 'Start'}</button>
120
+ <a class="button small ghost" href="/api/builder/${esc(b.name)}/download" title="Its folder (agent.yaml, instructions, tests) to run anywhere with agentkit">Download folder</a>
121
+ <button class="small ghost" data-bt="delete">Delete</button>
122
+ </div>
123
+ <div class="built-exe"><b>Single executable</b> <span class="hint">one file that runs this agent anywhere, no Node or agentkit needed (its engine's CLI must be installed and signed in)</span>
124
+ <div class="built-exe-row"><select id="built-platform">${(b.platforms ?? []).map((p) => `<option value="${esc(p)}" ${p === b.host ? 'selected' : ''}>${esc(p)}${p === b.host ? ' (this computer)' : ''}</option>`).join('')}</select>
125
+ <button class="small" data-bt="package" ${b.pending || b.specError ? 'disabled' : ''}>${b.pending?.startsWith('packaging') ? 'Building… (a minute or two)' : 'Build executable'}</button>
126
+ ${Object.entries(b.executables ?? {}).map(([p, x]) => `<a class="button small ghost" href="/api/builder/${esc(b.name)}/executable/${esc(p)}" title="Built ${esc(new Date(x.at).toLocaleString())}">⬇ ${esc(p)} · ${Math.round(x.size / 1e6)} MB</a>`).join('')}</div>
127
+ ${b.packageError ? `<p class="error">${esc(b.packageError)}</p>` : ''}</div>
128
+ ${b.specError ? `<p class="error">${esc(b.specError)}</p>` : ''}
129
+ ${b.missingEnv.length ? `<p class="hint warn">Set these in the environment before running it: ${esc(b.missingEnv.join(', '))}</p>` : ''}
130
+ <details open><summary><b>Analysis</b></summary><p>${esc(a.problem ?? '')}</p><div class="row three"><div><b>Inputs</b>${li(a.inputs)}</div><div><b>Outputs</b>${li(a.outputs)}</div><div><b>Risks</b>${li(a.risks)}</div></div>
131
+ ${b.tools?.length ? `<p><b>Tools</b></p><ul>${b.tools.map((t) => `<li><code>${esc(t.name)}</code> ${esc(t.why)}</li>`).join('')}</ul>` : ''}</details>
132
+ <details ${r ? 'open' : ''}><summary><b>Tests</b> ${r?.total ? `<span class="hint">${r.passed}/${r.total} pass · ${new Date(r.at).toLocaleString()}</span>` : '<span class="hint">not run yet</span>'}</summary>
133
+ ${r?.error ? `<p class="error">${esc(r.error)}</p>` : ''}
134
+ <ol class="built-tests">${(r?.list?.length ? r.list : b.tests ?? []).map((t) => `<li class="${t.pass === true ? 'pass' : t.pass === false ? 'fail' : ''}"><div><b>${t.pass === true ? '✓' : t.pass === false ? '✗' : '·'}</b> ${esc(t.input)}</div><div class="hint">Expect: ${esc(t.expect)}</div>${t.why ? `<div class="hint">${esc(t.why)}</div>` : ''}${t.result ? `<details><summary class="hint">Result</summary><pre>${esc(t.result)}</pre></details>` : ''}</li>`).join('')}</ol></details>
135
+ <details><summary><b>agent.yaml</b> and instructions</summary>
136
+ <textarea id="built-yaml" class="code-area" rows="16" spellcheck="false">${esc(b.yaml)}</textarea>
137
+ <textarea id="built-instr" class="code-area" rows="10" spellcheck="false">${esc(b.instructionsText)}</textarea>
138
+ <div class="dialog-actions"><button class="small primary" data-bt="save">Save</button></div></details>
139
+ ${b.runs.length ? `<details><summary><b>Recent runs</b></summary><ul class="built-runs">${b.runs.map((x) => `<li><span class="tag">${esc(x.status)}</span> <span class="hint">${esc(x.source ?? '')} · ${x.startedAt ? new Date(x.startedAt).toLocaleString() : ''}</span><div>${esc(String(x.result ?? '').slice(0, 300))}</div></li>`).join('')}</ul></details>` : ''}
140
+ <p class="hint">Task: ${esc(b.task ?? '')}</p>`;
141
+ if (!dlg.open) dlg.showModal();
142
+ clearInterval(poll);
143
+ if (b.pending) poll = setInterval(refresh, 3000);
144
+ }
145
+
146
+ async function refresh() {
147
+ if (!open || !dlg.open) return clearInterval(poll);
148
+ try {
149
+ const b = await api('GET', `/builder/${open.name}`);
150
+ if (b.pending !== open.pending || b.updatedAt !== open.updatedAt) show(b);
151
+ if (!b.pending) clearInterval(poll), load();
152
+ } catch {}
153
+ }
154
+
155
+ async function act(what) {
156
+ const name = open?.name;
157
+ try {
158
+ if (what === 'test') show(await api('POST', `/builder/${name}/test`));
159
+ else if (what === 'package') show(await api('POST', `/builder/${name}/package`, { platform: $('#built-platform').value }));
160
+ else if (what === 'start') {
161
+ show(await api('POST', `/builder/${name}/start`));
162
+ setTimeout(refresh, 1500);
163
+ } else if (what === 'stop') show(await api('POST', `/builder/${name}/stop`));
164
+ else if (what === 'save') {
165
+ show(await api('PUT', `/builder/${name}`, { yaml: $('#built-yaml').value, instructions: $('#built-instr').value }));
166
+ toast('Saved');
167
+ } else if (what === 'delete') {
168
+ if (!confirm(`Delete ${name} and its folder?`)) return;
169
+ await api('DELETE', `/builder/${name}`);
170
+ dlg.close();
171
+ }
172
+ load();
173
+ } catch (err) {
174
+ toast(`Error: ${err.message}`, 6000);
175
+ }
176
+ }
177
+
178
+ view.addEventListener('click', async (e) => {
179
+ if (e.target.closest('[data-bt="new"]')) return openBuild();
180
+ if (e.target.closest('[data-bt="connect"]')) return connect();
181
+ const rp = e.target.closest('[data-rp]');
182
+ if (rp) {
183
+ const key = rp.closest('[data-report]').dataset.report;
184
+ try {
185
+ if (rp.dataset.rp === 'forget') await api('DELETE', `/agentkit/agents/${encodeURIComponent(key)}`);
186
+ else {
187
+ rp.disabled = true;
188
+ await api('POST', `/agentkit/agents/${encodeURIComponent(key)}/approvals/${encodeURIComponent(rp.closest('[data-ap]').dataset.ap)}`, { allow: rp.dataset.rp === 'approve' });
189
+ toast(rp.dataset.rp === 'approve' ? 'Approved; the agent goes on' : 'Declined');
190
+ }
191
+ } catch (err) {
192
+ toast(`Error: ${err.message}`, 6000);
193
+ }
194
+ return;
195
+ }
196
+ const li = e.target.closest('[data-bt-open]');
197
+ if (li) show(await api('GET', `/builder/${li.dataset.btOpen}`));
198
+ });
199
+ body.addEventListener('click', (e) => {
200
+ const b = e.target.closest('[data-bt]');
201
+ if (b) act(b.dataset.bt);
202
+ });
203
+ $('#built-close').onclick = () => (clearInterval(poll), dlg.close());
204
+
205
+ load();
206
+ return { load, render };
207
+ }