heyio 1.2.4 → 1.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.
Files changed (65) hide show
  1. package/dist/api/server.js +267 -12
  2. package/dist/config.js +6 -0
  3. package/dist/copilot/agents.js +61 -4
  4. package/dist/copilot/ceremonies.js +12 -2
  5. package/dist/copilot/io-scheduler.js +9 -1
  6. package/dist/copilot/orchestrator.js +2 -0
  7. package/dist/copilot/scheduler.js +4 -0
  8. package/dist/copilot/skills.js +138 -6
  9. package/dist/copilot/token-tracker.js +89 -0
  10. package/dist/copilot/tools.js +27 -5
  11. package/dist/paths.js +1 -0
  12. package/dist/store/agent-events.js +19 -0
  13. package/dist/store/audit-log.js +71 -0
  14. package/dist/store/conversations.js +150 -0
  15. package/dist/store/db.js +111 -0
  16. package/dist/store/schedules.js +5 -1
  17. package/dist/store/squad-colors.js +21 -0
  18. package/dist/store/squads.js +6 -1
  19. package/dist/store/tasks.js +43 -0
  20. package/dist/store/token-usage.js +94 -0
  21. package/dist/wiki/backlinks.js +51 -0
  22. package/dist/wiki/fs.js +63 -1
  23. package/package.json +1 -1
  24. package/web-dist/assets/AuditLogView-xgSZ2MOJ.js +6 -0
  25. package/web-dist/assets/ChatView-BU3Jvu5y.js +11 -0
  26. package/web-dist/assets/FeedView-BwkWbe1p.js +6 -0
  27. package/web-dist/assets/HistoryView-Doh9Y3Na.js +1 -0
  28. package/web-dist/assets/LoginView-CoTEOrwE.js +1 -0
  29. package/web-dist/assets/{MarkdownContent.vue_vue_type_script_setup_true_lang-CEo_ckIb.js → MarkdownContent.vue_vue_type_script_setup_true_lang-CObjuCHH.js} +1 -1
  30. package/web-dist/assets/McpView-ByXoAnED.js +1 -0
  31. package/web-dist/assets/SchedulesView-BkUdRYwk.js +1 -0
  32. package/web-dist/assets/SettingsView-_q-IpzFy.js +1 -0
  33. package/web-dist/assets/SkillsView-_FkOdD2U.js +15 -0
  34. package/web-dist/assets/SquadDetailView-CV6_n_If.js +31 -0
  35. package/web-dist/assets/SquadHealthView-DmQqPq7H.js +11 -0
  36. package/web-dist/assets/SquadsView-Dkhtu5MQ.js +6 -0
  37. package/web-dist/assets/UsageView-CCS6pp6n.js +16 -0
  38. package/web-dist/assets/WikiView-CpXzff_L.js +31 -0
  39. package/web-dist/assets/api-CaqVk-rG.js +1 -0
  40. package/web-dist/assets/arrow-left-CkDjCT7Z.js +6 -0
  41. package/web-dist/assets/git-branch-Bu9s__XL.js +6 -0
  42. package/web-dist/assets/index-D3DNfwXI.css +1 -0
  43. package/web-dist/assets/{index-BQdXxKfc.js → index-DfdD_qE4.js} +56 -36
  44. package/web-dist/assets/{plus-Cvp1w2CO.js → plus-GvGwcjX5.js} +1 -1
  45. package/web-dist/assets/{x-O3fBd1Cr.js → save-fQ_rr5hX.js} +2 -7
  46. package/web-dist/assets/search-C3fxUixl.js +6 -0
  47. package/web-dist/assets/squad-colors-B8B_Y-lz.js +1 -0
  48. package/web-dist/assets/{trash-2-Cr3vrmL5.js → trash-2-Ba_1SAua.js} +1 -1
  49. package/web-dist/assets/triangle-alert-BTBlX3kg.js +6 -0
  50. package/web-dist/assets/x-CJifAZQa.js +6 -0
  51. package/web-dist/favicon.svg +9 -3
  52. package/web-dist/index.html +2 -2
  53. package/web-dist/logo.svg +10 -0
  54. package/web-dist/assets/ChatView-mZaaw3pd.js +0 -11
  55. package/web-dist/assets/FeedView-BHacQwXQ.js +0 -6
  56. package/web-dist/assets/LoginView-B6aSD9II.js +0 -1
  57. package/web-dist/assets/McpView-BAVRUHIE.js +0 -1
  58. package/web-dist/assets/SchedulesView-dOd1SQiP.js +0 -1
  59. package/web-dist/assets/SettingsView-CCDeEsVg.js +0 -1
  60. package/web-dist/assets/SkillsView-gCfQ35FQ.js +0 -1
  61. package/web-dist/assets/SquadDetailView-CQhFfZTc.js +0 -21
  62. package/web-dist/assets/SquadsView-CZFxtOao.js +0 -6
  63. package/web-dist/assets/WikiView-B0cuUFfm.js +0 -26
  64. package/web-dist/assets/api-DdW5uOZf.js +0 -1
  65. package/web-dist/assets/index-BbSJ0cfF.css +0 -1
@@ -1,9 +1,9 @@
1
- import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
2
- import { join, basename } from "node:path";
3
- import { exec } from "node:child_process";
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { join, basename, resolve, sep } from "node:path";
3
+ import { execFile } from "node:child_process";
4
4
  import { promisify } from "node:util";
5
5
  import { PATHS } from "../paths.js";
6
- const execAsync = promisify(exec);
6
+ const execFileAsync = promisify(execFile);
7
7
  export async function listSkills() {
8
8
  if (!existsSync(PATHS.skills))
9
9
  return [];
@@ -37,7 +37,7 @@ export async function addSkill(url) {
37
37
  if (existsSync(dest)) {
38
38
  throw new Error(`Skill "${slug}" is already installed.`);
39
39
  }
40
- await execAsync(`git clone --depth 1 ${url} ${dest}`);
40
+ await execFileAsync("git", ["clone", "--depth", "1", "--", url, dest]);
41
41
  // Verify SKILL.md exists
42
42
  if (!existsSync(join(dest, "SKILL.md"))) {
43
43
  rmSync(dest, { recursive: true, force: true });
@@ -59,13 +59,35 @@ export async function getSkillContent(slug) {
59
59
  return readFileSync(skillMd, "utf-8");
60
60
  }
61
61
  export async function updateSkillContent(slug, content) {
62
- const { writeFileSync } = await import("node:fs");
63
62
  const skillMd = join(PATHS.skills, slug, "SKILL.md");
64
63
  if (!existsSync(join(PATHS.skills, slug))) {
65
64
  throw new Error(`Skill "${slug}" not found.`);
66
65
  }
67
66
  writeFileSync(skillMd, content);
68
67
  }
68
+ export async function createSkill(slug, content) {
69
+ const cleanSlug = slug
70
+ .trim()
71
+ .replace(/[^a-z0-9-]/gi, "-")
72
+ .toLowerCase()
73
+ .replace(/-+/g, "-")
74
+ .replace(/^-|-$/g, "");
75
+ if (!cleanSlug) {
76
+ throw new Error("Skill title must contain at least one alphanumeric character.");
77
+ }
78
+ // Guard against path traversal: the resolved destination must be a direct
79
+ // child of the skills directory (not above or beside it).
80
+ const skillsRoot = resolve(PATHS.skills);
81
+ const dest = resolve(skillsRoot, cleanSlug);
82
+ if (!dest.startsWith(skillsRoot + sep)) {
83
+ throw new Error("Invalid skill slug.");
84
+ }
85
+ if (existsSync(dest)) {
86
+ throw new Error(`Skill "${cleanSlug}" already exists.`);
87
+ }
88
+ mkdirSync(dest, { recursive: true });
89
+ writeFileSync(join(dest, "SKILL.md"), content);
90
+ }
69
91
  export async function loadSkillDirectories() {
70
92
  if (!existsSync(PATHS.skills))
71
93
  return [];
@@ -74,4 +96,114 @@ export async function loadSkillDirectories() {
74
96
  .filter((e) => e.isDirectory() && existsSync(join(PATHS.skills, e.name, "SKILL.md")))
75
97
  .map((e) => join(PATHS.skills, e.name));
76
98
  }
99
+ const DISCOVERY_CACHE = new Map();
100
+ const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
101
+ /** Validate and return a safe slug, throwing if it contains unsafe characters. */
102
+ function validateSlug(slug) {
103
+ if (!slug || !/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/i.test(slug)) {
104
+ throw new Error("Invalid skill slug: must contain only letters, digits, and hyphens.");
105
+ }
106
+ return slug;
107
+ }
108
+ function parseAwesomeCopilotTable(markdown) {
109
+ const skills = [];
110
+ for (const line of markdown.split("\n")) {
111
+ if (!line.startsWith("|"))
112
+ continue;
113
+ const cells = line
114
+ .split("|")
115
+ .map((c) => c.trim())
116
+ .filter(Boolean);
117
+ if (cells.length < 2)
118
+ continue;
119
+ // First cell contains [slug](../skills/slug/SKILL.md)
120
+ const slugMatch = cells[0].match(/^\[([^\]]+)\]/);
121
+ if (!slugMatch)
122
+ continue;
123
+ const slug = slugMatch[1];
124
+ if (slug === "Name")
125
+ continue; // header row
126
+ // Second cell is the description (may contain <br /> markup)
127
+ const description = cells[1]
128
+ .replace(/<br\s*\/?>/gi, " ")
129
+ .replace(/\*\*([^*]+)\*\*/g, "$1")
130
+ .trim();
131
+ if (!description || description === "Description")
132
+ continue;
133
+ skills.push({ slug, name: slug, description, source: "awesome-copilot" });
134
+ }
135
+ return skills;
136
+ }
137
+ async function fetchAwesomeCopilotSkills() {
138
+ const url = "https://raw.githubusercontent.com/github/awesome-copilot/main/docs/README.skills.md";
139
+ const res = await fetch(url, { signal: AbortSignal.timeout(15_000) });
140
+ if (!res.ok)
141
+ throw new Error(`Failed to fetch awesome-copilot skills list: HTTP ${res.status}`);
142
+ const text = await res.text();
143
+ return parseAwesomeCopilotTable(text);
144
+ }
145
+ async function fetchSkillsShSkills() {
146
+ try {
147
+ const res = await fetch("https://skills.sh/api/skills", {
148
+ headers: { Accept: "application/json" },
149
+ signal: AbortSignal.timeout(10_000),
150
+ });
151
+ if (!res.ok)
152
+ return [];
153
+ const data = (await res.json());
154
+ return data.map((item) => ({
155
+ slug: item.slug ?? item.name ?? "",
156
+ name: item.name ?? item.slug ?? "",
157
+ description: item.description ?? "",
158
+ source: "skillssh",
159
+ }));
160
+ }
161
+ catch {
162
+ return [];
163
+ }
164
+ }
165
+ export async function discoverSkills(source, query) {
166
+ const cached = DISCOVERY_CACHE.get(source);
167
+ let skills;
168
+ if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
169
+ skills = cached.skills;
170
+ }
171
+ else {
172
+ skills =
173
+ source === "awesome-copilot"
174
+ ? await fetchAwesomeCopilotSkills()
175
+ : await fetchSkillsShSkills();
176
+ DISCOVERY_CACHE.set(source, { skills, fetchedAt: Date.now() });
177
+ }
178
+ if (query) {
179
+ const q = query.toLowerCase();
180
+ skills = skills.filter((s) => s.slug.toLowerCase().includes(q) || s.description.toLowerCase().includes(q));
181
+ }
182
+ return skills;
183
+ }
184
+ function remoteSkillMdUrl(source, safeSlug) {
185
+ const encodedSlug = encodeURIComponent(safeSlug);
186
+ if (source === "awesome-copilot") {
187
+ return `https://raw.githubusercontent.com/github/awesome-copilot/main/skills/${encodedSlug}/SKILL.md`;
188
+ }
189
+ return `https://skills.sh/skills/${encodedSlug}/SKILL.md`;
190
+ }
191
+ export async function fetchRemoteSkillPreview(source, slug) {
192
+ const safeSlug = validateSlug(slug);
193
+ const url = remoteSkillMdUrl(source, safeSlug);
194
+ const res = await fetch(url, { signal: AbortSignal.timeout(10_000) });
195
+ if (!res.ok)
196
+ throw new Error(`Failed to fetch preview for "${safeSlug}": HTTP ${res.status}`);
197
+ return res.text();
198
+ }
199
+ export async function installFromSource(source, slug) {
200
+ const safeSlug = validateSlug(slug);
201
+ const dest = join(PATHS.skills, safeSlug);
202
+ if (existsSync(dest)) {
203
+ throw new Error(`Skill "${safeSlug}" is already installed.`);
204
+ }
205
+ const content = await fetchRemoteSkillPreview(source, safeSlug);
206
+ mkdirSync(dest, { recursive: true });
207
+ writeFileSync(join(dest, "SKILL.md"), content);
208
+ }
77
209
  //# sourceMappingURL=skills.js.map
@@ -0,0 +1,89 @@
1
+ import { loadConfig } from "../config.js";
2
+ import { recordTokenUsage } from "../store/token-usage.js";
3
+ import { postFeedItem } from "../store/feed.js";
4
+ /**
5
+ * Default model pricing (USD per 1M tokens).
6
+ * Used when modelPricing is not configured.
7
+ */
8
+ export const DEFAULT_MODEL_PRICING = {
9
+ "gpt-4.1": { inputPer1M: 2.0, outputPer1M: 8.0 },
10
+ "gpt-5.2": { inputPer1M: 2.5, outputPer1M: 10.0 },
11
+ "gpt-5.2-codex": { inputPer1M: 3.0, outputPer1M: 12.0 },
12
+ "gpt-5.3-codex": { inputPer1M: 3.0, outputPer1M: 12.0 },
13
+ "gpt-5.4": { inputPer1M: 5.0, outputPer1M: 20.0 },
14
+ "gpt-5.5": { inputPer1M: 7.5, outputPer1M: 30.0 },
15
+ "gpt-5-mini": { inputPer1M: 0.15, outputPer1M: 0.60 },
16
+ "gpt-5.4-mini": { inputPer1M: 0.15, outputPer1M: 0.60 },
17
+ "claude-haiku-4.5": { inputPer1M: 0.80, outputPer1M: 4.0 },
18
+ "claude-sonnet-4.5": { inputPer1M: 3.0, outputPer1M: 15.0 },
19
+ "claude-sonnet-4.6": { inputPer1M: 3.0, outputPer1M: 15.0 },
20
+ "claude-opus-4.5": { inputPer1M: 15.0, outputPer1M: 75.0 },
21
+ "claude-opus-4.6": { inputPer1M: 15.0, outputPer1M: 75.0 },
22
+ "claude-opus-4.7": { inputPer1M: 15.0, outputPer1M: 75.0 },
23
+ };
24
+ /**
25
+ * Compute estimated cost in USD for the given token counts and model.
26
+ */
27
+ export function estimateCost(model, inputTokens, outputTokens) {
28
+ const config = loadConfig();
29
+ const pricing = config.modelPricing?.[model] ?? DEFAULT_MODEL_PRICING[model];
30
+ if (!pricing)
31
+ return 0;
32
+ return (inputTokens / 1_000_000) * pricing.inputPer1M + (outputTokens / 1_000_000) * pricing.outputPer1M;
33
+ }
34
+ /**
35
+ * Attach a token usage listener to a CopilotSession.
36
+ * Returns a `flush` function that, when called, persists accumulated
37
+ * token usage to the database and returns the totals.
38
+ *
39
+ * Call `flush` after the session ends (in a finally block).
40
+ */
41
+ export function attachTokenTracker(session, context) {
42
+ const accumulator = {};
43
+ const unsubscribe = session.on("assistant.usage", (event) => {
44
+ const data = event?.data;
45
+ if (!data?.model)
46
+ return;
47
+ const model = data.model;
48
+ const input = data.inputTokens ?? 0;
49
+ const output = data.outputTokens ?? 0;
50
+ if (!accumulator[model])
51
+ accumulator[model] = { inputTokens: 0, outputTokens: 0 };
52
+ accumulator[model].inputTokens += input;
53
+ accumulator[model].outputTokens += output;
54
+ });
55
+ return () => {
56
+ unsubscribe();
57
+ let totalInputTokens = 0;
58
+ let totalOutputTokens = 0;
59
+ let totalCostUsd = 0;
60
+ for (const [model, usage] of Object.entries(accumulator)) {
61
+ if (usage.inputTokens === 0 && usage.outputTokens === 0)
62
+ continue;
63
+ const costUsd = estimateCost(model, usage.inputTokens, usage.outputTokens);
64
+ recordTokenUsage({
65
+ squadId: context.squadId,
66
+ agentId: context.agentId,
67
+ taskId: context.taskId,
68
+ model,
69
+ inputTokens: usage.inputTokens,
70
+ outputTokens: usage.outputTokens,
71
+ costUsd,
72
+ });
73
+ totalInputTokens += usage.inputTokens;
74
+ totalOutputTokens += usage.outputTokens;
75
+ totalCostUsd += costUsd;
76
+ }
77
+ // Alert on runaway agents
78
+ const config = loadConfig();
79
+ const threshold = config.tokenAlertThreshold;
80
+ if (threshold && (totalInputTokens + totalOutputTokens) > threshold) {
81
+ const source = context.squadId ? `squad-${context.squadId}` : "system";
82
+ postFeedItem(source, "⚠️ Token usage alert", `A task consumed ${totalInputTokens + totalOutputTokens} tokens (threshold: ${threshold}). ` +
83
+ `Estimated cost: $${totalCostUsd.toFixed(4)}. ` +
84
+ (context.taskId ? `Task ID: ${context.taskId}` : ""));
85
+ }
86
+ return { totalInputTokens, totalOutputTokens, totalCostUsd };
87
+ };
88
+ }
89
+ //# sourceMappingURL=token-tracker.js.map
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { defineTool } from "@github/copilot-sdk";
3
+ import { addAuditEntry } from "../store/audit-log.js";
3
4
  export function createTools() {
4
5
  return [
5
6
  // --- Wiki Tools ---
@@ -54,6 +55,16 @@ export function createTools() {
54
55
  return `Page deleted: ${path}`;
55
56
  },
56
57
  }),
58
+ defineTool("wiki_backlinks", {
59
+ description: "Find all wiki pages that link to the given page",
60
+ parameters: z.object({
61
+ path: z.string().describe("Page path relative to pages/ (e.g., 'notes/todo.md')"),
62
+ }),
63
+ handler: async ({ path }) => {
64
+ const { getBacklinks } = await import("../wiki/backlinks.js");
65
+ return await getBacklinks(path);
66
+ },
67
+ }),
57
68
  // --- Squad Tools ---
58
69
  defineTool("squad_create", {
59
70
  description: "Create a new project squad. Research the chosen universe to assign character names — never hardcode.",
@@ -68,6 +79,9 @@ export function createTools() {
68
79
  const { createSquad } = await import("../store/squads.js");
69
80
  const squad = createSquad(name, universe, repo_url);
70
81
  let cloneMsg = "";
82
+ // Copy squad wiki templates into the new squad's wiki directory
83
+ const { copySquadTemplates } = await import("../wiki/fs.js");
84
+ await copySquadTemplates(squad.slug);
71
85
  if (repo_url) {
72
86
  const { exec } = await import("node:child_process");
73
87
  const { promisify } = await import("node:util");
@@ -99,7 +113,9 @@ export function createTools() {
99
113
  }
100
114
  }
101
115
  }
102
- return `Squad "${name}" created with universe "${universe}". ID: ${squad.id}, Slug: ${squad.slug}. Wiki path: ~/.io/wiki/squads/${squad.slug}/${cloneMsg}`;
116
+ const msg = `Squad "${name}" created with universe "${universe}". ID: ${squad.id}, Slug: ${squad.slug}. Wiki path: ~/.io/wiki/squads/${squad.slug}/${cloneMsg}`;
117
+ addAuditEntry("squad_created", `Squad "${name}" created (universe: ${universe})`, { squad_id: squad.id, name, universe, repo_url }, { squad_id: squad.id });
118
+ return msg;
103
119
  },
104
120
  }),
105
121
  defineTool("squad_add_agent", {
@@ -143,6 +159,7 @@ export function createTools() {
143
159
  }),
144
160
  handler: async ({ squad_id, task, instance_id }) => {
145
161
  const { delegateTask } = await import("./agents.js");
162
+ addAuditEntry("task_delegated", `Task delegated to squad ${squad_id}: ${task.slice(0, 200)}`, { squad_id, task: task.slice(0, 1000), instance_id }, { squad_id });
146
163
  const result = await delegateTask(squad_id, task, instance_id);
147
164
  return result;
148
165
  },
@@ -158,6 +175,7 @@ export function createTools() {
158
175
  }),
159
176
  handler: async ({ squad_id, task, execute_after }) => {
160
177
  const { squadMeeting } = await import("./ceremonies.js");
178
+ addAuditEntry("squad_meeting", `Planning meeting started for squad ${squad_id}: ${task.slice(0, 200)}`, { squad_id, task: task.slice(0, 1000), execute_after }, { squad_id });
161
179
  return await squadMeeting(squad_id, task, execute_after);
162
180
  },
163
181
  }),
@@ -257,12 +275,12 @@ export function createTools() {
257
275
  parameters: z.object({
258
276
  type: z.enum(["squad", "io"]).describe("Schedule type"),
259
277
  cron: z.string().describe("Cron expression (e.g., '0 9 * * 1-5')"),
260
- squad_id: z.string().optional().describe("Squad ID (required for squad schedules)"),
278
+ squad_id: z.string().describe("Target squad ID"),
261
279
  agenda: z
262
280
  .string()
263
281
  .optional()
264
282
  .describe("Agenda type for squad schedules (triage, prioritize, ideation, or custom)"),
265
- prompt: z.string().optional().describe("Prompt text for IO schedules"),
283
+ prompt: z.string().optional().describe("Prompt text for the schedule"),
266
284
  }),
267
285
  handler: async ({ type, cron, squad_id, agenda, prompt }) => {
268
286
  const { createSchedule } = await import("../store/schedules.js");
@@ -325,12 +343,16 @@ export function createTools() {
325
343
  maxBuffer: 1024 * 1024,
326
344
  env: { ...process.env, GH_PROMPT_DISABLED: "1" },
327
345
  });
328
- return stdout.trim() || "(no output)";
346
+ const output = stdout.trim() || "(no output)";
347
+ addAuditEntry("shell_command", `Command: ${command.slice(0, 200)}`, { command, cwd, output: output.slice(0, 500), exit_code: 0 });
348
+ return output;
329
349
  }
330
350
  catch (err) {
331
351
  const stderr = err.stderr?.toString().trim() ?? "";
332
352
  const stdout = err.stdout?.toString().trim() ?? "";
333
- return `Error (exit ${err.code}): ${stderr || stdout || err.message}`;
353
+ const output = `Error (exit ${err.code}): ${stderr || stdout || err.message}`;
354
+ addAuditEntry("shell_command", `Command: ${command.slice(0, 200)}`, { command, cwd, output: output.slice(0, 500), exit_code: err.code ?? 1 });
355
+ return output;
334
356
  }
335
357
  },
336
358
  }),
package/dist/paths.js CHANGED
@@ -7,6 +7,7 @@ export const PATHS = {
7
7
  db: join(IO_HOME, "io.db"),
8
8
  wiki: join(IO_HOME, "wiki"),
9
9
  wikiPages: join(IO_HOME, "wiki", "pages"),
10
+ wikiSquadTemplates: join(IO_HOME, "wiki", "templates", "squad"),
10
11
  skills: join(IO_HOME, "skills"),
11
12
  mcpConfig: join(IO_HOME, "mcp.json"),
12
13
  sessions: join(IO_HOME, "sessions"),
@@ -0,0 +1,19 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { getDb } from "./db.js";
3
+ export function addAgentEvent(taskId, type, summary, payload) {
4
+ const db = getDb();
5
+ const id = randomUUID();
6
+ db.prepare("INSERT INTO agent_events (id, task_id, type, summary, payload) VALUES (?, ?, ?, ?, ?)").run(id, taskId, type, summary, JSON.stringify(payload));
7
+ return db.prepare("SELECT * FROM agent_events WHERE id = ?").get(id);
8
+ }
9
+ export function getAgentEvents(taskId) {
10
+ const db = getDb();
11
+ return db
12
+ .prepare("SELECT * FROM agent_events WHERE task_id = ? ORDER BY created_at ASC")
13
+ .all(taskId);
14
+ }
15
+ export function clearAgentEvents(taskId) {
16
+ const db = getDb();
17
+ db.prepare("DELETE FROM agent_events WHERE task_id = ?").run(taskId);
18
+ }
19
+ //# sourceMappingURL=agent-events.js.map
@@ -0,0 +1,71 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { getDb } from "./db.js";
3
+ export function addAuditEntry(action_type, summary, payload, opts) {
4
+ const db = getDb();
5
+ const id = randomUUID();
6
+ db.prepare(`INSERT INTO audit_log (id, squad_id, agent_id, task_id, action_type, summary, payload)
7
+ VALUES (?, ?, ?, ?, ?, ?, ?)`).run(id, opts?.squad_id ?? null, opts?.agent_id ?? null, opts?.task_id ?? null, action_type, summary, JSON.stringify(payload));
8
+ return db.prepare("SELECT * FROM audit_log WHERE id = ?").get(id);
9
+ }
10
+ export function getAuditLog(filters = {}) {
11
+ const db = getDb();
12
+ const conditions = [];
13
+ const params = [];
14
+ if (filters.squad_id) {
15
+ conditions.push("squad_id = ?");
16
+ params.push(filters.squad_id);
17
+ }
18
+ if (filters.agent_id) {
19
+ conditions.push("agent_id = ?");
20
+ params.push(filters.agent_id);
21
+ }
22
+ if (filters.action_type) {
23
+ conditions.push("action_type = ?");
24
+ params.push(filters.action_type);
25
+ }
26
+ if (filters.from) {
27
+ conditions.push("created_at >= ?");
28
+ params.push(filters.from);
29
+ }
30
+ if (filters.to) {
31
+ conditions.push("created_at <= ?");
32
+ params.push(filters.to);
33
+ }
34
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
35
+ const limit = filters.limit ?? 50;
36
+ const offset = filters.offset ?? 0;
37
+ return db
38
+ .prepare(`SELECT * FROM audit_log ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`)
39
+ .all(...params, limit, offset);
40
+ }
41
+ export function countAuditLog(filters = {}) {
42
+ const db = getDb();
43
+ const conditions = [];
44
+ const params = [];
45
+ if (filters.squad_id) {
46
+ conditions.push("squad_id = ?");
47
+ params.push(filters.squad_id);
48
+ }
49
+ if (filters.agent_id) {
50
+ conditions.push("agent_id = ?");
51
+ params.push(filters.agent_id);
52
+ }
53
+ if (filters.action_type) {
54
+ conditions.push("action_type = ?");
55
+ params.push(filters.action_type);
56
+ }
57
+ if (filters.from) {
58
+ conditions.push("created_at >= ?");
59
+ params.push(filters.from);
60
+ }
61
+ if (filters.to) {
62
+ conditions.push("created_at <= ?");
63
+ params.push(filters.to);
64
+ }
65
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
66
+ const row = db
67
+ .prepare(`SELECT COUNT(*) as count FROM audit_log ${where}`)
68
+ .get(...params);
69
+ return row.count;
70
+ }
71
+ //# sourceMappingURL=audit-log.js.map
@@ -0,0 +1,150 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { getDb } from "./db.js";
3
+ function toMessage(row) {
4
+ return {
5
+ id: row.id,
6
+ conversationId: row.conversation_id,
7
+ role: row.role,
8
+ content: row.content,
9
+ source: row.source,
10
+ createdAt: row.created_at,
11
+ };
12
+ }
13
+ function toSummary(row) {
14
+ return {
15
+ id: row.id,
16
+ preview: row.preview,
17
+ messageCount: row.message_count,
18
+ startedAt: row.started_at,
19
+ updatedAt: row.updated_at,
20
+ };
21
+ }
22
+ export function saveMessage(conversationId, role, content, source) {
23
+ const db = getDb();
24
+ const id = randomUUID();
25
+ db.prepare("INSERT INTO conversation_messages (id, conversation_id, role, content, source) VALUES (?, ?, ?, ?, ?)").run(id, conversationId, role, content, source);
26
+ const row = db
27
+ .prepare("SELECT * FROM conversation_messages WHERE id = ?")
28
+ .get(id);
29
+ return toMessage(row);
30
+ }
31
+ export function getConversation(conversationId) {
32
+ const db = getDb();
33
+ const rows = db
34
+ .prepare("SELECT * FROM conversation_messages WHERE conversation_id = ? ORDER BY created_at ASC")
35
+ .all(conversationId);
36
+ return rows.map(toMessage);
37
+ }
38
+ export function listConversations(opts = {}) {
39
+ const db = getDb();
40
+ const limit = opts.limit ?? 50;
41
+ const offset = opts.offset ?? 0;
42
+ let where = "WHERE 1=1";
43
+ const params = [];
44
+ if (opts.from) {
45
+ where += " AND started_at >= ?";
46
+ params.push(opts.from);
47
+ }
48
+ if (opts.to) {
49
+ where += " AND updated_at <= ?";
50
+ params.push(opts.to);
51
+ }
52
+ const countRow = db
53
+ .prepare(`SELECT COUNT(*) as cnt FROM (
54
+ SELECT conversation_id as id,
55
+ MIN(created_at) as started_at,
56
+ MAX(created_at) as updated_at
57
+ FROM conversation_messages
58
+ GROUP BY conversation_id
59
+ ) ${where}`)
60
+ .get(...params);
61
+ const total = countRow.cnt;
62
+ const rows = db
63
+ .prepare(`SELECT
64
+ sub.id,
65
+ sub.started_at,
66
+ sub.updated_at,
67
+ sub.message_count,
68
+ first_msg.content as preview
69
+ FROM (
70
+ SELECT conversation_id as id,
71
+ MIN(created_at) as started_at,
72
+ MAX(created_at) as updated_at,
73
+ COUNT(*) as message_count
74
+ FROM conversation_messages
75
+ GROUP BY conversation_id
76
+ ) sub
77
+ JOIN conversation_messages first_msg
78
+ ON first_msg.conversation_id = sub.id
79
+ AND first_msg.role = 'user'
80
+ AND first_msg.created_at = (
81
+ SELECT MIN(created_at) FROM conversation_messages
82
+ WHERE conversation_id = sub.id AND role = 'user'
83
+ )
84
+ ${where}
85
+ ORDER BY sub.updated_at DESC
86
+ LIMIT ? OFFSET ?`)
87
+ .all(...params, limit, offset);
88
+ return { items: rows.map(toSummary), total };
89
+ }
90
+ export function searchConversations(query, opts = {}) {
91
+ const db = getDb();
92
+ const limit = opts.limit ?? 50;
93
+ const offset = opts.offset ?? 0;
94
+ let dateWhere = "";
95
+ const dateParams = [];
96
+ if (opts.from) {
97
+ dateWhere += " AND cm.created_at >= ?";
98
+ dateParams.push(opts.from);
99
+ }
100
+ if (opts.to) {
101
+ dateWhere += " AND cm.created_at <= ?";
102
+ dateParams.push(opts.to);
103
+ }
104
+ // Find distinct conversation IDs matching FTS query
105
+ const matchingIds = db
106
+ .prepare(`SELECT DISTINCT cm.conversation_id
107
+ FROM conversation_messages cm
108
+ JOIN conversation_messages_fts fts ON fts.rowid = cm.rowid
109
+ WHERE conversation_messages_fts MATCH ?${dateWhere}
110
+ LIMIT 500`)
111
+ .all(query, ...dateParams);
112
+ if (matchingIds.length === 0) {
113
+ return { items: [], total: 0 };
114
+ }
115
+ const idList = matchingIds.map((r) => r.conversation_id);
116
+ const placeholders = idList.map(() => "?").join(",");
117
+ const total = idList.length;
118
+ const rows = db
119
+ .prepare(`SELECT
120
+ sub.id,
121
+ sub.started_at,
122
+ sub.updated_at,
123
+ sub.message_count,
124
+ first_msg.content as preview
125
+ FROM (
126
+ SELECT conversation_id as id,
127
+ MIN(created_at) as started_at,
128
+ MAX(created_at) as updated_at,
129
+ COUNT(*) as message_count
130
+ FROM conversation_messages
131
+ WHERE conversation_id IN (${placeholders})
132
+ GROUP BY conversation_id
133
+ ) sub
134
+ JOIN conversation_messages first_msg
135
+ ON first_msg.conversation_id = sub.id
136
+ AND first_msg.role = 'user'
137
+ AND first_msg.created_at = (
138
+ SELECT MIN(created_at) FROM conversation_messages
139
+ WHERE conversation_id = sub.id AND role = 'user'
140
+ )
141
+ ORDER BY sub.updated_at DESC
142
+ LIMIT ? OFFSET ?`)
143
+ .all(...idList, limit, offset);
144
+ return { items: rows.map(toSummary), total };
145
+ }
146
+ export function deleteConversation(conversationId) {
147
+ const db = getDb();
148
+ db.prepare("DELETE FROM conversation_messages WHERE conversation_id = ?").run(conversationId);
149
+ }
150
+ //# sourceMappingURL=conversations.js.map