heyio 0.2.0 → 0.3.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 +16 -1
- package/dist/copilot/agents.js +304 -10
- package/dist/copilot/cron.js +136 -0
- package/dist/copilot/event-summary.js +286 -0
- package/dist/copilot/orchestrator.js +43 -2
- package/dist/copilot/scheduler.js +155 -0
- package/dist/copilot/system-message.js +52 -15
- package/dist/copilot/tools.js +295 -5
- package/dist/daemon.js +4 -0
- package/dist/store/db.js +25 -0
- package/dist/store/schedules.js +73 -0
- package/dist/store/squads.js +18 -0
- package/dist/store/tasks.js +14 -0
- package/dist/tui/index.js +62 -2
- package/package.json +1 -1
- package/web-dist/assets/index-BWGQix5_.css +1 -0
- package/web-dist/assets/index-BksyB2za.js +74 -0
- package/web-dist/index.html +2 -2
- package/web-dist/assets/index-B6FXWKsy.js +0 -74
- package/web-dist/assets/index-CXTrW8OO.css +0 -1
package/dist/copilot/tools.js
CHANGED
|
@@ -5,6 +5,43 @@ import { readFileSync, writeFileSync, readdirSync, statSync, existsSync, mkdirSy
|
|
|
5
5
|
import { join, dirname, resolve } from "path";
|
|
6
6
|
import { homedir } from "os";
|
|
7
7
|
import { UNIVERSES } from "./universes.js";
|
|
8
|
+
import { validateCron, nextRun } from "./cron.js";
|
|
9
|
+
import { createSchedule, deleteSchedule, getSchedule, listSchedules, setScheduleEnabled, } from "../store/schedules.js";
|
|
10
|
+
import { runScheduleNow } from "./scheduler.js";
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// QA / test coverage heuristics
|
|
13
|
+
//
|
|
14
|
+
// Every squad must have:
|
|
15
|
+
// 1. At least one agent designated as QA (is_qa === 1) - see squad_set_qa.
|
|
16
|
+
// 2. At least one agent whose role title implies a testing/quality focus.
|
|
17
|
+
//
|
|
18
|
+
// These are surfaced as warnings on squad_status, squad_agents, and
|
|
19
|
+
// squad_delegate so users can fix coverage gaps before promoting work.
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
const TEST_ROLE_KEYWORDS = ["test", "qa", "quality", "tester", "sdet", "qe"];
|
|
22
|
+
export function roleLooksLikeTesting(roleTitle) {
|
|
23
|
+
if (!roleTitle)
|
|
24
|
+
return false;
|
|
25
|
+
const lower = roleTitle.toLowerCase();
|
|
26
|
+
return TEST_ROLE_KEYWORDS.some((kw) => {
|
|
27
|
+
const re = new RegExp(`(^|[^a-z])${kw}([^a-z]|$)`);
|
|
28
|
+
return re.test(lower);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
export function assessSquadCoverage(agents) {
|
|
32
|
+
const hasQa = agents.some((a) => a.is_qa === 1);
|
|
33
|
+
const hasTestRole = agents.some((a) => roleLooksLikeTesting(a.role_title));
|
|
34
|
+
const missing = [];
|
|
35
|
+
if (!hasQa)
|
|
36
|
+
missing.push("QA reviewer (use squad_set_qa)");
|
|
37
|
+
if (!hasTestRole) {
|
|
38
|
+
missing.push("test/quality engineer (add an agent whose role_title contains 'test', 'qa', or 'quality')");
|
|
39
|
+
}
|
|
40
|
+
const warning = missing.length > 0
|
|
41
|
+
? `⚠️ Squad coverage gap: missing ${missing.join(" and ")}.`
|
|
42
|
+
: null;
|
|
43
|
+
return { hasQa, hasTestRole, missing, warning };
|
|
44
|
+
}
|
|
8
45
|
// Ensure child processes have HOME set (systemd services often don't)
|
|
9
46
|
function shellEnv() {
|
|
10
47
|
const env = { ...process.env };
|
|
@@ -111,10 +148,16 @@ export function createTools(deps) {
|
|
|
111
148
|
? UNIVERSES.find((u) => u.id === s.universe)?.name ?? s.universe
|
|
112
149
|
: "none";
|
|
113
150
|
const agents = deps.listSquadAgents(s.slug);
|
|
151
|
+
const lead = deps.getSquadLead(s.slug);
|
|
152
|
+
const leadLine = lead
|
|
153
|
+
? `\n ⭐ Team Lead: ${lead.character_name} (${lead.role_title})`
|
|
154
|
+
: "";
|
|
114
155
|
const agentList = agents.length > 0
|
|
115
156
|
? "\n Agents: " + agents.map((a) => `${a.character_name} (${a.role_title})`).join(", ")
|
|
116
157
|
: "\n Agents: none — use squad_add_agent to build the team";
|
|
117
|
-
|
|
158
|
+
const coverage = assessSquadCoverage(agents);
|
|
159
|
+
const coverageLine = coverage.warning ? `\n ${coverage.warning}` : "";
|
|
160
|
+
return `- **${s.name}** (\`${s.slug}\`) — ${s.status} — 🎬 ${universeName}${leadLine}${agentList}${coverageLine}\n 📁 ${s.projectPath}`;
|
|
118
161
|
})
|
|
119
162
|
.join("\n");
|
|
120
163
|
},
|
|
@@ -152,12 +195,17 @@ export function createTools(deps) {
|
|
|
152
195
|
}),
|
|
153
196
|
handler: async ({ slug, task, agent }) => {
|
|
154
197
|
console.error(`[io] squad_delegate called: ${slug}${agent ? ` → ${agent}` : ""} — ${task.slice(0, 100)}…`);
|
|
198
|
+
const roster = deps.listSquadAgents(slug);
|
|
199
|
+
const coverage = assessSquadCoverage(roster);
|
|
155
200
|
try {
|
|
156
201
|
const taskId = await deps.delegateToAgent(slug, task, (id, result) => {
|
|
157
202
|
console.error(`[io] Agent task ${id} completed for squad ${slug}`);
|
|
158
203
|
}, agent);
|
|
159
204
|
const agentLabel = agent ? `agent "${agent}" in squad "${slug}"` : `squad "${slug}"`;
|
|
160
|
-
|
|
205
|
+
const warningPrefix = coverage.warning
|
|
206
|
+
? `${coverage.warning} Reviews from this squad will not be vetoed by a designated QA agent until this is fixed.\n\n`
|
|
207
|
+
: "";
|
|
208
|
+
return `${warningPrefix}Task delegated to ${agentLabel}. Task ID: ${taskId}\n\nThe agent is working on this in the background. Use squad_task_status to check progress.`;
|
|
161
209
|
}
|
|
162
210
|
catch (err) {
|
|
163
211
|
return `Error delegating task: ${err instanceof Error ? err.message : String(err)}`;
|
|
@@ -373,8 +421,14 @@ export function createTools(deps) {
|
|
|
373
421
|
const universeName = squad.universe
|
|
374
422
|
? UNIVERSES.find((u) => u.id === squad.universe)?.name ?? squad.universe
|
|
375
423
|
: "none";
|
|
376
|
-
const lines = agents.map((a) =>
|
|
377
|
-
|
|
424
|
+
const lines = agents.map((a) => {
|
|
425
|
+
const leadBadge = a.is_lead === 1 ? " ⭐ [LEAD]" : "";
|
|
426
|
+
const qaBadge = a.is_qa === 1 ? " 🛡️ [QA]" : "";
|
|
427
|
+
return `- **${a.character_name}**${leadBadge}${qaBadge} — ${a.role_title} (${a.model_tier}) — ${a.status}${a.personality ? `\n _${a.personality}_` : ""}`;
|
|
428
|
+
});
|
|
429
|
+
const coverage = assessSquadCoverage(agents);
|
|
430
|
+
const coverageBlock = coverage.warning ? `\n\n${coverage.warning}` : "";
|
|
431
|
+
return `**${squad.name}** — 🎬 ${universeName}\n\n${lines.join("\n")}${coverageBlock}`;
|
|
378
432
|
},
|
|
379
433
|
});
|
|
380
434
|
// --- Squad remove agent ---
|
|
@@ -973,7 +1027,243 @@ export function createTools(deps) {
|
|
|
973
1027
|
}
|
|
974
1028
|
},
|
|
975
1029
|
});
|
|
976
|
-
|
|
1030
|
+
const squadSetQA = defineTool("squad_set_qa", {
|
|
1031
|
+
description: "Mark a squad agent as a QA reviewer with veto power. QA agents must approve before a PR is promoted from draft to ready.",
|
|
1032
|
+
skipPermission: true,
|
|
1033
|
+
parameters: z.object({
|
|
1034
|
+
slug: z.string().describe("Squad slug"),
|
|
1035
|
+
character_name: z.string().describe("Character name of the agent"),
|
|
1036
|
+
is_qa: z
|
|
1037
|
+
.boolean()
|
|
1038
|
+
.describe("Whether this agent is a QA reviewer (true) or not (false)"),
|
|
1039
|
+
}),
|
|
1040
|
+
handler: async ({ slug, character_name, is_qa }) => {
|
|
1041
|
+
try {
|
|
1042
|
+
const squad = deps.getSquad(slug);
|
|
1043
|
+
if (!squad)
|
|
1044
|
+
return `Squad not found: ${slug}`;
|
|
1045
|
+
const agents = deps.listSquadAgents(slug);
|
|
1046
|
+
const target = agents.find((a) => a.character_name === character_name);
|
|
1047
|
+
if (!target) {
|
|
1048
|
+
return `Agent "${character_name}" not found in squad "${slug}".`;
|
|
1049
|
+
}
|
|
1050
|
+
deps.setSquadQA(slug, character_name, is_qa);
|
|
1051
|
+
return is_qa
|
|
1052
|
+
? `🛡️ ${character_name} (${target.role_title}) is now a QA reviewer for squad "${squad.name}". They have veto power over PR promotion.`
|
|
1053
|
+
: `${character_name} is no longer a QA reviewer for squad "${squad.name}".`;
|
|
1054
|
+
}
|
|
1055
|
+
catch (err) {
|
|
1056
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
1057
|
+
}
|
|
1058
|
+
},
|
|
1059
|
+
});
|
|
1060
|
+
const squadTaskReviews = defineTool("squad_task_reviews", {
|
|
1061
|
+
description: "Get the peer reviews left on a completed task by the squad. Shows who approved or rejected and any comments.",
|
|
1062
|
+
skipPermission: true,
|
|
1063
|
+
parameters: z.object({
|
|
1064
|
+
task_id: z.string().describe("The task ID to fetch reviews for"),
|
|
1065
|
+
}),
|
|
1066
|
+
handler: async ({ task_id }) => {
|
|
1067
|
+
const reviews = deps.getTaskReviews(task_id);
|
|
1068
|
+
if (reviews.length === 0) {
|
|
1069
|
+
return `No reviews found for task ${task_id}.`;
|
|
1070
|
+
}
|
|
1071
|
+
// Look up reviewer roles (lead/qa) so we can flag where each verdict
|
|
1072
|
+
// came from. Lead and QA reviewers both have veto power.
|
|
1073
|
+
const rolesBySquad = new Map();
|
|
1074
|
+
const rolesFor = (squadSlug, character) => {
|
|
1075
|
+
let squadMap = rolesBySquad.get(squadSlug);
|
|
1076
|
+
if (!squadMap) {
|
|
1077
|
+
squadMap = new Map();
|
|
1078
|
+
for (const a of deps.listSquadAgents(squadSlug)) {
|
|
1079
|
+
squadMap.set(a.character_name, {
|
|
1080
|
+
is_lead: a.is_lead === 1,
|
|
1081
|
+
is_qa: a.is_qa === 1,
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
rolesBySquad.set(squadSlug, squadMap);
|
|
1085
|
+
}
|
|
1086
|
+
return squadMap.get(character) ?? { is_lead: false, is_qa: false };
|
|
1087
|
+
};
|
|
1088
|
+
return reviews
|
|
1089
|
+
.map((r) => {
|
|
1090
|
+
const { is_lead, is_qa } = rolesFor(r.squad_slug, r.reviewer_character);
|
|
1091
|
+
const approved = r.approved === 1;
|
|
1092
|
+
let badge = "";
|
|
1093
|
+
if (is_lead && is_qa)
|
|
1094
|
+
badge = "⭐🛡️ ";
|
|
1095
|
+
else if (is_lead)
|
|
1096
|
+
badge = "⭐ ";
|
|
1097
|
+
else if (is_qa)
|
|
1098
|
+
badge = "🛡️ ";
|
|
1099
|
+
const verdict = approved
|
|
1100
|
+
? `${badge}✅ APPROVED`
|
|
1101
|
+
: `${badge}❌ REJECTED`;
|
|
1102
|
+
const tags = [];
|
|
1103
|
+
if (is_lead)
|
|
1104
|
+
tags.push("lead");
|
|
1105
|
+
if (is_qa)
|
|
1106
|
+
tags.push("QA");
|
|
1107
|
+
const tagSuffix = tags.length ? ` _(${tags.join(", ")})_` : "";
|
|
1108
|
+
const veto = !approved && (is_lead || is_qa) ? " — **veto**" : "";
|
|
1109
|
+
const comments = r.comments ? `\n ${r.comments.replace(/\n/g, "\n ")}` : "";
|
|
1110
|
+
return `- **${r.reviewer_character}**${tagSuffix} — ${verdict}${veto}${comments}`;
|
|
1111
|
+
})
|
|
1112
|
+
.join("\n");
|
|
1113
|
+
},
|
|
1114
|
+
});
|
|
1115
|
+
const squadSetLead = defineTool("squad_set_lead", {
|
|
1116
|
+
description: "Designate an agent as the team lead for their squad. The lead receives delegated tasks (when no specific agent is targeted) and orchestrates the team by divvying subtasks to teammates.",
|
|
1117
|
+
skipPermission: true,
|
|
1118
|
+
parameters: z.object({
|
|
1119
|
+
slug: z.string().describe("Squad slug"),
|
|
1120
|
+
character_name: z
|
|
1121
|
+
.string()
|
|
1122
|
+
.describe("Character name of the agent to make team lead"),
|
|
1123
|
+
}),
|
|
1124
|
+
handler: async ({ slug, character_name }) => {
|
|
1125
|
+
try {
|
|
1126
|
+
const squad = deps.getSquad(slug);
|
|
1127
|
+
if (!squad)
|
|
1128
|
+
return `Squad not found: ${slug}`;
|
|
1129
|
+
const agents = deps.listSquadAgents(slug);
|
|
1130
|
+
const target = agents.find((a) => a.character_name === character_name);
|
|
1131
|
+
if (!target) {
|
|
1132
|
+
return `Agent "${character_name}" not found in squad "${slug}". Use squad_agents to list the roster.`;
|
|
1133
|
+
}
|
|
1134
|
+
deps.setSquadLead(slug, character_name);
|
|
1135
|
+
return `⭐ ${character_name} (${target.role_title}) is now the team lead for squad "${squad.name}".`;
|
|
1136
|
+
}
|
|
1137
|
+
catch (err) {
|
|
1138
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
1139
|
+
}
|
|
1140
|
+
},
|
|
1141
|
+
});
|
|
1142
|
+
// ---------------------------------------------------------------------------
|
|
1143
|
+
// Squad schedules — recurring stand-ups via cron-style expressions.
|
|
1144
|
+
// ---------------------------------------------------------------------------
|
|
1145
|
+
const KNOWN_AGENDA_ITEMS = ["triage", "prioritize", "ideation"];
|
|
1146
|
+
const squadScheduleCreate = defineTool("squad_schedule_create", {
|
|
1147
|
+
description: "Schedule a recurring stand-up for a squad. The squad wakes on the cron schedule, the team lead runs the agenda, and teammates are pulled in via delegate_to_teammate. Built-in agenda items: triage (process needs-triage issues), prioritize (pick highest-priority ready work and start it), ideation (brainstorm + open needs-review issues). Custom agenda items are passed through to the lead verbatim.",
|
|
1148
|
+
skipPermission: true,
|
|
1149
|
+
parameters: z.object({
|
|
1150
|
+
slug: z.string().describe("Squad slug to schedule"),
|
|
1151
|
+
name: z
|
|
1152
|
+
.string()
|
|
1153
|
+
.describe("Human-friendly name for this schedule, e.g. 'Daily 5AM stand-up'"),
|
|
1154
|
+
cron: z
|
|
1155
|
+
.string()
|
|
1156
|
+
.describe("Standard 5-field cron expression: 'minute hour dom month dow'. Examples: '0 5 * * *' = daily at 5:00, '0 9 * * 1-5' = 9AM weekdays, '*/15 * * * *' = every 15 minutes."),
|
|
1157
|
+
agenda: z
|
|
1158
|
+
.array(z.string())
|
|
1159
|
+
.min(1)
|
|
1160
|
+
.describe(`Ordered agenda. Built-in items: ${KNOWN_AGENDA_ITEMS.join(", ")}. You may include custom items; the team lead will improvise.`),
|
|
1161
|
+
notes: z
|
|
1162
|
+
.string()
|
|
1163
|
+
.optional()
|
|
1164
|
+
.describe("Optional operator notes appended to the stand-up prompt."),
|
|
1165
|
+
}),
|
|
1166
|
+
handler: async ({ slug, name, cron, agenda, notes }) => {
|
|
1167
|
+
const squad = deps.getSquad(slug);
|
|
1168
|
+
if (!squad)
|
|
1169
|
+
return `Squad not found: ${slug}`;
|
|
1170
|
+
const v = validateCron(cron);
|
|
1171
|
+
if (!v.ok)
|
|
1172
|
+
return `Invalid cron expression: ${v.error}`;
|
|
1173
|
+
const created = createSchedule({
|
|
1174
|
+
squadSlug: slug,
|
|
1175
|
+
name,
|
|
1176
|
+
cronExpr: cron,
|
|
1177
|
+
agenda,
|
|
1178
|
+
notes: notes ?? null,
|
|
1179
|
+
nextRunAt: v.next.toISOString(),
|
|
1180
|
+
});
|
|
1181
|
+
return `📅 Scheduled "${created.name}" for squad "${squad.name}" (id ${created.id}).\n- Cron: \`${cron}\`\n- Agenda: ${agenda.join(", ")}\n- Next run: ${v.next.toISOString()}`;
|
|
1182
|
+
},
|
|
1183
|
+
});
|
|
1184
|
+
const squadScheduleList = defineTool("squad_schedule_list", {
|
|
1185
|
+
description: "List squad stand-up schedules. Pass a slug to filter to one squad, or omit to list all.",
|
|
1186
|
+
skipPermission: true,
|
|
1187
|
+
parameters: z.object({
|
|
1188
|
+
slug: z.string().optional().describe("Optional squad slug to filter"),
|
|
1189
|
+
}),
|
|
1190
|
+
handler: async ({ slug }) => {
|
|
1191
|
+
const schedules = listSchedules(slug);
|
|
1192
|
+
if (schedules.length === 0) {
|
|
1193
|
+
return slug
|
|
1194
|
+
? `No schedules for squad "${slug}".`
|
|
1195
|
+
: "No squad schedules configured.";
|
|
1196
|
+
}
|
|
1197
|
+
return schedules
|
|
1198
|
+
.map((s) => {
|
|
1199
|
+
const enabled = s.enabled ? "▶️ enabled" : "⏸️ paused";
|
|
1200
|
+
const last = s.last_run_at ? `last ${s.last_run_at}` : "never run";
|
|
1201
|
+
const next = s.next_run_at ?? "—";
|
|
1202
|
+
return `- **${s.name}** (id ${s.id}) — squad \`${s.squad_slug}\` — ${enabled}\n cron: \`${s.cron_expr}\` — agenda: ${s.agenda.join(", ")}\n next: ${next} — ${last}${s.notes ? `\n notes: ${s.notes}` : ""}`;
|
|
1203
|
+
})
|
|
1204
|
+
.join("\n");
|
|
1205
|
+
},
|
|
1206
|
+
});
|
|
1207
|
+
const squadScheduleDelete = defineTool("squad_schedule_delete", {
|
|
1208
|
+
description: "Delete a squad schedule by id.",
|
|
1209
|
+
skipPermission: true,
|
|
1210
|
+
parameters: z.object({
|
|
1211
|
+
id: z.number().int().describe("Schedule id (from squad_schedule_list)"),
|
|
1212
|
+
}),
|
|
1213
|
+
handler: async ({ id }) => {
|
|
1214
|
+
const existing = getSchedule(id);
|
|
1215
|
+
if (!existing)
|
|
1216
|
+
return `Schedule ${id} not found.`;
|
|
1217
|
+
deleteSchedule(id);
|
|
1218
|
+
return `🗑️ Deleted schedule "${existing.name}" (id ${id}).`;
|
|
1219
|
+
},
|
|
1220
|
+
});
|
|
1221
|
+
const squadSchedulePause = defineTool("squad_schedule_pause", {
|
|
1222
|
+
description: "Pause a squad schedule so it stops firing (preserves config).",
|
|
1223
|
+
skipPermission: true,
|
|
1224
|
+
parameters: z.object({ id: z.number().int().describe("Schedule id") }),
|
|
1225
|
+
handler: async ({ id }) => {
|
|
1226
|
+
const existing = getSchedule(id);
|
|
1227
|
+
if (!existing)
|
|
1228
|
+
return `Schedule ${id} not found.`;
|
|
1229
|
+
setScheduleEnabled(id, false);
|
|
1230
|
+
return `⏸️ Paused schedule "${existing.name}" (id ${id}).`;
|
|
1231
|
+
},
|
|
1232
|
+
});
|
|
1233
|
+
const squadScheduleResume = defineTool("squad_schedule_resume", {
|
|
1234
|
+
description: "Resume a paused squad schedule. The next run is computed from now using the stored cron expression.",
|
|
1235
|
+
skipPermission: true,
|
|
1236
|
+
parameters: z.object({ id: z.number().int().describe("Schedule id") }),
|
|
1237
|
+
handler: async ({ id }) => {
|
|
1238
|
+
const existing = getSchedule(id);
|
|
1239
|
+
if (!existing)
|
|
1240
|
+
return `Schedule ${id} not found.`;
|
|
1241
|
+
setScheduleEnabled(id, true);
|
|
1242
|
+
try {
|
|
1243
|
+
const next = nextRun(existing.cron_expr);
|
|
1244
|
+
// Update next_run_at via the store's helper would be cleaner, but we
|
|
1245
|
+
// can also just re-run reconcile on next tick. Inline update:
|
|
1246
|
+
const { updateNextRun } = await import("../store/schedules.js");
|
|
1247
|
+
updateNextRun(id, next.toISOString());
|
|
1248
|
+
return `▶️ Resumed schedule "${existing.name}" (id ${id}). Next run: ${next.toISOString()}`;
|
|
1249
|
+
}
|
|
1250
|
+
catch (err) {
|
|
1251
|
+
return `Resumed schedule "${existing.name}" but failed to compute next run: ${err instanceof Error ? err.message : String(err)}`;
|
|
1252
|
+
}
|
|
1253
|
+
},
|
|
1254
|
+
});
|
|
1255
|
+
const squadScheduleRunNow = defineTool("squad_schedule_run_now", {
|
|
1256
|
+
description: "Manually fire a squad schedule immediately (useful for testing). Does not affect the next scheduled occurrence.",
|
|
1257
|
+
skipPermission: true,
|
|
1258
|
+
parameters: z.object({ id: z.number().int().describe("Schedule id") }),
|
|
1259
|
+
handler: async ({ id }) => {
|
|
1260
|
+
const result = await runScheduleNow(id);
|
|
1261
|
+
if (!result.ok)
|
|
1262
|
+
return `Failed: ${result.error}`;
|
|
1263
|
+
return `🚀 Fired schedule ${id} now. Use squad_task_status to follow the resulting stand-up.`;
|
|
1264
|
+
},
|
|
1265
|
+
});
|
|
1266
|
+
return [wikiRead, wikiWrite, wikiSearch, wikiDelete, wikiList, squadCreate, squadRecall, squadStatus, squadLogDecision, squadDelegate, squadTaskStatus, squadDelete, squadAnalyze, squadAddAgent, squadAgents, squadRemoveAgent, squadSetLead, squadSetQA, squadTaskReviews, squadScheduleCreate, squadScheduleList, squadScheduleDelete, squadSchedulePause, squadScheduleResume, squadScheduleRunNow, skillList, skillInstall, skillRemove, skillSearch, configUpdate, checkUpdate, shell, fileOps, bash, readFile, viewTool, grepTool, strReplaceEditor, github];
|
|
977
1267
|
}
|
|
978
1268
|
function walkDirectory(dir, maxDepth = 3, depth = 0) {
|
|
979
1269
|
if (depth >= maxDepth)
|
package/dist/daemon.js
CHANGED
|
@@ -4,6 +4,7 @@ import { startApiServer, setMessageHandler as setApiHandler } from "./api/server
|
|
|
4
4
|
import { createBot, startBot, stopBot, sendProactiveMessage, setMessageHandler as setTelegramHandler } from "./telegram/bot.js";
|
|
5
5
|
import { getDb, closeDb } from "./store/db.js";
|
|
6
6
|
import { clearStaleTasks } from "./store/tasks.js";
|
|
7
|
+
import { startScheduler, stopScheduler } from "./copilot/scheduler.js";
|
|
7
8
|
import { config } from "./config.js";
|
|
8
9
|
import { ensureWikiStructure } from "./wiki/fs.js";
|
|
9
10
|
import { autoUpdate } from "./update.js";
|
|
@@ -90,6 +91,8 @@ export async function startDaemon() {
|
|
|
90
91
|
else {
|
|
91
92
|
console.log("[io] Telegram not configured — skipping bot. Set telegramBotToken in ~/.io/config.json");
|
|
92
93
|
}
|
|
94
|
+
// Start the squad scheduler (background cron-style stand-ups).
|
|
95
|
+
startScheduler();
|
|
93
96
|
console.log("[io] IO is fully operational.");
|
|
94
97
|
// Notify Telegram if restarting
|
|
95
98
|
if (config.telegramEnabled && process.env.IO_RESTARTED === "1") {
|
|
@@ -117,6 +120,7 @@ async function shutdown() {
|
|
|
117
120
|
}
|
|
118
121
|
catch { /* best effort */ }
|
|
119
122
|
}
|
|
123
|
+
stopScheduler();
|
|
120
124
|
await shutdownOrchestrator();
|
|
121
125
|
try {
|
|
122
126
|
await stopClient();
|
package/dist/store/db.js
CHANGED
|
@@ -71,6 +71,31 @@ export function getDb() {
|
|
|
71
71
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
72
72
|
UNIQUE(squad_slug, character_name)
|
|
73
73
|
)`,
|
|
74
|
+
`ALTER TABLE squad_agents ADD COLUMN is_lead INTEGER NOT NULL DEFAULT 0`,
|
|
75
|
+
`ALTER TABLE squad_agents ADD COLUMN is_qa INTEGER NOT NULL DEFAULT 0`,
|
|
76
|
+
`CREATE TABLE IF NOT EXISTS squad_task_reviews (
|
|
77
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
78
|
+
task_id TEXT NOT NULL,
|
|
79
|
+
squad_slug TEXT NOT NULL,
|
|
80
|
+
reviewer_character TEXT NOT NULL,
|
|
81
|
+
approved INTEGER NOT NULL DEFAULT 0,
|
|
82
|
+
comments TEXT,
|
|
83
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
84
|
+
)`,
|
|
85
|
+
`CREATE TABLE IF NOT EXISTS squad_schedules (
|
|
86
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
87
|
+
squad_slug TEXT NOT NULL,
|
|
88
|
+
name TEXT NOT NULL,
|
|
89
|
+
cron_expr TEXT NOT NULL,
|
|
90
|
+
agenda TEXT NOT NULL,
|
|
91
|
+
notes TEXT,
|
|
92
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
93
|
+
last_run_at DATETIME,
|
|
94
|
+
next_run_at DATETIME,
|
|
95
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
96
|
+
)`,
|
|
97
|
+
`CREATE INDEX IF NOT EXISTS idx_squad_schedules_due
|
|
98
|
+
ON squad_schedules (enabled, next_run_at)`,
|
|
74
99
|
];
|
|
75
100
|
for (const migration of migrations) {
|
|
76
101
|
try {
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { getDb } from "./db.js";
|
|
2
|
+
function rowToSchedule(row) {
|
|
3
|
+
let agenda = [];
|
|
4
|
+
try {
|
|
5
|
+
agenda = JSON.parse(row.agenda);
|
|
6
|
+
if (!Array.isArray(agenda))
|
|
7
|
+
agenda = [];
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
agenda = [];
|
|
11
|
+
}
|
|
12
|
+
return { ...row, agenda };
|
|
13
|
+
}
|
|
14
|
+
export function createSchedule(input) {
|
|
15
|
+
const db = getDb();
|
|
16
|
+
const info = db
|
|
17
|
+
.prepare(`INSERT INTO squad_schedules
|
|
18
|
+
(squad_slug, name, cron_expr, agenda, notes, enabled, next_run_at)
|
|
19
|
+
VALUES (?, ?, ?, ?, ?, 1, ?)`)
|
|
20
|
+
.run(input.squadSlug, input.name, input.cronExpr, JSON.stringify(input.agenda), input.notes ?? null, input.nextRunAt);
|
|
21
|
+
const id = Number(info.lastInsertRowid);
|
|
22
|
+
return getSchedule(id);
|
|
23
|
+
}
|
|
24
|
+
export function getSchedule(id) {
|
|
25
|
+
const row = getDb()
|
|
26
|
+
.prepare("SELECT * FROM squad_schedules WHERE id = ?")
|
|
27
|
+
.get(id);
|
|
28
|
+
return row ? rowToSchedule(row) : undefined;
|
|
29
|
+
}
|
|
30
|
+
export function listSchedules(squadSlug) {
|
|
31
|
+
const rows = squadSlug
|
|
32
|
+
? getDb()
|
|
33
|
+
.prepare("SELECT * FROM squad_schedules WHERE squad_slug = ? ORDER BY id ASC")
|
|
34
|
+
.all(squadSlug)
|
|
35
|
+
: getDb()
|
|
36
|
+
.prepare("SELECT * FROM squad_schedules ORDER BY squad_slug, id ASC")
|
|
37
|
+
.all();
|
|
38
|
+
return rows.map(rowToSchedule);
|
|
39
|
+
}
|
|
40
|
+
export function listDueSchedules(now) {
|
|
41
|
+
const iso = now.toISOString();
|
|
42
|
+
const rows = getDb()
|
|
43
|
+
.prepare(`SELECT * FROM squad_schedules
|
|
44
|
+
WHERE enabled = 1
|
|
45
|
+
AND next_run_at IS NOT NULL
|
|
46
|
+
AND next_run_at <= ?
|
|
47
|
+
ORDER BY next_run_at ASC`)
|
|
48
|
+
.all(iso);
|
|
49
|
+
return rows.map(rowToSchedule);
|
|
50
|
+
}
|
|
51
|
+
export function deleteSchedule(id) {
|
|
52
|
+
const info = getDb()
|
|
53
|
+
.prepare("DELETE FROM squad_schedules WHERE id = ?")
|
|
54
|
+
.run(id);
|
|
55
|
+
return info.changes > 0;
|
|
56
|
+
}
|
|
57
|
+
export function setScheduleEnabled(id, enabled) {
|
|
58
|
+
const info = getDb()
|
|
59
|
+
.prepare("UPDATE squad_schedules SET enabled = ? WHERE id = ?")
|
|
60
|
+
.run(enabled ? 1 : 0, id);
|
|
61
|
+
return info.changes > 0;
|
|
62
|
+
}
|
|
63
|
+
export function recordScheduleRun(id, ranAt, nextRunAt) {
|
|
64
|
+
getDb()
|
|
65
|
+
.prepare("UPDATE squad_schedules SET last_run_at = ?, next_run_at = ? WHERE id = ?")
|
|
66
|
+
.run(ranAt.toISOString(), nextRunAt, id);
|
|
67
|
+
}
|
|
68
|
+
export function updateNextRun(id, nextRunAt) {
|
|
69
|
+
getDb()
|
|
70
|
+
.prepare("UPDATE squad_schedules SET next_run_at = ? WHERE id = ?")
|
|
71
|
+
.run(nextRunAt, id);
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=schedules.js.map
|
package/dist/store/squads.js
CHANGED
|
@@ -115,4 +115,22 @@ export function getDecisionsSummary(squadSlug) {
|
|
|
115
115
|
})
|
|
116
116
|
.join("\n");
|
|
117
117
|
}
|
|
118
|
+
export function setSquadLead(squadSlug, characterName) {
|
|
119
|
+
const db = getDb();
|
|
120
|
+
const tx = db.transaction(() => {
|
|
121
|
+
db.prepare("UPDATE squad_agents SET is_lead = 0 WHERE squad_slug = ?").run(squadSlug);
|
|
122
|
+
db.prepare("UPDATE squad_agents SET is_lead = 1 WHERE squad_slug = ? AND character_name = ?").run(squadSlug, characterName);
|
|
123
|
+
});
|
|
124
|
+
tx();
|
|
125
|
+
}
|
|
126
|
+
export function getSquadLead(squadSlug) {
|
|
127
|
+
return getDb()
|
|
128
|
+
.prepare("SELECT * FROM squad_agents WHERE squad_slug = ? AND is_lead = 1 LIMIT 1")
|
|
129
|
+
.get(squadSlug);
|
|
130
|
+
}
|
|
131
|
+
export function setSquadQA(squadSlug, characterName, isQA) {
|
|
132
|
+
getDb()
|
|
133
|
+
.prepare("UPDATE squad_agents SET is_qa = ? WHERE squad_slug = ? AND character_name = ?")
|
|
134
|
+
.run(isQA ? 1 : 0, squadSlug, characterName);
|
|
135
|
+
}
|
|
118
136
|
//# sourceMappingURL=squads.js.map
|
package/dist/store/tasks.js
CHANGED
|
@@ -39,4 +39,18 @@ export function listRecentTasks(limit = 50) {
|
|
|
39
39
|
.prepare("SELECT * FROM agent_tasks ORDER BY datetime(started_at) DESC, task_id DESC LIMIT ?")
|
|
40
40
|
.all(limit);
|
|
41
41
|
}
|
|
42
|
+
export function createReview(taskId, squadSlug, reviewerCharacter, approved, comments) {
|
|
43
|
+
const db = getDb();
|
|
44
|
+
const info = db
|
|
45
|
+
.prepare("INSERT INTO squad_task_reviews (task_id, squad_slug, reviewer_character, approved, comments) VALUES (?, ?, ?, ?, ?)")
|
|
46
|
+
.run(taskId, squadSlug, reviewerCharacter, approved ? 1 : 0, comments ?? null);
|
|
47
|
+
return db
|
|
48
|
+
.prepare("SELECT * FROM squad_task_reviews WHERE id = ?")
|
|
49
|
+
.get(info.lastInsertRowid);
|
|
50
|
+
}
|
|
51
|
+
export function getTaskReviews(taskId) {
|
|
52
|
+
return getDb()
|
|
53
|
+
.prepare("SELECT * FROM squad_task_reviews WHERE task_id = ? ORDER BY created_at ASC, id ASC")
|
|
54
|
+
.all(taskId);
|
|
55
|
+
}
|
|
42
56
|
//# sourceMappingURL=tasks.js.map
|
package/dist/tui/index.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { createInterface } from "readline";
|
|
2
|
+
import { listRecentTasks, getTask } from "../store/tasks.js";
|
|
3
|
+
import { getTaskEvents } from "../copilot/agents.js";
|
|
4
|
+
import { summarize } from "../copilot/event-summary.js";
|
|
2
5
|
let messageHandler;
|
|
3
6
|
export function setMessageHandler(handler) {
|
|
4
7
|
messageHandler = handler;
|
|
@@ -8,9 +11,49 @@ const WELCOME_BANNER = `
|
|
|
8
11
|
║ IO — AI Assistant ║
|
|
9
12
|
╚══════════════════════════════════════╝
|
|
10
13
|
Type a message to chat. Commands:
|
|
11
|
-
/status
|
|
12
|
-
/
|
|
14
|
+
/status — show status
|
|
15
|
+
/activity [id|N] — show summarized activity for a task (default: most recent)
|
|
16
|
+
/verbose — toggle verbose mode (raw event detail in /activity)
|
|
17
|
+
/quit — exit
|
|
13
18
|
`;
|
|
19
|
+
let verbose = false;
|
|
20
|
+
function renderActivity(taskIdArg) {
|
|
21
|
+
const recent = listRecentTasks(20);
|
|
22
|
+
let task = undefined;
|
|
23
|
+
if (!taskIdArg) {
|
|
24
|
+
task = recent[0];
|
|
25
|
+
}
|
|
26
|
+
else if (/^\d+$/.test(taskIdArg)) {
|
|
27
|
+
task = recent[parseInt(taskIdArg, 10) - 1];
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
task = getTask(taskIdArg);
|
|
31
|
+
}
|
|
32
|
+
if (!task) {
|
|
33
|
+
console.log("[io] No task found for activity view.");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const events = getTaskEvents(task.task_id);
|
|
37
|
+
if (events.length === 0) {
|
|
38
|
+
console.log(`[io] No buffered activity for task ${task.task_id} (${task.status}).`);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const activity = summarize(events);
|
|
42
|
+
console.log(`[io] Activity for task ${task.task_id} (${task.agent_slug}, ${task.status}) — ${activity.length} entries${verbose ? " (verbose)" : ""}`);
|
|
43
|
+
for (const e of activity) {
|
|
44
|
+
const ts = new Date(e.ts).toISOString().slice(11, 19);
|
|
45
|
+
console.log(` ${ts} ${e.icon} ${e.summary}`);
|
|
46
|
+
if (verbose) {
|
|
47
|
+
if (e.detail) {
|
|
48
|
+
for (const line of e.detail.split(/\r?\n/))
|
|
49
|
+
console.log(` ${line}`);
|
|
50
|
+
}
|
|
51
|
+
else if (e.raw && typeof e.raw === "object") {
|
|
52
|
+
console.log(" " + JSON.stringify(e.raw).slice(0, 400));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
14
57
|
function clearLine() {
|
|
15
58
|
process.stdout.write("\r\x1b[K");
|
|
16
59
|
}
|
|
@@ -38,6 +81,23 @@ export async function startTui() {
|
|
|
38
81
|
rl.prompt();
|
|
39
82
|
return;
|
|
40
83
|
}
|
|
84
|
+
if (trimmed === "/verbose") {
|
|
85
|
+
verbose = !verbose;
|
|
86
|
+
console.log(`[io] Verbose mode ${verbose ? "ON" : "OFF"}`);
|
|
87
|
+
rl.prompt();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (trimmed === "/activity" || trimmed.startsWith("/activity ")) {
|
|
91
|
+
const arg = trimmed.slice("/activity".length).trim() || undefined;
|
|
92
|
+
try {
|
|
93
|
+
renderActivity(arg);
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
console.error(`[io] /activity failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
97
|
+
}
|
|
98
|
+
rl.prompt();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
41
101
|
if (!messageHandler) {
|
|
42
102
|
console.log("[io] No message handler registered.");
|
|
43
103
|
rl.prompt();
|
package/package.json
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.bg-gray-750[data-v-6c7a72da]{background-color:#2d3748}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.fixed{position:fixed}.inset-0{top:0;right:0;bottom:0;left:0}.z-50{z-index:50}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.mr-1{margin-right:.25rem}.mt-1{margin-top:.25rem}.mt-12{margin-top:3rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.grid{display:grid}.h-2{height:.5rem}.h-3{height:.75rem}.h-4{height:1rem}.h-full{height:100%}.h-screen{height:100vh}.max-h-48{max-height:12rem}.max-h-96{max-height:24rem}.max-h-\[85vh\]{max-height:85vh}.w-2{width:.5rem}.w-3{width:.75rem}.w-64{width:16rem}.w-full{width:100%}.min-w-0{min-width:0px}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-\[75\%\]{max-width:75%}.max-w-sm{max-width:24rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.rotate-90{--tw-rotate: 90deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes bounce{0%,to{transform:translateY(-25%);animation-timing-function:cubic-bezier(.8,0,1,1)}50%{transform:none;animation-timing-function:cubic-bezier(0,0,.2,1)}}.animate-bounce{animation:bounce 1s infinite}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.divide-gray-700>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:rgb(55 65 81 / var(--tw-divide-opacity, 1))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-sm{border-radius:.125rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-blue-800{--tw-border-opacity: 1;border-color:rgb(30 64 175 / var(--tw-border-opacity, 1))}.border-gray-600{--tw-border-opacity: 1;border-color:rgb(75 85 99 / var(--tw-border-opacity, 1))}.border-gray-700{--tw-border-opacity: 1;border-color:rgb(55 65 81 / var(--tw-border-opacity, 1))}.border-gray-800{--tw-border-opacity: 1;border-color:rgb(31 41 55 / var(--tw-border-opacity, 1))}.bg-black\/70{background-color:#000000b3}.bg-blue-400{--tw-bg-opacity: 1;background-color:rgb(96 165 250 / var(--tw-bg-opacity, 1))}.bg-blue-600{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity, 1))}.bg-blue-700{--tw-bg-opacity: 1;background-color:rgb(29 78 216 / var(--tw-bg-opacity, 1))}.bg-blue-800{--tw-bg-opacity: 1;background-color:rgb(30 64 175 / var(--tw-bg-opacity, 1))}.bg-blue-900{--tw-bg-opacity: 1;background-color:rgb(30 58 138 / var(--tw-bg-opacity, 1))}.bg-blue-950{--tw-bg-opacity: 1;background-color:rgb(23 37 84 / var(--tw-bg-opacity, 1))}.bg-emerald-900{--tw-bg-opacity: 1;background-color:rgb(6 78 59 / var(--tw-bg-opacity, 1))}.bg-gray-500{--tw-bg-opacity: 1;background-color:rgb(107 114 128 / var(--tw-bg-opacity, 1))}.bg-gray-700{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity, 1))}.bg-gray-800{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.bg-gray-900{--tw-bg-opacity: 1;background-color:rgb(17 24 39 / var(--tw-bg-opacity, 1))}.bg-gray-950{--tw-bg-opacity: 1;background-color:rgb(3 7 18 / var(--tw-bg-opacity, 1))}.bg-green-400{--tw-bg-opacity: 1;background-color:rgb(74 222 128 / var(--tw-bg-opacity, 1))}.bg-green-600{--tw-bg-opacity: 1;background-color:rgb(22 163 74 / var(--tw-bg-opacity, 1))}.bg-green-900{--tw-bg-opacity: 1;background-color:rgb(20 83 45 / var(--tw-bg-opacity, 1))}.bg-purple-900{--tw-bg-opacity: 1;background-color:rgb(88 28 135 / var(--tw-bg-opacity, 1))}.bg-red-600{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity, 1))}.bg-red-800{--tw-bg-opacity: 1;background-color:rgb(153 27 27 / var(--tw-bg-opacity, 1))}.bg-red-900{--tw-bg-opacity: 1;background-color:rgb(127 29 29 / var(--tw-bg-opacity, 1))}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-yellow-900{--tw-bg-opacity: 1;background-color:rgb(113 63 18 / var(--tw-bg-opacity, 1))}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pl-6{padding-left:1.5rem}.pt-4{padding-top:1rem}.text-left{text-align:left}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.font-sans{font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji"}.text-2xl{font-size:1.5rem;line-height:2rem}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.italic{font-style:italic}.leading-none{line-height:1}.tracking-wider{letter-spacing:.05em}.text-blue-100{--tw-text-opacity: 1;color:rgb(219 234 254 / var(--tw-text-opacity, 1))}.text-blue-200{--tw-text-opacity: 1;color:rgb(191 219 254 / var(--tw-text-opacity, 1))}.text-blue-300{--tw-text-opacity: 1;color:rgb(147 197 253 / var(--tw-text-opacity, 1))}.text-blue-400{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.text-emerald-200{--tw-text-opacity: 1;color:rgb(167 243 208 / var(--tw-text-opacity, 1))}.text-gray-100{--tw-text-opacity: 1;color:rgb(243 244 246 / var(--tw-text-opacity, 1))}.text-gray-200{--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity, 1))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.text-green-100{--tw-text-opacity: 1;color:rgb(220 252 231 / var(--tw-text-opacity, 1))}.text-green-300{--tw-text-opacity: 1;color:rgb(134 239 172 / var(--tw-text-opacity, 1))}.text-purple-100{--tw-text-opacity: 1;color:rgb(243 232 255 / var(--tw-text-opacity, 1))}.text-purple-200{--tw-text-opacity: 1;color:rgb(233 213 255 / var(--tw-text-opacity, 1))}.text-purple-300{--tw-text-opacity: 1;color:rgb(216 180 254 / var(--tw-text-opacity, 1))}.text-red-100{--tw-text-opacity: 1;color:rgb(254 226 226 / var(--tw-text-opacity, 1))}.text-red-200{--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity, 1))}.text-red-300{--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity, 1))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-yellow-100{--tw-text-opacity: 1;color:rgb(254 249 195 / var(--tw-text-opacity, 1))}.text-yellow-200{--tw-text-opacity: 1;color:rgb(254 240 138 / var(--tw-text-opacity, 1))}.placeholder-gray-500::-moz-placeholder{--tw-placeholder-opacity: 1;color:rgb(107 114 128 / var(--tw-placeholder-opacity, 1))}.placeholder-gray-500::placeholder{--tw-placeholder-opacity: 1;color:rgb(107 114 128 / var(--tw-placeholder-opacity, 1))}.shadow-2xl{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}html{color-scheme:dark}body{--tw-bg-opacity: 1;background-color:rgb(3 7 18 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(243 244 246 / var(--tw-text-opacity, 1))}.hover\:border-blue-500:hover{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity, 1))}.hover\:bg-blue-600:hover{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity, 1))}.hover\:bg-blue-700:hover{--tw-bg-opacity: 1;background-color:rgb(29 78 216 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-600:hover{--tw-bg-opacity: 1;background-color:rgb(75 85 99 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-800:hover{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.hover\:bg-green-700:hover{--tw-bg-opacity: 1;background-color:rgb(21 128 61 / var(--tw-bg-opacity, 1))}.hover\:bg-red-700:hover{--tw-bg-opacity: 1;background-color:rgb(185 28 28 / var(--tw-bg-opacity, 1))}.hover\:text-gray-100:hover{--tw-text-opacity: 1;color:rgb(243 244 246 / var(--tw-text-opacity, 1))}.hover\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.focus\:border-blue-500:focus{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity, 1))}.focus\:border-transparent:focus{border-color:transparent}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-blue-600:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(37 99 235 / var(--tw-ring-opacity, 1))}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:bg-gray-700:disabled{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity, 1))}.disabled\:opacity-50:disabled{opacity:.5}
|