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.
@@ -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
- return `- **${s.name}** (\`${s.slug}\`) — ${s.status} — 🎬 ${universeName}${agentList}\n 📁 ${s.projectPath}`;
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
- return `Task delegated to ${agentLabel}. Task ID: ${taskId}\n\nThe agent is working on this in the background. Use squad_task_status to check progress.`;
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) => `- **${a.character_name}** — ${a.role_title} (${a.model_tier}) — ${a.status}${a.personality ? `\n _${a.personality}_` : ""}`);
377
- return `**${squad.name}** 🎬 ${universeName}\n\n${lines.join("\n")}`;
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
- return [wikiRead, wikiWrite, wikiSearch, wikiDelete, wikiList, squadCreate, squadRecall, squadStatus, squadLogDecision, squadDelegate, squadTaskStatus, squadDelete, squadAnalyze, squadAddAgent, squadAgents, squadRemoveAgent, skillList, skillInstall, skillRemove, skillSearch, configUpdate, checkUpdate, shell, fileOps, bash, readFile, viewTool, grepTool, strReplaceEditor, github];
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
@@ -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
@@ -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 — show status
12
- /quit exit
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "heyio",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "IO — a personal AI assistant built on the GitHub Copilot SDK",
5
5
  "bin": {
6
6
  "io": "dist/index.js"
@@ -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}