@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.
Files changed (62) hide show
  1. package/README.md +120 -52
  2. package/dashboard/out/404.html +1 -1
  3. package/dashboard/out/_next/static/chunks/app/_not-found/{page-ad40673d821037f6.js → page-5cb94002960ab71a.js} +1 -1
  4. package/dashboard/out/_next/static/chunks/app/layout-6249f74085ad56b1.js +1 -0
  5. package/dashboard/out/_next/static/chunks/app/page-0a5ee03ddf4553ab.js +1 -0
  6. package/dashboard/out/_next/static/chunks/{main-app-1d848b791b823fa6.js → main-app-0398d52862f5c730.js} +1 -1
  7. package/dashboard/out/_next/static/css/a13af72b10a7d74f.css +1 -0
  8. package/dashboard/out/index.html +1 -1
  9. package/dashboard/out/index.txt +4 -4
  10. package/docs/images/agency_cli_ps.png +0 -0
  11. package/docs/images/agency_ui_ai_prodivder_settings.png +0 -0
  12. package/docs/images/agency_ui_aws_settings.png +0 -0
  13. package/docs/images/agency_ui_identity_settings.png +0 -0
  14. package/docs/images/agency_ui_import_skills.jpeg +0 -0
  15. package/docs/images/agency_ui_knowledge.png +0 -0
  16. package/docs/images/agency_ui_mission_control.png +0 -0
  17. package/docs/images/agency_ui_skills_marketplace.png +0 -0
  18. package/docs/images/agent_ui_agent_config.png +0 -0
  19. package/package.json +1 -1
  20. package/src/api/db/migrations/004_nullable_human_refs.ts +129 -0
  21. package/src/api/db/migrations/005_agent_skills.ts +14 -0
  22. package/src/api/db/migrations/006_runtime_machine.ts +24 -0
  23. package/src/api/db/seed.ts +62 -46
  24. package/src/api/index.ts +11 -4
  25. package/src/api/lib/deploy.ts +412 -0
  26. package/src/api/lib/env-vars.ts +19 -0
  27. package/src/api/lib/exec.ts +77 -0
  28. package/src/api/lib/fleet-sync.ts +49 -32
  29. package/src/api/lib/fs-store.ts +350 -0
  30. package/src/api/lib/import-skills.ts +105 -0
  31. package/src/api/lib/metrics.ts +183 -0
  32. package/src/api/lib/processes.ts +82 -12
  33. package/src/api/lib/provision-openclaw.ts +376 -0
  34. package/src/api/lib/remote-deploy.ts +77 -0
  35. package/src/api/lib/ssh.ts +97 -0
  36. package/src/api/lib/sync-skills.ts +171 -0
  37. package/src/api/lib/tunnels.ts +7 -38
  38. package/src/api/routes/agents.ts +184 -132
  39. package/src/api/routes/documents.ts +24 -5
  40. package/src/api/routes/knowledge.ts +7 -5
  41. package/src/api/routes/machines.ts +107 -0
  42. package/src/api/routes/messages.ts +23 -19
  43. package/src/api/routes/repos.ts +74 -0
  44. package/src/api/routes/role-configs.ts +29 -46
  45. package/src/api/routes/skills.ts +198 -40
  46. package/src/api/routes/tasks.ts +24 -11
  47. package/src/cli/commands/init.ts +47 -18
  48. package/src/cli/commands/machines.ts +97 -0
  49. package/src/cli/commands/ps.ts +6 -4
  50. package/src/cli/commands/repos.ts +78 -0
  51. package/src/cli/commands/ssh.ts +14 -36
  52. package/src/cli/index.ts +5 -1
  53. package/src/daemon.ts +120 -1
  54. package/src/templates/solo/agents-config.md +39 -0
  55. package/src/templates/solo/agents.md +41 -0
  56. package/src/templates/solo/heartbeat.md +48 -0
  57. package/src/templates/solo/tools.md +35 -0
  58. package/dashboard/out/_next/static/chunks/app/layout-056f12675e691d12.js +0 -1
  59. package/dashboard/out/_next/static/chunks/app/page-80f01fdbb09b43c8.js +0 -1
  60. package/dashboard/out/_next/static/css/27d1ea794f04e96a.css +0 -1
  61. /package/dashboard/out/_next/static/{BRrkKiSxqTmex5oLQlOY5 → TCwq0pYHnHPlPIcOGjzu7}/_buildManifest.js +0 -0
  62. /package/dashboard/out/_next/static/{BRrkKiSxqTmex5oLQlOY5 → TCwq0pYHnHPlPIcOGjzu7}/_ssgManifest.js +0 -0
@@ -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 { startLocal, stopLocal } from "../lib/processes.js";
9
- import { startTunnel, stopTunnel } from "../lib/tunnels.js";
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 VALID_LOCATIONS = new Set(["docker", "ec2", "local"]);
15
+ const VALID_RUNTIMES = new Set(["system", "docker"]);
14
16
 
15
- // Map of allowed file names to role_config config_type values
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", // memory is part of soul for now
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 DB role_configs)
110
+ // List available roles (from filesystem)
27
111
  agents.get("/roles", async (c) => {
28
- const rows = await db
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
- return c.json(rows);
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
- return c.json(agent);
128
+
129
+ return c.json({
130
+ ...agent,
131
+ metrics: getMetrics(agent.name),
132
+ });
45
133
  });
46
134
 
47
- // Get agent config file from DB
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 config = await db
58
- .selectFrom("role_configs")
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: config.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 config = await db
76
- .selectFrom("role_configs")
77
- .where("role", "=", agent.role)
78
- .where("config_type", "=", c.req.param("configType"))
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
- location: string;
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 (!VALID_LOCATIONS.has(body.location)) {
100
- return c.json({ error: "invalid location: must be docker, ec2, or local" }, 400);
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
- location: body.location,
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
- location: body.location,
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
- location?: string;
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.location !== undefined) {
159
- if (!VALID_LOCATIONS.has(body.location)) {
160
- return c.json({ error: "invalid location" }, 400);
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("location", body.location);
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.location !== undefined ||
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
- location: updated.location ?? "local",
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 docker container if running
191
- if (agent.location === "docker") {
287
+ // Stop agent if running
288
+ if (agent.status === "active") {
192
289
  try {
193
- const proc = Bun.spawn(
194
- ["docker", "compose", "-f", "docker-compose.agents.yml", "stop", `agent-${agent.name}`],
195
- { cwd: process.cwd(), stdout: "inherit", stderr: "inherit" }
196
- );
197
- await proc.exited;
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
- if (agent.location === "local") {
213
- startLocal(agent.name, agent.role);
214
- await db
215
- .updateTable("agents")
216
- .where("id", "=", agent.id)
217
- .set({ status: "active", updated_at: new Date().toISOString() })
218
- .execute();
219
- return c.json({ status: "deployed", method: "local" });
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: "docker" });
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
- if (agent.location === "local") {
271
- const stopped = stopLocal(agent.name);
272
- if (!stopped) {
273
- return c.json({ error: "no local process found" }, 400);
274
- }
275
- await db
276
- .updateTable("agents")
277
- .where("id", "=", agent.id)
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: string;
25
+ task_id?: string; from?: string;
26
26
  }>();
27
- const agent = await resolveAgent(body.from);
28
- if (!agent) return c.json({ error: "unknown agent" }, 400);
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.id,
37
+ created_by: agent?.id ?? null,
37
38
  }).returningAll().executeTakeFirstOrThrow();
38
39
 
39
- await logActivity("document_created", agent.id, `Created doc: ${doc.title}`, doc.task_id);
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: string;
35
+ key: string; content: string; from?: string;
36
36
  task_id?: string; tags?: string[];
37
37
  }>();
38
- const agent = await resolveAgent(body.from);
39
- if (!agent) return c.json({ error: "unknown agent" }, 400);
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: agent.id,
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: agent.id,
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
+ });