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.
- package/.claude/settings.local.json +28 -0
- package/dist/app.d.ts +10 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +121 -0
- package/dist/app.js.map +1 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +19 -0
- package/dist/config.js.map +1 -0
- package/dist/db/database.d.ts +5 -0
- package/dist/db/database.d.ts.map +1 -0
- package/dist/db/database.js +39 -0
- package/dist/db/database.js.map +1 -0
- package/dist/db/schema.d.ts +3 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +621 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +4 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +9 -0
- package/dist/logger.js.map +1 -0
- package/dist/middleware/auth.d.ts +13 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +733 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/routes/agents.d.ts +3 -0
- package/dist/routes/agents.d.ts.map +1 -0
- package/dist/routes/agents.js +1058 -0
- package/dist/routes/agents.js.map +1 -0
- package/dist/routes/issues.d.ts +4 -0
- package/dist/routes/issues.d.ts.map +1 -0
- package/dist/routes/issues.js +946 -0
- package/dist/routes/issues.js.map +1 -0
- package/dist/routes/knowledge.d.ts +3 -0
- package/dist/routes/knowledge.d.ts.map +1 -0
- package/dist/routes/knowledge.js +117 -0
- package/dist/routes/knowledge.js.map +1 -0
- package/dist/routes/memories.d.ts +3 -0
- package/dist/routes/memories.d.ts.map +1 -0
- package/dist/routes/memories.js +115 -0
- package/dist/routes/memories.js.map +1 -0
- package/dist/routes/messages.d.ts +3 -0
- package/dist/routes/messages.d.ts.map +1 -0
- package/dist/routes/messages.js +130 -0
- package/dist/routes/messages.js.map +1 -0
- package/dist/routes/projects.d.ts +3 -0
- package/dist/routes/projects.d.ts.map +1 -0
- package/dist/routes/projects.js +754 -0
- package/dist/routes/projects.js.map +1 -0
- package/dist/routes/templates.d.ts +3 -0
- package/dist/routes/templates.d.ts.map +1 -0
- package/dist/routes/templates.js +117 -0
- package/dist/routes/templates.js.map +1 -0
- package/dist/routes/ui.d.ts +3 -0
- package/dist/routes/ui.d.ts.map +1 -0
- package/dist/routes/ui.js +38 -0
- package/dist/routes/ui.js.map +1 -0
- package/dist/services/agent-hierarchy.d.ts +14 -0
- package/dist/services/agent-hierarchy.d.ts.map +1 -0
- package/dist/services/agent-hierarchy.js +58 -0
- package/dist/services/agent-hierarchy.js.map +1 -0
- package/dist/services/agent-issue-batch.d.ts +17 -0
- package/dist/services/agent-issue-batch.d.ts.map +1 -0
- package/dist/services/agent-issue-batch.js +57 -0
- package/dist/services/agent-issue-batch.js.map +1 -0
- package/dist/services/controller.d.ts +4 -0
- package/dist/services/controller.d.ts.map +1 -0
- package/dist/services/controller.js +237 -0
- package/dist/services/controller.js.map +1 -0
- package/dist/services/langgraph-runner.d.ts +33 -0
- package/dist/services/langgraph-runner.d.ts.map +1 -0
- package/dist/services/langgraph-runner.js +478 -0
- package/dist/services/langgraph-runner.js.map +1 -0
- package/dist/services/orchestrator.d.ts +9 -0
- package/dist/services/orchestrator.d.ts.map +1 -0
- package/dist/services/orchestrator.js +116 -0
- package/dist/services/orchestrator.js.map +1 -0
- package/dist/services/pre-controller.d.ts +7 -0
- package/dist/services/pre-controller.d.ts.map +1 -0
- package/dist/services/pre-controller.js +101 -0
- package/dist/services/pre-controller.js.map +1 -0
- package/dist/services/process-manager.d.ts +67 -0
- package/dist/services/process-manager.d.ts.map +1 -0
- package/dist/services/process-manager.js +938 -0
- package/dist/services/process-manager.js.map +1 -0
- package/dist/services/project-permissions.d.ts +84 -0
- package/dist/services/project-permissions.d.ts.map +1 -0
- package/dist/services/project-permissions.js +129 -0
- package/dist/services/project-permissions.js.map +1 -0
- package/dist/services/scheduler.d.ts +6 -0
- package/dist/services/scheduler.d.ts.map +1 -0
- package/dist/services/scheduler.js +300 -0
- package/dist/services/scheduler.js.map +1 -0
- package/dist/services/system-prompt.d.ts +3 -0
- package/dist/services/system-prompt.d.ts.map +1 -0
- package/dist/services/system-prompt.js +285 -0
- package/dist/services/system-prompt.js.map +1 -0
- package/dist/services/terminal.d.ts +18 -0
- package/dist/services/terminal.d.ts.map +1 -0
- package/dist/services/terminal.js +222 -0
- package/dist/services/terminal.js.map +1 -0
- package/dist/services/websocket.d.ts +15 -0
- package/dist/services/websocket.d.ts.map +1 -0
- package/dist/services/websocket.js +204 -0
- package/dist/services/websocket.js.map +1 -0
- package/dist/types.d.ts +108 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/env.ini +18 -0
- package/package.json +38 -0
- package/project_id +0 -0
- package/public/admin-users.html +188 -0
- package/public/agent.html +199 -0
- package/public/css/issues.css +275 -0
- package/public/css/style.css +1299 -0
- package/public/index.html +166 -0
- package/public/issue.html +76 -0
- package/public/js/agent.js +19 -0
- package/public/js/common.js +735 -0
- package/public/js/dashboard.js +772 -0
- package/public/js/files-panel.js +703 -0
- package/public/js/interactive-terminal.js +201 -0
- package/public/js/issue-renderer.js +559 -0
- package/public/js/issue.js +57 -0
- package/public/js/project.js +2425 -0
- package/public/js/terminal.js +564 -0
- package/public/project.html +430 -0
- package/public/terminal.html +67 -0
- package/public/vendor/marked.js +74 -0
- package/public/vendor/xterm-addon-fit.js +2 -0
- package/public/vendor/xterm.css +209 -0
- package/public/vendor/xterm.js +2 -0
- package/send_message_and_update_issue.js +65 -0
- package/tsconfig.json +19 -0
- 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
|