agentopia 1.0.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 (140) hide show
  1. package/.claude/settings.local.json +28 -0
  2. package/dist/app.d.ts +10 -0
  3. package/dist/app.d.ts.map +1 -0
  4. package/dist/app.js +121 -0
  5. package/dist/app.js.map +1 -0
  6. package/dist/config.d.ts +9 -0
  7. package/dist/config.d.ts.map +1 -0
  8. package/dist/config.js +19 -0
  9. package/dist/config.js.map +1 -0
  10. package/dist/db/database.d.ts +5 -0
  11. package/dist/db/database.d.ts.map +1 -0
  12. package/dist/db/database.js +39 -0
  13. package/dist/db/database.js.map +1 -0
  14. package/dist/db/schema.d.ts +3 -0
  15. package/dist/db/schema.d.ts.map +1 -0
  16. package/dist/db/schema.js +621 -0
  17. package/dist/db/schema.js.map +1 -0
  18. package/dist/index.d.ts +2 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +49 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/logger.d.ts +4 -0
  23. package/dist/logger.d.ts.map +1 -0
  24. package/dist/logger.js +9 -0
  25. package/dist/logger.js.map +1 -0
  26. package/dist/middleware/auth.d.ts +13 -0
  27. package/dist/middleware/auth.d.ts.map +1 -0
  28. package/dist/middleware/auth.js +733 -0
  29. package/dist/middleware/auth.js.map +1 -0
  30. package/dist/routes/agents.d.ts +3 -0
  31. package/dist/routes/agents.d.ts.map +1 -0
  32. package/dist/routes/agents.js +1058 -0
  33. package/dist/routes/agents.js.map +1 -0
  34. package/dist/routes/issues.d.ts +4 -0
  35. package/dist/routes/issues.d.ts.map +1 -0
  36. package/dist/routes/issues.js +946 -0
  37. package/dist/routes/issues.js.map +1 -0
  38. package/dist/routes/knowledge.d.ts +3 -0
  39. package/dist/routes/knowledge.d.ts.map +1 -0
  40. package/dist/routes/knowledge.js +117 -0
  41. package/dist/routes/knowledge.js.map +1 -0
  42. package/dist/routes/memories.d.ts +3 -0
  43. package/dist/routes/memories.d.ts.map +1 -0
  44. package/dist/routes/memories.js +115 -0
  45. package/dist/routes/memories.js.map +1 -0
  46. package/dist/routes/messages.d.ts +3 -0
  47. package/dist/routes/messages.d.ts.map +1 -0
  48. package/dist/routes/messages.js +130 -0
  49. package/dist/routes/messages.js.map +1 -0
  50. package/dist/routes/projects.d.ts +3 -0
  51. package/dist/routes/projects.d.ts.map +1 -0
  52. package/dist/routes/projects.js +754 -0
  53. package/dist/routes/projects.js.map +1 -0
  54. package/dist/routes/templates.d.ts +3 -0
  55. package/dist/routes/templates.d.ts.map +1 -0
  56. package/dist/routes/templates.js +117 -0
  57. package/dist/routes/templates.js.map +1 -0
  58. package/dist/routes/ui.d.ts +3 -0
  59. package/dist/routes/ui.d.ts.map +1 -0
  60. package/dist/routes/ui.js +38 -0
  61. package/dist/routes/ui.js.map +1 -0
  62. package/dist/services/agent-hierarchy.d.ts +14 -0
  63. package/dist/services/agent-hierarchy.d.ts.map +1 -0
  64. package/dist/services/agent-hierarchy.js +58 -0
  65. package/dist/services/agent-hierarchy.js.map +1 -0
  66. package/dist/services/agent-issue-batch.d.ts +17 -0
  67. package/dist/services/agent-issue-batch.d.ts.map +1 -0
  68. package/dist/services/agent-issue-batch.js +57 -0
  69. package/dist/services/agent-issue-batch.js.map +1 -0
  70. package/dist/services/controller.d.ts +4 -0
  71. package/dist/services/controller.d.ts.map +1 -0
  72. package/dist/services/controller.js +237 -0
  73. package/dist/services/controller.js.map +1 -0
  74. package/dist/services/langgraph-runner.d.ts +33 -0
  75. package/dist/services/langgraph-runner.d.ts.map +1 -0
  76. package/dist/services/langgraph-runner.js +478 -0
  77. package/dist/services/langgraph-runner.js.map +1 -0
  78. package/dist/services/orchestrator.d.ts +9 -0
  79. package/dist/services/orchestrator.d.ts.map +1 -0
  80. package/dist/services/orchestrator.js +116 -0
  81. package/dist/services/orchestrator.js.map +1 -0
  82. package/dist/services/pre-controller.d.ts +7 -0
  83. package/dist/services/pre-controller.d.ts.map +1 -0
  84. package/dist/services/pre-controller.js +101 -0
  85. package/dist/services/pre-controller.js.map +1 -0
  86. package/dist/services/process-manager.d.ts +67 -0
  87. package/dist/services/process-manager.d.ts.map +1 -0
  88. package/dist/services/process-manager.js +938 -0
  89. package/dist/services/process-manager.js.map +1 -0
  90. package/dist/services/project-permissions.d.ts +84 -0
  91. package/dist/services/project-permissions.d.ts.map +1 -0
  92. package/dist/services/project-permissions.js +129 -0
  93. package/dist/services/project-permissions.js.map +1 -0
  94. package/dist/services/scheduler.d.ts +6 -0
  95. package/dist/services/scheduler.d.ts.map +1 -0
  96. package/dist/services/scheduler.js +300 -0
  97. package/dist/services/scheduler.js.map +1 -0
  98. package/dist/services/system-prompt.d.ts +3 -0
  99. package/dist/services/system-prompt.d.ts.map +1 -0
  100. package/dist/services/system-prompt.js +285 -0
  101. package/dist/services/system-prompt.js.map +1 -0
  102. package/dist/services/terminal.d.ts +18 -0
  103. package/dist/services/terminal.d.ts.map +1 -0
  104. package/dist/services/terminal.js +222 -0
  105. package/dist/services/terminal.js.map +1 -0
  106. package/dist/services/websocket.d.ts +15 -0
  107. package/dist/services/websocket.d.ts.map +1 -0
  108. package/dist/services/websocket.js +204 -0
  109. package/dist/services/websocket.js.map +1 -0
  110. package/dist/types.d.ts +108 -0
  111. package/dist/types.d.ts.map +1 -0
  112. package/dist/types.js +3 -0
  113. package/dist/types.js.map +1 -0
  114. package/env.ini +18 -0
  115. package/package.json +38 -0
  116. package/project_id +0 -0
  117. package/public/admin-users.html +188 -0
  118. package/public/agent.html +199 -0
  119. package/public/css/issues.css +275 -0
  120. package/public/css/style.css +1299 -0
  121. package/public/index.html +166 -0
  122. package/public/issue.html +76 -0
  123. package/public/js/agent.js +19 -0
  124. package/public/js/common.js +735 -0
  125. package/public/js/dashboard.js +772 -0
  126. package/public/js/files-panel.js +703 -0
  127. package/public/js/interactive-terminal.js +201 -0
  128. package/public/js/issue-renderer.js +559 -0
  129. package/public/js/issue.js +57 -0
  130. package/public/js/project.js +2425 -0
  131. package/public/js/terminal.js +564 -0
  132. package/public/project.html +430 -0
  133. package/public/terminal.html +67 -0
  134. package/public/vendor/marked.js +74 -0
  135. package/public/vendor/xterm-addon-fit.js +2 -0
  136. package/public/vendor/xterm.css +209 -0
  137. package/public/vendor/xterm.js +2 -0
  138. package/send_message_and_update_issue.js +65 -0
  139. package/tsconfig.json +19 -0
  140. package/update_round2_and_create_round3.js +284 -0
@@ -0,0 +1,754 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.registerProjectRoutes = registerProjectRoutes;
37
+ const uuid_1 = require("uuid");
38
+ const child_process_1 = require("child_process");
39
+ const path = __importStar(require("path"));
40
+ const os = __importStar(require("os"));
41
+ const fs = __importStar(require("fs"));
42
+ const database_1 = require("../db/database");
43
+ const process_manager_1 = require("../services/process-manager");
44
+ const config_1 = require("../config");
45
+ const auth_1 = require("../middleware/auth");
46
+ const project_permissions_1 = require("../services/project-permissions");
47
+ function normalizeOrchestratorEngine(value) {
48
+ if (value === undefined)
49
+ return null;
50
+ const engine = String(value).toLowerCase();
51
+ if (engine === 'native' || engine === 'langgraph')
52
+ return engine;
53
+ return null;
54
+ }
55
+ function buildSqlPlaceholders(values) {
56
+ return values.map(() => '?').join(', ');
57
+ }
58
+ function getProjectOwnerSummary(db, projectId) {
59
+ return db.prepare(`SELECT u.id, u.username, u.display_name, u.role
60
+ FROM projects p
61
+ LEFT JOIN users u ON u.id = p.owner_id
62
+ WHERE p.id = ?`).get(projectId);
63
+ }
64
+ function getProjectMemberCount(db, projectId) {
65
+ const row = db.prepare(`SELECT COUNT(*) as count
66
+ FROM (
67
+ SELECT owner_id as user_id
68
+ FROM projects
69
+ WHERE id = ? AND owner_id IS NOT NULL
70
+ UNION
71
+ SELECT user_id
72
+ FROM project_members
73
+ WHERE project_id = ?
74
+ ) members`).get(projectId, projectId);
75
+ return row?.count || 0;
76
+ }
77
+ function serializeProject(db, project, user, localhostBypass) {
78
+ const permission = (0, project_permissions_1.getProjectPermission)(db, project.id, user, localhostBypass);
79
+ const owner = getProjectOwnerSummary(db, project.id);
80
+ return {
81
+ ...project,
82
+ permission_level: permission.level,
83
+ can_manage: permission.canManage,
84
+ owner,
85
+ member_count: getProjectMemberCount(db, project.id),
86
+ };
87
+ }
88
+ function registerProjectRoutes(fastify) {
89
+ // Dashboard summary — aggregate stats across all projects
90
+ fastify.get('/api/dashboard/summary', async (request) => {
91
+ const db = (0, database_1.getDatabase)();
92
+ const { user, localhostBypass } = (0, project_permissions_1.getProjectRequestContext)(request);
93
+ const projectIds = (0, project_permissions_1.listAccessibleProjectIds)(db, user, localhostBypass);
94
+ if (projectIds.length === 0) {
95
+ return {
96
+ agents: { total: 0, running: 0, error_count: 0 },
97
+ issues: { total: 0, open: 0 },
98
+ total_cost_usd: 0,
99
+ last_activity: {},
100
+ };
101
+ }
102
+ const placeholders = buildSqlPlaceholders(projectIds);
103
+ const agentStats = db.prepare(`SELECT COUNT(*) as total,
104
+ SUM(CASE WHEN status = 'running' THEN 1 ELSE 0 END) as running,
105
+ SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) as error_count
106
+ FROM agents
107
+ WHERE project_id IN (${placeholders})`).get(...projectIds);
108
+ const issueStats = db.prepare(`SELECT COUNT(*) as total,
109
+ SUM(CASE WHEN status IN ('open', 'in_progress') THEN 1 ELSE 0 END) as open_count
110
+ FROM issues
111
+ WHERE project_id IN (${placeholders})`).get(...projectIds);
112
+ // Total cost across all projects — only last record per run_id (cost is cumulative)
113
+ const costRows = db.prepare(`SELECT c.content FROM conversation_logs c
114
+ INNER JOIN (
115
+ SELECT MAX(cl.id) as max_id
116
+ FROM conversation_logs cl
117
+ JOIN agents a ON cl.agent_id = a.id
118
+ WHERE cl.stream = 'cost' AND a.project_id IN (${placeholders})
119
+ GROUP BY cl.run_id
120
+ ) latest
121
+ ON c.id = latest.max_id`).all(...projectIds);
122
+ let totalCost = 0;
123
+ let totalInputTokens = 0;
124
+ let totalOutputTokens = 0;
125
+ for (const c of costRows) {
126
+ try {
127
+ const data = JSON.parse(c.content);
128
+ totalCost += data.cost_usd || 0;
129
+ totalInputTokens += data.input_tokens || 0;
130
+ totalOutputTokens += data.output_tokens || 0;
131
+ }
132
+ catch { }
133
+ }
134
+ // Last activity per project (most recent agent started_at or issue updated_at)
135
+ const projectActivity = db.prepare(`SELECT p.id,
136
+ MAX(COALESCE(a.started_at, a.finished_at)) as last_agent_activity,
137
+ MAX(i.updated_at) as last_issue_activity
138
+ FROM projects p
139
+ LEFT JOIN agents a ON a.project_id = p.id
140
+ LEFT JOIN issues i ON i.project_id = p.id
141
+ WHERE p.id IN (${placeholders})
142
+ GROUP BY p.id`).all(...projectIds);
143
+ const lastActivityMap = {};
144
+ for (const row of projectActivity) {
145
+ const times = [row.last_agent_activity, row.last_issue_activity].filter(Boolean);
146
+ lastActivityMap[row.id] = times.length ? times.sort().pop() : null;
147
+ }
148
+ return {
149
+ agents: { total: agentStats.total || 0, running: agentStats.running || 0, error_count: agentStats.error_count || 0 },
150
+ issues: { total: issueStats.total || 0, open: issueStats.open_count || 0 },
151
+ total_cost_usd: totalCost,
152
+ total_input_tokens: totalInputTokens,
153
+ total_output_tokens: totalOutputTokens,
154
+ last_activity: lastActivityMap,
155
+ };
156
+ });
157
+ // Dashboard usage by project — stacked bar chart data
158
+ fastify.get('/api/dashboard/usage-by-project', async (request) => {
159
+ const db = (0, database_1.getDatabase)();
160
+ const period = request.query.period || 'day';
161
+ const { user, localhostBypass } = (0, project_permissions_1.getProjectRequestContext)(request);
162
+ const projectIds = (0, project_permissions_1.listAccessibleProjectIds)(db, user, localhostBypass);
163
+ if (projectIds.length === 0) {
164
+ return {
165
+ period,
166
+ time_buckets: [],
167
+ projects: [],
168
+ data: {},
169
+ };
170
+ }
171
+ const placeholders = buildSqlPlaceholders(projectIds);
172
+ // Only last cost record per run_id (cost is cumulative)
173
+ const rows = db.prepare(`SELECT c.content, c.created_at, p.id as project_id, p.name as project_name
174
+ FROM conversation_logs c
175
+ INNER JOIN (
176
+ SELECT MAX(cl.id) as max_id
177
+ FROM conversation_logs cl
178
+ JOIN agents a2 ON cl.agent_id = a2.id
179
+ WHERE cl.stream = 'cost' AND a2.project_id IN (${placeholders})
180
+ GROUP BY cl.run_id
181
+ ) latest ON c.id = latest.max_id
182
+ JOIN agents a ON c.agent_id = a.id
183
+ JOIN projects p ON a.project_id = p.id
184
+ ORDER BY c.created_at`).all(...projectIds);
185
+ // Aggregate by time bucket + project
186
+ const buckets = {};
187
+ const projectNames = {};
188
+ for (const row of rows) {
189
+ try {
190
+ const data = JSON.parse(row.content);
191
+ const costUsd = data.cost_usd || 0;
192
+ const inputTokens = data.input_tokens || 0;
193
+ const outputTokens = data.output_tokens || 0;
194
+ if (!row.created_at)
195
+ continue;
196
+ let key;
197
+ const date = row.created_at.slice(0, 10);
198
+ if (period === 'hour') {
199
+ key = row.created_at.slice(0, 13);
200
+ }
201
+ else if (period === 'week') {
202
+ const d = new Date(date);
203
+ d.setDate(d.getDate() - d.getDay());
204
+ key = d.toISOString().slice(0, 10);
205
+ }
206
+ else {
207
+ key = date;
208
+ }
209
+ projectNames[row.project_id] = row.project_name;
210
+ if (!buckets[key])
211
+ buckets[key] = {};
212
+ if (!buckets[key][row.project_id])
213
+ buckets[key][row.project_id] = { cost: 0, input_tokens: 0, output_tokens: 0 };
214
+ buckets[key][row.project_id].cost += costUsd;
215
+ buckets[key][row.project_id].input_tokens += inputTokens;
216
+ buckets[key][row.project_id].output_tokens += outputTokens;
217
+ }
218
+ catch { }
219
+ }
220
+ const timeBuckets = Object.keys(buckets).sort();
221
+ const projects = Object.entries(projectNames).map(([id, name]) => ({ id, name }));
222
+ return {
223
+ period,
224
+ time_buckets: timeBuckets,
225
+ projects,
226
+ data: Object.fromEntries(timeBuckets.map(t => [t, buckets[t]])),
227
+ };
228
+ });
229
+ // Generate project metadata from user description using AI
230
+ fastify.post('/api/generate-project', async (request, reply) => {
231
+ const { description, tool_path } = request.body;
232
+ if (!description)
233
+ return reply.code(400).send({ error: 'description is required' });
234
+ const tool = (tool_path || config_1.config.defaultCommandTemplate || '').trim() || 'cld';
235
+ const prompt = `Given the user's input below, generate a JSON object. IMPORTANT: Use the SAME LANGUAGE as the user's input (if Chinese, respond in Chinese; if English, respond in English).
236
+
237
+ Fields:
238
+ - "name": short project name in English (lowercase, hyphens, max 30 chars)
239
+ - "description": one-line summary (max 100 chars, same language as user)
240
+ - "task_description": detailed instructions for the controller agent (2-5 sentences, same language as user)
241
+ - "controller_role": role description for the controller agent (same language as user)
242
+ - "working_directory": if the user mentions a path or directory, extract it here (absolute path); otherwise null
243
+
244
+ User's input: "${description.replace(/"/g, '\\"')}"
245
+
246
+ Respond with ONLY valid JSON, no markdown, no explanation.`;
247
+ try {
248
+ const lowerTool = tool.toLowerCase();
249
+ let cmd;
250
+ if (lowerTool.startsWith('cld') || lowerTool.startsWith('claude')) {
251
+ // Claude Code / cld — keep existing non-interactive print mode
252
+ cmd = `${tool} -p`;
253
+ }
254
+ else if (lowerTool.startsWith('gemini')) {
255
+ // Gemini CLI — use text output mode with -p prompt flag
256
+ cmd = `${tool} --output-format text -p`;
257
+ }
258
+ else if (lowerTool.startsWith('codex')) {
259
+ // Codex CLI — non-interactive exec. We avoid --json here to keep
260
+ // output as plain text so that JSON extraction via regex still works.
261
+ // PROMPT is read from stdin when '-' is used.
262
+ cmd = 'codex exec --sandbox workspace-write --skip-git-repo-check -';
263
+ }
264
+ else {
265
+ // Fallback: run the tool as-is, reading prompt from stdin
266
+ cmd = tool;
267
+ }
268
+ const result = (0, child_process_1.execSync)(`echo ${JSON.stringify(prompt)} | ${cmd}`, {
269
+ timeout: 60000,
270
+ encoding: 'utf-8',
271
+ env: { ...process.env },
272
+ }).trim();
273
+ // Extract JSON from response (handle possible markdown wrapping)
274
+ const jsonMatch = result.match(/\{[\s\S]*\}/);
275
+ if (!jsonMatch) {
276
+ return reply.code(500).send({ error: 'AI did not return valid JSON', raw: result });
277
+ }
278
+ const parsed = JSON.parse(jsonMatch[0]);
279
+ return { ...parsed };
280
+ }
281
+ catch (e) {
282
+ return reply.code(500).send({ error: 'Failed to generate: ' + (e.message || '').slice(0, 200) });
283
+ }
284
+ });
285
+ // Project cost summary with per-run breakdowns and time-series support
286
+ fastify.get('/api/projects/:id/costs', async (request, reply) => {
287
+ const db = (0, database_1.getDatabase)();
288
+ const pid = request.params.id;
289
+ const period = request.query.period; // day | week | month
290
+ const access = (0, project_permissions_1.ensureProjectAccess)(db, request, reply, pid);
291
+ if (!access)
292
+ return;
293
+ // Only last cost record per run_id (cost is cumulative)
294
+ const costs = db.prepare(`SELECT c.content, c.run_id, c.created_at, a.name as agent_name
295
+ FROM conversation_logs c
296
+ INNER JOIN (SELECT MAX(cl.id) as max_id FROM conversation_logs cl JOIN agents al ON cl.agent_id = al.id WHERE al.project_id = ? AND cl.stream = 'cost' GROUP BY cl.run_id) latest ON c.id = latest.max_id
297
+ JOIN agents a ON c.agent_id = a.id
298
+ ORDER BY c.created_at`).all(pid);
299
+ let totalCost = 0;
300
+ let totalInput = 0;
301
+ let totalOutput = 0;
302
+ const byAgent = {};
303
+ const runs = [];
304
+ const timeSeries = {};
305
+ const timeSeriesByAgent = {};
306
+ for (const c of costs) {
307
+ try {
308
+ const data = JSON.parse(c.content);
309
+ const costUsd = data.cost_usd || 0;
310
+ const inputTokens = data.input_tokens || 0;
311
+ const outputTokens = data.output_tokens || 0;
312
+ totalCost += costUsd;
313
+ totalInput += inputTokens;
314
+ totalOutput += outputTokens;
315
+ if (!byAgent[c.agent_name])
316
+ byAgent[c.agent_name] = { cost: 0, runs: 0, input_tokens: 0, output_tokens: 0 };
317
+ byAgent[c.agent_name].cost += costUsd;
318
+ byAgent[c.agent_name].runs++;
319
+ byAgent[c.agent_name].input_tokens += inputTokens;
320
+ byAgent[c.agent_name].output_tokens += outputTokens;
321
+ runs.push({
322
+ run_id: c.run_id,
323
+ agent_name: c.agent_name,
324
+ cost_usd: costUsd,
325
+ input_tokens: inputTokens,
326
+ output_tokens: outputTokens,
327
+ timestamp: c.created_at,
328
+ });
329
+ // Build time-series buckets
330
+ if (period && c.created_at) {
331
+ let key;
332
+ const date = c.created_at.slice(0, 10); // YYYY-MM-DD
333
+ if (period === 'hour') {
334
+ key = c.created_at.slice(0, 13); // YYYY-MM-DD HH
335
+ }
336
+ else if (period === 'day') {
337
+ key = date;
338
+ }
339
+ else if (period === 'week') {
340
+ const d = new Date(date);
341
+ const day = d.getDay();
342
+ d.setDate(d.getDate() - day);
343
+ key = d.toISOString().slice(0, 10);
344
+ }
345
+ else if (period === 'month') {
346
+ key = date.slice(0, 7); // YYYY-MM
347
+ }
348
+ else {
349
+ key = date;
350
+ }
351
+ if (!timeSeries[key])
352
+ timeSeries[key] = { cost: 0, runs: 0 };
353
+ timeSeries[key].cost += costUsd;
354
+ timeSeries[key].runs++;
355
+ // Per-agent time-series
356
+ if (!timeSeriesByAgent[c.agent_name])
357
+ timeSeriesByAgent[c.agent_name] = {};
358
+ if (!timeSeriesByAgent[c.agent_name][key])
359
+ timeSeriesByAgent[c.agent_name][key] = { cost: 0, runs: 0 };
360
+ timeSeriesByAgent[c.agent_name][key].cost += costUsd;
361
+ timeSeriesByAgent[c.agent_name][key].runs++;
362
+ }
363
+ }
364
+ catch { }
365
+ }
366
+ const result = {
367
+ total_cost_usd: totalCost,
368
+ total_input_tokens: totalInput,
369
+ total_output_tokens: totalOutput,
370
+ by_agent: byAgent,
371
+ runs,
372
+ };
373
+ if (period) {
374
+ result.time_series = Object.entries(timeSeries)
375
+ .sort(([a], [b]) => a.localeCompare(b))
376
+ .map(([period_start, data]) => ({ period_start, ...data }));
377
+ // Per-agent time-series breakdown
378
+ result.time_series_by_agent = Object.fromEntries(Object.entries(timeSeriesByAgent).map(([agent, series]) => [
379
+ agent,
380
+ Object.entries(series)
381
+ .sort(([a], [b]) => a.localeCompare(b))
382
+ .map(([period_start, data]) => ({ period_start, ...data })),
383
+ ]));
384
+ }
385
+ return result;
386
+ });
387
+ // Git log — aggregate recent commits from all agents' working directories
388
+ fastify.get('/api/projects/:id/git-log', async (request, reply) => {
389
+ const db = (0, database_1.getDatabase)();
390
+ const pid = request.params.id;
391
+ const limit = Math.min(parseInt(request.query.limit || '20', 10), 100);
392
+ const access = (0, project_permissions_1.ensureProjectAccess)(db, request, reply, pid);
393
+ if (!access)
394
+ return;
395
+ const agents = db.prepare('SELECT id, name, working_directory FROM agents WHERE project_id = ?').all(pid);
396
+ const seen = new Set(); // deduplicate by commit hash
397
+ const commits = [];
398
+ // Collect unique working directories
399
+ const dirToAgents = new Map();
400
+ for (const agent of agents) {
401
+ let dir = agent.working_directory;
402
+ if (!dir)
403
+ continue;
404
+ if (dir.startsWith('~/'))
405
+ dir = path.join(os.homedir(), dir.slice(2));
406
+ if (!dirToAgents.has(dir))
407
+ dirToAgents.set(dir, []);
408
+ dirToAgents.get(dir).push(agent.name);
409
+ }
410
+ for (const [dir, agentNames] of dirToAgents) {
411
+ try {
412
+ if (!fs.existsSync(path.join(dir, '.git')) && !fs.existsSync(dir))
413
+ continue;
414
+ const output = (0, child_process_1.execSync)(`git log --format='%H|%an|%s|%ai' -n ${limit}`, { cwd: dir, timeout: 2000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
415
+ if (!output)
416
+ continue;
417
+ for (const line of output.split('\n')) {
418
+ const parts = line.split('|');
419
+ if (parts.length < 4)
420
+ continue;
421
+ const hash = parts[0];
422
+ if (seen.has(hash))
423
+ continue;
424
+ seen.add(hash);
425
+ commits.push({
426
+ hash,
427
+ short_hash: hash.slice(0, 7),
428
+ author: parts[1],
429
+ message: parts[2],
430
+ date: parts.slice(3).join('|'), // date may contain |
431
+ repo_path: dir,
432
+ agent_name: agentNames[0],
433
+ });
434
+ }
435
+ }
436
+ catch {
437
+ // Not a git repo or git failed — skip gracefully
438
+ }
439
+ }
440
+ // Sort by date descending and limit
441
+ commits.sort((a, b) => b.date > a.date ? 1 : -1);
442
+ return commits.slice(0, limit);
443
+ });
444
+ // Project activity timeline
445
+ fastify.get('/api/projects/:id/activity', async (request, reply) => {
446
+ const db = (0, database_1.getDatabase)();
447
+ const limit = parseInt(request.query.limit || '50', 10);
448
+ const pid = request.params.id;
449
+ const access = (0, project_permissions_1.ensureProjectAccess)(db, request, reply, pid);
450
+ if (!access)
451
+ return;
452
+ // Combine: issue events + agent status changes + comments into a unified timeline
453
+ const issues = db.prepare("SELECT 'issue' as event_type, id, number, title, status, created_by as actor, created_at as time FROM issues WHERE project_id = ? ORDER BY created_at DESC LIMIT ?").all(pid, limit);
454
+ const comments = db.prepare("SELECT 'comment' as event_type, c.id, c.body, c.author_id as actor, c.created_at as time, i.number as issue_number, i.title as issue_title FROM issue_comments c JOIN issues i ON c.issue_id = i.id WHERE i.project_id = ? ORDER BY c.created_at DESC LIMIT ?").all(pid, limit);
455
+ const agentRuns = db.prepare("SELECT 'agent_run' as event_type, a.id, a.name, a.status as agent_status, a.started_at as time FROM agents a WHERE a.project_id = ? AND a.started_at IS NOT NULL ORDER BY a.started_at DESC LIMIT ?").all(pid, limit);
456
+ // Merge and sort by time DESC
457
+ const all = [...issues, ...comments, ...agentRuns]
458
+ .sort((a, b) => b.time > a.time ? 1 : -1)
459
+ .slice(0, limit);
460
+ return all;
461
+ });
462
+ // Recent orchestration decision runs (for graph visualization)
463
+ fastify.get('/api/projects/:id/orchestration-runs', async (request, reply) => {
464
+ const db = (0, database_1.getDatabase)();
465
+ const pid = request.params.id;
466
+ const limit = Math.min(Math.max(parseInt(request.query.limit || '20', 10), 1), 100);
467
+ const access = (0, project_permissions_1.ensureProjectAccess)(db, request, reply, pid);
468
+ if (!access)
469
+ return;
470
+ const rows = db.prepare("SELECT id, project_id, engine, decision, controller_agent_id, controller_started, controller_run_id, controller_pid, dispatch_count, dispatch_summary, reasons, actions, dispatch_results, created_at FROM orchestration_runs WHERE project_id = ? ORDER BY id DESC LIMIT ?").all(pid, limit);
471
+ const parseJson = (raw, fallback) => {
472
+ if (typeof raw !== 'string' || raw.trim() === '')
473
+ return fallback;
474
+ try {
475
+ return JSON.parse(raw);
476
+ }
477
+ catch {
478
+ return fallback;
479
+ }
480
+ };
481
+ return rows.map((row) => ({
482
+ ...row,
483
+ controller_started: !!row.controller_started,
484
+ reasons: parseJson(row.reasons, []),
485
+ actions: parseJson(row.actions, []),
486
+ dispatch_results: parseJson(row.dispatch_results, []),
487
+ }));
488
+ });
489
+ // List projects (with optional embedded stats for dashboard performance)
490
+ fastify.get('/api/projects', async (request) => {
491
+ const db = (0, database_1.getDatabase)();
492
+ const { user, localhostBypass } = (0, project_permissions_1.getProjectRequestContext)(request);
493
+ const projects = (0, project_permissions_1.listAccessibleProjects)(db, user, localhostBypass).map((project) => serializeProject(db, project, user, localhostBypass));
494
+ if (request.query.with_stats !== '1')
495
+ return projects;
496
+ // Single-pass stats: avoids N+2 frontend requests per project
497
+ return projects.map(p => {
498
+ const agentStats = db.prepare("SELECT COUNT(*) as total, SUM(CASE WHEN status='running' THEN 1 ELSE 0 END) as running, SUM(CASE WHEN status='error' THEN 1 ELSE 0 END) as error_count FROM agents WHERE project_id = ?").get(p.id);
499
+ const issueStats = db.prepare("SELECT COUNT(*) as total, SUM(CASE WHEN status IN ('open','in_progress') THEN 1 ELSE 0 END) as open_count FROM issues WHERE project_id = ?").get(p.id);
500
+ const userIssues = db.prepare("SELECT number, title FROM issues WHERE project_id = ? AND assigned_to = 'user' AND status IN ('open','in_progress') ORDER BY priority DESC LIMIT 10").all(p.id);
501
+ return {
502
+ ...p,
503
+ stats: {
504
+ agents: agentStats.total || 0,
505
+ running: agentStats.running || 0,
506
+ agentError: agentStats.error_count || 0,
507
+ issues: issueStats.total || 0,
508
+ openIssues: issueStats.open_count || 0,
509
+ userIssues,
510
+ },
511
+ };
512
+ });
513
+ });
514
+ // Create project
515
+ fastify.post('/api/projects', async (request, reply) => {
516
+ const { name, description, task_description, command_template, orchestrator_engine, working_directory, controller_role } = request.body;
517
+ if (!task_description) {
518
+ return reply.code(400).send({ error: 'task_description is required' });
519
+ }
520
+ const db = (0, database_1.getDatabase)();
521
+ const id = (0, uuid_1.v4)();
522
+ const tmpl = command_template || config_1.config.defaultCommandTemplate;
523
+ const orchestratorEngine = normalizeOrchestratorEngine(orchestrator_engine);
524
+ const { user } = (0, project_permissions_1.getProjectRequestContext)(request);
525
+ const ownerId = user && !(0, auth_1.isLegacyAuthUser)(user) ? user.id : null;
526
+ if (orchestrator_engine !== undefined && orchestratorEngine === null) {
527
+ return reply.code(400).send({ error: 'Invalid orchestrator_engine. Use native or langgraph.' });
528
+ }
529
+ db.prepare(`
530
+ INSERT INTO projects (id, name, description, task_description, command_template, orchestrator_engine, owner_id, status)
531
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'active')
532
+ `).run(id, name, description || '', task_description, tmpl, orchestratorEngine || config_1.config.defaultOrchestratorEngine, ownerId);
533
+ if (ownerId) {
534
+ db.prepare(`
535
+ INSERT INTO project_members (id, project_id, user_id, role)
536
+ VALUES (?, ?, ?, 'owner')
537
+ ON CONFLICT(project_id, user_id) DO UPDATE SET role = 'owner'
538
+ `).run((0, uuid_1.v4)(), id, ownerId);
539
+ }
540
+ // Create default controller agent (with Sonnet model for cost efficiency)
541
+ const controllerId = (0, uuid_1.v4)();
542
+ const ctrlRole = controller_role || 'Main controller agent that manages and coordinates other agents';
543
+ const ctrlCommandTemplate = `${tmpl} --model claude-sonnet-4-6`;
544
+ db.prepare(`
545
+ INSERT INTO agents (id, project_id, name, role, is_controller, working_directory, command_template, status)
546
+ VALUES (?, ?, ?, ?, 1, ?, ?, 'idle')
547
+ `).run(controllerId, id, `${name || 'project'}-controller`, ctrlRole, working_directory || null, ctrlCommandTemplate);
548
+ // Create default assistant agent
549
+ const assistantId = (0, uuid_1.v4)();
550
+ db.prepare(`
551
+ INSERT INTO agents (id, project_id, name, role, is_controller, working_directory, status)
552
+ VALUES (?, ?, ?, ?, 0, ?, 'idle')
553
+ `).run(assistantId, id, `${name || 'project'}-assistant`, 'Assistant to the controller. Handles analysis, code execution, data processing, and research tasks delegated by the controller.', working_directory || null);
554
+ const project = db.prepare('SELECT * FROM projects WHERE id = ?').get(id);
555
+ return reply.code(201).send(serializeProject(db, project, user, false));
556
+ });
557
+ // Get project
558
+ fastify.get('/api/projects/:id', async (request, reply) => {
559
+ const db = (0, database_1.getDatabase)();
560
+ const access = (0, project_permissions_1.ensureProjectAccess)(db, request, reply, request.params.id);
561
+ if (!access)
562
+ return;
563
+ const project = db.prepare('SELECT * FROM projects WHERE id = ?').get(request.params.id);
564
+ if (!project)
565
+ return reply.code(404).send({ error: 'Project not found' });
566
+ return serializeProject(db, project, access.user, access.localhostBypass);
567
+ });
568
+ // Update project
569
+ fastify.put('/api/projects/:id', async (request, reply) => {
570
+ const db = (0, database_1.getDatabase)();
571
+ const access = (0, project_permissions_1.ensureProjectAccess)(db, request, reply, request.params.id, true);
572
+ if (!access)
573
+ return;
574
+ const existing = db.prepare('SELECT * FROM projects WHERE id = ?').get(request.params.id);
575
+ if (!existing)
576
+ return reply.code(404).send({ error: 'Project not found' });
577
+ const { name, description, task_description, command_template, orchestrator_engine, status } = request.body;
578
+ const orchestratorEngine = normalizeOrchestratorEngine(orchestrator_engine);
579
+ if (orchestrator_engine !== undefined && orchestratorEngine === null) {
580
+ return reply.code(400).send({ error: 'Invalid orchestrator_engine. Use native or langgraph.' });
581
+ }
582
+ db.prepare(`
583
+ UPDATE projects SET
584
+ name = COALESCE(?, name),
585
+ description = COALESCE(?, description),
586
+ task_description = COALESCE(?, task_description),
587
+ command_template = COALESCE(?, command_template),
588
+ orchestrator_engine = COALESCE(?, orchestrator_engine),
589
+ status = COALESCE(?, status),
590
+ updated_at = datetime('now')
591
+ WHERE id = ?
592
+ `).run(name ?? null, description ?? null, task_description ?? null, command_template ?? null, orchestratorEngine ?? null, status ?? null, request.params.id);
593
+ const updated = db.prepare('SELECT * FROM projects WHERE id = ?').get(request.params.id);
594
+ return serializeProject(db, updated, access.user, access.localhostBypass);
595
+ });
596
+ fastify.get('/api/projects/:id/members', async (request, reply) => {
597
+ const db = (0, database_1.getDatabase)();
598
+ const access = (0, project_permissions_1.ensureProjectAccess)(db, request, reply, request.params.id);
599
+ if (!access)
600
+ return;
601
+ const members = db.prepare(`SELECT pm.*,
602
+ u.username,
603
+ u.display_name,
604
+ u.role as user_role
605
+ FROM project_members pm
606
+ JOIN users u ON u.id = pm.user_id
607
+ WHERE pm.project_id = ?
608
+ ORDER BY CASE pm.role WHEN 'owner' THEN 0 ELSE 1 END, COALESCE(u.display_name, u.username), u.username`).all(request.params.id);
609
+ return { members };
610
+ });
611
+ fastify.post('/api/projects/:id/members', async (request, reply) => {
612
+ const db = (0, database_1.getDatabase)();
613
+ const access = (0, project_permissions_1.ensureProjectAccess)(db, request, reply, request.params.id, true);
614
+ if (!access)
615
+ return;
616
+ const { user_id, username, role } = request.body;
617
+ if (!user_id && !username) {
618
+ return reply.code(400).send({ error: 'user_id or username is required' });
619
+ }
620
+ if (role && role !== 'member') {
621
+ return reply.code(400).send({ error: 'Only member role is supported' });
622
+ }
623
+ const project = db.prepare('SELECT owner_id FROM projects WHERE id = ?').get(request.params.id);
624
+ if (!project)
625
+ return reply.code(404).send({ error: 'Project not found' });
626
+ const targetUser = user_id
627
+ ? db.prepare('SELECT id, username, display_name, role FROM users WHERE id = ?').get(user_id)
628
+ : db.prepare('SELECT id, username, display_name, role FROM users WHERE username = ?').get(username);
629
+ if (!targetUser) {
630
+ return reply.code(404).send({ error: 'User not found' });
631
+ }
632
+ if (project.owner_id === targetUser.id) {
633
+ return reply.code(400).send({ error: 'Project owner already has access' });
634
+ }
635
+ const existingMember = db.prepare('SELECT * FROM project_members WHERE project_id = ? AND user_id = ?').get(request.params.id, targetUser.id);
636
+ if (existingMember?.role === 'owner') {
637
+ return reply.code(400).send({ error: 'Cannot change project owner membership via share API' });
638
+ }
639
+ if (existingMember) {
640
+ db.prepare("UPDATE project_members SET role = 'member' WHERE id = ?").run(existingMember.id);
641
+ }
642
+ else {
643
+ db.prepare("INSERT INTO project_members (id, project_id, user_id, role) VALUES (?, ?, ?, 'member')").run((0, uuid_1.v4)(), request.params.id, targetUser.id);
644
+ }
645
+ const member = db.prepare(`SELECT pm.*,
646
+ u.username,
647
+ u.display_name,
648
+ u.role as user_role
649
+ FROM project_members pm
650
+ JOIN users u ON u.id = pm.user_id
651
+ WHERE pm.project_id = ? AND pm.user_id = ?`).get(request.params.id, targetUser.id);
652
+ return reply.code(existingMember ? 200 : 201).send(member);
653
+ });
654
+ fastify.delete('/api/projects/:id/members/:userId', async (request, reply) => {
655
+ const db = (0, database_1.getDatabase)();
656
+ const access = (0, project_permissions_1.ensureProjectAccess)(db, request, reply, request.params.id, true);
657
+ if (!access)
658
+ return;
659
+ const project = db.prepare('SELECT owner_id FROM projects WHERE id = ?').get(request.params.id);
660
+ if (!project)
661
+ return reply.code(404).send({ error: 'Project not found' });
662
+ if (project.owner_id === request.params.userId) {
663
+ return reply.code(400).send({ error: 'Cannot remove project owner' });
664
+ }
665
+ const existingMember = db.prepare('SELECT * FROM project_members WHERE project_id = ? AND user_id = ?').get(request.params.id, request.params.userId);
666
+ if (!existingMember) {
667
+ return reply.code(404).send({ error: 'Project member not found' });
668
+ }
669
+ db.prepare('DELETE FROM project_members WHERE id = ?').run(existingMember.id);
670
+ return { success: true };
671
+ });
672
+ // Export project data as JSON
673
+ fastify.get('/api/projects/:id/export', async (request, reply) => {
674
+ const db = (0, database_1.getDatabase)();
675
+ const pid = request.params.id;
676
+ const access = (0, project_permissions_1.ensureProjectAccess)(db, request, reply, pid);
677
+ if (!access)
678
+ return;
679
+ const project = db.prepare('SELECT * FROM projects WHERE id = ?').get(pid);
680
+ if (!project)
681
+ return reply.code(404).send({ error: 'Project not found' });
682
+ const agents = db.prepare('SELECT id, name, role, is_controller, status, started_at, finished_at, created_at FROM agents WHERE project_id = ?').all(pid);
683
+ const issues = db.prepare('SELECT * FROM issues WHERE project_id = ? ORDER BY number').all(pid);
684
+ const milestones = db.prepare('SELECT * FROM milestones WHERE project_id = ?').all(pid);
685
+ // Cost summary — only last cost record per run_id (cost is cumulative)
686
+ const costRows = db.prepare(`SELECT c.content FROM conversation_logs c
687
+ INNER JOIN (SELECT MAX(cl.id) as max_id FROM conversation_logs cl JOIN agents al ON cl.agent_id = al.id WHERE al.project_id = ? AND cl.stream = 'cost' GROUP BY cl.run_id) latest
688
+ ON c.id = latest.max_id`).all(pid);
689
+ let totalCost = 0;
690
+ let totalInput = 0;
691
+ let totalOutput = 0;
692
+ for (const c of costRows) {
693
+ try {
694
+ const data = JSON.parse(c.content);
695
+ totalCost += data.cost_usd || 0;
696
+ totalInput += data.input_tokens || 0;
697
+ totalOutput += data.output_tokens || 0;
698
+ }
699
+ catch { }
700
+ }
701
+ reply.header('Content-Type', 'application/json');
702
+ reply.header('Content-Disposition', `attachment; filename="${project.name || 'project'}-export.json"`);
703
+ return {
704
+ exported_at: new Date().toISOString(),
705
+ project,
706
+ agents,
707
+ issues,
708
+ milestones,
709
+ cost_summary: { total_cost_usd: totalCost, total_input_tokens: totalInput, total_output_tokens: totalOutput },
710
+ };
711
+ });
712
+ // Export issues as CSV
713
+ fastify.get('/api/projects/:id/export/issues.csv', async (request, reply) => {
714
+ const db = (0, database_1.getDatabase)();
715
+ const pid = request.params.id;
716
+ const access = (0, project_permissions_1.ensureProjectAccess)(db, request, reply, pid);
717
+ if (!access)
718
+ return;
719
+ const project = db.prepare('SELECT * FROM projects WHERE id = ?').get(pid);
720
+ if (!project)
721
+ return reply.code(404).send({ error: 'Project not found' });
722
+ const issues = db.prepare('SELECT number, title, status, priority, labels, assigned_to, created_by, created_at, updated_at FROM issues WHERE project_id = ? ORDER BY number').all(pid);
723
+ const csvHeader = 'number,title,status,priority,labels,assigned_to,created_by,created_at,updated_at';
724
+ const csvRows = issues.map((i) => {
725
+ const escape = (v) => {
726
+ const s = String(v ?? '');
727
+ return s.includes(',') || s.includes('"') || s.includes('\n') ? `"${s.replace(/"/g, '""')}"` : s;
728
+ };
729
+ return [i.number, i.title, i.status, i.priority, i.labels, i.assigned_to, i.created_by, i.created_at, i.updated_at].map(escape).join(',');
730
+ });
731
+ reply.header('Content-Type', 'text/csv');
732
+ reply.header('Content-Disposition', `attachment; filename="${project.name || 'project'}-issues.csv"`);
733
+ return [csvHeader, ...csvRows].join('\n');
734
+ });
735
+ // Delete project
736
+ fastify.delete('/api/projects/:id', async (request, reply) => {
737
+ const db = (0, database_1.getDatabase)();
738
+ const access = (0, project_permissions_1.ensureProjectAccess)(db, request, reply, request.params.id, true);
739
+ if (!access)
740
+ return;
741
+ const existing = db.prepare('SELECT * FROM projects WHERE id = ?').get(request.params.id);
742
+ if (!existing)
743
+ return reply.code(404).send({ error: 'Project not found' });
744
+ // Stop all running agents before deleting
745
+ const agents = db.prepare('SELECT * FROM agents WHERE project_id = ?').all(request.params.id);
746
+ for (const agent of agents) {
747
+ if ((0, process_manager_1.isAgentRunning)(agent.id))
748
+ (0, process_manager_1.stopAgentProcess)(agent.id);
749
+ }
750
+ db.prepare('DELETE FROM projects WHERE id = ?').run(request.params.id);
751
+ return { success: true };
752
+ });
753
+ }
754
+ //# sourceMappingURL=projects.js.map