@jx0/agency 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +120 -52
- package/dashboard/out/404.html +1 -1
- package/dashboard/out/_next/static/chunks/app/_not-found/{page-ad40673d821037f6.js → page-5cb94002960ab71a.js} +1 -1
- package/dashboard/out/_next/static/chunks/app/layout-6249f74085ad56b1.js +1 -0
- package/dashboard/out/_next/static/chunks/app/page-0a5ee03ddf4553ab.js +1 -0
- package/dashboard/out/_next/static/chunks/{main-app-1d848b791b823fa6.js → main-app-0398d52862f5c730.js} +1 -1
- package/dashboard/out/_next/static/css/a13af72b10a7d74f.css +1 -0
- package/dashboard/out/index.html +1 -1
- package/dashboard/out/index.txt +4 -4
- package/docs/images/agency_cli_ps.png +0 -0
- package/docs/images/agency_ui_ai_prodivder_settings.png +0 -0
- package/docs/images/agency_ui_aws_settings.png +0 -0
- package/docs/images/agency_ui_identity_settings.png +0 -0
- package/docs/images/agency_ui_import_skills.jpeg +0 -0
- package/docs/images/agency_ui_knowledge.png +0 -0
- package/docs/images/agency_ui_mission_control.png +0 -0
- package/docs/images/agency_ui_skills_marketplace.png +0 -0
- package/docs/images/agent_ui_agent_config.png +0 -0
- package/package.json +1 -1
- package/src/api/db/migrations/004_nullable_human_refs.ts +129 -0
- package/src/api/db/migrations/005_agent_skills.ts +14 -0
- package/src/api/db/migrations/006_runtime_machine.ts +24 -0
- package/src/api/db/seed.ts +62 -46
- package/src/api/index.ts +11 -4
- package/src/api/lib/deploy.ts +412 -0
- package/src/api/lib/env-vars.ts +19 -0
- package/src/api/lib/exec.ts +77 -0
- package/src/api/lib/fleet-sync.ts +49 -32
- package/src/api/lib/fs-store.ts +350 -0
- package/src/api/lib/import-skills.ts +105 -0
- package/src/api/lib/metrics.ts +183 -0
- package/src/api/lib/processes.ts +82 -12
- package/src/api/lib/provision-openclaw.ts +376 -0
- package/src/api/lib/remote-deploy.ts +77 -0
- package/src/api/lib/ssh.ts +97 -0
- package/src/api/lib/sync-skills.ts +171 -0
- package/src/api/lib/tunnels.ts +7 -38
- package/src/api/routes/agents.ts +184 -132
- package/src/api/routes/documents.ts +24 -5
- package/src/api/routes/knowledge.ts +7 -5
- package/src/api/routes/machines.ts +107 -0
- package/src/api/routes/messages.ts +23 -19
- package/src/api/routes/repos.ts +74 -0
- package/src/api/routes/role-configs.ts +29 -46
- package/src/api/routes/skills.ts +198 -40
- package/src/api/routes/tasks.ts +24 -11
- package/src/cli/commands/init.ts +47 -18
- package/src/cli/commands/machines.ts +97 -0
- package/src/cli/commands/ps.ts +6 -4
- package/src/cli/commands/repos.ts +78 -0
- package/src/cli/commands/ssh.ts +14 -36
- package/src/cli/index.ts +5 -1
- package/src/daemon.ts +120 -1
- package/src/templates/solo/agents-config.md +39 -0
- package/src/templates/solo/agents.md +41 -0
- package/src/templates/solo/heartbeat.md +48 -0
- package/src/templates/solo/tools.md +35 -0
- package/dashboard/out/_next/static/chunks/app/layout-056f12675e691d12.js +0 -1
- package/dashboard/out/_next/static/chunks/app/page-80f01fdbb09b43c8.js +0 -1
- package/dashboard/out/_next/static/css/27d1ea794f04e96a.css +0 -1
- /package/dashboard/out/_next/static/{BRrkKiSxqTmex5oLQlOY5 → TCwq0pYHnHPlPIcOGjzu7}/_buildManifest.js +0 -0
- /package/dashboard/out/_next/static/{BRrkKiSxqTmex5oLQlOY5 → TCwq0pYHnHPlPIcOGjzu7}/_ssgManifest.js +0 -0
package/src/api/routes/agents.ts
CHANGED
|
@@ -1,50 +1,138 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
1
3
|
import { Hono } from "hono";
|
|
2
4
|
import { db } from "../db/client.js";
|
|
3
5
|
import { resolveAgent } from "../lib/resolve-agent.js";
|
|
4
6
|
import {
|
|
5
7
|
writeAgentToFleet,
|
|
6
8
|
removeAgentFromFleet,
|
|
9
|
+
readFleet,
|
|
7
10
|
} from "../lib/fleet-sync.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { readFleet } from "../lib/fleet-sync.js";
|
|
11
|
+
import { listRoles, getRoleConfig } from "../lib/fs-store.js";
|
|
12
|
+
import { getMetrics } from "../lib/metrics.js";
|
|
11
13
|
|
|
12
14
|
const NAME_RE = /^[a-z][a-z0-9-]{1,30}$/;
|
|
13
|
-
const
|
|
15
|
+
const VALID_RUNTIMES = new Set(["system", "docker"]);
|
|
14
16
|
|
|
15
|
-
// Map of allowed file names to
|
|
17
|
+
// Map of allowed file names to config_type values
|
|
16
18
|
const FILE_TO_CONFIG_TYPE: Record<string, string> = {
|
|
17
19
|
"SOUL.md": "soul",
|
|
18
20
|
"USER.md": "identity",
|
|
19
21
|
"AGENTS.md": "agents",
|
|
20
|
-
"MEMORY.md": "soul",
|
|
22
|
+
"MEMORY.md": "soul",
|
|
21
23
|
"TOOLS.md": "tools",
|
|
22
24
|
};
|
|
23
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Write a docker-compose.agents.yml with a service for the given agent.
|
|
28
|
+
* Merges into any existing services so multiple agents can coexist.
|
|
29
|
+
*/
|
|
30
|
+
export function generateAgentCompose(agentName: string): void {
|
|
31
|
+
const composePath = path.join(process.cwd(), "docker-compose.agents.yml");
|
|
32
|
+
const serviceName = `agent-${agentName}`;
|
|
33
|
+
|
|
34
|
+
// Parse existing compose file to preserve other agent services
|
|
35
|
+
let existing: { services: Record<string, Record<string, unknown>> } = { services: {} };
|
|
36
|
+
if (fs.existsSync(composePath)) {
|
|
37
|
+
try {
|
|
38
|
+
const raw = fs.readFileSync(composePath, "utf-8");
|
|
39
|
+
// Parse our simple YAML format: extract service blocks
|
|
40
|
+
let currentService = "";
|
|
41
|
+
let currentVolumes = false;
|
|
42
|
+
for (const line of raw.split("\n")) {
|
|
43
|
+
const svcMatch = line.match(/^ (agent-[\w-]+):$/);
|
|
44
|
+
if (svcMatch) {
|
|
45
|
+
currentService = svcMatch[1];
|
|
46
|
+
existing.services[currentService] = {};
|
|
47
|
+
currentVolumes = false;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (currentService) {
|
|
51
|
+
const kvMatch = line.match(/^ (\w+): (.+)$/);
|
|
52
|
+
if (kvMatch) {
|
|
53
|
+
existing.services[currentService][kvMatch[1]] = kvMatch[2];
|
|
54
|
+
currentVolumes = false;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (line.match(/^ volumes:$/)) {
|
|
58
|
+
currentVolumes = true;
|
|
59
|
+
existing.services[currentService].volumes = [];
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (currentVolumes) {
|
|
63
|
+
const volMatch = line.match(/^ - (.+)$/);
|
|
64
|
+
if (volMatch) {
|
|
65
|
+
(existing.services[currentService].volumes as string[]).push(volMatch[1]);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} catch {}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const service = {
|
|
74
|
+
image: "agency-agent",
|
|
75
|
+
container_name: serviceName,
|
|
76
|
+
restart: "unless-stopped",
|
|
77
|
+
extra_hosts: ["host.docker.internal:host-gateway"],
|
|
78
|
+
volumes: [`/root/.openclaw-${agentName}:/root/.openclaw-${agentName}`],
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
existing.services[serviceName] = service;
|
|
82
|
+
|
|
83
|
+
// Write as YAML manually (simple enough structure)
|
|
84
|
+
let yaml = "services:\n";
|
|
85
|
+
for (const [name, svc] of Object.entries(existing.services)) {
|
|
86
|
+
const s = svc as Record<string, unknown>;
|
|
87
|
+
yaml += ` ${name}:\n`;
|
|
88
|
+
yaml += ` image: ${s.image}\n`;
|
|
89
|
+
yaml += ` container_name: ${s.container_name}\n`;
|
|
90
|
+
yaml += ` restart: ${s.restart}\n`;
|
|
91
|
+
if (Array.isArray(s.extra_hosts) && s.extra_hosts.length > 0) {
|
|
92
|
+
yaml += ` extra_hosts:\n`;
|
|
93
|
+
for (const h of s.extra_hosts) {
|
|
94
|
+
yaml += ` - ${h}\n`;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (Array.isArray(s.volumes) && s.volumes.length > 0) {
|
|
98
|
+
yaml += ` volumes:\n`;
|
|
99
|
+
for (const v of s.volumes) {
|
|
100
|
+
yaml += ` - ${v}\n`;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
fs.writeFileSync(composePath, yaml);
|
|
106
|
+
}
|
|
107
|
+
|
|
24
108
|
export const agents = new Hono();
|
|
25
109
|
|
|
26
|
-
// List available roles (from
|
|
110
|
+
// List available roles (from filesystem)
|
|
27
111
|
agents.get("/roles", async (c) => {
|
|
28
|
-
|
|
29
|
-
.selectFrom("role_configs")
|
|
30
|
-
.select("role")
|
|
31
|
-
.distinct()
|
|
32
|
-
.execute();
|
|
33
|
-
return c.json(rows.map((r) => r.role));
|
|
112
|
+
return c.json(listRoles());
|
|
34
113
|
});
|
|
35
114
|
|
|
36
115
|
agents.get("/", async (c) => {
|
|
37
116
|
const rows = await db.selectFrom("agents").selectAll().execute();
|
|
38
|
-
|
|
117
|
+
|
|
118
|
+
const enriched = rows.map((r) => ({
|
|
119
|
+
...r,
|
|
120
|
+
metrics: getMetrics(r.name),
|
|
121
|
+
}));
|
|
122
|
+
return c.json(enriched);
|
|
39
123
|
});
|
|
40
124
|
|
|
41
125
|
agents.get("/:name", async (c) => {
|
|
42
126
|
const agent = await resolveAgent(c.req.param("name"));
|
|
43
127
|
if (!agent) return c.json({ error: "not found" }, 404);
|
|
44
|
-
|
|
128
|
+
|
|
129
|
+
return c.json({
|
|
130
|
+
...agent,
|
|
131
|
+
metrics: getMetrics(agent.name),
|
|
132
|
+
});
|
|
45
133
|
});
|
|
46
134
|
|
|
47
|
-
// Get agent config file from
|
|
135
|
+
// Get agent config file from filesystem
|
|
48
136
|
agents.get("/:name/files/:filename", async (c) => {
|
|
49
137
|
const filename = c.req.param("filename");
|
|
50
138
|
const configType = FILE_TO_CONFIG_TYPE[filename];
|
|
@@ -54,17 +142,11 @@ agents.get("/:name/files/:filename", async (c) => {
|
|
|
54
142
|
const agent = await resolveAgent(c.req.param("name"));
|
|
55
143
|
if (!agent) return c.json({ error: "not found" }, 404);
|
|
56
144
|
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
.where("role", "=", agent.role)
|
|
60
|
-
.where("config_type", "=", configType)
|
|
61
|
-
.selectAll()
|
|
62
|
-
.executeTakeFirst();
|
|
63
|
-
|
|
64
|
-
if (!config) {
|
|
145
|
+
const content = getRoleConfig(agent.role, configType);
|
|
146
|
+
if (content === null) {
|
|
65
147
|
return c.json({ error: "file not found" }, 404);
|
|
66
148
|
}
|
|
67
|
-
return c.json({ filename, content
|
|
149
|
+
return c.json({ filename, content });
|
|
68
150
|
});
|
|
69
151
|
|
|
70
152
|
// Get agent's role config by config_type directly
|
|
@@ -72,15 +154,10 @@ agents.get("/:name/config/:configType", async (c) => {
|
|
|
72
154
|
const agent = await resolveAgent(c.req.param("name"));
|
|
73
155
|
if (!agent) return c.json({ error: "not found" }, 404);
|
|
74
156
|
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
.selectAll()
|
|
80
|
-
.executeTakeFirst();
|
|
81
|
-
|
|
82
|
-
if (!config) return c.json({ error: "not found" }, 404);
|
|
83
|
-
return c.json(config);
|
|
157
|
+
const configType = c.req.param("configType");
|
|
158
|
+
const content = getRoleConfig(agent.role, configType);
|
|
159
|
+
if (content === null) return c.json({ error: "not found" }, 404);
|
|
160
|
+
return c.json({ role: agent.role, config_type: configType, content });
|
|
84
161
|
});
|
|
85
162
|
|
|
86
163
|
// Create agent
|
|
@@ -88,7 +165,8 @@ agents.post("/", async (c) => {
|
|
|
88
165
|
const body = await c.req.json<{
|
|
89
166
|
name: string;
|
|
90
167
|
role: string;
|
|
91
|
-
|
|
168
|
+
runtime: string; // "system" | "docker"
|
|
169
|
+
machine: string; // required machine name
|
|
92
170
|
slack_bot_token?: string;
|
|
93
171
|
slack_app_token?: string;
|
|
94
172
|
}>();
|
|
@@ -96,8 +174,16 @@ agents.post("/", async (c) => {
|
|
|
96
174
|
if (!NAME_RE.test(body.name)) {
|
|
97
175
|
return c.json({ error: "invalid name: must match /^[a-z][a-z0-9-]{1,30}$/" }, 400);
|
|
98
176
|
}
|
|
99
|
-
if (!
|
|
100
|
-
return c.json({ error: "invalid
|
|
177
|
+
if (!VALID_RUNTIMES.has(body.runtime)) {
|
|
178
|
+
return c.json({ error: "invalid runtime: must be system or docker" }, 400);
|
|
179
|
+
}
|
|
180
|
+
if (!body.machine) {
|
|
181
|
+
return c.json({ error: "machine is required" }, 400);
|
|
182
|
+
}
|
|
183
|
+
// Validate machine exists
|
|
184
|
+
const { readMachines } = await import("../routes/machines.js");
|
|
185
|
+
if (!readMachines().find((m: any) => m.name === body.machine)) {
|
|
186
|
+
return c.json({ error: `machine "${body.machine}" not found` }, 400);
|
|
101
187
|
}
|
|
102
188
|
|
|
103
189
|
const existing = await db
|
|
@@ -112,7 +198,8 @@ agents.post("/", async (c) => {
|
|
|
112
198
|
// Write to fleet.json
|
|
113
199
|
await writeAgentToFleet(body.name, {
|
|
114
200
|
role: body.role,
|
|
115
|
-
|
|
201
|
+
runtime: body.runtime,
|
|
202
|
+
machine: body.machine,
|
|
116
203
|
...(body.slack_bot_token ? { slackBotToken: body.slack_bot_token } : {}),
|
|
117
204
|
...(body.slack_app_token ? { slackAppToken: body.slack_app_token } : {}),
|
|
118
205
|
});
|
|
@@ -124,7 +211,8 @@ agents.post("/", async (c) => {
|
|
|
124
211
|
id: crypto.randomUUID(),
|
|
125
212
|
name: body.name,
|
|
126
213
|
role: body.role,
|
|
127
|
-
|
|
214
|
+
runtime: body.runtime,
|
|
215
|
+
machine: body.machine,
|
|
128
216
|
slack_bot_token: body.slack_bot_token ?? null,
|
|
129
217
|
slack_app_token: body.slack_app_token ?? null,
|
|
130
218
|
status: "idle",
|
|
@@ -146,7 +234,8 @@ agents.patch("/:name", async (c) => {
|
|
|
146
234
|
status?: string;
|
|
147
235
|
current_task?: string | null;
|
|
148
236
|
role?: string;
|
|
149
|
-
|
|
237
|
+
runtime?: string;
|
|
238
|
+
machine?: string | null;
|
|
150
239
|
slack_bot_token?: string | null;
|
|
151
240
|
slack_app_token?: string | null;
|
|
152
241
|
}>();
|
|
@@ -155,11 +244,14 @@ agents.patch("/:name", async (c) => {
|
|
|
155
244
|
if (body.status !== undefined) q = q.set("status", body.status);
|
|
156
245
|
if (body.current_task !== undefined) q = q.set("current_task", body.current_task);
|
|
157
246
|
if (body.role !== undefined) q = q.set("role", body.role);
|
|
158
|
-
if (body.
|
|
159
|
-
if (!
|
|
160
|
-
return c.json({ error: "invalid
|
|
247
|
+
if (body.runtime !== undefined) {
|
|
248
|
+
if (!VALID_RUNTIMES.has(body.runtime)) {
|
|
249
|
+
return c.json({ error: "invalid runtime" }, 400);
|
|
161
250
|
}
|
|
162
|
-
q = q.set("
|
|
251
|
+
q = q.set("runtime", body.runtime);
|
|
252
|
+
}
|
|
253
|
+
if (body.machine !== undefined) {
|
|
254
|
+
q = q.set("machine", body.machine);
|
|
163
255
|
}
|
|
164
256
|
if (body.slack_bot_token !== undefined) q = q.set("slack_bot_token", body.slack_bot_token);
|
|
165
257
|
if (body.slack_app_token !== undefined) q = q.set("slack_app_token", body.slack_app_token);
|
|
@@ -168,12 +260,17 @@ agents.patch("/:name", async (c) => {
|
|
|
168
260
|
const updated = await q.returningAll().executeTakeFirstOrThrow();
|
|
169
261
|
|
|
170
262
|
// Sync config fields to fleet.json
|
|
171
|
-
if (body.role !== undefined || body.
|
|
263
|
+
if (body.role !== undefined || body.runtime !== undefined ||
|
|
264
|
+
body.machine !== undefined ||
|
|
172
265
|
body.slack_bot_token !== undefined ||
|
|
173
266
|
body.slack_app_token !== undefined) {
|
|
267
|
+
const fleet = readFleet();
|
|
268
|
+
const existing = fleet.agents[agent.name] ?? { role: updated.role, runtime: updated.runtime ?? "system" };
|
|
174
269
|
await writeAgentToFleet(agent.name, {
|
|
270
|
+
...existing,
|
|
175
271
|
role: updated.role,
|
|
176
|
-
|
|
272
|
+
runtime: updated.runtime ?? "system",
|
|
273
|
+
machine: body.machine !== undefined ? (body.machine ?? undefined) : (existing.machine ?? undefined),
|
|
177
274
|
...(updated.slack_bot_token ? { slackBotToken: updated.slack_bot_token } : {}),
|
|
178
275
|
...(updated.slack_app_token ? { slackAppToken: updated.slack_app_token } : {}),
|
|
179
276
|
});
|
|
@@ -187,17 +284,28 @@ agents.delete("/:name", async (c) => {
|
|
|
187
284
|
const agent = await resolveAgent(c.req.param("name"));
|
|
188
285
|
if (!agent) return c.json({ error: "not found" }, 404);
|
|
189
286
|
|
|
190
|
-
// Stop
|
|
191
|
-
if (agent.
|
|
287
|
+
// Stop agent if running
|
|
288
|
+
if (agent.status === "active") {
|
|
192
289
|
try {
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
290
|
+
const { stop } = await import("../lib/deploy.js");
|
|
291
|
+
await stop({
|
|
292
|
+
name: agent.name,
|
|
293
|
+
role: agent.role,
|
|
294
|
+
runtime: agent.runtime ?? "system",
|
|
295
|
+
machine: agent.machine ?? "",
|
|
296
|
+
});
|
|
198
297
|
} catch {}
|
|
199
298
|
}
|
|
200
299
|
|
|
300
|
+
// Unlink related records before deleting
|
|
301
|
+
await db.updateTable("tasks").set({ created_by: null }).where("created_by", "=", agent.id).execute();
|
|
302
|
+
await db.deleteFrom("task_assignees").where("agent_id", "=", agent.id).execute();
|
|
303
|
+
await db.updateTable("messages").set({ from_agent: null }).where("from_agent", "=", agent.id).execute();
|
|
304
|
+
await db.updateTable("activities").set({ agent_id: null }).where("agent_id", "=", agent.id).execute();
|
|
305
|
+
await db.deleteFrom("notifications").where("target_agent", "=", agent.id).execute();
|
|
306
|
+
await db.updateTable("notifications").set({ source_agent: null }).where("source_agent", "=", agent.id).execute();
|
|
307
|
+
await db.updateTable("documents").set({ created_by: null }).where("created_by", "=", agent.id).execute();
|
|
308
|
+
|
|
201
309
|
await db.deleteFrom("agents").where("id", "=", agent.id).execute();
|
|
202
310
|
await removeAgentFromFleet(agent.name);
|
|
203
311
|
|
|
@@ -209,57 +317,25 @@ agents.post("/:name/deploy", async (c) => {
|
|
|
209
317
|
const agent = await resolveAgent(c.req.param("name"));
|
|
210
318
|
if (!agent) return c.json({ error: "not found" }, 404);
|
|
211
319
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
await
|
|
215
|
-
.
|
|
216
|
-
|
|
217
|
-
.
|
|
218
|
-
.
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
if (agent.location === "ec2") {
|
|
223
|
-
const fleet = readFleet();
|
|
224
|
-
const fleetAgent = fleet.agents[agent.name];
|
|
225
|
-
const host = fleetAgent?.host;
|
|
226
|
-
if (!host) {
|
|
227
|
-
return c.json({ error: "No host configured for EC2 agent. Set 'host' in fleet.json or agent config." }, 400);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// Open reverse SSH tunnel so agent can reach our API at localhost:3100
|
|
231
|
-
try {
|
|
232
|
-
await startTunnel(agent.name, host);
|
|
233
|
-
} catch (err: any) {
|
|
234
|
-
return c.json({ error: `Failed to open SSH tunnel: ${err.message}` }, 500);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
await db
|
|
238
|
-
.updateTable("agents")
|
|
239
|
-
.where("id", "=", agent.id)
|
|
240
|
-
.set({ status: "active", updated_at: new Date().toISOString() })
|
|
241
|
-
.execute();
|
|
242
|
-
return c.json({ status: "deployed", method: "ec2", tunnel: true });
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (agent.location === "docker") {
|
|
246
|
-
const proc = Bun.spawn(
|
|
247
|
-
["docker", "compose", "-f", "docker-compose.agents.yml", "up", "-d", `agent-${agent.name}`],
|
|
248
|
-
{ cwd: process.cwd(), stdout: "inherit", stderr: "inherit" }
|
|
249
|
-
);
|
|
250
|
-
const exitCode = await proc.exited;
|
|
251
|
-
if (exitCode !== 0) {
|
|
252
|
-
return c.json({ error: "docker compose up failed" }, 500);
|
|
253
|
-
}
|
|
320
|
+
try {
|
|
321
|
+
const { deploy } = await import("../lib/deploy.js");
|
|
322
|
+
const result = await deploy({
|
|
323
|
+
name: agent.name,
|
|
324
|
+
role: agent.role,
|
|
325
|
+
runtime: agent.runtime ?? "system",
|
|
326
|
+
machine: agent.machine ?? "",
|
|
327
|
+
slack_bot_token: agent.slack_bot_token,
|
|
328
|
+
slack_app_token: agent.slack_app_token,
|
|
329
|
+
});
|
|
254
330
|
await db
|
|
255
331
|
.updateTable("agents")
|
|
256
332
|
.where("id", "=", agent.id)
|
|
257
333
|
.set({ status: "active", updated_at: new Date().toISOString() })
|
|
258
334
|
.execute();
|
|
259
|
-
return c.json({ status: "deployed", method:
|
|
335
|
+
return c.json({ status: "deployed", method: result.method });
|
|
336
|
+
} catch (err: any) {
|
|
337
|
+
return c.json({ error: err.message }, 500);
|
|
260
338
|
}
|
|
261
|
-
|
|
262
|
-
return c.json({ error: "unsupported location" }, 400);
|
|
263
339
|
});
|
|
264
340
|
|
|
265
341
|
// Stop agent
|
|
@@ -267,45 +343,21 @@ agents.post("/:name/stop", async (c) => {
|
|
|
267
343
|
const agent = await resolveAgent(c.req.param("name"));
|
|
268
344
|
if (!agent) return c.json({ error: "not found" }, 404);
|
|
269
345
|
|
|
270
|
-
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
.
|
|
277
|
-
|
|
278
|
-
.set({ status: "idle", updated_at: new Date().toISOString() })
|
|
279
|
-
.execute();
|
|
280
|
-
return c.json({ status: "stopped" });
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
if (agent.location === "docker") {
|
|
284
|
-
const proc = Bun.spawn(
|
|
285
|
-
["docker", "compose", "-f", "docker-compose.agents.yml", "stop", `agent-${agent.name}`],
|
|
286
|
-
{ cwd: process.cwd(), stdout: "inherit", stderr: "inherit" }
|
|
287
|
-
);
|
|
288
|
-
const exitCode = await proc.exited;
|
|
289
|
-
if (exitCode !== 0) {
|
|
290
|
-
return c.json({ error: "docker compose stop failed" }, 500);
|
|
291
|
-
}
|
|
346
|
+
try {
|
|
347
|
+
const { stop } = await import("../lib/deploy.js");
|
|
348
|
+
await stop({
|
|
349
|
+
name: agent.name,
|
|
350
|
+
role: agent.role,
|
|
351
|
+
runtime: agent.runtime ?? "system",
|
|
352
|
+
machine: agent.machine ?? "",
|
|
353
|
+
});
|
|
292
354
|
await db
|
|
293
355
|
.updateTable("agents")
|
|
294
356
|
.where("id", "=", agent.id)
|
|
295
357
|
.set({ status: "idle", updated_at: new Date().toISOString() })
|
|
296
358
|
.execute();
|
|
297
359
|
return c.json({ status: "stopped" });
|
|
360
|
+
} catch (err: any) {
|
|
361
|
+
return c.json({ error: err.message }, 500);
|
|
298
362
|
}
|
|
299
|
-
|
|
300
|
-
if (agent.location === "ec2") {
|
|
301
|
-
stopTunnel(agent.name);
|
|
302
|
-
await db
|
|
303
|
-
.updateTable("agents")
|
|
304
|
-
.where("id", "=", agent.id)
|
|
305
|
-
.set({ status: "idle", updated_at: new Date().toISOString() })
|
|
306
|
-
.execute();
|
|
307
|
-
return c.json({ status: "stopped", tunnel: "closed" });
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
return c.json({ error: "stop not supported for this agent location" }, 400);
|
|
311
363
|
});
|
|
@@ -22,10 +22,11 @@ documents.get("/:id", async (c) => {
|
|
|
22
22
|
documents.post("/", async (c) => {
|
|
23
23
|
const body = await c.req.json<{
|
|
24
24
|
title: string; content: string; doc_type?: string;
|
|
25
|
-
task_id?: string; from
|
|
25
|
+
task_id?: string; from?: string;
|
|
26
26
|
}>();
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
// "human" or missing from = NULL (human user)
|
|
28
|
+
const agent = body.from && body.from !== "human" ? await resolveAgent(body.from) : null;
|
|
29
|
+
if (body.from && body.from !== "human" && !agent) return c.json({ error: "unknown agent" }, 400);
|
|
29
30
|
|
|
30
31
|
const doc = await db.insertInto("documents").values({
|
|
31
32
|
id: crypto.randomUUID(),
|
|
@@ -33,9 +34,27 @@ documents.post("/", async (c) => {
|
|
|
33
34
|
content: body.content,
|
|
34
35
|
doc_type: body.doc_type ?? "general",
|
|
35
36
|
task_id: body.task_id ?? null,
|
|
36
|
-
created_by: agent
|
|
37
|
+
created_by: agent?.id ?? null,
|
|
37
38
|
}).returningAll().executeTakeFirstOrThrow();
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
if (agent) {
|
|
41
|
+
await logActivity("document_created", agent.id, `Created doc: ${doc.title}`, doc.task_id);
|
|
42
|
+
}
|
|
40
43
|
return c.json(doc, 201);
|
|
41
44
|
});
|
|
45
|
+
|
|
46
|
+
documents.put("/:id", async (c) => {
|
|
47
|
+
const id = c.req.param("id");
|
|
48
|
+
const body = await c.req.json<{ title?: string; content?: string; doc_type?: string }>();
|
|
49
|
+
|
|
50
|
+
const existing = await db.selectFrom("documents").where("id", "=", id).selectAll().executeTakeFirst();
|
|
51
|
+
if (!existing) return c.json({ error: "not found" }, 404);
|
|
52
|
+
|
|
53
|
+
const updated = await db.updateTable("documents").set({
|
|
54
|
+
...(body.title !== undefined && { title: body.title }),
|
|
55
|
+
...(body.content !== undefined && { content: body.content }),
|
|
56
|
+
...(body.doc_type !== undefined && { doc_type: body.doc_type }),
|
|
57
|
+
}).where("id", "=", id).returningAll().executeTakeFirstOrThrow();
|
|
58
|
+
|
|
59
|
+
return c.json(updated);
|
|
60
|
+
});
|
|
@@ -32,25 +32,27 @@ knowledge.get("/", async (c) => {
|
|
|
32
32
|
|
|
33
33
|
knowledge.post("/", async (c) => {
|
|
34
34
|
const body = await c.req.json<{
|
|
35
|
-
key: string; content: string; from
|
|
35
|
+
key: string; content: string; from?: string;
|
|
36
36
|
task_id?: string; tags?: string[];
|
|
37
37
|
}>();
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
// "human" or missing from = NULL (human user)
|
|
39
|
+
const agent = body.from && body.from !== "human" ? await resolveAgent(body.from) : null;
|
|
40
|
+
if (body.from && body.from !== "human" && !agent) return c.json({ error: "unknown agent" }, 400);
|
|
40
41
|
|
|
41
42
|
const tagsJson = JSON.stringify(body.tags ?? []);
|
|
43
|
+
const sourceId = agent?.id ?? null;
|
|
42
44
|
|
|
43
45
|
const row = await db.insertInto("knowledge").values({
|
|
44
46
|
id: crypto.randomUUID(),
|
|
45
47
|
key: body.key,
|
|
46
48
|
content: body.content,
|
|
47
|
-
source:
|
|
49
|
+
source: sourceId ?? "human",
|
|
48
50
|
task_id: body.task_id ?? null,
|
|
49
51
|
tags: tagsJson,
|
|
50
52
|
})
|
|
51
53
|
.onConflict((oc) => oc.column("key").doUpdateSet({
|
|
52
54
|
content: body.content,
|
|
53
|
-
source:
|
|
55
|
+
source: sourceId ?? "human",
|
|
54
56
|
tags: tagsJson,
|
|
55
57
|
}))
|
|
56
58
|
.returningAll()
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
|
|
5
|
+
export interface Machine {
|
|
6
|
+
name: string;
|
|
7
|
+
host: string;
|
|
8
|
+
user: string;
|
|
9
|
+
port: number;
|
|
10
|
+
auth: "local" | "key" | "password";
|
|
11
|
+
ssh_key?: string;
|
|
12
|
+
password?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function resolveMachinesPath(): string {
|
|
16
|
+
return process.env.MACHINES_PATH ?? path.resolve(process.cwd(), ".agency", "machines.json");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function readMachines(): Machine[] {
|
|
20
|
+
const p = resolveMachinesPath();
|
|
21
|
+
if (!fs.existsSync(p)) return [];
|
|
22
|
+
return JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function writeMachines(machines: Machine[]): void {
|
|
26
|
+
const p = resolveMachinesPath();
|
|
27
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
28
|
+
fs.writeFileSync(p, JSON.stringify(machines, null, 2) + "\n");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function mask(m: Machine): Record<string, unknown> {
|
|
32
|
+
return {
|
|
33
|
+
name: m.name,
|
|
34
|
+
host: m.host,
|
|
35
|
+
user: m.user,
|
|
36
|
+
port: m.port,
|
|
37
|
+
auth: m.auth,
|
|
38
|
+
ssh_key: m.ssh_key ? "********" : undefined,
|
|
39
|
+
password: m.password ? "********" : undefined,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const machines = new Hono();
|
|
44
|
+
|
|
45
|
+
machines.get("/", (c) => {
|
|
46
|
+
return c.json(readMachines().map(mask));
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
machines.post("/", async (c) => {
|
|
50
|
+
const body = await c.req.json<Machine>();
|
|
51
|
+
if (!body.name || !body.host || !body.user) {
|
|
52
|
+
return c.json({ error: "name, host, and user are required" }, 400);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const list = readMachines();
|
|
56
|
+
if (list.find((m) => m.name === body.name)) {
|
|
57
|
+
return c.json({ error: "machine already exists" }, 409);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (body.auth === "local" && list.some((m) => m.auth === "local")) {
|
|
61
|
+
return c.json({ error: "a local machine already exists" }, 409);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
list.push({
|
|
65
|
+
name: body.name,
|
|
66
|
+
host: body.host,
|
|
67
|
+
user: body.user,
|
|
68
|
+
port: body.port || 22,
|
|
69
|
+
auth: body.auth || "key",
|
|
70
|
+
ssh_key: body.ssh_key,
|
|
71
|
+
password: body.password,
|
|
72
|
+
});
|
|
73
|
+
writeMachines(list);
|
|
74
|
+
return c.json(mask(list[list.length - 1]), 201);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
machines.put("/:name", async (c) => {
|
|
78
|
+
const name = c.req.param("name");
|
|
79
|
+
const list = readMachines();
|
|
80
|
+
const idx = list.findIndex((m) => m.name === name);
|
|
81
|
+
if (idx === -1) return c.json({ error: "not found" }, 404);
|
|
82
|
+
|
|
83
|
+
const body = await c.req.json<Partial<Machine>>();
|
|
84
|
+
const existing = list[idx];
|
|
85
|
+
list[idx] = {
|
|
86
|
+
name: existing.name,
|
|
87
|
+
host: body.host ?? existing.host,
|
|
88
|
+
user: body.user ?? existing.user,
|
|
89
|
+
port: body.port ?? existing.port,
|
|
90
|
+
auth: body.auth ?? existing.auth,
|
|
91
|
+
ssh_key: body.ssh_key !== undefined ? body.ssh_key : existing.ssh_key,
|
|
92
|
+
password: body.password !== undefined ? body.password : existing.password,
|
|
93
|
+
};
|
|
94
|
+
writeMachines(list);
|
|
95
|
+
return c.json(mask(list[idx]));
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
machines.delete("/:name", (c) => {
|
|
99
|
+
const name = c.req.param("name");
|
|
100
|
+
const list = readMachines();
|
|
101
|
+
const filtered = list.filter((m) => m.name !== name);
|
|
102
|
+
if (filtered.length === list.length) {
|
|
103
|
+
return c.json({ error: "not found" }, 404);
|
|
104
|
+
}
|
|
105
|
+
writeMachines(filtered);
|
|
106
|
+
return c.json({ ok: true });
|
|
107
|
+
});
|