maestro-agent 0.0.1 → 0.0.3

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 (111) hide show
  1. package/README.md +316 -2
  2. package/bin/maestro.ts +5 -0
  3. package/dist/maestro +0 -0
  4. package/dist/web/apple-touch-icon.png +0 -0
  5. package/dist/web/assets/Connections-BMA04Ycg.js +11 -0
  6. package/dist/web/assets/GanttView-DXjh0gxg.js +49 -0
  7. package/dist/web/assets/Home-Ct3Ho0Qt.js +1 -0
  8. package/dist/web/assets/HooksCrons--0kyVJcR.js +11 -0
  9. package/dist/web/assets/ProjectDetail-B_IqEpFu.js +1 -0
  10. package/dist/web/assets/Roles-D1tIQzto.js +24 -0
  11. package/dist/web/assets/Settings-yts4LUmH.js +11 -0
  12. package/dist/web/assets/Skills-DbuNLjIV.js +12 -0
  13. package/dist/web/assets/Wizard-vJol8-Y4.js +11 -0
  14. package/dist/web/assets/WorkspaceChat-DrsLs4m2.js +56 -0
  15. package/dist/web/assets/WorkspaceDashboard-B9vgrd2Z.js +6 -0
  16. package/dist/web/assets/WorkspaceNew-DoNGYHCG.js +1 -0
  17. package/dist/web/assets/WorkspaceProjects-DDp3mUse.js +6 -0
  18. package/dist/web/assets/WorkspaceSchedules-BTjmCbYG.js +1 -0
  19. package/dist/web/assets/WorkspaceTasks-mPU-bhKR.js +41 -0
  20. package/dist/web/assets/activity-CIA8bIA4.js +6 -0
  21. package/dist/web/assets/addon-fit-BlxrFPDK.js +1 -0
  22. package/dist/web/assets/arrow-right-S7ID7nDp.js +6 -0
  23. package/dist/web/assets/badge-DDTUzWIi.js +1 -0
  24. package/dist/web/assets/circle-check-B3P1qK0Z.js +6 -0
  25. package/dist/web/assets/clock-f9aYZox0.js +6 -0
  26. package/dist/web/assets/index-BRo4Du_s.js +11 -0
  27. package/dist/web/assets/index-C7kx39S9.js +196 -0
  28. package/dist/web/assets/index-D6LSdZea.css +1 -0
  29. package/dist/web/assets/plus-BHnOxbns.js +6 -0
  30. package/dist/web/assets/refresh-cw-BWX04Hg3.js +6 -0
  31. package/dist/web/assets/save-BLbb_9xz.js +6 -0
  32. package/dist/web/assets/sparkles-CDr6Dw1e.js +6 -0
  33. package/dist/web/assets/trash-2-9-ThEdey.js +6 -0
  34. package/dist/web/assets/useEventStream-DXt2Hmei.js +1 -0
  35. package/dist/web/assets/x-DVdKPXXy.js +6 -0
  36. package/dist/web/assets/xterm-DYP7pi_n.css +32 -0
  37. package/dist/web/assets/xterm-DlVFs1Kw.js +9 -0
  38. package/dist/web/favicon-512.png +0 -0
  39. package/dist/web/favicon.png +0 -0
  40. package/dist/web/index.html +15 -0
  41. package/package.json +49 -6
  42. package/src/api/agents.ts +76 -0
  43. package/src/api/audit.ts +19 -0
  44. package/src/api/autopilot.ts +73 -0
  45. package/src/api/chat.ts +801 -0
  46. package/src/api/chief.ts +84 -0
  47. package/src/api/config.ts +39 -0
  48. package/src/api/gantt.ts +72 -0
  49. package/src/api/hooks.ts +54 -0
  50. package/src/api/inbox.ts +125 -0
  51. package/src/api/lark.ts +32 -0
  52. package/src/api/memory.ts +37 -0
  53. package/src/api/ops.ts +89 -0
  54. package/src/api/projects.ts +105 -0
  55. package/src/api/roles.ts +123 -0
  56. package/src/api/runtimes.ts +62 -0
  57. package/src/api/scheduled-tasks.ts +203 -0
  58. package/src/api/sessions.ts +479 -0
  59. package/src/api/skills.ts +386 -0
  60. package/src/api/tasks.ts +457 -0
  61. package/src/api/telegram.ts +94 -0
  62. package/src/api/templates.ts +36 -0
  63. package/src/api/webhooks.ts +20 -0
  64. package/src/api/workspaces.ts +150 -0
  65. package/src/bridges/lark/index.ts +213 -0
  66. package/src/bridges/telegram/index.ts +273 -0
  67. package/src/bridges/telegram/polling.ts +185 -0
  68. package/src/chat/index.ts +86 -0
  69. package/src/chief/index.ts +461 -0
  70. package/src/core/cli.ts +333 -0
  71. package/src/core/db.ts +53 -0
  72. package/src/core/event-bus.ts +33 -0
  73. package/src/core/index.ts +6 -0
  74. package/src/core/migrations.ts +303 -0
  75. package/src/core/router.ts +69 -0
  76. package/src/core/schema.sql +232 -0
  77. package/src/core/server.ts +308 -0
  78. package/src/core/validate.ts +22 -0
  79. package/src/discovery/index.ts +194 -0
  80. package/src/gateway/adapters/telegram.ts +148 -0
  81. package/src/gateway/index.ts +31 -0
  82. package/src/gateway/manager.ts +176 -0
  83. package/src/gateway/types.ts +77 -0
  84. package/src/inbox/index.ts +500 -0
  85. package/src/ops/artifact-sync.ts +65 -0
  86. package/src/ops/autopilot.ts +338 -0
  87. package/src/ops/gc.ts +252 -0
  88. package/src/ops/index.ts +226 -0
  89. package/src/ops/project-serial.ts +52 -0
  90. package/src/ops/role-dispatch.ts +111 -0
  91. package/src/ops/runtime-scheduler.ts +447 -0
  92. package/src/ops/task-blocking.ts +65 -0
  93. package/src/ops/task-deps.ts +37 -0
  94. package/src/ops/task-workspace.ts +60 -0
  95. package/src/roles/index.ts +258 -0
  96. package/src/roles/prompt-assembler.ts +85 -0
  97. package/src/roles/workspace-role.ts +155 -0
  98. package/src/scheduler/index.ts +461 -0
  99. package/src/session/output-parser.ts +75 -0
  100. package/src/session/realtime-parser.ts +40 -0
  101. package/src/skills/builtin.ts +155 -0
  102. package/src/skills/skill-extractor.ts +452 -0
  103. package/src/skills/skill-md.ts +282 -0
  104. package/src/transport/http-api.ts +75 -0
  105. package/src/transport/index.ts +4 -0
  106. package/src/transport/local-pty.ts +119 -0
  107. package/src/transport/ssh.ts +176 -0
  108. package/src/transport/types.ts +20 -0
  109. package/src/workflows/index.ts +231 -0
  110. package/index.js +0 -1
  111. package/maestro-agent-0.0.1.tgz +0 -0
@@ -0,0 +1,84 @@
1
+ import type { Router } from "../core/router";
2
+ import { body, json } from "../core/router";
3
+ import type { HubContext } from "../core/server";
4
+ import { applyChiefDecision, proposeRoster, runChiefHeartbeat } from "../chief";
5
+
6
+ export function registerChiefRoutes(router: Router, ctx: HubContext) {
7
+ router.post("/api/chief/workspaces/:id/propose-roster", (_req, params) => {
8
+ try {
9
+ return json(proposeRoster(ctx.db, params.id), 201);
10
+ } catch (err: any) {
11
+ return json({ error: err.message }, 404);
12
+ }
13
+ });
14
+
15
+ router.post("/api/chief/heartbeat", async () => {
16
+ const result = await runChiefHeartbeat(ctx.db);
17
+ const messages = ctx.db.query("SELECT * FROM inbox_message WHERE from_actor = 'chief' ORDER BY created_at DESC LIMIT 20").all() as any[];
18
+ return json({
19
+ ...result,
20
+ proposals: messages.filter((message) => message.kind === "proposal").length,
21
+ messages,
22
+ });
23
+ });
24
+
25
+ router.post("/api/chief/decisions", async (req) => {
26
+ const input = await req.json();
27
+ return json(applyChiefDecision(ctx.db, input), 201);
28
+ });
29
+
30
+ router.post("/api/chief/dialogue", async (req) => {
31
+ const input = await body(req);
32
+ const text = String(input.text || input.message || "");
33
+ const decisions = parseChiefDialogue(text, input.context || {});
34
+ const results = decisions.map((decision: any) => applyChiefDecision(ctx.db, decision));
35
+ return json({ decisions, results }, 201);
36
+ });
37
+ }
38
+
39
+ function parseChiefDialogue(text: string, context: Record<string, any>) {
40
+ const lower = text.toLowerCase();
41
+ const decisions: Array<{ tool: string; args: Record<string, any> }> = [];
42
+
43
+ if (lower.includes("add role") || lower.includes("create role") || lower.includes("增加角色") || lower.includes("新增角色")) {
44
+ const name = context.role_name || extractQuoted(text) || "Dynamic Role";
45
+ decisions.push({
46
+ tool: "create_role",
47
+ args: {
48
+ name,
49
+ workspace_id: context.workspace_id,
50
+ project_id: context.project_id,
51
+ capabilities: context.capabilities || [],
52
+ preferred_runtimes: context.preferred_runtimes || [],
53
+ headcount: context.headcount || 1,
54
+ rationale: text,
55
+ },
56
+ });
57
+ }
58
+
59
+ if (lower.includes("task flow") || lower.includes("workflow") || lower.includes("modify task") || lower.includes("修改任务流") || lower.includes("调整任务流")) {
60
+ if (context.task_id || context.parent_task_id) {
61
+ decisions.push({
62
+ tool: "update_task_flow",
63
+ args: {
64
+ mode: context.mode || (context.depends_on ? "add_dependency" : context.parent_task_id ? "spawn_followup" : "add_dependency"),
65
+ task_id: context.task_id,
66
+ parent_task_id: context.parent_task_id || context.task_id,
67
+ depends_on: context.depends_on || [],
68
+ title: context.title,
69
+ description: context.description,
70
+ required_capabilities: context.required_capabilities || [],
71
+ priority: context.priority,
72
+ rationale: text,
73
+ },
74
+ });
75
+ }
76
+ }
77
+
78
+ return decisions;
79
+ }
80
+
81
+ function extractQuoted(text: string) {
82
+ const match = text.match(/"([^"]+)"|'([^']+)'|“([^”]+)”/);
83
+ return match ? match[1] || match[2] || match[3] : "";
84
+ }
@@ -0,0 +1,39 @@
1
+ import type { Router } from "../core/router";
2
+ import { json, body } from "../core/router";
3
+ import type { HubContext } from "../core/server";
4
+ import { now } from "../core/db";
5
+
6
+ export function registerConfigRoutes(router: Router, ctx: HubContext) {
7
+ router.get("/api/config", () => {
8
+ const rows = ctx.db.query("SELECT key, value FROM config").all() as Array<{ key: string; value: string }>;
9
+ const config: Record<string, string> = {};
10
+ for (const row of rows) config[row.key] = row.value;
11
+ return json(config);
12
+ });
13
+
14
+ router.patch("/api/config", async (req) => {
15
+ const updates = await body<Record<string, string>>(req);
16
+ const ts = now();
17
+ for (const [key, value] of Object.entries(updates)) {
18
+ ctx.db.run(
19
+ "INSERT INTO config (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at",
20
+ [key, String(value), ts]
21
+ );
22
+ }
23
+ const rows = ctx.db.query("SELECT key, value FROM config").all() as Array<{ key: string; value: string }>;
24
+ const config: Record<string, string> = {};
25
+ for (const row of rows) config[row.key] = row.value;
26
+ return json(config);
27
+ });
28
+
29
+ router.get("/api/config/:key", (_req, params) => {
30
+ const row = ctx.db.query("SELECT value FROM config WHERE key = ?").get(params.key) as any;
31
+ if (!row) return json({ error: "Key not found" }, 404);
32
+ return json({ key: params.key, value: row.value });
33
+ });
34
+ }
35
+
36
+ export function getConfig(ctx: HubContext, key: string, defaultValue: string = ""): string {
37
+ const row = ctx.db.query("SELECT value FROM config WHERE key = ?").get(key) as any;
38
+ return row?.value ?? defaultValue;
39
+ }
@@ -0,0 +1,72 @@
1
+ import type { Router } from "../core/router";
2
+ import { json } from "../core/router";
3
+ import type { HubContext } from "../core/server";
4
+ import { getTaskBlockingDetails } from "../ops/task-blocking";
5
+
6
+ export function registerGanttRoutes(router: Router, ctx: HubContext) {
7
+ // 空间级甘特图:按 Project 分组展示所有 Task
8
+ router.get("/api/workspaces/:workspaceId/gantt", (req, params) => {
9
+ const workspaceId = params.workspaceId;
10
+ const url = new URL(req.url);
11
+ const projectFilter = url.searchParams.get("project_id");
12
+
13
+ const workspace = ctx.db.query("SELECT id FROM workspace WHERE id = ?").get(workspaceId);
14
+ if (!workspace) return json({ error: "Workspace not found" }, 404);
15
+
16
+ let projectSql = "SELECT id, name, status FROM project WHERE workspace_id = ?";
17
+ const projectParams: any[] = [workspaceId];
18
+ if (projectFilter) {
19
+ projectSql += " AND id = ?";
20
+ projectParams.push(projectFilter);
21
+ }
22
+ projectSql += " ORDER BY created_at";
23
+
24
+ const projects = ctx.db.query(projectSql).all(...projectParams) as any[];
25
+
26
+ const result = projects.map((project: any) => {
27
+ const tasks = ctx.db.query(
28
+ `SELECT id, title, status, priority, start_date, end_date, progress,
29
+ parent_task_id, assignee_agent_id, created_at
30
+ FROM task WHERE project_id = ? ORDER BY created_at`
31
+ ).all(project.id) as any[];
32
+
33
+ const taskIds = tasks.map((t: any) => t.id);
34
+ const blockingByTask = getTaskBlockingDetails(ctx.db, taskIds);
35
+ const deps: Record<string, string[]> = {};
36
+ if (taskIds.length > 0) {
37
+ const placeholders = taskIds.map(() => "?").join(",");
38
+ const depRows = ctx.db.query(
39
+ `SELECT task_id, depends_on FROM task_dependency WHERE task_id IN (${placeholders})`
40
+ ).all(...taskIds) as any[];
41
+ for (const row of depRows) {
42
+ if (!deps[row.task_id]) deps[row.task_id] = [];
43
+ deps[row.task_id].push(row.depends_on);
44
+ }
45
+ }
46
+
47
+ return {
48
+ id: project.id,
49
+ name: project.name,
50
+ status: project.status,
51
+ tasks: tasks.map((t: any) => ({
52
+ id: t.id,
53
+ title: t.title,
54
+ status: t.status,
55
+ priority: t.priority,
56
+ start_date: t.start_date,
57
+ end_date: t.end_date,
58
+ progress: t.progress ?? 0,
59
+ parent_task_id: t.parent_task_id,
60
+ assignee_agent_id: t.assignee_agent_id,
61
+ created_at: t.created_at,
62
+ dependencies: deps[t.id] || [],
63
+ dependency_details: blockingByTask[t.id]?.dependencies || [],
64
+ blocking_dependencies: blockingByTask[t.id]?.blocking_dependencies || [],
65
+ blocked_reason: blockingByTask[t.id]?.blocked_reason || null,
66
+ })),
67
+ };
68
+ });
69
+
70
+ return json({ projects: result });
71
+ });
72
+ }
@@ -0,0 +1,54 @@
1
+ import type { Router } from "../core/router";
2
+ import { body, json } from "../core/router";
3
+ import type { HubContext } from "../core/server";
4
+ import { generateId, now } from "../core/db";
5
+ import { required } from "../core/validate";
6
+ import { executeHooksForEvent, runDueCrons } from "../scheduler";
7
+
8
+ export function registerHookRoutes(router: Router, ctx: HubContext) {
9
+ router.get("/api/hooks", () => {
10
+ return json(ctx.db.query("SELECT * FROM hook_binding ORDER BY created_at DESC").all());
11
+ });
12
+
13
+ router.post("/api/hooks", async (req) => {
14
+ const input = await body(req);
15
+ const { scope, scope_id, event, when_expr, action } = input;
16
+ required({ scope, scope_id, event, action }, ["scope", "scope_id", "event", "action"]);
17
+ const id = generateId("hook");
18
+ const ts = now();
19
+ ctx.db.run(
20
+ "INSERT INTO hook_binding (id, scope, scope_id, event, when_expr, action, enabled, created_at) VALUES (?, ?, ?, ?, ?, ?, 1, ?)",
21
+ [id, scope, scope_id, event, when_expr || null, action, ts]
22
+ );
23
+ return json(ctx.db.query("SELECT * FROM hook_binding WHERE id = ?").get(id), 201);
24
+ });
25
+
26
+ router.post("/api/hooks/dispatch", async (req) => {
27
+ const input = await body(req);
28
+ const { event, payload } = input;
29
+ required({ event }, ["event"]);
30
+ return json(executeHooksForEvent(ctx.db, event, payload || {}));
31
+ });
32
+
33
+ router.get("/api/crons", () => {
34
+ return json(ctx.db.query("SELECT * FROM cron_binding ORDER BY created_at DESC").all());
35
+ });
36
+
37
+ router.post("/api/crons", async (req) => {
38
+ const input = await body(req);
39
+ const { scope, scope_id, cron_expr, action } = input;
40
+ required({ scope, scope_id, cron_expr, action }, ["scope", "scope_id", "cron_expr", "action"]);
41
+ const id = generateId("cron");
42
+ const ts = now();
43
+ ctx.db.run(
44
+ "INSERT INTO cron_binding (id, scope, scope_id, cron_expr, action, enabled, created_at) VALUES (?, ?, ?, ?, ?, 1, ?)",
45
+ [id, scope, scope_id, cron_expr, action, ts]
46
+ );
47
+ return json(ctx.db.query("SELECT * FROM cron_binding WHERE id = ?").get(id), 201);
48
+ });
49
+
50
+ router.post("/api/crons/run", async (req) => {
51
+ const input = await body(req).catch(() => ({}));
52
+ return json(runDueCrons(ctx.db, input.now || now()));
53
+ });
54
+ }
@@ -0,0 +1,125 @@
1
+ import type { Router } from "../core/router";
2
+ import { body, json } from "../core/router";
3
+ import type { HubContext } from "../core/server";
4
+ import { generateId, now } from "../core/db";
5
+ import { required } from "../core/validate";
6
+ import { getWorkflowForTask } from "../workflows";
7
+ import { findProjectExecutionBlocker, isTaskAssignedToAgent } from "../ops/project-serial";
8
+
9
+ export function registerInboxRoutes(router: Router, ctx: HubContext) {
10
+ router.get("/api/inbox", (req) => {
11
+ const url = new URL(req.url);
12
+ const to = url.searchParams.get("to");
13
+ const status = url.searchParams.get("status");
14
+ let sql = "SELECT * FROM inbox_message WHERE 1=1";
15
+ const params: any[] = [];
16
+ if (to) { sql += " AND to_actor = ?"; params.push(to); }
17
+ if (status) { sql += " AND status = ?"; params.push(status); }
18
+ sql += " ORDER BY created_at DESC";
19
+ return json(ctx.db.query(sql).all(...params));
20
+ });
21
+
22
+ // Long-poll: wait for an available task (or return immediately if one exists)
23
+ router.get("/api/inbox/poll", (req) => {
24
+ const url = new URL(req.url);
25
+ const agentId = url.searchParams.get("agent_id");
26
+ const skill = url.searchParams.get("skill") || null;
27
+ const timeoutMs = Math.min(Number(url.searchParams.get("timeout") || "30000"), 60000);
28
+
29
+ if (!agentId) return json({ error: "agent_id required" }, 400);
30
+
31
+ // Check immediately
32
+ const task = findOpenTask(ctx, agentId, skill);
33
+ if (task) return json(task);
34
+
35
+ // Long-poll: wait for new work or for a busy project to become available.
36
+ return new Promise<Response>((resolve) => {
37
+ const events = ["task.created", "task.transitioned", "task.complete", "task.return", "task.abandon"];
38
+ let settled = false;
39
+ const cleanup = () => {
40
+ settled = true;
41
+ for (const event of events) ctx.bus.removeListener(event, onTask);
42
+ };
43
+
44
+ const timer = setTimeout(() => {
45
+ if (settled) return;
46
+ cleanup();
47
+ resolve(new Response(null, { status: 204 }));
48
+ }, timeoutMs);
49
+
50
+ const onTask = () => {
51
+ if (settled) return;
52
+ const found = findOpenTask(ctx, agentId, skill);
53
+ if (found) {
54
+ clearTimeout(timer);
55
+ cleanup();
56
+ resolve(json(found));
57
+ }
58
+ };
59
+
60
+ for (const event of events) ctx.bus.on(event, onTask);
61
+ });
62
+ });
63
+
64
+ router.post("/api/inbox/messages", async (req) => {
65
+ const input = await body(req);
66
+ const { kind, from_actor, to_actor, subject, body: messageBody, ref_json } = input;
67
+ required({ kind, from_actor, to_actor }, ["kind", "from_actor", "to_actor"]);
68
+ const id = generateId("msg");
69
+ const ts = now();
70
+ ctx.db.run(
71
+ `INSERT INTO inbox_message (id, kind, from_actor, to_actor, subject, body, ref_json, status, created_at)
72
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'unread', ?)`,
73
+ [id, kind, from_actor, to_actor, subject || null, messageBody || null, stringifyRef(ref_json), ts]
74
+ );
75
+ return json(ctx.db.query("SELECT * FROM inbox_message WHERE id = ?").get(id), 201);
76
+ });
77
+
78
+ router.post("/api/inbox/messages/:id/read", (_req, params) => {
79
+ const ts = now();
80
+ ctx.db.run("UPDATE inbox_message SET status = 'read', read_at = ? WHERE id = ?", [ts, params.id]);
81
+ const row = ctx.db.query("SELECT * FROM inbox_message WHERE id = ?").get(params.id);
82
+ if (!row) return json({ error: "Not found" }, 404);
83
+ return json(row);
84
+ });
85
+
86
+ router.post("/api/inbox/messages/:id/reply", async (req, params) => {
87
+ const original = ctx.db.query("SELECT * FROM inbox_message WHERE id = ?").get(params.id) as any;
88
+ if (!original) return json({ error: "Not found" }, 404);
89
+ const input = await body(req);
90
+ const fromActor = input.from_actor || original.to_actor;
91
+ const id = generateId("msg");
92
+ const ts = now();
93
+ const ref = JSON.stringify({ reply_to: params.id });
94
+ ctx.db.run(
95
+ `INSERT INTO inbox_message (id, kind, from_actor, to_actor, subject, body, ref_json, status, created_at)
96
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'unread', ?)`,
97
+ [id, "reply", fromActor, original.from_actor, original.subject, input.body || null, ref, ts]
98
+ );
99
+ ctx.db.run("UPDATE inbox_message SET status = 'replied' WHERE id = ?", [params.id]);
100
+ return json(ctx.db.query("SELECT * FROM inbox_message WHERE id = ?").get(id), 201);
101
+ });
102
+ }
103
+
104
+ function findOpenTask(ctx: HubContext, agentId: string, skill: string | null): any {
105
+ const tasks = ctx.db.query("SELECT * FROM task ORDER BY priority DESC, created_at ASC").all() as any[];
106
+ return tasks.find((task) => {
107
+ const workflow = getWorkflowForTask(ctx.db, task.id);
108
+ if (task.status !== workflow.initial_status) return false;
109
+ if (!isTaskAssignedToAgent(task, agentId)) return false;
110
+ const claimAction = workflow.actions.find((action) => action.id === "claim" && action.from.includes(task.status));
111
+ if (findProjectExecutionBlocker(ctx.db, {
112
+ projectId: task.project_id,
113
+ excludeTaskId: task.id,
114
+ claimedStatus: claimAction?.to,
115
+ })) return false;
116
+ if (!skill) return true;
117
+ const requiredCapabilities: string[] = JSON.parse(task.required_capabilities_json || "[]");
118
+ return requiredCapabilities.length === 0 || requiredCapabilities.includes(skill);
119
+ }) || null;
120
+ }
121
+
122
+ function stringifyRef(ref: unknown): string | null {
123
+ if (ref === undefined || ref === null) return null;
124
+ return typeof ref === "string" ? ref : JSON.stringify(ref);
125
+ }
@@ -0,0 +1,32 @@
1
+ import type { Router } from "../core/router";
2
+ import { body, json } from "../core/router";
3
+ import type { HubContext } from "../core/server";
4
+ import { handleLarkBatchInbox, handleLarkCommand, handleLarkWebhook, sendLarkNotification, verifyLarkSignature } from "../bridges/lark";
5
+
6
+ export function registerLarkRoutes(router: Router, ctx: HubContext) {
7
+ router.post("/api/bridges/lark/events", async (req) => {
8
+ const raw = await req.text();
9
+ const input = raw ? JSON.parse(raw) : {};
10
+ const secret = process.env.MAESTRO_LARK_SIGNING_SECRET;
11
+ if (secret) {
12
+ const timestamp = req.headers.get("x-lark-request-timestamp") || "";
13
+ const signature = req.headers.get("x-lark-signature") || "";
14
+ const ok = await verifyLarkSignature({ timestamp, signature, body: raw, secret });
15
+ if (!ok) return json({ error: "Invalid Lark signature" }, 401);
16
+ }
17
+ if (input.event || input.type) {
18
+ return json(await handleLarkWebhook(ctx.db, input, { verificationToken: process.env.MAESTRO_LARK_VERIFICATION_TOKEN }));
19
+ }
20
+ return json(handleLarkCommand(ctx.db, input.text || ""));
21
+ });
22
+
23
+ router.post("/api/bridges/lark/inbox/batch", async (req) => {
24
+ const input = await body(req);
25
+ return json(handleLarkBatchInbox(ctx.db, input));
26
+ });
27
+
28
+ router.post("/api/bridges/lark/notify", async (req) => {
29
+ const input = await body(req);
30
+ return json(await sendLarkNotification(input));
31
+ });
32
+ }
@@ -0,0 +1,37 @@
1
+ import type { Router } from "../core/router";
2
+ import { body, json } from "../core/router";
3
+ import type { HubContext } from "../core/server";
4
+ import { generateId, now } from "../core/db";
5
+ import { required } from "../core/validate";
6
+
7
+ export function registerMemoryRoutes(router: Router, ctx: HubContext) {
8
+ router.get("/api/memory", (req) => {
9
+ const url = new URL(req.url);
10
+ const scope = url.searchParams.get("scope");
11
+ const scopeId = url.searchParams.get("scope_id");
12
+ const key = url.searchParams.get("key");
13
+ let sql = "SELECT * FROM memory WHERE 1=1";
14
+ const params: any[] = [];
15
+ if (scope) { sql += " AND scope = ?"; params.push(scope); }
16
+ if (scopeId) { sql += " AND scope_id = ?"; params.push(scopeId); }
17
+ if (key) { sql += " AND key = ?"; params.push(key); }
18
+ sql += " ORDER BY updated_at DESC";
19
+ return json(ctx.db.query(sql).all(...params));
20
+ });
21
+
22
+ router.post("/api/memory", async (req) => {
23
+ const input = await body(req);
24
+ const { scope, scope_id, key, value } = input;
25
+ required({ scope, scope_id, key }, ["scope", "scope_id", "key"]);
26
+ const ts = now();
27
+ const existing = ctx.db.query("SELECT id FROM memory WHERE scope = ? AND scope_id = ? AND key = ?").get(scope, scope_id, key) as any;
28
+ const id = existing?.id || generateId("mem");
29
+ ctx.db.run(
30
+ `INSERT INTO memory (id, scope, scope_id, key, value, updated_at)
31
+ VALUES (?, ?, ?, ?, ?, ?)
32
+ ON CONFLICT(scope, scope_id, key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`,
33
+ [id, scope, scope_id, key, value ?? null, ts]
34
+ );
35
+ return json(ctx.db.query("SELECT * FROM memory WHERE id = ?").get(id), existing ? 200 : 201);
36
+ });
37
+ }
package/src/api/ops.ts ADDED
@@ -0,0 +1,89 @@
1
+ import type { Router } from "../core/router";
2
+ import { body, json } from "../core/router";
3
+ import type { HubContext } from "../core/server";
4
+ import { closeDb, getDb, initSchema } from "../core/db";
5
+ import { createBackup, latestSoakEvidence, opsStats, restoreBackup, smokeRuntimes, soakRuntimes } from "../ops";
6
+ import { gcAll, gcArtifacts, gcDryRun, gcFull, gcOrphans } from "../ops/gc";
7
+ import { createTaskWorkDir, ensureTaskWorkDir, removeTaskWorkDir } from "../ops/task-workspace";
8
+
9
+ export function registerOpsRoutes(router: Router, ctx: HubContext) {
10
+ router.get("/api/ops/stats", () => {
11
+ return json(opsStats(ctx.db));
12
+ });
13
+
14
+ router.get("/api/ops/evidence/soak/latest", () => {
15
+ const evidence = latestSoakEvidence(ctx.hubDir);
16
+ if (!evidence) return json({ error: "No soak evidence" }, 404);
17
+ return json(evidence);
18
+ });
19
+
20
+ router.post("/api/ops/backup", () => {
21
+ return json(createBackup(ctx.hubDir), 201);
22
+ });
23
+
24
+ router.post("/api/ops/restore", async (req) => {
25
+ const input = await body<{ path: string }>(req);
26
+ closeDb(ctx.hubDir);
27
+ const result = restoreBackup(ctx.hubDir, input.path);
28
+ ctx.db = getDb(ctx.hubDir);
29
+ initSchema(ctx.db, ctx.hubDir);
30
+ ctx.bus.setDb(ctx.db);
31
+ return json(result);
32
+ });
33
+
34
+ router.post("/api/ops/smoke/runtimes", async (req) => {
35
+ const input = await body<{ runtimes: any[] }>(req);
36
+ return json(await smokeRuntimes(ctx.db, input.runtimes || []), 201);
37
+ });
38
+
39
+ router.post("/api/ops/soak/runtimes", async (req) => {
40
+ const input = await body<{ runtimes: any[]; duration_ms?: number; fast_forward?: boolean }>(req);
41
+ return json(await soakRuntimes(ctx.hubDir, ctx.db, input), 201);
42
+ });
43
+
44
+ // --- Task Workspace ---
45
+
46
+ router.post("/api/ops/tasks/:taskId/workdir", (req, params) => {
47
+ const dir = createTaskWorkDir(ctx.db, ctx.hubDir, params.taskId);
48
+ return json({ taskId: params.taskId, workDir: dir }, 201);
49
+ });
50
+
51
+ router.get("/api/ops/tasks/:taskId/workdir", (req, params) => {
52
+ const dir = ensureTaskWorkDir(ctx.db, ctx.hubDir, params.taskId);
53
+ return json({ taskId: params.taskId, workDir: dir });
54
+ });
55
+
56
+ router.delete("/api/ops/tasks/:taskId/workdir", (req, params) => {
57
+ const removed = removeTaskWorkDir(ctx.db, params.taskId);
58
+ return json({ taskId: params.taskId, removed });
59
+ });
60
+
61
+ // --- GC ---
62
+
63
+ router.get("/api/ops/gc/dry-run", async (req) => {
64
+ const url = new URL(req.url);
65
+ const retentionDays = Number(url.searchParams.get("retention_days") || 7);
66
+ const result = gcDryRun(ctx.db, ctx.hubDir, { retentionDays });
67
+ return json(result);
68
+ });
69
+
70
+ router.post("/api/ops/gc", async (req) => {
71
+ const input = await body<{ level?: string; retention_days?: number }>(req);
72
+ const config = { retentionDays: input.retention_days ?? 7 };
73
+ let result;
74
+ switch (input.level) {
75
+ case "artifact":
76
+ result = gcArtifacts(ctx.db, ctx.hubDir, config);
77
+ break;
78
+ case "full":
79
+ result = gcFull(ctx.db, ctx.hubDir, config);
80
+ break;
81
+ case "orphan":
82
+ result = gcOrphans(ctx.db, ctx.hubDir);
83
+ break;
84
+ default:
85
+ result = gcAll(ctx.db, ctx.hubDir, config);
86
+ }
87
+ return json(result);
88
+ });
89
+ }
@@ -0,0 +1,105 @@
1
+ import type { Router } from "../core/router";
2
+ import { json, body } from "../core/router";
3
+ import type { HubContext } from "../core/server";
4
+ import { generateId, now } from "../core/db";
5
+ import { required } from "../core/validate";
6
+ import { existsSync, readFileSync } from "fs";
7
+ import { join } from "path";
8
+
9
+ export function registerProjectRoutes(router: Router, ctx: HubContext) {
10
+ router.get("/api/projects", () => {
11
+ const rows = ctx.db.query(`
12
+ SELECT project.*, workspace.name AS workspace_name
13
+ FROM project
14
+ LEFT JOIN workspace ON workspace.id = project.workspace_id
15
+ ORDER BY project.created_at DESC
16
+ `).all();
17
+ return json(rows);
18
+ });
19
+
20
+ router.post("/api/workspaces/:wid/projects", async (req, params) => {
21
+ const workspace = ctx.db.query("SELECT * FROM workspace WHERE id = ?").get(params.wid) as any;
22
+ if (!workspace) return json({ error: "Workspace not found" }, 404);
23
+ const { name, charter_template, rationale, created_by, auto_kickoff, workdir } = await body(req);
24
+ required({ name }, ["name"]);
25
+ const id = generateId("proj");
26
+ const ts = now();
27
+ ctx.db.run(
28
+ "INSERT INTO project (id, workspace_id, name, workdir, charter_template, status, rationale, created_by, created_at, updated_at) VALUES (?, ?, ?, ?, ?, 'active', ?, ?, ?, ?)",
29
+ [id, params.wid, name, normalizeWorkdir(workdir), charter_template || null, rationale || null, created_by || "user", ts, ts]
30
+ );
31
+ ctx.bus.publish("project.created", { id, workspace_id: params.wid, name });
32
+
33
+ let initial_tasks: any[] | undefined;
34
+ if (auto_kickoff) {
35
+ initial_tasks = [];
36
+ const templatePath = join(import.meta.dir, "../../templates/kickoff-tasks.json");
37
+ if (existsSync(templatePath)) {
38
+ try {
39
+ const tasks = JSON.parse(readFileSync(templatePath, "utf-8")) as any[];
40
+ for (const t of tasks) {
41
+ const taskId = generateId("task");
42
+ ctx.db.run(
43
+ `INSERT INTO task (id, project_id, title, description, status, required_capabilities_json, priority, lineage_depth, created_by, created_at, updated_at)
44
+ VALUES (?, ?, ?, ?, 'open', ?, ?, 0, 'template', ?, ?)`,
45
+ [taskId, id, t.title, t.description || null, JSON.stringify(t.skill ? [t.skill] : []), t.priority || 0, ts, ts]
46
+ );
47
+ initial_tasks.push({ id: taskId, title: t.title, status: "open", priority: t.priority || 0 });
48
+ ctx.bus.publish("task.created", { id: taskId, project_id: id, title: t.title });
49
+ }
50
+ } catch {}
51
+ }
52
+ }
53
+
54
+ const response: any = { id, workspace_id: params.wid, name, workdir: normalizeWorkdir(workdir), status: "active", created_at: ts };
55
+ if (initial_tasks) response.initial_tasks = initial_tasks;
56
+ return json(response, 201);
57
+ });
58
+
59
+ router.get("/api/projects/:id", (_req, params) => {
60
+ const row = ctx.db.query("SELECT * FROM project WHERE id = ?").get(params.id);
61
+ if (!row) return json({ error: "Not found" }, 404);
62
+ return json(row);
63
+ });
64
+
65
+ router.patch("/api/projects/:id", async (req, params) => {
66
+ const updates = await body(req);
67
+ const fields: string[] = [];
68
+ const values: any[] = [];
69
+ for (const key of ["name", "workdir", "charter_template", "status"]) {
70
+ if (updates[key] !== undefined) {
71
+ fields.push(`${key} = ?`);
72
+ values.push(key === "workdir" ? normalizeWorkdir(updates[key]) : updates[key]);
73
+ }
74
+ }
75
+ if (fields.length === 0) return json({ error: "No fields to update" }, 400);
76
+ fields.push("updated_at = ?");
77
+ values.push(now());
78
+ values.push(params.id);
79
+ ctx.db.run(`UPDATE project SET ${fields.join(", ")} WHERE id = ?`, values);
80
+ const row = ctx.db.query("SELECT * FROM project WHERE id = ?").get(params.id);
81
+ return json(row);
82
+ });
83
+
84
+ router.post("/api/projects/:id/archive", (_req, params) => {
85
+ const project = ctx.db.query("SELECT * FROM project WHERE id = ?").get(params.id) as any;
86
+ if (!project) return json({ error: "Not found" }, 404);
87
+ if (project.status !== "active") return json({ error: "Only active projects can be archived" }, 409);
88
+ const ts = now();
89
+ ctx.db.run("UPDATE project SET status = 'archived', updated_at = ? WHERE id = ?", [ts, params.id]);
90
+ ctx.bus.publish("project.archived", { id: params.id, workspace_id: project.workspace_id });
91
+ const row = ctx.db.query("SELECT * FROM project WHERE id = ?").get(params.id);
92
+ return json(row);
93
+ });
94
+
95
+ router.get("/api/workspaces/:wid/projects", (_req, params) => {
96
+ const rows = ctx.db.query("SELECT * FROM project WHERE workspace_id = ? ORDER BY created_at DESC").all(params.wid);
97
+ return json(rows);
98
+ });
99
+ }
100
+
101
+ function normalizeWorkdir(value: unknown): string | null {
102
+ if (typeof value !== "string") return null;
103
+ const trimmed = value.trim();
104
+ return trimmed.length > 0 ? trimmed : null;
105
+ }