@jx0/agency 0.2.1 → 0.4.1
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 +407 -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 → BIIuuS2pf7AQlPcSE2A6K}/_buildManifest.js +0 -0
- /package/dashboard/out/_next/static/{BRrkKiSxqTmex5oLQlOY5 → BIIuuS2pf7AQlPcSE2A6K}/_ssgManifest.js +0 -0
|
@@ -24,31 +24,35 @@ messages.get("/:taskId/messages", async (c) => {
|
|
|
24
24
|
|
|
25
25
|
messages.post("/:taskId/messages", async (c) => {
|
|
26
26
|
const taskId = c.req.param("taskId");
|
|
27
|
-
const { from, content } = await c.req.json<{ from
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
const { from, content } = await c.req.json<{ from?: string; content: string }>();
|
|
28
|
+
// "human" or missing from = NULL (human user)
|
|
29
|
+
const agent = from && from !== "human" ? await resolveAgent(from) : null;
|
|
30
|
+
if (from && from !== "human" && !agent) return c.json({ error: "unknown agent" }, 400);
|
|
30
31
|
|
|
31
32
|
const msg = await db.insertInto("messages").values({
|
|
32
33
|
id: crypto.randomUUID(),
|
|
33
|
-
task_id: taskId, from_agent: agent
|
|
34
|
+
task_id: taskId, from_agent: agent?.id ?? null, content,
|
|
34
35
|
}).returningAll().executeTakeFirstOrThrow();
|
|
35
36
|
|
|
36
|
-
// Collect notification targets
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
37
|
+
// Collect notification targets (only if from an agent)
|
|
38
|
+
if (agent) {
|
|
39
|
+
const mentionIds = await parseMentions(content, agent.id);
|
|
40
|
+
const subscriberIds = await getTaskSubscribers(taskId, agent.id);
|
|
41
|
+
const targetIds = new Set([...mentionIds, ...subscriberIds]);
|
|
42
|
+
|
|
43
|
+
// Create notifications
|
|
44
|
+
for (const targetId of targetIds) {
|
|
45
|
+
await db.insertInto("notifications").values({
|
|
46
|
+
id: crypto.randomUUID(),
|
|
47
|
+
target_agent: targetId,
|
|
48
|
+
source_agent: agent.id,
|
|
49
|
+
task_id: taskId,
|
|
50
|
+
content: `${from}: ${content}`,
|
|
51
|
+
}).execute();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
await logActivity("message", agent.id, `Message on task`, taskId);
|
|
50
55
|
}
|
|
51
56
|
|
|
52
|
-
await logActivity("message", agent.id, `Message on task`, taskId);
|
|
53
57
|
return c.json(msg, 201);
|
|
54
58
|
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
|
|
5
|
+
export interface Repo {
|
|
6
|
+
name: string;
|
|
7
|
+
url: string;
|
|
8
|
+
default_branch: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function resolveReposPath(): string {
|
|
12
|
+
return process.env.REPOS_PATH ?? path.resolve(process.cwd(), ".agency", "repos.json");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function readRepos(): Repo[] {
|
|
16
|
+
const p = resolveReposPath();
|
|
17
|
+
if (!fs.existsSync(p)) return [];
|
|
18
|
+
return JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function writeRepos(repos: Repo[]): void {
|
|
22
|
+
const p = resolveReposPath();
|
|
23
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
24
|
+
fs.writeFileSync(p, JSON.stringify(repos, null, 2) + "\n");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const repos = new Hono();
|
|
28
|
+
|
|
29
|
+
repos.get("/", (c) => {
|
|
30
|
+
return c.json(readRepos());
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
repos.post("/", async (c) => {
|
|
34
|
+
const body = await c.req.json<{ name: string; url: string; default_branch?: string }>();
|
|
35
|
+
if (!body.name || !body.url) {
|
|
36
|
+
return c.json({ error: "name and url are required" }, 400);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const list = readRepos();
|
|
40
|
+
if (list.find((r) => r.name === body.name)) {
|
|
41
|
+
return c.json({ error: "repo already exists" }, 409);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
list.push({ name: body.name, url: body.url, default_branch: body.default_branch || "main" });
|
|
45
|
+
writeRepos(list);
|
|
46
|
+
return c.json({ ok: true }, 201);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
repos.put("/:name", async (c) => {
|
|
50
|
+
const name = c.req.param("name");
|
|
51
|
+
const list = readRepos();
|
|
52
|
+
const idx = list.findIndex((r) => r.name === name);
|
|
53
|
+
if (idx === -1) return c.json({ error: "not found" }, 404);
|
|
54
|
+
|
|
55
|
+
const body = await c.req.json<Partial<Repo>>();
|
|
56
|
+
list[idx] = {
|
|
57
|
+
name: list[idx].name,
|
|
58
|
+
url: body.url ?? list[idx].url,
|
|
59
|
+
default_branch: body.default_branch ?? list[idx].default_branch,
|
|
60
|
+
};
|
|
61
|
+
writeRepos(list);
|
|
62
|
+
return c.json(list[idx]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
repos.delete("/:name", (c) => {
|
|
66
|
+
const name = c.req.param("name");
|
|
67
|
+
const list = readRepos();
|
|
68
|
+
const filtered = list.filter((r) => r.name !== name);
|
|
69
|
+
if (filtered.length === list.length) {
|
|
70
|
+
return c.json({ error: "not found" }, 404);
|
|
71
|
+
}
|
|
72
|
+
writeRepos(filtered);
|
|
73
|
+
return c.json({ ok: true });
|
|
74
|
+
});
|
|
@@ -1,27 +1,31 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
listRoleConfigs,
|
|
4
|
+
getRoleConfig,
|
|
5
|
+
putRoleConfig,
|
|
6
|
+
deleteRoleConfig,
|
|
7
|
+
deleteRole,
|
|
8
|
+
} from "../lib/fs-store.js";
|
|
9
|
+
import { pushRoleToAllAgents } from "../lib/sync-skills.js";
|
|
3
10
|
|
|
4
11
|
export const roleConfigs = new Hono();
|
|
5
12
|
|
|
6
13
|
// List all role configs, optional ?role=
|
|
7
14
|
roleConfigs.get("/", async (c) => {
|
|
8
|
-
const role = c.req.query("role");
|
|
9
|
-
|
|
10
|
-
if (role) q = q.where("role", "=", role);
|
|
11
|
-
const rows = await q.orderBy("role").orderBy("config_type").execute();
|
|
15
|
+
const role = c.req.query("role") || undefined;
|
|
16
|
+
const rows = listRoleConfigs(role);
|
|
12
17
|
return c.json(rows);
|
|
13
18
|
});
|
|
14
19
|
|
|
15
20
|
// Get one role config by role + config_type
|
|
16
21
|
roleConfigs.get("/:role/:configType", async (c) => {
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return c.json(row);
|
|
22
|
+
const content = getRoleConfig(c.req.param("role"), c.req.param("configType"));
|
|
23
|
+
if (content === null) return c.json({ error: "not found" }, 404);
|
|
24
|
+
return c.json({
|
|
25
|
+
role: c.req.param("role"),
|
|
26
|
+
config_type: c.req.param("configType"),
|
|
27
|
+
content,
|
|
28
|
+
});
|
|
25
29
|
});
|
|
26
30
|
|
|
27
31
|
// Upsert a role config
|
|
@@ -30,42 +34,21 @@ roleConfigs.put("/:role/:configType", async (c) => {
|
|
|
30
34
|
const configType = c.req.param("configType");
|
|
31
35
|
const { content } = await c.req.json<{ content: string }>();
|
|
32
36
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
.where("config_type", "=", configType)
|
|
37
|
-
.selectAll()
|
|
38
|
-
.executeTakeFirst();
|
|
39
|
-
|
|
40
|
-
if (existing) {
|
|
41
|
-
const updated = await db
|
|
42
|
-
.updateTable("role_configs")
|
|
43
|
-
.where("id", "=", existing.id)
|
|
44
|
-
.set({ content, updated_at: new Date().toISOString() })
|
|
45
|
-
.returningAll()
|
|
46
|
-
.executeTakeFirstOrThrow();
|
|
47
|
-
return c.json(updated);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const row = await db
|
|
51
|
-
.insertInto("role_configs")
|
|
52
|
-
.values({
|
|
53
|
-
id: crypto.randomUUID(),
|
|
54
|
-
role,
|
|
55
|
-
config_type: configType,
|
|
56
|
-
content,
|
|
57
|
-
})
|
|
58
|
-
.returningAll()
|
|
59
|
-
.executeTakeFirstOrThrow();
|
|
60
|
-
return c.json(row, 201);
|
|
37
|
+
putRoleConfig(role, configType, content);
|
|
38
|
+
pushRoleToAllAgents(role).catch(() => {});
|
|
39
|
+
return c.json({ role, config_type: configType, content });
|
|
61
40
|
});
|
|
62
41
|
|
|
63
42
|
// Delete a role config
|
|
64
43
|
roleConfigs.delete("/:role/:configType", async (c) => {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
44
|
+
deleteRoleConfig(c.req.param("role"), c.req.param("configType"));
|
|
45
|
+
return c.json({ ok: true });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Delete an entire role
|
|
49
|
+
roleConfigs.delete("/:role", async (c) => {
|
|
50
|
+
const role = c.req.param("role");
|
|
51
|
+
const deleted = deleteRole(role);
|
|
52
|
+
if (!deleted) return c.json({ error: "role not found" }, 404);
|
|
70
53
|
return c.json({ ok: true });
|
|
71
54
|
});
|
package/src/api/routes/skills.ts
CHANGED
|
@@ -1,33 +1,144 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
|
-
import {
|
|
2
|
+
import { listSkills, getSkill, putSkill, deleteSkill, listSkillFiles, getSkillFile, putSkillFile, deleteSkillFile } from "../lib/fs-store.js";
|
|
3
|
+
import { pushSkillsToAllAgents } from "../lib/sync-skills.js";
|
|
4
|
+
import { cloneRepo, scanSkills, cleanupRepo, copySkillDir } from "../lib/import-skills.js";
|
|
3
5
|
|
|
4
6
|
export const skills = new Hono();
|
|
5
7
|
|
|
8
|
+
function withId(skill: { name: string; body: string; category: string; tags: string[] }) {
|
|
9
|
+
return { id: skill.name, ...skill };
|
|
10
|
+
}
|
|
11
|
+
|
|
6
12
|
// List skills, optional ?category=, ?search=
|
|
7
13
|
skills.get("/", async (c) => {
|
|
8
14
|
const category = c.req.query("category");
|
|
9
|
-
const search = c.req.query("search");
|
|
15
|
+
const search = c.req.query("search")?.toLowerCase();
|
|
10
16
|
|
|
11
|
-
let
|
|
12
|
-
if (category)
|
|
17
|
+
let rows = listSkills();
|
|
18
|
+
if (category) rows = rows.filter((r) => r.category === category);
|
|
13
19
|
if (search) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
])
|
|
20
|
+
rows = rows.filter(
|
|
21
|
+
(r) =>
|
|
22
|
+
r.name.toLowerCase().includes(search) ||
|
|
23
|
+
r.body.toLowerCase().includes(search)
|
|
19
24
|
);
|
|
20
25
|
}
|
|
21
26
|
|
|
22
|
-
|
|
23
|
-
|
|
27
|
+
return c.json(rows.map(withId));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const KNOWN_REPOS = [
|
|
31
|
+
"https://github.com/anthropics/skills",
|
|
32
|
+
"https://github.com/obra/superpowers",
|
|
33
|
+
"https://github.com/ComposioHQ/awesome-claude-skills",
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
// In-memory cache for available skills (5 min TTL)
|
|
37
|
+
let availableCache: { skills: { name: string; description: string; category: string; repo: string }[]; ts: number } | null = null;
|
|
38
|
+
const CACHE_TTL = 5 * 60 * 1000;
|
|
39
|
+
|
|
40
|
+
// List all available skills from known repos (cached)
|
|
41
|
+
skills.get("/available", async (c) => {
|
|
42
|
+
const force = c.req.query("refresh") === "1";
|
|
43
|
+
if (!force && availableCache && Date.now() - availableCache.ts < CACHE_TTL) {
|
|
44
|
+
return c.json(availableCache.skills);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const all: typeof availableCache["skills"] = [];
|
|
48
|
+
await Promise.allSettled(
|
|
49
|
+
KNOWN_REPOS.map(async (url) => {
|
|
50
|
+
let repoDir: string | undefined;
|
|
51
|
+
try {
|
|
52
|
+
repoDir = await cloneRepo(url);
|
|
53
|
+
const found = await scanSkills(repoDir);
|
|
54
|
+
const repo = url.replace(/\.git$/, "").split("/").slice(-2).join("/");
|
|
55
|
+
for (const s of found) {
|
|
56
|
+
all.push({ name: s.name, description: s.description, category: s.category, repo });
|
|
57
|
+
}
|
|
58
|
+
} finally {
|
|
59
|
+
if (repoDir) cleanupRepo(repoDir);
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
);
|
|
63
|
+
all.sort((a, b) => a.name.localeCompare(b.name));
|
|
64
|
+
availableCache = { skills: all, ts: Date.now() };
|
|
65
|
+
return c.json(all);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Preview skills available in a GitHub repo
|
|
69
|
+
skills.get("/import/preview", async (c) => {
|
|
70
|
+
const url = c.req.query("url");
|
|
71
|
+
if (!url) return c.json({ error: "url query param required" }, 400);
|
|
72
|
+
|
|
73
|
+
let repoDir: string | undefined;
|
|
74
|
+
try {
|
|
75
|
+
repoDir = await cloneRepo(url);
|
|
76
|
+
const available = await scanSkills(repoDir);
|
|
77
|
+
const repoName = url.replace(/\.git$/, "").split("/").slice(-2).join("/");
|
|
78
|
+
return c.json({ repo: repoName, skills: available.map(({ body: _, ...s }) => s) });
|
|
79
|
+
} catch (err: any) {
|
|
80
|
+
return c.json({ error: err.message }, 500);
|
|
81
|
+
} finally {
|
|
82
|
+
if (repoDir) cleanupRepo(repoDir);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Import skills from a GitHub repo
|
|
87
|
+
skills.post("/import", async (c) => {
|
|
88
|
+
const { url, skills: requested, overwrite } = await c.req.json<{
|
|
89
|
+
url: string;
|
|
90
|
+
skills?: string[];
|
|
91
|
+
overwrite?: boolean;
|
|
92
|
+
}>();
|
|
93
|
+
if (!url) return c.json({ error: "url is required" }, 400);
|
|
94
|
+
|
|
95
|
+
let repoDir: string | undefined;
|
|
96
|
+
try {
|
|
97
|
+
repoDir = await cloneRepo(url);
|
|
98
|
+
let available = await scanSkills(repoDir);
|
|
99
|
+
|
|
100
|
+
if (requested?.length) {
|
|
101
|
+
const set = new Set(requested);
|
|
102
|
+
available = available.filter((s) => set.has(s.name));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const imported: string[] = [];
|
|
106
|
+
const skipped: string[] = [];
|
|
107
|
+
const errors: string[] = [];
|
|
108
|
+
|
|
109
|
+
for (const skill of available) {
|
|
110
|
+
try {
|
|
111
|
+
const existing = getSkill(skill.name);
|
|
112
|
+
if (existing && !overwrite) {
|
|
113
|
+
skipped.push(skill.name);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
// Copy entire skill directory if sourcePath available, otherwise just SKILL.md
|
|
117
|
+
if (skill.sourcePath) {
|
|
118
|
+
copySkillDir(skill.sourcePath, skill.name);
|
|
119
|
+
} else {
|
|
120
|
+
putSkill(skill.name, skill.body, skill.category);
|
|
121
|
+
}
|
|
122
|
+
imported.push(skill.name);
|
|
123
|
+
} catch (err: any) {
|
|
124
|
+
errors.push(`${skill.name}: ${err.message}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
pushSkillsToAllAgents().catch(() => {});
|
|
129
|
+
return c.json({ imported, skipped, errors });
|
|
130
|
+
} catch (err: any) {
|
|
131
|
+
return c.json({ error: err.message }, 500);
|
|
132
|
+
} finally {
|
|
133
|
+
if (repoDir) cleanupRepo(repoDir);
|
|
134
|
+
}
|
|
24
135
|
});
|
|
25
136
|
|
|
26
|
-
// Get one skill
|
|
27
|
-
skills.get("/:
|
|
28
|
-
const
|
|
29
|
-
if (!
|
|
30
|
-
return c.json(
|
|
137
|
+
// Get one skill by name
|
|
138
|
+
skills.get("/:name", async (c) => {
|
|
139
|
+
const skill = getSkill(c.req.param("name"));
|
|
140
|
+
if (!skill) return c.json({ error: "not found" }, 404);
|
|
141
|
+
return c.json(withId(skill));
|
|
31
142
|
});
|
|
32
143
|
|
|
33
144
|
// Create skill
|
|
@@ -36,41 +147,88 @@ skills.post("/", async (c) => {
|
|
|
36
147
|
name: string; body: string; category?: string; tags?: string[];
|
|
37
148
|
}>();
|
|
38
149
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
.values({
|
|
42
|
-
id: crypto.randomUUID(),
|
|
43
|
-
name: body.name,
|
|
44
|
-
body: body.body,
|
|
45
|
-
category: body.category ?? "general",
|
|
46
|
-
tags: JSON.stringify(body.tags ?? []),
|
|
47
|
-
})
|
|
48
|
-
.returningAll()
|
|
49
|
-
.executeTakeFirstOrThrow();
|
|
150
|
+
putSkill(body.name, body.body, body.category ?? "general", body.tags ?? []);
|
|
151
|
+
pushSkillsToAllAgents().catch(() => {});
|
|
50
152
|
|
|
51
|
-
|
|
153
|
+
const skill = getSkill(body.name)!;
|
|
154
|
+
return c.json(withId(skill), 201);
|
|
52
155
|
});
|
|
53
156
|
|
|
54
157
|
// Update skill
|
|
55
|
-
skills.put("/:
|
|
56
|
-
const
|
|
158
|
+
skills.put("/:name", async (c) => {
|
|
159
|
+
const name = c.req.param("name");
|
|
160
|
+
const existing = getSkill(name);
|
|
161
|
+
if (!existing) return c.json({ error: "not found" }, 404);
|
|
162
|
+
|
|
57
163
|
const body = await c.req.json<{
|
|
58
164
|
name?: string; body?: string; category?: string; tags?: string[];
|
|
59
165
|
}>();
|
|
60
166
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
167
|
+
const newName = body.name ?? name;
|
|
168
|
+
const newBody = body.body ?? existing.body;
|
|
169
|
+
const newCategory = body.category ?? existing.category;
|
|
170
|
+
const newTags = body.tags ?? existing.tags;
|
|
171
|
+
|
|
172
|
+
// If renamed, delete old first
|
|
173
|
+
if (newName !== name) {
|
|
174
|
+
deleteSkill(name);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
putSkill(newName, newBody, newCategory, newTags);
|
|
178
|
+
pushSkillsToAllAgents().catch(() => {});
|
|
67
179
|
|
|
68
|
-
|
|
69
|
-
return c.json({ ...row, tags: JSON.parse(row.tags) });
|
|
180
|
+
return c.json(withId(getSkill(newName)!));
|
|
70
181
|
});
|
|
71
182
|
|
|
72
183
|
// Delete skill
|
|
73
|
-
skills.delete("/:
|
|
74
|
-
|
|
184
|
+
skills.delete("/:name", async (c) => {
|
|
185
|
+
deleteSkill(c.req.param("name"));
|
|
186
|
+
pushSkillsToAllAgents().catch(() => {});
|
|
187
|
+
return c.json({ ok: true });
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// ── Skill Files ──
|
|
191
|
+
|
|
192
|
+
// List files in a skill directory
|
|
193
|
+
skills.get("/:name/files", async (c) => {
|
|
194
|
+
const files = listSkillFiles(c.req.param("name"));
|
|
195
|
+
if (!files) return c.json({ error: "skill not found" }, 404);
|
|
196
|
+
return c.json(files);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Get a specific file from a skill
|
|
200
|
+
skills.get("/:name/files/*", async (c) => {
|
|
201
|
+
const name = c.req.param("name");
|
|
202
|
+
const filePath = c.req.path.split(`/skills/${name}/files/`)[1];
|
|
203
|
+
if (!filePath) return c.json({ error: "file path required" }, 400);
|
|
204
|
+
|
|
205
|
+
const content = getSkillFile(name, filePath);
|
|
206
|
+
if (content === null) return c.json({ error: "file not found" }, 404);
|
|
207
|
+
return c.json({ path: filePath, content });
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Create/update a file in a skill
|
|
211
|
+
skills.put("/:name/files/*", async (c) => {
|
|
212
|
+
const name = c.req.param("name");
|
|
213
|
+
const filePath = c.req.path.split(`/skills/${name}/files/`)[1];
|
|
214
|
+
if (!filePath) return c.json({ error: "file path required" }, 400);
|
|
215
|
+
|
|
216
|
+
const { content } = await c.req.json<{ content: string }>();
|
|
217
|
+
if (content === undefined) return c.json({ error: "content required" }, 400);
|
|
218
|
+
|
|
219
|
+
putSkillFile(name, filePath, content);
|
|
220
|
+
pushSkillsToAllAgents().catch(() => {});
|
|
221
|
+
return c.json({ ok: true, path: filePath });
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Delete a file from a skill
|
|
225
|
+
skills.delete("/:name/files/*", async (c) => {
|
|
226
|
+
const name = c.req.param("name");
|
|
227
|
+
const filePath = c.req.path.split(`/skills/${name}/files/`)[1];
|
|
228
|
+
if (!filePath) return c.json({ error: "file path required" }, 400);
|
|
229
|
+
|
|
230
|
+
const deleted = deleteSkillFile(name, filePath);
|
|
231
|
+
if (!deleted) return c.json({ error: "file not found or cannot be deleted" }, 404);
|
|
232
|
+
pushSkillsToAllAgents().catch(() => {});
|
|
75
233
|
return c.json({ ok: true });
|
|
76
234
|
});
|
package/src/api/routes/tasks.ts
CHANGED
|
@@ -60,18 +60,19 @@ tasks.get("/:id", async (c) => {
|
|
|
60
60
|
|
|
61
61
|
tasks.post("/", async (c) => {
|
|
62
62
|
const body = await c.req.json<{
|
|
63
|
-
title: string; description: string; from
|
|
63
|
+
title: string; description: string; from?: string;
|
|
64
64
|
design?: string; acceptance?: string; priority?: number;
|
|
65
65
|
task_type?: string; parent_id?: string; assign?: string;
|
|
66
66
|
}>();
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
// "human" or missing from = NULL (human user)
|
|
68
|
+
const agent = body.from && body.from !== "human" ? await resolveAgent(body.from) : null;
|
|
69
|
+
if (body.from && body.from !== "human" && !agent) return c.json({ error: "unknown agent" }, 400);
|
|
69
70
|
|
|
70
71
|
const task = await db.insertInto("tasks").values({
|
|
71
72
|
id: crypto.randomUUID(),
|
|
72
73
|
title: body.title,
|
|
73
74
|
description: body.description,
|
|
74
|
-
created_by: agent
|
|
75
|
+
created_by: agent?.id ?? null,
|
|
75
76
|
design: body.design ?? null,
|
|
76
77
|
acceptance: body.acceptance ?? null,
|
|
77
78
|
priority: body.priority ?? 2,
|
|
@@ -80,7 +81,9 @@ tasks.post("/", async (c) => {
|
|
|
80
81
|
status: body.assign ? "assigned" : "inbox",
|
|
81
82
|
}).returningAll().executeTakeFirstOrThrow();
|
|
82
83
|
|
|
83
|
-
|
|
84
|
+
if (agent) {
|
|
85
|
+
await logActivity("task_created", agent.id, `Created task: ${task.title}`, task.id);
|
|
86
|
+
}
|
|
84
87
|
|
|
85
88
|
if (body.assign) {
|
|
86
89
|
const assignee = await resolveAgent(body.assign);
|
|
@@ -96,10 +99,10 @@ tasks.patch("/:id", async (c) => {
|
|
|
96
99
|
const id = c.req.param("id");
|
|
97
100
|
const body = await c.req.json<{
|
|
98
101
|
status?: string; priority?: number; description?: string;
|
|
99
|
-
design?: string; acceptance?: string; title?: string; from
|
|
102
|
+
design?: string; acceptance?: string; title?: string; from?: string;
|
|
100
103
|
}>();
|
|
101
|
-
|
|
102
|
-
|
|
104
|
+
// "human" or missing from = NULL (human user)
|
|
105
|
+
const agent = body.from && body.from !== "human" ? await resolveAgent(body.from) : null;
|
|
103
106
|
|
|
104
107
|
let q = db.updateTable("tasks").where("id", "=", id);
|
|
105
108
|
if (body.status !== undefined) q = q.set("status", body.status);
|
|
@@ -112,7 +115,7 @@ tasks.patch("/:id", async (c) => {
|
|
|
112
115
|
|
|
113
116
|
const updated = await q.returningAll().executeTakeFirstOrThrow();
|
|
114
117
|
|
|
115
|
-
if (body.status) {
|
|
118
|
+
if (body.status && agent) {
|
|
116
119
|
await logActivity("status_changed", agent.id, `Status → ${body.status}`, id);
|
|
117
120
|
}
|
|
118
121
|
|
|
@@ -125,7 +128,18 @@ tasks.post("/:id/assign", async (c) => {
|
|
|
125
128
|
const agent = await resolveAgent(agentName);
|
|
126
129
|
if (!agent) return c.json({ error: "unknown agent" }, 400);
|
|
127
130
|
|
|
128
|
-
|
|
131
|
+
// Check if already assigned
|
|
132
|
+
const existing = await db
|
|
133
|
+
.selectFrom("task_assignees")
|
|
134
|
+
.where("task_id", "=", id)
|
|
135
|
+
.where("agent_id", "=", agent.id)
|
|
136
|
+
.selectAll()
|
|
137
|
+
.executeTakeFirst();
|
|
138
|
+
|
|
139
|
+
if (!existing) {
|
|
140
|
+
await db.insertInto("task_assignees").values({ task_id: id, agent_id: agent.id }).execute();
|
|
141
|
+
await logActivity("assigned", agent.id, `Assigned to ${agentName}`, id);
|
|
142
|
+
}
|
|
129
143
|
|
|
130
144
|
// inbox → assigned
|
|
131
145
|
await db.updateTable("tasks")
|
|
@@ -135,7 +149,6 @@ tasks.post("/:id/assign", async (c) => {
|
|
|
135
149
|
.set("updated_at", new Date().toISOString())
|
|
136
150
|
.execute();
|
|
137
151
|
|
|
138
|
-
await logActivity("assigned", agent.id, `Assigned to ${agentName}`, id);
|
|
139
152
|
return c.json({ ok: true });
|
|
140
153
|
});
|
|
141
154
|
|