heyio 1.2.4 → 1.4.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/dist/api/server.js +289 -12
- package/dist/config.js +6 -0
- package/dist/copilot/agents.js +100 -5
- package/dist/copilot/ceremonies.js +12 -2
- package/dist/copilot/io-scheduler.js +9 -1
- package/dist/copilot/orchestrator.js +4 -0
- package/dist/copilot/scheduler.js +7 -2
- package/dist/copilot/skills.js +138 -6
- package/dist/copilot/squad-tools.js +102 -0
- package/dist/copilot/system-message.js +2 -1
- package/dist/copilot/token-tracker.js +89 -0
- package/dist/copilot/tools.js +27 -5
- package/dist/copilot/trigger-schedule.js +31 -0
- package/dist/paths.js +1 -0
- package/dist/store/agent-events.js +19 -0
- package/dist/store/audit-log.js +71 -0
- package/dist/store/conversations.js +150 -0
- package/dist/store/db.js +111 -0
- package/dist/store/schedules.js +9 -1
- package/dist/store/squad-colors.js +23 -0
- package/dist/store/squads.js +6 -1
- package/dist/store/tasks.js +43 -0
- package/dist/store/token-usage.js +94 -0
- package/dist/wiki/backlinks.js +51 -0
- package/dist/wiki/fs.js +63 -1
- package/dist/wiki/search.js +13 -2
- package/package.json +1 -1
- package/web-dist/assets/AuditLogView-DqxVzjd_.js +6 -0
- package/web-dist/assets/ChatView-BBopM_A3.js +1 -0
- package/web-dist/assets/FeedView-Bo4p1stx.js +6 -0
- package/web-dist/assets/HistoryView-ChTuQvXr.js +1 -0
- package/web-dist/assets/LoginView-AnOP3Mau.js +1 -0
- package/web-dist/assets/McpView-DPcihjuB.js +1 -0
- package/web-dist/assets/SchedulesView-B2o3vMm-.js +6 -0
- package/web-dist/assets/SettingsView-rtMUmH43.js +1 -0
- package/web-dist/assets/SkillsView-D_NHLk7C.js +15 -0
- package/web-dist/assets/SquadDetailView-BKXLWvwn.js +26 -0
- package/web-dist/assets/SquadHealthView-CVJiAgVW.js +11 -0
- package/web-dist/assets/SquadsView-fammrB7r.js +6 -0
- package/web-dist/assets/UsageView-Cy5Mbprb.js +16 -0
- package/web-dist/assets/WikiView-B5TOMnOg.js +36 -0
- package/web-dist/assets/arrow-left-CGMB1w_A.js +6 -0
- package/web-dist/assets/git-branch-C_Hu39uh.js +6 -0
- package/web-dist/assets/index-CQ_szaoT.css +1 -0
- package/web-dist/assets/index-CiZnRvN4.js +253 -0
- package/web-dist/assets/{plus-Cvp1w2CO.js → plus-DIBAaEMT.js} +1 -1
- package/web-dist/assets/{x-O3fBd1Cr.js → save-Chqlu7QA.js} +2 -7
- package/web-dist/assets/search-Cl8HcIsG.js +6 -0
- package/web-dist/assets/squad-colors-B8B_Y-lz.js +1 -0
- package/web-dist/assets/{trash-2-Cr3vrmL5.js → trash-2-CQSzbVIr.js} +1 -1
- package/web-dist/assets/triangle-alert-C1OjMvP5.js +6 -0
- package/web-dist/assets/x-DThJHYFm.js +6 -0
- package/web-dist/favicon.svg +9 -3
- package/web-dist/index.html +2 -2
- package/web-dist/logo.svg +10 -0
- package/web-dist/assets/ChatView-mZaaw3pd.js +0 -11
- package/web-dist/assets/FeedView-BHacQwXQ.js +0 -6
- package/web-dist/assets/LoginView-B6aSD9II.js +0 -1
- package/web-dist/assets/MarkdownContent.vue_vue_type_script_setup_true_lang-CEo_ckIb.js +0 -56
- package/web-dist/assets/McpView-BAVRUHIE.js +0 -1
- package/web-dist/assets/SchedulesView-dOd1SQiP.js +0 -1
- package/web-dist/assets/SettingsView-CCDeEsVg.js +0 -1
- package/web-dist/assets/SkillsView-gCfQ35FQ.js +0 -1
- package/web-dist/assets/SquadDetailView-CQhFfZTc.js +0 -21
- package/web-dist/assets/SquadsView-CZFxtOao.js +0 -6
- package/web-dist/assets/WikiView-B0cuUFfm.js +0 -26
- package/web-dist/assets/api-DdW5uOZf.js +0 -1
- package/web-dist/assets/index-BQdXxKfc.js +0 -138
- package/web-dist/assets/index-BbSJ0cfF.css +0 -1
package/dist/api/server.js
CHANGED
|
@@ -6,14 +6,21 @@ import { loadConfig, saveConfig } from "../config.js";
|
|
|
6
6
|
import { createAuthMiddleware } from "./auth.js";
|
|
7
7
|
import { sendToOrchestrator } from "../copilot/orchestrator.js";
|
|
8
8
|
import { listSquads, getSquad, getAgentsForSquad } from "../store/squads.js";
|
|
9
|
-
import { getTasksForSquad } from "../store/tasks.js";
|
|
10
|
-
import { getInstancesForSquad } from "../store/instances.js";
|
|
9
|
+
import { getTasksForSquad, getSquadTaskMetrics } from "../store/tasks.js";
|
|
10
|
+
import { getInstancesForSquad, destroyInstance } from "../store/instances.js";
|
|
11
|
+
import { getAgentEvents } from "../store/agent-events.js";
|
|
12
|
+
import { getAuditLog, countAuditLog } from "../store/audit-log.js";
|
|
11
13
|
import { getFeedItems, markFeedItemRead, deleteFeedItem, getUnreadCount, } from "../store/feed.js";
|
|
12
14
|
import { listSchedules, createSchedule, deleteSchedule, toggleSchedule } from "../store/schedules.js";
|
|
15
|
+
import { triggerSchedule } from "../copilot/trigger-schedule.js";
|
|
13
16
|
import { listServers, toggleMcpServer, addMcpServer, removeMcpServer } from "../mcp/index.js";
|
|
14
|
-
import { listSkills, addSkill, removeSkill, getSkillContent, updateSkillContent } from "../copilot/skills.js";
|
|
15
|
-
import { readPage, writePage, deletePage, listPages } from "../wiki/fs.js";
|
|
17
|
+
import { listSkills, addSkill, createSkill, removeSkill, getSkillContent, updateSkillContent, discoverSkills, installFromSource, fetchRemoteSkillPreview } from "../copilot/skills.js";
|
|
18
|
+
import { readPage, writePage, deletePage, listPages, listTemplates, readTemplate, writeTemplate, deleteTemplate } from "../wiki/fs.js";
|
|
16
19
|
import { searchPages } from "../wiki/search.js";
|
|
20
|
+
import { getBacklinks } from "../wiki/backlinks.js";
|
|
21
|
+
import { saveMessage, getConversation, listConversations, searchConversations, deleteConversation, } from "../store/conversations.js";
|
|
22
|
+
import { getTokenUsageSummary, getTokenUsageBySquad, getTokenUsageByAgent, getDailyTokenUsage, } from "../store/token-usage.js";
|
|
23
|
+
import { DEFAULT_MODEL_PRICING } from "../copilot/token-tracker.js";
|
|
17
24
|
import { randomUUID } from "node:crypto";
|
|
18
25
|
const __filename = fileURLToPath(import.meta.url);
|
|
19
26
|
const __dirname = dirname(__filename);
|
|
@@ -57,22 +64,94 @@ export async function startApiServer(config) {
|
|
|
57
64
|
});
|
|
58
65
|
// --- Chat ---
|
|
59
66
|
app.post("/api/message", async (req, res) => {
|
|
60
|
-
const { prompt } = req.body;
|
|
67
|
+
const { prompt, conversationId: clientConvId } = req.body;
|
|
61
68
|
if (!prompt || typeof prompt !== "string") {
|
|
62
69
|
res.status(400).json({ error: "prompt is required" });
|
|
63
70
|
return;
|
|
64
71
|
}
|
|
72
|
+
const conversationId = (typeof clientConvId === "string" && clientConvId) ? clientConvId : randomUUID();
|
|
73
|
+
// Persist the user message
|
|
74
|
+
saveMessage(conversationId, "user", prompt, "web");
|
|
65
75
|
// Stream response via SSE, send final to HTTP response
|
|
66
76
|
await sendToOrchestrator(prompt, "web", (content, done) => {
|
|
67
77
|
broadcast("message_delta", { content, done });
|
|
68
78
|
if (done) {
|
|
69
|
-
|
|
79
|
+
// Persist the assistant response
|
|
80
|
+
saveMessage(conversationId, "assistant", content, "web");
|
|
81
|
+
res.json({ content, conversationId });
|
|
70
82
|
}
|
|
71
83
|
});
|
|
72
84
|
});
|
|
85
|
+
// --- History ---
|
|
86
|
+
app.get("/api/history", (req, res) => {
|
|
87
|
+
const q = req.query.q;
|
|
88
|
+
const from = req.query.from;
|
|
89
|
+
const to = req.query.to;
|
|
90
|
+
const limit = parseInt(req.query.limit) || 50;
|
|
91
|
+
const offset = parseInt(req.query.offset) || 0;
|
|
92
|
+
if (q) {
|
|
93
|
+
res.json(searchConversations(q, { limit, offset, from, to }));
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
res.json(listConversations({ limit, offset, from, to }));
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
app.get("/api/history/:id", (req, res) => {
|
|
100
|
+
const messages = getConversation(req.params.id);
|
|
101
|
+
if (messages.length === 0) {
|
|
102
|
+
res.status(404).json({ error: "Conversation not found" });
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
res.json(messages);
|
|
106
|
+
});
|
|
107
|
+
app.delete("/api/history/:id", (req, res) => {
|
|
108
|
+
deleteConversation(req.params.id);
|
|
109
|
+
res.json({ ok: true });
|
|
110
|
+
});
|
|
73
111
|
// --- Squads ---
|
|
74
112
|
app.get("/api/squads", (_req, res) => {
|
|
75
|
-
|
|
113
|
+
const data = listSquads();
|
|
114
|
+
const instanceCounts = {};
|
|
115
|
+
for (const squad of data.squads) {
|
|
116
|
+
instanceCounts[squad.id] = getInstancesForSquad(squad.id).length;
|
|
117
|
+
}
|
|
118
|
+
res.json({ ...data, instanceCounts });
|
|
119
|
+
});
|
|
120
|
+
// --- Squad Health Dashboard ---
|
|
121
|
+
app.get("/api/squads/health", (_req, res) => {
|
|
122
|
+
const { squads, agents } = listSquads();
|
|
123
|
+
const health = squads.map((squad) => {
|
|
124
|
+
const squadAgents = agents.filter((a) => a.squad_id === squad.id);
|
|
125
|
+
const instances = getInstancesForSquad(squad.id);
|
|
126
|
+
const metrics = getSquadTaskMetrics(squad.id);
|
|
127
|
+
return {
|
|
128
|
+
id: squad.id,
|
|
129
|
+
name: squad.name,
|
|
130
|
+
universe: squad.universe,
|
|
131
|
+
agentCount: squadAgents.length,
|
|
132
|
+
activeInstanceCount: instances.length,
|
|
133
|
+
activeInstances: instances.map((inst) => ({
|
|
134
|
+
id: inst.id,
|
|
135
|
+
branch: inst.branch,
|
|
136
|
+
lastActivity: inst.last_activity,
|
|
137
|
+
})),
|
|
138
|
+
tasksTotal: metrics.tasksTotal,
|
|
139
|
+
tasksCompleted: metrics.tasksCompleted,
|
|
140
|
+
tasksCompletedRecent: metrics.tasksCompletedRecent,
|
|
141
|
+
tasksPending: metrics.tasksPending,
|
|
142
|
+
tasksInProgress: metrics.tasksInProgress,
|
|
143
|
+
tasksFailed: metrics.tasksFailed,
|
|
144
|
+
avgCycleTimeMinutes: metrics.avgCycleTimeMinutes,
|
|
145
|
+
isStalled: metrics.isStalled,
|
|
146
|
+
recentTasks: metrics.recentTasks.map((t) => ({
|
|
147
|
+
id: t.id,
|
|
148
|
+
description: t.description,
|
|
149
|
+
status: t.status,
|
|
150
|
+
updatedAt: t.updated_at,
|
|
151
|
+
})),
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
res.json({ health });
|
|
76
155
|
});
|
|
77
156
|
app.get("/api/squads/:id", (req, res) => {
|
|
78
157
|
const squad = getSquad(req.params.id);
|
|
@@ -85,6 +164,50 @@ export async function startApiServer(config) {
|
|
|
85
164
|
const instances = getInstancesForSquad(req.params.id);
|
|
86
165
|
res.json({ squad, agents, tasks, instances });
|
|
87
166
|
});
|
|
167
|
+
app.delete("/api/instances/:id", async (req, res) => {
|
|
168
|
+
try {
|
|
169
|
+
await destroyInstance(req.params.id);
|
|
170
|
+
res.json({ ok: true });
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
const msg = err?.message ?? "Unknown error";
|
|
174
|
+
const status = msg.toLowerCase().includes("not found") ? 404 : 500;
|
|
175
|
+
res.status(status).json({ error: msg });
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
// --- Task Events ---
|
|
179
|
+
app.get("/api/tasks/:taskId/events", (req, res) => {
|
|
180
|
+
const events = getAgentEvents(req.params.taskId);
|
|
181
|
+
res.json(events);
|
|
182
|
+
});
|
|
183
|
+
// --- Stop Task ---
|
|
184
|
+
app.post("/api/tasks/:taskId/stop", async (req, res) => {
|
|
185
|
+
try {
|
|
186
|
+
const { stopTask } = await import("../copilot/agents.js");
|
|
187
|
+
await stopTask(req.params.taskId);
|
|
188
|
+
res.json({ ok: true });
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
const msg = err?.message ?? "Unknown error";
|
|
192
|
+
const isNotRunning = msg.toLowerCase().includes("not currently running") || msg.toLowerCase().includes("already completed");
|
|
193
|
+
res.status(isNotRunning ? 404 : 500).json({ error: msg });
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
// --- Audit Log ---
|
|
197
|
+
app.get("/api/audit-log", (req, res) => {
|
|
198
|
+
const squad_id = req.query.squad_id;
|
|
199
|
+
const agent_id = req.query.agent_id;
|
|
200
|
+
const action_type = req.query.action_type;
|
|
201
|
+
const from = req.query.from;
|
|
202
|
+
const to = req.query.to;
|
|
203
|
+
const limit = parseInt(req.query.limit) || 50;
|
|
204
|
+
const offset = parseInt(req.query.offset) || 0;
|
|
205
|
+
const filters = { squad_id, agent_id, action_type, from, to, limit, offset };
|
|
206
|
+
res.json({
|
|
207
|
+
entries: getAuditLog(filters),
|
|
208
|
+
total: countAuditLog(filters),
|
|
209
|
+
});
|
|
210
|
+
});
|
|
88
211
|
// --- Feed ---
|
|
89
212
|
app.get("/api/feed", (req, res) => {
|
|
90
213
|
const unreadOnly = req.query.unread === "true";
|
|
@@ -129,14 +252,62 @@ export async function startApiServer(config) {
|
|
|
129
252
|
const skills = await listSkills();
|
|
130
253
|
res.json(skills);
|
|
131
254
|
});
|
|
255
|
+
app.get("/api/skills/discover", async (req, res) => {
|
|
256
|
+
const source = req.query.source;
|
|
257
|
+
if (source !== "awesome-copilot" && source !== "skillssh") {
|
|
258
|
+
res.status(400).json({ error: "source must be 'awesome-copilot' or 'skillssh'" });
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const q = req.query.q;
|
|
262
|
+
try {
|
|
263
|
+
const skills = await discoverSkills(source, q);
|
|
264
|
+
res.json(skills);
|
|
265
|
+
}
|
|
266
|
+
catch (err) {
|
|
267
|
+
res.status(502).json({ error: err.message });
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
app.get("/api/skills/preview", async (req, res) => {
|
|
271
|
+
const source = req.query.source;
|
|
272
|
+
const slug = req.query.slug;
|
|
273
|
+
if (source !== "awesome-copilot" && source !== "skillssh") {
|
|
274
|
+
res.status(400).json({ error: "source must be 'awesome-copilot' or 'skillssh'" });
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (!slug) {
|
|
278
|
+
res.status(400).json({ error: "slug is required" });
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
try {
|
|
282
|
+
const content = await fetchRemoteSkillPreview(source, slug);
|
|
283
|
+
res.json({ content });
|
|
284
|
+
}
|
|
285
|
+
catch (err) {
|
|
286
|
+
res.status(502).json({ error: err.message });
|
|
287
|
+
}
|
|
288
|
+
});
|
|
132
289
|
app.post("/api/skills", async (req, res) => {
|
|
133
290
|
try {
|
|
134
|
-
const { url } = req.body;
|
|
135
|
-
if (
|
|
136
|
-
|
|
291
|
+
const { url, source, slug, content } = req.body;
|
|
292
|
+
if (source && slug) {
|
|
293
|
+
if (source !== "awesome-copilot" && source !== "skillssh") {
|
|
294
|
+
res.status(400).json({ error: "source must be 'awesome-copilot' or 'skillssh'" });
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
await installFromSource(source, slug);
|
|
298
|
+
}
|
|
299
|
+
else if (url && typeof url === "string") {
|
|
300
|
+
// Git-clone method
|
|
301
|
+
await addSkill(url);
|
|
302
|
+
}
|
|
303
|
+
else if (slug && typeof slug === "string" && content && typeof content === "string") {
|
|
304
|
+
// Direct-creation method
|
|
305
|
+
await createSkill(slug, content);
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
res.status(400).json({ error: "Provide 'url' (git clone), 'source' + 'slug' (community install), or 'slug' + 'content' (direct create)" });
|
|
137
309
|
return;
|
|
138
310
|
}
|
|
139
|
-
await addSkill(url);
|
|
140
311
|
res.status(201).json({ ok: true });
|
|
141
312
|
}
|
|
142
313
|
catch (err) {
|
|
@@ -214,13 +385,66 @@ export async function startApiServer(config) {
|
|
|
214
385
|
const results = await searchPages(query);
|
|
215
386
|
res.json(results);
|
|
216
387
|
});
|
|
388
|
+
app.get("/api/wiki/backlinks/*path", async (req, res) => {
|
|
389
|
+
const raw = req.params.path;
|
|
390
|
+
const pagePath = Array.isArray(raw) ? raw.join("/") : raw;
|
|
391
|
+
const backlinks = await getBacklinks(pagePath);
|
|
392
|
+
res.json(backlinks);
|
|
393
|
+
});
|
|
394
|
+
// --- Wiki Templates ---
|
|
395
|
+
app.get("/api/wiki/templates/squad", async (_req, res) => {
|
|
396
|
+
const files = await listTemplates();
|
|
397
|
+
res.json(files);
|
|
398
|
+
});
|
|
399
|
+
app.get("/api/wiki/template/squad/*path", async (req, res) => {
|
|
400
|
+
try {
|
|
401
|
+
const raw = req.params.path;
|
|
402
|
+
const templatePath = Array.isArray(raw) ? raw.join("/") : raw;
|
|
403
|
+
const content = await readTemplate(templatePath);
|
|
404
|
+
res.json({ path: templatePath, content });
|
|
405
|
+
}
|
|
406
|
+
catch (err) {
|
|
407
|
+
res.status(404).json({ error: err.message });
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
app.put("/api/wiki/template/squad/*path", async (req, res) => {
|
|
411
|
+
const raw = req.params.path;
|
|
412
|
+
const templatePath = Array.isArray(raw) ? raw.join("/") : raw;
|
|
413
|
+
const { content } = req.body;
|
|
414
|
+
await writeTemplate(templatePath, content);
|
|
415
|
+
res.json({ ok: true });
|
|
416
|
+
});
|
|
417
|
+
app.delete("/api/wiki/template/squad/*path", async (req, res) => {
|
|
418
|
+
try {
|
|
419
|
+
const raw = req.params.path;
|
|
420
|
+
const templatePath = Array.isArray(raw) ? raw.join("/") : raw;
|
|
421
|
+
await deleteTemplate(templatePath);
|
|
422
|
+
res.json({ ok: true });
|
|
423
|
+
}
|
|
424
|
+
catch (err) {
|
|
425
|
+
res.status(404).json({ error: err.message });
|
|
426
|
+
}
|
|
427
|
+
});
|
|
217
428
|
// --- Schedules ---
|
|
218
429
|
app.get("/api/schedules", (_req, res) => {
|
|
219
430
|
const type = undefined; // return all
|
|
220
431
|
res.json(listSchedules(type));
|
|
221
432
|
});
|
|
222
433
|
app.post("/api/schedules", (req, res) => {
|
|
223
|
-
const
|
|
434
|
+
const { type, cron, squad_id, agenda, prompt } = req.body ?? {};
|
|
435
|
+
if (type !== "squad" && type !== "io") {
|
|
436
|
+
res.status(400).json({ error: "type must be 'squad' or 'io'" });
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
if (!cron || typeof cron !== "string") {
|
|
440
|
+
res.status(400).json({ error: "cron is required" });
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
if (!squad_id || typeof squad_id !== "string" || !squad_id.trim()) {
|
|
444
|
+
res.status(400).json({ error: "squad_id is required" });
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
const schedule = createSchedule({ type, cron, squad_id, agenda, prompt });
|
|
224
448
|
res.json(schedule);
|
|
225
449
|
});
|
|
226
450
|
app.put("/api/schedules/:id", (req, res) => {
|
|
@@ -230,6 +454,14 @@ export async function startApiServer(config) {
|
|
|
230
454
|
}
|
|
231
455
|
res.json({ ok: true });
|
|
232
456
|
});
|
|
457
|
+
app.post("/api/schedules/:id/trigger", (req, res) => {
|
|
458
|
+
const schedule = triggerSchedule(req.params.id);
|
|
459
|
+
if (!schedule) {
|
|
460
|
+
res.status(404).json({ error: "Schedule not found" });
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
res.json({ ok: true, schedule });
|
|
464
|
+
});
|
|
233
465
|
app.delete("/api/schedules/:id", (req, res) => {
|
|
234
466
|
deleteSchedule(req.params.id);
|
|
235
467
|
res.json({ ok: true });
|
|
@@ -284,6 +516,51 @@ export async function startApiServer(config) {
|
|
|
284
516
|
saveConfig(updates);
|
|
285
517
|
res.json({ ok: true });
|
|
286
518
|
});
|
|
519
|
+
// --- Token Usage ---
|
|
520
|
+
app.get("/api/token-usage/summary", (req, res) => {
|
|
521
|
+
const since = req.query.since;
|
|
522
|
+
res.json(getTokenUsageSummary({ since }));
|
|
523
|
+
});
|
|
524
|
+
app.get("/api/token-usage/by-squad", (req, res) => {
|
|
525
|
+
const since = req.query.since;
|
|
526
|
+
res.json(getTokenUsageBySquad({ since }));
|
|
527
|
+
});
|
|
528
|
+
app.get("/api/token-usage/by-agent", (req, res) => {
|
|
529
|
+
const since = req.query.since;
|
|
530
|
+
const squadId = req.query.squad_id;
|
|
531
|
+
res.json(getTokenUsageByAgent({ since, squadId }));
|
|
532
|
+
});
|
|
533
|
+
app.get("/api/token-usage/daily", (req, res) => {
|
|
534
|
+
const days = parseInt(req.query.days) || 30;
|
|
535
|
+
res.json(getDailyTokenUsage(days));
|
|
536
|
+
});
|
|
537
|
+
app.get("/api/token-usage/pricing", (_req, res) => {
|
|
538
|
+
const config = loadConfig();
|
|
539
|
+
const merged = { ...DEFAULT_MODEL_PRICING, ...(config.modelPricing ?? {}) };
|
|
540
|
+
res.json(merged);
|
|
541
|
+
});
|
|
542
|
+
app.put("/api/token-usage/pricing", (req, res) => {
|
|
543
|
+
const pricing = req.body;
|
|
544
|
+
if (typeof pricing !== "object" || pricing === null) {
|
|
545
|
+
res.status(400).json({ error: "Expected object body" });
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
saveConfig({ modelPricing: pricing });
|
|
549
|
+
res.json({ ok: true });
|
|
550
|
+
});
|
|
551
|
+
app.get("/api/token-usage/alert-threshold", (_req, res) => {
|
|
552
|
+
const config = loadConfig();
|
|
553
|
+
res.json({ tokenAlertThreshold: config.tokenAlertThreshold ?? null });
|
|
554
|
+
});
|
|
555
|
+
app.put("/api/token-usage/alert-threshold", (req, res) => {
|
|
556
|
+
const { tokenAlertThreshold } = req.body;
|
|
557
|
+
if (tokenAlertThreshold !== null && typeof tokenAlertThreshold !== "number") {
|
|
558
|
+
res.status(400).json({ error: "tokenAlertThreshold must be a number or null" });
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
saveConfig({ tokenAlertThreshold: tokenAlertThreshold ?? undefined });
|
|
562
|
+
res.json({ ok: true });
|
|
563
|
+
});
|
|
287
564
|
// --- Health (unauthenticated) ---
|
|
288
565
|
app.get("/health", (_req, res) => {
|
|
289
566
|
res.json({ status: "ok", version: process.env.npm_package_version ?? "unknown" });
|
package/dist/config.js
CHANGED
|
@@ -2,6 +2,10 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { PATHS } from "./paths.js";
|
|
5
|
+
const ModelPriceSchema = z.object({
|
|
6
|
+
inputPer1M: z.number(),
|
|
7
|
+
outputPer1M: z.number(),
|
|
8
|
+
});
|
|
5
9
|
const ConfigSchema = z.object({
|
|
6
10
|
telegramBotToken: z.string().optional(),
|
|
7
11
|
authorizedUserId: z.number().optional(),
|
|
@@ -17,6 +21,8 @@ const ConfigSchema = z.object({
|
|
|
17
21
|
.default("meaningful"),
|
|
18
22
|
backgroundNotifyTelegram: z.boolean().default(true),
|
|
19
23
|
watchdogEnabled: z.boolean().default(true),
|
|
24
|
+
modelPricing: z.record(z.string(), ModelPriceSchema).optional(),
|
|
25
|
+
tokenAlertThreshold: z.number().optional(),
|
|
20
26
|
});
|
|
21
27
|
let cachedConfig;
|
|
22
28
|
export function loadConfig() {
|
package/dist/copilot/agents.js
CHANGED
|
@@ -1,15 +1,47 @@
|
|
|
1
1
|
import { approveAll } from "@github/copilot-sdk";
|
|
2
2
|
import { getClient } from "./client.js";
|
|
3
|
-
import { getLeadForSquad, getAgentsForSquad, updateAgentStatus } from "../store/squads.js";
|
|
4
|
-
import { createTask, updateTaskStatus } from "../store/tasks.js";
|
|
3
|
+
import { getLeadForSquad, getAgentsForSquad, updateAgentStatus, getSquad } from "../store/squads.js";
|
|
4
|
+
import { createTask, updateTaskStatus, getTask } from "../store/tasks.js";
|
|
5
5
|
import { touchInstanceActivity } from "../store/instances.js";
|
|
6
6
|
import { selectModel, classifyComplexity } from "./model-router.js";
|
|
7
7
|
import { postFeedItem } from "../store/feed.js";
|
|
8
|
+
import { attachTokenTracker } from "./token-tracker.js";
|
|
9
|
+
import { addAuditEntry } from "../store/audit-log.js";
|
|
10
|
+
import { addAgentEvent } from "../store/agent-events.js";
|
|
11
|
+
import { createSquadTools } from "./squad-tools.js";
|
|
12
|
+
import { loadSkillDirectories } from "./skills.js";
|
|
13
|
+
import { getMcpServersForSession } from "../mcp/registry.js";
|
|
14
|
+
// Registry of active agent sessions keyed by task ID
|
|
15
|
+
const activeSessions = new Map();
|
|
16
|
+
/**
|
|
17
|
+
* Stop a running agent by task ID. Disconnects the session and marks the task as stopped.
|
|
18
|
+
*/
|
|
19
|
+
export async function stopTask(taskId) {
|
|
20
|
+
const session = activeSessions.get(taskId);
|
|
21
|
+
if (!session) {
|
|
22
|
+
throw new Error(`Task is not currently running or has already completed`);
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
await session.disconnect();
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
activeSessions.delete(taskId);
|
|
29
|
+
}
|
|
30
|
+
updateTaskStatus(taskId, "stopped", "Stopped by user");
|
|
31
|
+
addAgentEvent(taskId, "status", "Task stopped by user", { reason: "user_requested" });
|
|
32
|
+
// Reset agent status to idle
|
|
33
|
+
const task = getTask(taskId);
|
|
34
|
+
if (task?.agent_id) {
|
|
35
|
+
updateAgentStatus(task.agent_id, "idle");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
8
38
|
export async function delegateTask(squadId, task, instanceId) {
|
|
9
39
|
const lead = getLeadForSquad(squadId);
|
|
10
40
|
if (!lead) {
|
|
11
41
|
throw new Error("Squad has no team lead. Add a lead agent first.");
|
|
12
42
|
}
|
|
43
|
+
const squad = getSquad(squadId);
|
|
44
|
+
const squadSlug = squad?.slug ?? squadId;
|
|
13
45
|
const agents = getAgentsForSquad(squadId);
|
|
14
46
|
const taskRecord = createTask(squadId, task, instanceId, lead.id);
|
|
15
47
|
// Update lead status
|
|
@@ -21,6 +53,8 @@ export async function delegateTask(squadId, task, instanceId) {
|
|
|
21
53
|
// Select model based on task complexity
|
|
22
54
|
const tier = classifyComplexity(task);
|
|
23
55
|
const model = await selectModel(tier);
|
|
56
|
+
// Audit: task delegated
|
|
57
|
+
addAuditEntry("task_delegated", `Task delegated to ${lead.character_name} (${lead.role_title})`, { task: task.slice(0, 500), model }, { squad_id: squadId, agent_id: lead.id, task_id: taskRecord.id });
|
|
24
58
|
// Create ephemeral agent session for the lead
|
|
25
59
|
const client = await getClient();
|
|
26
60
|
const agentRoster = agents
|
|
@@ -58,11 +92,18 @@ ${lead.persona ? `## Personality:\n${lead.persona}` : ""}
|
|
|
58
92
|
`;
|
|
59
93
|
let result;
|
|
60
94
|
try {
|
|
95
|
+
// Load squad-scoped tools, skills, and MCP servers
|
|
96
|
+
const squadTools = createSquadTools(squadSlug, squadId);
|
|
97
|
+
const skillDirs = await loadSkillDirectories();
|
|
98
|
+
const mcpServers = getMcpServersForSession();
|
|
61
99
|
const session = await client.createSession({
|
|
62
100
|
model,
|
|
63
101
|
streaming: true,
|
|
64
102
|
workingDirectory: process.cwd(),
|
|
65
103
|
systemMessage: { content: systemMessage },
|
|
104
|
+
tools: squadTools,
|
|
105
|
+
skillDirectories: skillDirs,
|
|
106
|
+
mcpServers,
|
|
66
107
|
onPermissionRequest: approveAll,
|
|
67
108
|
infiniteSessions: {
|
|
68
109
|
enabled: true,
|
|
@@ -70,25 +111,79 @@ ${lead.persona ? `## Personality:\n${lead.persona}` : ""}
|
|
|
70
111
|
bufferExhaustionThreshold: 0.95,
|
|
71
112
|
},
|
|
72
113
|
});
|
|
114
|
+
// Register session so it can be stopped externally
|
|
115
|
+
activeSessions.set(taskRecord.id, session);
|
|
116
|
+
const flushTokens = attachTokenTracker(session, {
|
|
117
|
+
squadId,
|
|
118
|
+
agentId: lead.id,
|
|
119
|
+
taskId: taskRecord.id,
|
|
120
|
+
});
|
|
73
121
|
try {
|
|
74
|
-
|
|
75
|
-
|
|
122
|
+
// Mark task as in progress and record start event
|
|
123
|
+
updateTaskStatus(taskRecord.id, "in_progress");
|
|
124
|
+
addAgentEvent(taskRecord.id, "status", `Task started by ${lead.character_name}`, {
|
|
125
|
+
agent: lead.character_name,
|
|
126
|
+
role: lead.role_title,
|
|
127
|
+
task,
|
|
128
|
+
});
|
|
129
|
+
// Capture streaming message deltas and broadcast via SSE
|
|
130
|
+
let accumulatedMessage = "";
|
|
131
|
+
const { broadcast } = await import("../api/server.js");
|
|
132
|
+
const unsubscribeDelta = session.on("assistant.message_delta", (event) => {
|
|
133
|
+
const delta = event.data?.deltaContent ?? "";
|
|
134
|
+
if (delta) {
|
|
135
|
+
accumulatedMessage += delta;
|
|
136
|
+
broadcast("agent_event", {
|
|
137
|
+
taskId: taskRecord.id,
|
|
138
|
+
type: "message_delta",
|
|
139
|
+
summary: accumulatedMessage,
|
|
140
|
+
payload: { delta, accumulated: accumulatedMessage },
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
try {
|
|
145
|
+
const response = await session.sendAndWait({ prompt: `Task delegated to you:\n\n${task}` }, 600_000);
|
|
146
|
+
result = response?.data?.content ?? "Task completed (no response content).";
|
|
147
|
+
// Record the final message event if we have meaningful content
|
|
148
|
+
if (accumulatedMessage.trim()) {
|
|
149
|
+
addAgentEvent(taskRecord.id, "message", accumulatedMessage, {
|
|
150
|
+
agent: lead.character_name,
|
|
151
|
+
content: accumulatedMessage,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
unsubscribeDelta();
|
|
157
|
+
}
|
|
76
158
|
}
|
|
77
159
|
finally {
|
|
160
|
+
activeSessions.delete(taskRecord.id);
|
|
161
|
+
flushTokens();
|
|
78
162
|
await session.disconnect();
|
|
79
163
|
}
|
|
80
164
|
}
|
|
81
165
|
catch (err) {
|
|
82
166
|
const errMsg = err instanceof Error ? err.message : "Unknown error";
|
|
167
|
+
addAgentEvent(taskRecord.id, "status", `Task failed: ${errMsg}`, { error: errMsg });
|
|
83
168
|
updateTaskStatus(taskRecord.id, "failed", errMsg);
|
|
84
169
|
updateAgentStatus(lead.id, "idle");
|
|
170
|
+
// Audit: task failed
|
|
171
|
+
addAuditEntry("task_failed", `Task failed: ${errMsg.slice(0, 200)}`, { error: errMsg }, { squad_id: squadId, agent_id: lead.id, task_id: taskRecord.id });
|
|
85
172
|
throw err;
|
|
86
173
|
}
|
|
87
174
|
// Update task and agent status
|
|
88
175
|
updateTaskStatus(taskRecord.id, "done", result);
|
|
89
176
|
updateAgentStatus(lead.id, "idle");
|
|
177
|
+
// Audit: task completed
|
|
178
|
+
addAuditEntry("task_completed", `Task completed by ${lead.character_name}`, { result: result.slice(0, 500) }, { squad_id: squadId, agent_id: lead.id, task_id: taskRecord.id });
|
|
179
|
+
// Record completion event
|
|
180
|
+
addAgentEvent(taskRecord.id, "status", `Task completed by ${lead.character_name}`, {
|
|
181
|
+
agent: lead.character_name,
|
|
182
|
+
result: result.slice(0, 500),
|
|
183
|
+
});
|
|
90
184
|
// Post to feed
|
|
91
|
-
|
|
185
|
+
const squadSource = `squad-${squadSlug}`;
|
|
186
|
+
postFeedItem(squadSource, `Task completed by ${lead.character_name}`, result.slice(0, 2000));
|
|
92
187
|
return result;
|
|
93
188
|
}
|
|
94
189
|
//# sourceMappingURL=agents.js.map
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { approveAll } from "@github/copilot-sdk";
|
|
2
2
|
import { getClient } from "./client.js";
|
|
3
|
-
import { getLeadForSquad, getAgentsForSquad } from "../store/squads.js";
|
|
3
|
+
import { getLeadForSquad, getAgentsForSquad, getSquad } from "../store/squads.js";
|
|
4
4
|
import { selectModel } from "./model-router.js";
|
|
5
5
|
import { postFeedItem } from "../store/feed.js";
|
|
6
|
+
import { attachTokenTracker } from "./token-tracker.js";
|
|
6
7
|
function buildFacilitatorPrompt(lead, agents, task) {
|
|
7
8
|
const roster = agents
|
|
8
9
|
.filter((a) => !a.is_lead)
|
|
@@ -96,6 +97,7 @@ export async function planningMeeting(squadId, task) {
|
|
|
96
97
|
systemMessage: { content: buildSpecialistPrompt(agent, task) },
|
|
97
98
|
onPermissionRequest: approveAll,
|
|
98
99
|
});
|
|
100
|
+
const flushTokens = attachTokenTracker(session, { squadId, agentId: agent.id });
|
|
99
101
|
try {
|
|
100
102
|
const response = await session.sendAndWait({ prompt: "Please provide your planning input for this task." }, 60_000);
|
|
101
103
|
return {
|
|
@@ -105,6 +107,7 @@ export async function planningMeeting(squadId, task) {
|
|
|
105
107
|
};
|
|
106
108
|
}
|
|
107
109
|
finally {
|
|
110
|
+
flushTokens();
|
|
108
111
|
await session.disconnect();
|
|
109
112
|
}
|
|
110
113
|
}));
|
|
@@ -124,6 +127,10 @@ export async function planningMeeting(squadId, task) {
|
|
|
124
127
|
systemMessage: { content: buildFacilitatorPrompt(lead, agents, task) },
|
|
125
128
|
onPermissionRequest: approveAll,
|
|
126
129
|
});
|
|
130
|
+
const flushFacilitatorTokens = attachTokenTracker(facilitatorSession, {
|
|
131
|
+
squadId,
|
|
132
|
+
agentId: lead.id,
|
|
133
|
+
});
|
|
127
134
|
let plan;
|
|
128
135
|
try {
|
|
129
136
|
const prompt = `Here is the input gathered from your team:\n\n${inputsSummary}\n\nNow synthesize this into a clear, structured action plan.`;
|
|
@@ -131,6 +138,7 @@ export async function planningMeeting(squadId, task) {
|
|
|
131
138
|
plan = response?.data?.content ?? "Planning meeting completed but no plan was produced.";
|
|
132
139
|
}
|
|
133
140
|
finally {
|
|
141
|
+
flushFacilitatorTokens();
|
|
134
142
|
await facilitatorSession.disconnect();
|
|
135
143
|
}
|
|
136
144
|
return {
|
|
@@ -143,7 +151,9 @@ export async function squadMeeting(squadId, task, executeAfter) {
|
|
|
143
151
|
const summary = `## Planning Meeting Complete\n\n**Participants:** ${result.participants.join(", ")}\n\n${result.plan}`;
|
|
144
152
|
if (!executeAfter) {
|
|
145
153
|
// Post to feed and wait for user to trigger execution
|
|
146
|
-
|
|
154
|
+
const squad = getSquad(squadId);
|
|
155
|
+
const squadSource = squad ? `squad-${squad.slug}` : `squad-${squadId}`;
|
|
156
|
+
postFeedItem(squadSource, "Planning meeting complete — awaiting approval", summary);
|
|
147
157
|
return summary;
|
|
148
158
|
}
|
|
149
159
|
// Execute: delegate with the plan as additional context
|
|
@@ -15,14 +15,22 @@ function checkIoSchedules() {
|
|
|
15
15
|
continue;
|
|
16
16
|
if (!isDue(schedule.cron, schedule.last_run, now))
|
|
17
17
|
continue;
|
|
18
|
+
if (!schedule.squad_id) {
|
|
19
|
+
console.warn(`[io-scheduler] Schedule ${schedule.id} skipped: missing squad_id.`);
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
18
22
|
updateScheduleLastRun(schedule.id);
|
|
19
|
-
sendToOrchestrator(schedule
|
|
23
|
+
sendToOrchestrator(buildSquadScopedPrompt(schedule), "io-scheduler", (_text, done) => {
|
|
20
24
|
if (done) {
|
|
21
25
|
console.log(`[io-scheduler] Schedule ${schedule.id} completed.`);
|
|
22
26
|
}
|
|
23
27
|
});
|
|
24
28
|
}
|
|
25
29
|
}
|
|
30
|
+
export function buildSquadScopedPrompt(schedule) {
|
|
31
|
+
const squadId = schedule.squad_id ?? "unknown";
|
|
32
|
+
return `[Squad Schedule] Run for squad ${squadId}. Prompt: ${schedule.prompt}`;
|
|
33
|
+
}
|
|
26
34
|
function isDue(cron, lastRun, now) {
|
|
27
35
|
const parts = cron.split(" ");
|
|
28
36
|
if (parts.length !== 5)
|
|
@@ -4,7 +4,9 @@ import { loadConfig } from "../config.js";
|
|
|
4
4
|
import { buildSystemMessage } from "./system-message.js";
|
|
5
5
|
import { createTools } from "./tools.js";
|
|
6
6
|
import { loadSkillDirectories } from "./skills.js";
|
|
7
|
+
import { getMcpServersForSession } from "../mcp/registry.js";
|
|
7
8
|
import { resetClient } from "./client.js";
|
|
9
|
+
import { addAuditEntry } from "../store/audit-log.js";
|
|
8
10
|
let orchestratorSession;
|
|
9
11
|
let sessionCreatePromise;
|
|
10
12
|
let healthCheckInterval;
|
|
@@ -44,6 +46,7 @@ async function createOrResumeSession(client, opts) {
|
|
|
44
46
|
systemMessage: { content: systemMessage },
|
|
45
47
|
tools,
|
|
46
48
|
skillDirectories: skillDirs,
|
|
49
|
+
mcpServers: getMcpServersForSession(),
|
|
47
50
|
onPermissionRequest: approveAll,
|
|
48
51
|
infiniteSessions: {
|
|
49
52
|
enabled: true,
|
|
@@ -80,6 +83,7 @@ function startHealthCheck(client, opts) {
|
|
|
80
83
|
healthCheckInterval.unref();
|
|
81
84
|
}
|
|
82
85
|
export async function sendToOrchestrator(prompt, source, callback) {
|
|
86
|
+
addAuditEntry("message_received", `Message from ${source}: ${prompt.slice(0, 200)}`, { source, prompt: prompt.slice(0, 1000) });
|
|
83
87
|
messageQueue.push({ prompt, source, callback });
|
|
84
88
|
if (!processing)
|
|
85
89
|
processQueue();
|