@jx0/agency 0.2.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 (85) hide show
  1. package/README.md +272 -0
  2. package/bin/agency.js +2 -0
  3. package/dashboard/out/404.html +1 -0
  4. package/dashboard/out/_next/static/chunks/255-67e8754147461423.js +1 -0
  5. package/dashboard/out/_next/static/chunks/4bd1b696-c023c6e3521b1417.js +1 -0
  6. package/dashboard/out/_next/static/chunks/app/_not-found/page-ad40673d821037f6.js +1 -0
  7. package/dashboard/out/_next/static/chunks/app/layout-056f12675e691d12.js +1 -0
  8. package/dashboard/out/_next/static/chunks/app/page-80f01fdbb09b43c8.js +1 -0
  9. package/dashboard/out/_next/static/chunks/framework-de98b93a850cfc71.js +1 -0
  10. package/dashboard/out/_next/static/chunks/main-1a0dcce460eb61ce.js +1 -0
  11. package/dashboard/out/_next/static/chunks/main-app-1d848b791b823fa6.js +1 -0
  12. package/dashboard/out/_next/static/chunks/pages/_app-7d307437aca18ad4.js +1 -0
  13. package/dashboard/out/_next/static/chunks/pages/_error-cb2a52f75f2162e2.js +1 -0
  14. package/dashboard/out/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  15. package/dashboard/out/_next/static/chunks/webpack-4e6bf084ac60582b.js +1 -0
  16. package/dashboard/out/_next/static/css/27d1ea794f04e96a.css +1 -0
  17. package/dashboard/out/_next/static/pU1nwWH_dNUOCI8y4nl3C/_buildManifest.js +1 -0
  18. package/dashboard/out/_next/static/pU1nwWH_dNUOCI8y4nl3C/_ssgManifest.js +1 -0
  19. package/dashboard/out/index.html +1 -0
  20. package/dashboard/out/index.txt +19 -0
  21. package/docs/images/agency_cli_ps.png +0 -0
  22. package/docs/images/agency_ui_ai_prodivder_settings.png +0 -0
  23. package/docs/images/agency_ui_aws_settings.png +0 -0
  24. package/docs/images/agency_ui_identity_settings.png +0 -0
  25. package/docs/images/agency_ui_mission_control.png +0 -0
  26. package/docs/images/agent_ui_agent_config.png +0 -0
  27. package/package.json +31 -0
  28. package/src/api/db/client.ts +16 -0
  29. package/src/api/db/migrate.ts +37 -0
  30. package/src/api/db/migrations/001_initial.ts +193 -0
  31. package/src/api/db/migrations/002_configs.ts +76 -0
  32. package/src/api/db/migrations/003_settings_columns.ts +13 -0
  33. package/src/api/db/seed.ts +142 -0
  34. package/src/api/db/types.ts +126 -0
  35. package/src/api/index.ts +73 -0
  36. package/src/api/lib/activity.ts +13 -0
  37. package/src/api/lib/fleet-sync.ts +156 -0
  38. package/src/api/lib/mentions.ts +59 -0
  39. package/src/api/lib/processes.ts +45 -0
  40. package/src/api/lib/resolve-agent.ts +5 -0
  41. package/src/api/lib/tunnels.ts +99 -0
  42. package/src/api/routes/activities.ts +27 -0
  43. package/src/api/routes/agents.ts +311 -0
  44. package/src/api/routes/documents.ts +41 -0
  45. package/src/api/routes/knowledge.ts +60 -0
  46. package/src/api/routes/messages.ts +54 -0
  47. package/src/api/routes/notifications.ts +40 -0
  48. package/src/api/routes/oauth.ts +171 -0
  49. package/src/api/routes/role-configs.ts +71 -0
  50. package/src/api/routes/settings.ts +94 -0
  51. package/src/api/routes/skills.ts +76 -0
  52. package/src/api/routes/tasks.ts +154 -0
  53. package/src/cli/commands/config.ts +42 -0
  54. package/src/cli/commands/daemon.ts +173 -0
  55. package/src/cli/commands/doc.ts +47 -0
  56. package/src/cli/commands/init.ts +105 -0
  57. package/src/cli/commands/learn.ts +51 -0
  58. package/src/cli/commands/logs.ts +31 -0
  59. package/src/cli/commands/msg.ts +18 -0
  60. package/src/cli/commands/ps.ts +19 -0
  61. package/src/cli/commands/recall.ts +18 -0
  62. package/src/cli/commands/skills.ts +66 -0
  63. package/src/cli/commands/ssh.ts +68 -0
  64. package/src/cli/commands/start.ts +14 -0
  65. package/src/cli/commands/status.ts +33 -0
  66. package/src/cli/commands/stop.ts +11 -0
  67. package/src/cli/commands/tasks.ts +150 -0
  68. package/src/cli/index.ts +70 -0
  69. package/src/cli/lib/api.ts +16 -0
  70. package/src/cli/lib/config.ts +5 -0
  71. package/src/cli/lib/find-root.ts +32 -0
  72. package/src/cli/lib/prompt.ts +20 -0
  73. package/src/daemon.ts +83 -0
  74. package/src/templates/implementer/agents-config.md +44 -0
  75. package/src/templates/implementer/agents.md +32 -0
  76. package/src/templates/implementer/heartbeat.md +47 -0
  77. package/src/templates/implementer/tools.md +33 -0
  78. package/src/templates/orchestrator/agents-config.md +44 -0
  79. package/src/templates/orchestrator/agents.md +27 -0
  80. package/src/templates/orchestrator/heartbeat.md +40 -0
  81. package/src/templates/orchestrator/tools.md +40 -0
  82. package/src/templates/shared/environment.md +20 -0
  83. package/src/templates/shared/memory.md +20 -0
  84. package/src/templates/shared/soul.md +26 -0
  85. package/src/templates/shared/user.md +12 -0
@@ -0,0 +1,41 @@
1
+ import { Hono } from "hono";
2
+ import { db } from "../db/client.js";
3
+ import { resolveAgent } from "../lib/resolve-agent.js";
4
+ import { logActivity } from "../lib/activity.js";
5
+
6
+ export const documents = new Hono();
7
+
8
+ documents.get("/", async (c) => {
9
+ const taskId = c.req.query("task_id");
10
+ let q = db.selectFrom("documents").selectAll();
11
+ if (taskId) q = q.where("task_id", "=", taskId);
12
+ const rows = await q.orderBy("created_at", "desc").execute();
13
+ return c.json(rows);
14
+ });
15
+
16
+ documents.get("/:id", async (c) => {
17
+ const doc = await db.selectFrom("documents").where("id", "=", c.req.param("id")).selectAll().executeTakeFirst();
18
+ if (!doc) return c.json({ error: "not found" }, 404);
19
+ return c.json(doc);
20
+ });
21
+
22
+ documents.post("/", async (c) => {
23
+ const body = await c.req.json<{
24
+ title: string; content: string; doc_type?: string;
25
+ task_id?: string; from: string;
26
+ }>();
27
+ const agent = await resolveAgent(body.from);
28
+ if (!agent) return c.json({ error: "unknown agent" }, 400);
29
+
30
+ const doc = await db.insertInto("documents").values({
31
+ id: crypto.randomUUID(),
32
+ title: body.title,
33
+ content: body.content,
34
+ doc_type: body.doc_type ?? "general",
35
+ task_id: body.task_id ?? null,
36
+ created_by: agent.id,
37
+ }).returningAll().executeTakeFirstOrThrow();
38
+
39
+ await logActivity("document_created", agent.id, `Created doc: ${doc.title}`, doc.task_id);
40
+ return c.json(doc, 201);
41
+ });
@@ -0,0 +1,60 @@
1
+ import { Hono } from "hono";
2
+ import { db } from "../db/client.js";
3
+ import { resolveAgent } from "../lib/resolve-agent.js";
4
+
5
+ export const knowledge = new Hono();
6
+
7
+ knowledge.get("/", async (c) => {
8
+ const tags = c.req.query("tags");
9
+ const search = c.req.query("search");
10
+
11
+ let q = db.selectFrom("knowledge").selectAll();
12
+
13
+ if (tags) {
14
+ const tagArr = tags.split(",");
15
+ for (const tag of tagArr) {
16
+ q = q.where("tags", "like", `%"${tag}"%`);
17
+ }
18
+ }
19
+
20
+ if (search) {
21
+ q = q.where((eb) =>
22
+ eb.or([
23
+ eb("content", "like", `%${search}%`),
24
+ eb("key", "like", `%${search}%`),
25
+ ])
26
+ );
27
+ }
28
+
29
+ const rows = await q.orderBy("created_at", "desc").execute();
30
+ return c.json(rows.map((r) => ({ ...r, tags: JSON.parse(r.tags) })));
31
+ });
32
+
33
+ knowledge.post("/", async (c) => {
34
+ const body = await c.req.json<{
35
+ key: string; content: string; from: string;
36
+ task_id?: string; tags?: string[];
37
+ }>();
38
+ const agent = await resolveAgent(body.from);
39
+ if (!agent) return c.json({ error: "unknown agent" }, 400);
40
+
41
+ const tagsJson = JSON.stringify(body.tags ?? []);
42
+
43
+ const row = await db.insertInto("knowledge").values({
44
+ id: crypto.randomUUID(),
45
+ key: body.key,
46
+ content: body.content,
47
+ source: agent.id,
48
+ task_id: body.task_id ?? null,
49
+ tags: tagsJson,
50
+ })
51
+ .onConflict((oc) => oc.column("key").doUpdateSet({
52
+ content: body.content,
53
+ source: agent.id,
54
+ tags: tagsJson,
55
+ }))
56
+ .returningAll()
57
+ .executeTakeFirstOrThrow();
58
+
59
+ return c.json({ ...row, tags: JSON.parse(row.tags) }, 201);
60
+ });
@@ -0,0 +1,54 @@
1
+ import { Hono } from "hono";
2
+ import { db } from "../db/client.js";
3
+ import { resolveAgent } from "../lib/resolve-agent.js";
4
+ import { logActivity } from "../lib/activity.js";
5
+ import { parseMentions, getTaskSubscribers } from "../lib/mentions.js";
6
+
7
+ export const messages = new Hono();
8
+
9
+ messages.get("/:taskId/messages", async (c) => {
10
+ const taskId = c.req.param("taskId");
11
+ const rows = await db
12
+ .selectFrom("messages")
13
+ .leftJoin("agents", "agents.id", "messages.from_agent")
14
+ .where("task_id", "=", taskId)
15
+ .select([
16
+ "messages.id", "messages.task_id", "messages.from_agent",
17
+ "messages.content", "messages.created_at",
18
+ "agents.name as from_name",
19
+ ])
20
+ .orderBy("messages.created_at", "asc")
21
+ .execute();
22
+ return c.json(rows);
23
+ });
24
+
25
+ messages.post("/:taskId/messages", async (c) => {
26
+ const taskId = c.req.param("taskId");
27
+ const { from, content } = await c.req.json<{ from: string; content: string }>();
28
+ const agent = await resolveAgent(from);
29
+ if (!agent) return c.json({ error: "unknown agent" }, 400);
30
+
31
+ const msg = await db.insertInto("messages").values({
32
+ id: crypto.randomUUID(),
33
+ task_id: taskId, from_agent: agent.id, content,
34
+ }).returningAll().executeTakeFirstOrThrow();
35
+
36
+ // Collect notification targets
37
+ const mentionIds = await parseMentions(content, agent.id);
38
+ const subscriberIds = await getTaskSubscribers(taskId, agent.id);
39
+ const targetIds = new Set([...mentionIds, ...subscriberIds]);
40
+
41
+ // Create notifications
42
+ for (const targetId of targetIds) {
43
+ await db.insertInto("notifications").values({
44
+ id: crypto.randomUUID(),
45
+ target_agent: targetId,
46
+ source_agent: agent.id,
47
+ task_id: taskId,
48
+ content: `${from}: ${content}`,
49
+ }).execute();
50
+ }
51
+
52
+ await logActivity("message", agent.id, `Message on task`, taskId);
53
+ return c.json(msg, 201);
54
+ });
@@ -0,0 +1,40 @@
1
+ import { Hono } from "hono";
2
+ import { db } from "../db/client.js";
3
+ import { resolveAgent } from "../lib/resolve-agent.js";
4
+
5
+ export const notifications = new Hono();
6
+
7
+ notifications.get("/pending", async (c) => {
8
+ const rows = await db
9
+ .selectFrom("notifications")
10
+ .innerJoin("agents", "agents.id", "notifications.target_agent")
11
+ .where("delivered", "=", 0)
12
+ .select([
13
+ "notifications.id", "notifications.target_agent", "notifications.source_agent",
14
+ "notifications.task_id", "notifications.content", "notifications.delivered",
15
+ "notifications.created_at",
16
+ "agents.name as target_name", "agents.session_key",
17
+ ])
18
+ .orderBy("notifications.created_at", "asc")
19
+ .execute();
20
+ return c.json(rows);
21
+ });
22
+
23
+ notifications.get("/pending/:agentName", async (c) => {
24
+ const agent = await resolveAgent(c.req.param("agentName"));
25
+ if (!agent) return c.json({ error: "not found" }, 404);
26
+ const rows = await db
27
+ .selectFrom("notifications")
28
+ .where("target_agent", "=", agent.id)
29
+ .where("delivered", "=", 0)
30
+ .selectAll()
31
+ .orderBy("created_at", "asc")
32
+ .execute();
33
+ return c.json(rows);
34
+ });
35
+
36
+ notifications.post("/deliver/:id", async (c) => {
37
+ const id = c.req.param("id");
38
+ await db.updateTable("notifications").where("id", "=", id).set("delivered", 1).execute();
39
+ return c.json({ ok: true });
40
+ });
@@ -0,0 +1,171 @@
1
+ import { Hono } from "hono";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import { db } from "../db/client.js";
5
+
6
+ export const oauth = new Hono();
7
+
8
+ // In-memory PKCE store (single-user system)
9
+ let pendingVerifier: string | null = null;
10
+
11
+ function base64UrlEncode(buf: ArrayBuffer): string {
12
+ return Buffer.from(buf)
13
+ .toString("base64")
14
+ .replace(/\+/g, "-")
15
+ .replace(/\//g, "_")
16
+ .replace(/=+$/, "");
17
+ }
18
+
19
+ async function generateCodeChallenge(verifier: string): Promise<string> {
20
+ const encoder = new TextEncoder();
21
+ const data = encoder.encode(verifier);
22
+ const digest = await crypto.subtle.digest("SHA-256", data);
23
+ return base64UrlEncode(digest);
24
+ }
25
+
26
+ function generateCodeVerifier(): string {
27
+ const buf = new Uint8Array(32);
28
+ crypto.getRandomValues(buf);
29
+ return base64UrlEncode(buf.buffer);
30
+ }
31
+
32
+ const CLIENT_ID = ""; // Must be configured — extract from Claude Code or set via env
33
+ const CLAUDE_CREDENTIALS_PATH = path.join(
34
+ process.env.HOME ?? "",
35
+ ".claude",
36
+ ".credentials.json"
37
+ );
38
+ const REDIRECT_URI = "http://localhost:3100/oauth/claude/callback";
39
+ const AUTH_URL = "https://console.anthropic.com/oauth/authorize";
40
+ const TOKEN_URL = "https://console.anthropic.com/oauth/token";
41
+
42
+ // Import credentials from ~/.claude/.credentials.json
43
+ oauth.post("/claude/import", async (c) => {
44
+ try {
45
+ if (!fs.existsSync(CLAUDE_CREDENTIALS_PATH)) {
46
+ return c.json({ error: "No Claude Code credentials found at ~/.claude/.credentials.json" }, 404);
47
+ }
48
+ const raw = fs.readFileSync(CLAUDE_CREDENTIALS_PATH, "utf-8");
49
+ const creds = JSON.parse(raw);
50
+ const oauth = creds.claudeAiOauth;
51
+ if (!oauth?.accessToken) {
52
+ return c.json({ error: "No OAuth tokens found in credentials file" }, 400);
53
+ }
54
+
55
+ const expiresAt = oauth.expiresAt
56
+ ? new Date(oauth.expiresAt).toISOString()
57
+ : "";
58
+
59
+ const tokenSettings = [
60
+ { key: "ai.oauth_access_token", value: oauth.accessToken },
61
+ { key: "ai.oauth_refresh_token", value: oauth.refreshToken ?? "" },
62
+ { key: "ai.oauth_expires_at", value: expiresAt },
63
+ { key: "ai.oauth_subscription_type", value: oauth.subscriptionType ?? "" },
64
+ { key: "ai.auth_method", value: "oauth" },
65
+ ];
66
+
67
+ for (const s of tokenSettings) {
68
+ await db
69
+ .updateTable("settings")
70
+ .where("key", "=", s.key)
71
+ .set({ value: s.value, updated_at: new Date().toISOString() })
72
+ .execute();
73
+ }
74
+
75
+ return c.json({ ok: true, subscriptionType: oauth.subscriptionType ?? "" });
76
+ } catch (err: any) {
77
+ return c.json({ error: err.message }, 500);
78
+ }
79
+ });
80
+
81
+ // Step 1: Generate authorize URL
82
+ oauth.get("/claude/authorize", async (c) => {
83
+ const clientId = process.env.CLAUDE_OAUTH_CLIENT_ID || CLIENT_ID;
84
+ if (!clientId) {
85
+ return c.json({ error: "CLAUDE_OAUTH_CLIENT_ID not configured" }, 400);
86
+ }
87
+
88
+ const codeVerifier = generateCodeVerifier();
89
+ const codeChallenge = await generateCodeChallenge(codeVerifier);
90
+ pendingVerifier = codeVerifier;
91
+
92
+ const params = new URLSearchParams({
93
+ response_type: "code",
94
+ client_id: clientId,
95
+ redirect_uri: REDIRECT_URI,
96
+ code_challenge: codeChallenge,
97
+ code_challenge_method: "S256",
98
+ scope: "user:inference",
99
+ });
100
+
101
+ return c.json({ url: `${AUTH_URL}?${params.toString()}` });
102
+ });
103
+
104
+ // Step 2: Handle callback, exchange code for tokens
105
+ oauth.get("/claude/callback", async (c) => {
106
+ const code = c.req.query("code");
107
+ const error = c.req.query("error");
108
+
109
+ if (error) {
110
+ return c.html(`<html><body><p>OAuth error: ${error}</p><script>setTimeout(()=>window.close(),2000)</script></body></html>`);
111
+ }
112
+
113
+ if (!code || !pendingVerifier) {
114
+ return c.html(`<html><body><p>Missing code or verifier</p><script>setTimeout(()=>window.close(),2000)</script></body></html>`);
115
+ }
116
+
117
+ const clientId = process.env.CLAUDE_OAUTH_CLIENT_ID || CLIENT_ID;
118
+ const verifier = pendingVerifier;
119
+ pendingVerifier = null;
120
+
121
+ try {
122
+ const tokenRes = await fetch(TOKEN_URL, {
123
+ method: "POST",
124
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
125
+ body: new URLSearchParams({
126
+ grant_type: "authorization_code",
127
+ code,
128
+ redirect_uri: REDIRECT_URI,
129
+ client_id: clientId,
130
+ code_verifier: verifier,
131
+ }),
132
+ });
133
+
134
+ if (!tokenRes.ok) {
135
+ const err = await tokenRes.text();
136
+ return c.html(`<html><body><p>Token exchange failed: ${err}</p><script>setTimeout(()=>window.close(),3000)</script></body></html>`);
137
+ }
138
+
139
+ const tokens = await tokenRes.json() as {
140
+ access_token: string;
141
+ refresh_token?: string;
142
+ expires_in?: number;
143
+ subscription_type?: string;
144
+ };
145
+
146
+ const expiresAt = tokens.expires_in
147
+ ? new Date(Date.now() + tokens.expires_in * 1000).toISOString()
148
+ : "";
149
+
150
+ // Store tokens in settings
151
+ const tokenSettings = [
152
+ { key: "ai.oauth_access_token", value: tokens.access_token },
153
+ { key: "ai.oauth_refresh_token", value: tokens.refresh_token ?? "" },
154
+ { key: "ai.oauth_expires_at", value: expiresAt },
155
+ { key: "ai.oauth_subscription_type", value: tokens.subscription_type ?? "" },
156
+ { key: "ai.auth_method", value: "oauth" },
157
+ ];
158
+
159
+ for (const s of tokenSettings) {
160
+ await db
161
+ .updateTable("settings")
162
+ .where("key", "=", s.key)
163
+ .set({ value: s.value, updated_at: new Date().toISOString() })
164
+ .execute();
165
+ }
166
+
167
+ return c.html(`<html><body><p>Connected successfully! This window will close.</p><script>setTimeout(()=>window.close(),1000)</script></body></html>`);
168
+ } catch (err: any) {
169
+ return c.html(`<html><body><p>Error: ${err.message}</p><script>setTimeout(()=>window.close(),3000)</script></body></html>`);
170
+ }
171
+ });
@@ -0,0 +1,71 @@
1
+ import { Hono } from "hono";
2
+ import { db } from "../db/client.js";
3
+
4
+ export const roleConfigs = new Hono();
5
+
6
+ // List all role configs, optional ?role=
7
+ roleConfigs.get("/", async (c) => {
8
+ const role = c.req.query("role");
9
+ let q = db.selectFrom("role_configs").selectAll();
10
+ if (role) q = q.where("role", "=", role);
11
+ const rows = await q.orderBy("role").orderBy("config_type").execute();
12
+ return c.json(rows);
13
+ });
14
+
15
+ // Get one role config by role + config_type
16
+ roleConfigs.get("/:role/:configType", async (c) => {
17
+ const row = await db
18
+ .selectFrom("role_configs")
19
+ .where("role", "=", c.req.param("role"))
20
+ .where("config_type", "=", c.req.param("configType"))
21
+ .selectAll()
22
+ .executeTakeFirst();
23
+ if (!row) return c.json({ error: "not found" }, 404);
24
+ return c.json(row);
25
+ });
26
+
27
+ // Upsert a role config
28
+ roleConfigs.put("/:role/:configType", async (c) => {
29
+ const role = c.req.param("role");
30
+ const configType = c.req.param("configType");
31
+ const { content } = await c.req.json<{ content: string }>();
32
+
33
+ const existing = await db
34
+ .selectFrom("role_configs")
35
+ .where("role", "=", role)
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);
61
+ });
62
+
63
+ // Delete a role config
64
+ roleConfigs.delete("/:role/:configType", async (c) => {
65
+ await db
66
+ .deleteFrom("role_configs")
67
+ .where("role", "=", c.req.param("role"))
68
+ .where("config_type", "=", c.req.param("configType"))
69
+ .execute();
70
+ return c.json({ ok: true });
71
+ });
@@ -0,0 +1,94 @@
1
+ import { Hono } from "hono";
2
+ import { db } from "../db/client.js";
3
+
4
+ export const settings = new Hono();
5
+
6
+ const MASKED = "********";
7
+
8
+ // List all settings, optional ?category=
9
+ settings.get("/", async (c) => {
10
+ const category = c.req.query("category");
11
+ let q = db.selectFrom("settings").selectAll();
12
+ if (category) q = q.where("category", "=", category);
13
+ const rows = await q.orderBy("category").orderBy("key").execute();
14
+ // Mask sensitive values
15
+ const masked = rows.map((r) => ({
16
+ ...r,
17
+ value: r.sensitive ? MASKED : r.value,
18
+ }));
19
+ return c.json(masked);
20
+ });
21
+
22
+ // Get one setting
23
+ settings.get("/:key", async (c) => {
24
+ const key = c.req.param("key");
25
+ const row = await db.selectFrom("settings").where("key", "=", key).selectAll().executeTakeFirst();
26
+ if (!row) return c.json({ error: "not found" }, 404);
27
+ return c.json({
28
+ ...row,
29
+ value: row.sensitive ? MASKED : row.value,
30
+ });
31
+ });
32
+
33
+ // Upsert a setting
34
+ settings.put("/:key", async (c) => {
35
+ const key = c.req.param("key");
36
+ const body = await c.req.json<{
37
+ value: string;
38
+ category?: string;
39
+ description?: string;
40
+ sensitive?: number;
41
+ input_type?: string;
42
+ }>();
43
+
44
+ // If value is the mask placeholder, skip updating value (user didn't change it)
45
+ const skipValue = body.value === MASKED;
46
+
47
+ const existing = await db.selectFrom("settings").where("key", "=", key).selectAll().executeTakeFirst();
48
+
49
+ if (existing) {
50
+ const updates: Record<string, any> = {
51
+ updated_at: new Date().toISOString(),
52
+ };
53
+ if (!skipValue) updates.value = body.value;
54
+ if (body.category !== undefined) updates.category = body.category;
55
+ if (body.description !== undefined) updates.description = body.description;
56
+ if (body.sensitive !== undefined) updates.sensitive = body.sensitive;
57
+ if (body.input_type !== undefined) updates.input_type = body.input_type;
58
+
59
+ const updated = await db
60
+ .updateTable("settings")
61
+ .where("key", "=", key)
62
+ .set(updates)
63
+ .returningAll()
64
+ .executeTakeFirstOrThrow();
65
+ return c.json({
66
+ ...updated,
67
+ value: updated.sensitive ? MASKED : updated.value,
68
+ });
69
+ }
70
+
71
+ const row = await db
72
+ .insertInto("settings")
73
+ .values({
74
+ key,
75
+ value: skipValue ? "" : body.value,
76
+ category: body.category ?? "general",
77
+ description: body.description ?? null,
78
+ sensitive: body.sensitive ?? 0,
79
+ input_type: body.input_type ?? "text",
80
+ })
81
+ .returningAll()
82
+ .executeTakeFirstOrThrow();
83
+ return c.json({
84
+ ...row,
85
+ value: row.sensitive ? MASKED : row.value,
86
+ }, 201);
87
+ });
88
+
89
+ // Delete a setting
90
+ settings.delete("/:key", async (c) => {
91
+ const key = c.req.param("key");
92
+ await db.deleteFrom("settings").where("key", "=", key).execute();
93
+ return c.json({ ok: true });
94
+ });
@@ -0,0 +1,76 @@
1
+ import { Hono } from "hono";
2
+ import { db } from "../db/client.js";
3
+
4
+ export const skills = new Hono();
5
+
6
+ // List skills, optional ?category=, ?search=
7
+ skills.get("/", async (c) => {
8
+ const category = c.req.query("category");
9
+ const search = c.req.query("search");
10
+
11
+ let q = db.selectFrom("skills").selectAll();
12
+ if (category) q = q.where("category", "=", category);
13
+ if (search) {
14
+ q = q.where((eb) =>
15
+ eb.or([
16
+ eb("name", "like", `%${search}%`),
17
+ eb("body", "like", `%${search}%`),
18
+ ])
19
+ );
20
+ }
21
+
22
+ const rows = await q.orderBy("name").execute();
23
+ return c.json(rows.map((r) => ({ ...r, tags: JSON.parse(r.tags) })));
24
+ });
25
+
26
+ // Get one skill
27
+ skills.get("/:id", async (c) => {
28
+ const row = await db.selectFrom("skills").where("id", "=", c.req.param("id")).selectAll().executeTakeFirst();
29
+ if (!row) return c.json({ error: "not found" }, 404);
30
+ return c.json({ ...row, tags: JSON.parse(row.tags) });
31
+ });
32
+
33
+ // Create skill
34
+ skills.post("/", async (c) => {
35
+ const body = await c.req.json<{
36
+ name: string; body: string; category?: string; tags?: string[];
37
+ }>();
38
+
39
+ const row = await db
40
+ .insertInto("skills")
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();
50
+
51
+ return c.json({ ...row, tags: JSON.parse(row.tags) }, 201);
52
+ });
53
+
54
+ // Update skill
55
+ skills.put("/:id", async (c) => {
56
+ const id = c.req.param("id");
57
+ const body = await c.req.json<{
58
+ name?: string; body?: string; category?: string; tags?: string[];
59
+ }>();
60
+
61
+ let q = db.updateTable("skills").where("id", "=", id);
62
+ if (body.name !== undefined) q = q.set("name", body.name);
63
+ if (body.body !== undefined) q = q.set("body", body.body);
64
+ if (body.category !== undefined) q = q.set("category", body.category);
65
+ if (body.tags !== undefined) q = q.set("tags", JSON.stringify(body.tags));
66
+ q = q.set("updated_at", new Date().toISOString());
67
+
68
+ const row = await q.returningAll().executeTakeFirstOrThrow();
69
+ return c.json({ ...row, tags: JSON.parse(row.tags) });
70
+ });
71
+
72
+ // Delete skill
73
+ skills.delete("/:id", async (c) => {
74
+ await db.deleteFrom("skills").where("id", "=", c.req.param("id")).execute();
75
+ return c.json({ ok: true });
76
+ });