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 +11 -0
- package/README.md +3 -3
- package/bin/agentkit.js +10 -0
- package/docs/agentkit.md +161 -0
- package/package.json +7 -3
- package/public/app.js +39 -5
- package/public/built.js +207 -0
- package/public/index.html +25 -0
- package/public/machine.js +4 -0
- package/public/style.css +33 -0
- package/public/termclip.js +92 -0
- package/src/agentkit/agent.js +171 -0
- package/src/agentkit/approvals.js +97 -0
- package/src/agentkit/cli.js +153 -0
- package/src/agentkit/engines.js +115 -0
- package/src/agentkit/http.js +131 -0
- package/src/agentkit/package.js +114 -0
- package/src/agentkit/report.js +106 -0
- package/src/agentkit/sea-main.js +25 -0
- package/src/agentkit/service.js +118 -0
- package/src/agentkit/spec.js +109 -0
- package/src/agentkit/stdio.js +95 -0
- package/src/agentkit/triggers.js +60 -0
- package/src/agentkit-reports.js +117 -0
- package/src/builder.js +332 -0
- package/src/manager.js +5 -0
- package/src/server.js +63 -5
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.
|
|
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.
|
|
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.
|
|
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.
|
package/bin/agentkit.js
ADDED
|
@@ -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));
|
package/docs/agentkit.md
ADDED
|
@@ -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.
|
|
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 === '
|
|
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
|
});
|
package/public/built.js
ADDED
|
@@ -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
|
+
}
|