heyio 1.2.4 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/dist/api/server.js +289 -12
  2. package/dist/config.js +6 -0
  3. package/dist/copilot/agents.js +100 -5
  4. package/dist/copilot/ceremonies.js +12 -2
  5. package/dist/copilot/io-scheduler.js +9 -1
  6. package/dist/copilot/orchestrator.js +4 -0
  7. package/dist/copilot/scheduler.js +7 -2
  8. package/dist/copilot/skills.js +138 -6
  9. package/dist/copilot/squad-tools.js +102 -0
  10. package/dist/copilot/system-message.js +2 -1
  11. package/dist/copilot/token-tracker.js +89 -0
  12. package/dist/copilot/tools.js +27 -5
  13. package/dist/copilot/trigger-schedule.js +31 -0
  14. package/dist/paths.js +1 -0
  15. package/dist/store/agent-events.js +19 -0
  16. package/dist/store/audit-log.js +71 -0
  17. package/dist/store/conversations.js +150 -0
  18. package/dist/store/db.js +111 -0
  19. package/dist/store/schedules.js +9 -1
  20. package/dist/store/squad-colors.js +23 -0
  21. package/dist/store/squads.js +6 -1
  22. package/dist/store/tasks.js +43 -0
  23. package/dist/store/token-usage.js +94 -0
  24. package/dist/wiki/backlinks.js +51 -0
  25. package/dist/wiki/fs.js +63 -1
  26. package/dist/wiki/search.js +13 -2
  27. package/package.json +1 -1
  28. package/web-dist/assets/AuditLogView-DqxVzjd_.js +6 -0
  29. package/web-dist/assets/ChatView-BBopM_A3.js +1 -0
  30. package/web-dist/assets/FeedView-Bo4p1stx.js +6 -0
  31. package/web-dist/assets/HistoryView-ChTuQvXr.js +1 -0
  32. package/web-dist/assets/LoginView-AnOP3Mau.js +1 -0
  33. package/web-dist/assets/McpView-DPcihjuB.js +1 -0
  34. package/web-dist/assets/SchedulesView-B2o3vMm-.js +6 -0
  35. package/web-dist/assets/SettingsView-rtMUmH43.js +1 -0
  36. package/web-dist/assets/SkillsView-D_NHLk7C.js +15 -0
  37. package/web-dist/assets/SquadDetailView-BKXLWvwn.js +26 -0
  38. package/web-dist/assets/SquadHealthView-CVJiAgVW.js +11 -0
  39. package/web-dist/assets/SquadsView-fammrB7r.js +6 -0
  40. package/web-dist/assets/UsageView-Cy5Mbprb.js +16 -0
  41. package/web-dist/assets/WikiView-B5TOMnOg.js +36 -0
  42. package/web-dist/assets/arrow-left-CGMB1w_A.js +6 -0
  43. package/web-dist/assets/git-branch-C_Hu39uh.js +6 -0
  44. package/web-dist/assets/index-CQ_szaoT.css +1 -0
  45. package/web-dist/assets/index-CiZnRvN4.js +253 -0
  46. package/web-dist/assets/{plus-Cvp1w2CO.js → plus-DIBAaEMT.js} +1 -1
  47. package/web-dist/assets/{x-O3fBd1Cr.js → save-Chqlu7QA.js} +2 -7
  48. package/web-dist/assets/search-Cl8HcIsG.js +6 -0
  49. package/web-dist/assets/squad-colors-B8B_Y-lz.js +1 -0
  50. package/web-dist/assets/{trash-2-Cr3vrmL5.js → trash-2-CQSzbVIr.js} +1 -1
  51. package/web-dist/assets/triangle-alert-C1OjMvP5.js +6 -0
  52. package/web-dist/assets/x-DThJHYFm.js +6 -0
  53. package/web-dist/favicon.svg +9 -3
  54. package/web-dist/index.html +2 -2
  55. package/web-dist/logo.svg +10 -0
  56. package/web-dist/assets/ChatView-mZaaw3pd.js +0 -11
  57. package/web-dist/assets/FeedView-BHacQwXQ.js +0 -6
  58. package/web-dist/assets/LoginView-B6aSD9II.js +0 -1
  59. package/web-dist/assets/MarkdownContent.vue_vue_type_script_setup_true_lang-CEo_ckIb.js +0 -56
  60. package/web-dist/assets/McpView-BAVRUHIE.js +0 -1
  61. package/web-dist/assets/SchedulesView-dOd1SQiP.js +0 -1
  62. package/web-dist/assets/SettingsView-CCDeEsVg.js +0 -1
  63. package/web-dist/assets/SkillsView-gCfQ35FQ.js +0 -1
  64. package/web-dist/assets/SquadDetailView-CQhFfZTc.js +0 -21
  65. package/web-dist/assets/SquadsView-CZFxtOao.js +0 -6
  66. package/web-dist/assets/WikiView-B0cuUFfm.js +0 -26
  67. package/web-dist/assets/api-DdW5uOZf.js +0 -1
  68. package/web-dist/assets/index-BQdXxKfc.js +0 -138
  69. package/web-dist/assets/index-BbSJ0cfF.css +0 -1
@@ -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
package/dist/store/db.js CHANGED
@@ -2,6 +2,7 @@ import Database from "better-sqlite3";
2
2
  import { existsSync, mkdirSync } from "node:fs";
3
3
  import { dirname } from "node:path";
4
4
  import { PATHS } from "../paths.js";
5
+ import { pickSquadColor } from "./squad-colors.js";
5
6
  let db;
6
7
  export function getDb() {
7
8
  if (db)
@@ -128,6 +129,116 @@ function runMigrations(db) {
128
129
  }
129
130
  setSchemaVersion(db, 2);
130
131
  }
132
+ if (version < 3) {
133
+ db.exec(`
134
+ CREATE TABLE IF NOT EXISTS agent_events (
135
+ id TEXT PRIMARY KEY,
136
+ task_id TEXT NOT NULL,
137
+ type TEXT NOT NULL,
138
+ summary TEXT NOT NULL DEFAULT '',
139
+ payload TEXT NOT NULL DEFAULT '{}',
140
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
141
+ );
142
+
143
+ CREATE INDEX IF NOT EXISTS idx_agent_events_task_id ON agent_events (task_id);
144
+ `);
145
+ setSchemaVersion(db, 3);
146
+ }
147
+ if (version < 4) {
148
+ db.exec(`
149
+ ALTER TABLE squads ADD COLUMN color TEXT;
150
+ `);
151
+ const squads = db
152
+ .prepare("SELECT id FROM squads WHERE color IS NULL ORDER BY created_at")
153
+ .all();
154
+ const update = db.prepare("UPDATE squads SET color = ? WHERE id = ?");
155
+ const usedColors = [];
156
+ for (let i = 0; i < squads.length; i++) {
157
+ const color = pickSquadColor(usedColors);
158
+ usedColors.push(color);
159
+ update.run(color, squads[i].id);
160
+ }
161
+ setSchemaVersion(db, 4);
162
+ }
163
+ if (version < 5) {
164
+ db.exec(`
165
+ CREATE TABLE IF NOT EXISTS conversation_messages (
166
+ id TEXT PRIMARY KEY,
167
+ conversation_id TEXT NOT NULL,
168
+ role TEXT NOT NULL CHECK(role IN ('user', 'assistant')),
169
+ content TEXT NOT NULL,
170
+ source TEXT NOT NULL DEFAULT 'web',
171
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
172
+ );
173
+
174
+ CREATE INDEX IF NOT EXISTS idx_conv_messages_conversation_id
175
+ ON conversation_messages(conversation_id);
176
+
177
+ CREATE INDEX IF NOT EXISTS idx_conv_messages_created_at
178
+ ON conversation_messages(created_at);
179
+
180
+ CREATE VIRTUAL TABLE IF NOT EXISTS conversation_messages_fts
181
+ USING fts5(content, content=conversation_messages, content_rowid=rowid);
182
+
183
+ CREATE TRIGGER IF NOT EXISTS conv_messages_fts_insert
184
+ AFTER INSERT ON conversation_messages BEGIN
185
+ INSERT INTO conversation_messages_fts(rowid, content) VALUES (new.rowid, new.content);
186
+ END;
187
+
188
+ CREATE TRIGGER IF NOT EXISTS conv_messages_fts_update
189
+ AFTER UPDATE ON conversation_messages BEGIN
190
+ INSERT INTO conversation_messages_fts(conversation_messages_fts, rowid, content) VALUES ('delete', old.rowid, old.content);
191
+ INSERT INTO conversation_messages_fts(rowid, content) VALUES (new.rowid, new.content);
192
+ END;
193
+
194
+ CREATE TRIGGER IF NOT EXISTS conv_messages_fts_delete
195
+ AFTER DELETE ON conversation_messages BEGIN
196
+ INSERT INTO conversation_messages_fts(conversation_messages_fts, rowid, content) VALUES ('delete', old.rowid, old.content);
197
+ END;
198
+ `);
199
+ setSchemaVersion(db, 5);
200
+ }
201
+ if (version < 6) {
202
+ db.exec(`
203
+ CREATE TABLE IF NOT EXISTS audit_log (
204
+ id TEXT PRIMARY KEY,
205
+ squad_id TEXT REFERENCES squads(id) ON DELETE SET NULL,
206
+ agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,
207
+ task_id TEXT,
208
+ action_type TEXT NOT NULL,
209
+ summary TEXT NOT NULL DEFAULT '',
210
+ payload TEXT NOT NULL DEFAULT '{}',
211
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
212
+ );
213
+
214
+ CREATE INDEX IF NOT EXISTS idx_audit_log_squad_id ON audit_log (squad_id);
215
+ CREATE INDEX IF NOT EXISTS idx_audit_log_agent_id ON audit_log (agent_id);
216
+ CREATE INDEX IF NOT EXISTS idx_audit_log_action_type ON audit_log (action_type);
217
+ CREATE INDEX IF NOT EXISTS idx_audit_log_created_at ON audit_log (created_at);
218
+ `);
219
+ setSchemaVersion(db, 6);
220
+ }
221
+ if (version < 7) {
222
+ db.exec(`
223
+ CREATE TABLE IF NOT EXISTS token_usage (
224
+ id TEXT PRIMARY KEY,
225
+ squad_id TEXT REFERENCES squads(id) ON DELETE CASCADE,
226
+ agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,
227
+ task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
228
+ model TEXT NOT NULL,
229
+ input_tokens INTEGER NOT NULL DEFAULT 0,
230
+ output_tokens INTEGER NOT NULL DEFAULT 0,
231
+ cost_usd REAL NOT NULL DEFAULT 0,
232
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
233
+ );
234
+
235
+ CREATE INDEX IF NOT EXISTS idx_token_usage_squad ON token_usage(squad_id);
236
+ CREATE INDEX IF NOT EXISTS idx_token_usage_agent ON token_usage(agent_id);
237
+ CREATE INDEX IF NOT EXISTS idx_token_usage_task ON token_usage(task_id);
238
+ CREATE INDEX IF NOT EXISTS idx_token_usage_created ON token_usage(created_at);
239
+ `);
240
+ setSchemaVersion(db, 7);
241
+ }
131
242
  }
132
243
  function getSchemaVersion(db) {
133
244
  const row = db.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get();
@@ -1,10 +1,18 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { getDb } from "./db.js";
3
3
  export function createSchedule(input) {
4
+ const squadId = input.squad_id.trim();
5
+ if (!squadId) {
6
+ throw new Error("squad_id is required");
7
+ }
4
8
  const db = getDb();
5
9
  const id = randomUUID();
6
10
  db.prepare(`INSERT INTO schedules (id, type, squad_id, cron, agenda, prompt)
7
- VALUES (?, ?, ?, ?, ?, ?)`).run(id, input.type, input.squad_id ?? null, input.cron, input.agenda ?? "", input.prompt ?? "");
11
+ VALUES (?, ?, ?, ?, ?, ?)`).run(id, input.type, squadId, input.cron, input.agenda ?? "", input.prompt ?? "");
12
+ return db.prepare("SELECT * FROM schedules WHERE id = ?").get(id);
13
+ }
14
+ export function getSchedule(id) {
15
+ const db = getDb();
8
16
  return db.prepare("SELECT * FROM schedules WHERE id = ?").get(id);
9
17
  }
10
18
  export function listSchedules(type) {
@@ -0,0 +1,23 @@
1
+ // Palette complementary to the site's brand gradient (magenta #E43A9C → violet #F041FF)
2
+ // Avoiding direct brand colors; using analogous/complementary hues that look cohesive
3
+ export const SQUAD_COLOR_PALETTE = [
4
+ "#06b6d4", // cyan (complementary to magenta)
5
+ "#14b8a6", // teal
6
+ "#f59e0b", // amber (warm contrast)
7
+ "#8b5cf6", // violet (analogous)
8
+ "#3b82f6", // blue
9
+ "#10b981", // emerald
10
+ "#f43f5e", // rose (analogous warm)
11
+ "#6366f1", // indigo
12
+ ];
13
+ const DEFAULT_SQUAD_COLOR = "#3b82f6";
14
+ export function pickSquadColor(usedColors, random = Math.random) {
15
+ const used = new Set(usedColors.filter(Boolean).map((c) => c.toLowerCase()));
16
+ const available = SQUAD_COLOR_PALETTE.filter((c) => !used.has(c.toLowerCase()));
17
+ const source = available.length > 0 ? available : SQUAD_COLOR_PALETTE;
18
+ if (source.length === 0)
19
+ return DEFAULT_SQUAD_COLOR;
20
+ const index = Math.floor(random() * source.length);
21
+ return source[index] ?? DEFAULT_SQUAD_COLOR;
22
+ }
23
+ //# sourceMappingURL=squad-colors.js.map
@@ -1,10 +1,15 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { getDb } from "./db.js";
3
+ import { pickSquadColor } from "./squad-colors.js";
3
4
  export function createSquad(name, universe, repoUrl) {
4
5
  const db = getDb();
5
6
  const id = randomUUID();
6
7
  const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
7
- db.prepare("INSERT INTO squads (id, name, slug, universe, repo_url) VALUES (?, ?, ?, ?, ?)").run(id, name, slug, universe, repoUrl ?? null);
8
+ const usedColors = db
9
+ .prepare("SELECT color FROM squads WHERE color IS NOT NULL")
10
+ .all();
11
+ const color = pickSquadColor(usedColors.map((row) => row.color));
12
+ db.prepare("INSERT INTO squads (id, name, slug, universe, color, repo_url) VALUES (?, ?, ?, ?, ?, ?)").run(id, name, slug, universe, color, repoUrl ?? null);
8
13
  return db.prepare("SELECT * FROM squads WHERE id = ?").get(id);
9
14
  }
10
15
  export function getSquad(id) {
@@ -32,4 +32,47 @@ export function getActiveTasksForInstance(instanceId) {
32
32
  .prepare("SELECT * FROM tasks WHERE instance_id = ? AND status NOT IN ('done', 'failed') ORDER BY created_at")
33
33
  .all(instanceId);
34
34
  }
35
+ /** Squads with active tasks and no update within this window are flagged as stalled. */
36
+ const STALL_THRESHOLD_MS = 60 * 60 * 1000; // 60 minutes
37
+ export function getSquadTaskMetrics(squadId) {
38
+ const db = getDb();
39
+ const row = db
40
+ .prepare(`SELECT
41
+ COUNT(*) AS total,
42
+ SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END) AS completed,
43
+ SUM(CASE WHEN status = 'done' AND updated_at >= datetime('now', '-7 days') THEN 1 ELSE 0 END) AS completed_recent,
44
+ SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) AS pending,
45
+ SUM(CASE WHEN status = 'in_progress' THEN 1 ELSE 0 END) AS in_progress,
46
+ SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed,
47
+ AVG(CASE WHEN status = 'done'
48
+ THEN (julianday(updated_at) - julianday(created_at)) * 1440
49
+ ELSE NULL END) AS avg_cycle_minutes,
50
+ MAX(CASE WHEN status IN ('pending', 'in_progress') THEN updated_at ELSE NULL END) AS last_active_update
51
+ FROM tasks WHERE squad_id = ?`)
52
+ .get(squadId);
53
+ // A squad is considered stalled if it has active tasks but none have been
54
+ // updated within STALL_THRESHOLD_MS.
55
+ const hasActiveTasks = row.pending + row.in_progress > 0;
56
+ const isStalled = hasActiveTasks &&
57
+ row.last_active_update !== null &&
58
+ new Date(row.last_active_update + "Z").getTime() <
59
+ Date.now() - STALL_THRESHOLD_MS;
60
+ const recentTasks = db
61
+ .prepare("SELECT * FROM tasks WHERE squad_id = ? ORDER BY updated_at DESC LIMIT 5")
62
+ .all(squadId);
63
+ return {
64
+ squadId,
65
+ tasksTotal: row.total ?? 0,
66
+ tasksCompleted: row.completed ?? 0,
67
+ tasksCompletedRecent: row.completed_recent ?? 0,
68
+ tasksPending: row.pending ?? 0,
69
+ tasksInProgress: row.in_progress ?? 0,
70
+ tasksFailed: row.failed ?? 0,
71
+ avgCycleTimeMinutes: row.avg_cycle_minutes != null
72
+ ? Math.round(row.avg_cycle_minutes * 10) / 10
73
+ : null,
74
+ isStalled,
75
+ recentTasks,
76
+ };
77
+ }
35
78
  //# sourceMappingURL=tasks.js.map
@@ -0,0 +1,94 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { getDb } from "./db.js";
3
+ export function recordTokenUsage(params) {
4
+ const db = getDb();
5
+ const id = randomUUID();
6
+ db.prepare(`INSERT INTO token_usage (id, squad_id, agent_id, task_id, model, input_tokens, output_tokens, cost_usd)
7
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(id, params.squadId ?? null, params.agentId ?? null, params.taskId ?? null, params.model, params.inputTokens, params.outputTokens, params.costUsd);
8
+ return db.prepare("SELECT * FROM token_usage WHERE id = ?").get(id);
9
+ }
10
+ export function getTokenUsageSummary(opts) {
11
+ const db = getDb();
12
+ let sql = `SELECT
13
+ COUNT(*) as total_records,
14
+ COALESCE(SUM(input_tokens), 0) as total_input_tokens,
15
+ COALESCE(SUM(output_tokens), 0) as total_output_tokens,
16
+ COALESCE(SUM(input_tokens + output_tokens), 0) as total_tokens,
17
+ COALESCE(SUM(cost_usd), 0) as total_cost_usd
18
+ FROM token_usage WHERE 1=1`;
19
+ const params = [];
20
+ if (opts?.since) {
21
+ sql += " AND created_at >= ?";
22
+ params.push(opts.since);
23
+ }
24
+ return db.prepare(sql).get(...params);
25
+ }
26
+ export function getTokenUsageBySquad(opts) {
27
+ const db = getDb();
28
+ let sql = `SELECT
29
+ s.id,
30
+ s.name,
31
+ COALESCE(SUM(t.input_tokens), 0) as total_input_tokens,
32
+ COALESCE(SUM(t.output_tokens), 0) as total_output_tokens,
33
+ COALESCE(SUM(t.input_tokens + t.output_tokens), 0) as total_tokens,
34
+ COALESCE(SUM(t.cost_usd), 0) as total_cost_usd,
35
+ COUNT(t.id) as record_count
36
+ FROM squads s
37
+ LEFT JOIN token_usage t ON t.squad_id = s.id`;
38
+ const params = [];
39
+ if (opts?.since) {
40
+ sql += " AND t.created_at >= ?";
41
+ params.push(opts.since);
42
+ }
43
+ sql += " GROUP BY s.id ORDER BY total_tokens DESC";
44
+ return db.prepare(sql).all(...params);
45
+ }
46
+ export function getTokenUsageByAgent(opts) {
47
+ const db = getDb();
48
+ let sql = `SELECT
49
+ a.id,
50
+ a.character_name as name,
51
+ COALESCE(SUM(t.input_tokens), 0) as total_input_tokens,
52
+ COALESCE(SUM(t.output_tokens), 0) as total_output_tokens,
53
+ COALESCE(SUM(t.input_tokens + t.output_tokens), 0) as total_tokens,
54
+ COALESCE(SUM(t.cost_usd), 0) as total_cost_usd,
55
+ COUNT(t.id) as record_count
56
+ FROM agents a
57
+ LEFT JOIN token_usage t ON t.agent_id = a.id`;
58
+ const params = [];
59
+ const conditions = [];
60
+ if (opts?.squadId) {
61
+ conditions.push("a.squad_id = ?");
62
+ params.push(opts.squadId);
63
+ }
64
+ if (opts?.since) {
65
+ conditions.push("(t.created_at >= ? OR t.created_at IS NULL)");
66
+ params.push(opts.since);
67
+ }
68
+ if (conditions.length > 0) {
69
+ sql += " WHERE " + conditions.join(" AND ");
70
+ }
71
+ sql += " GROUP BY a.id ORDER BY total_tokens DESC";
72
+ return db.prepare(sql).all(...params);
73
+ }
74
+ export function getDailyTokenUsage(days = 30) {
75
+ const db = getDb();
76
+ const sql = `SELECT
77
+ date(created_at) as date,
78
+ COALESCE(SUM(input_tokens), 0) as total_input_tokens,
79
+ COALESCE(SUM(output_tokens), 0) as total_output_tokens,
80
+ COALESCE(SUM(input_tokens + output_tokens), 0) as total_tokens,
81
+ COALESCE(SUM(cost_usd), 0) as total_cost_usd
82
+ FROM token_usage
83
+ WHERE created_at >= date('now', '-' || ? || ' days')
84
+ GROUP BY date(created_at)
85
+ ORDER BY date ASC`;
86
+ return db.prepare(sql).all(days);
87
+ }
88
+ export function getTokenUsageForTask(taskId) {
89
+ const db = getDb();
90
+ return db
91
+ .prepare("SELECT * FROM token_usage WHERE task_id = ? ORDER BY created_at ASC")
92
+ .all(taskId);
93
+ }
94
+ //# sourceMappingURL=token-usage.js.map
@@ -0,0 +1,51 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join, dirname, resolve } from "node:path";
3
+ import { PATHS } from "../paths.js";
4
+ import { listPages } from "./fs.js";
5
+ export async function getBacklinks(targetPath) {
6
+ if (!existsSync(PATHS.wikiPages))
7
+ return [];
8
+ const pages = await listPages();
9
+ const backlinks = [];
10
+ // Normalize the target path (ensure .md extension)
11
+ const normalizedTarget = targetPath.endsWith(".md") ? targetPath : `${targetPath}.md`;
12
+ for (const pagePath of pages) {
13
+ if (pagePath === normalizedTarget)
14
+ continue; // Skip self-references
15
+ const fullPath = join(PATHS.wikiPages, pagePath);
16
+ const content = readFileSync(fullPath, "utf-8");
17
+ if (pageLinksTo(content, pagePath, normalizedTarget)) {
18
+ backlinks.push(pagePath);
19
+ }
20
+ }
21
+ return backlinks;
22
+ }
23
+ function pageLinksTo(content, fromPage, targetPage) {
24
+ const fromDir = dirname(fromPage);
25
+ const targetWithoutExt = targetPage.replace(/\.md$/, "");
26
+ // Check standard markdown links: [text](url)
27
+ const markdownLinkRegex = /\[([^\]]*)\]\(([^)]+)\)/g;
28
+ let match;
29
+ while ((match = markdownLinkRegex.exec(content)) !== null) {
30
+ const linkHref = match[2].split("#")[0].trim(); // Strip anchors and whitespace
31
+ if (!linkHref || linkHref.startsWith("http://") || linkHref.startsWith("https://"))
32
+ continue;
33
+ const resolvedLink = resolve("/", fromDir, linkHref).slice(1); // Resolve relative path, strip leading /
34
+ const resolvedWithoutExt = resolvedLink.endsWith(".md") ? resolvedLink.slice(0, -3) : resolvedLink;
35
+ if (resolvedWithoutExt === targetWithoutExt)
36
+ return true;
37
+ }
38
+ // Check wiki-style links: [[page]] or [[page|display text]]
39
+ // Wiki-style links are resolved from the wiki root (absolute), not relative to the current page
40
+ const wikiLinkRegex = /\[\[([^\]]+)\]\]/g;
41
+ while ((match = wikiLinkRegex.exec(content)) !== null) {
42
+ const linkTarget = match[1].split("|")[0].trim(); // Handle [[page|display text]]
43
+ if (!linkTarget)
44
+ continue;
45
+ const resolvedWithoutExt = linkTarget.endsWith(".md") ? linkTarget.slice(0, -3) : linkTarget;
46
+ if (resolvedWithoutExt === targetWithoutExt)
47
+ return true;
48
+ }
49
+ return false;
50
+ }
51
+ //# sourceMappingURL=backlinks.js.map
package/dist/wiki/fs.js CHANGED
@@ -1,7 +1,19 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync, cpSync } from "node:fs";
2
2
  import { join, dirname } from "node:path";
3
3
  import { readdirSync } from "node:fs";
4
4
  import { PATHS } from "../paths.js";
5
+ function safeJoin(base, userPath) {
6
+ // Strip path traversal by removing any '..' and absolute path components
7
+ const sanitized = userPath
8
+ .split(/[/\\]/)
9
+ .filter((segment) => segment !== ".." && segment !== ".")
10
+ .join("/");
11
+ const resolved = join(base, sanitized);
12
+ if (!resolved.startsWith(base)) {
13
+ throw new Error("Invalid path: traversal outside base directory is not allowed");
14
+ }
15
+ return resolved;
16
+ }
5
17
  export async function readPage(path) {
6
18
  const fullPath = join(PATHS.wikiPages, path);
7
19
  if (!existsSync(fullPath)) {
@@ -43,4 +55,54 @@ export async function listPages(dir) {
43
55
  walk(root, "");
44
56
  return results;
45
57
  }
58
+ export async function listTemplates() {
59
+ const root = PATHS.wikiSquadTemplates;
60
+ if (!existsSync(root))
61
+ return [];
62
+ const results = [];
63
+ const walk = (current, prefix) => {
64
+ const entries = readdirSync(current, { withFileTypes: true });
65
+ for (const entry of entries) {
66
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
67
+ if (entry.isDirectory()) {
68
+ walk(join(current, entry.name), rel);
69
+ }
70
+ else {
71
+ results.push(rel);
72
+ }
73
+ }
74
+ };
75
+ walk(root, "");
76
+ return results;
77
+ }
78
+ export async function readTemplate(path) {
79
+ const fullPath = safeJoin(PATHS.wikiSquadTemplates, path);
80
+ if (!existsSync(fullPath)) {
81
+ throw new Error(`Template not found: ${path}`);
82
+ }
83
+ return readFileSync(fullPath, "utf-8");
84
+ }
85
+ export async function writeTemplate(path, content) {
86
+ const fullPath = safeJoin(PATHS.wikiSquadTemplates, path);
87
+ const dir = dirname(fullPath);
88
+ if (!existsSync(dir))
89
+ mkdirSync(dir, { recursive: true });
90
+ writeFileSync(fullPath, content);
91
+ }
92
+ export async function deleteTemplate(path) {
93
+ const fullPath = safeJoin(PATHS.wikiSquadTemplates, path);
94
+ if (!existsSync(fullPath)) {
95
+ throw new Error(`Template not found: ${path}`);
96
+ }
97
+ rmSync(fullPath);
98
+ }
99
+ export async function copySquadTemplates(slug) {
100
+ const templateDir = PATHS.wikiSquadTemplates;
101
+ if (!existsSync(templateDir))
102
+ return;
103
+ const destDir = join(PATHS.wikiPages, "squads", slug);
104
+ if (!existsSync(destDir))
105
+ mkdirSync(destDir, { recursive: true });
106
+ cpSync(templateDir, destDir, { recursive: true, force: false, errorOnExist: false });
107
+ }
46
108
  //# sourceMappingURL=fs.js.map
@@ -2,7 +2,18 @@ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { PATHS } from "../paths.js";
4
4
  export async function searchPages(query) {
5
- if (!existsSync(PATHS.wikiPages))
5
+ return searchInDirectory(PATHS.wikiPages, query);
6
+ }
7
+ /**
8
+ * Search wiki pages within a specific subfolder (e.g., "squads/my-squad").
9
+ * Results have paths relative to the subfolder.
10
+ */
11
+ export async function searchSquadPages(query, subdir) {
12
+ const root = join(PATHS.wikiPages, subdir);
13
+ return searchInDirectory(root, query);
14
+ }
15
+ function searchInDirectory(root, query) {
16
+ if (!existsSync(root))
6
17
  return [];
7
18
  const results = [];
8
19
  const lower = query.toLowerCase();
@@ -30,7 +41,7 @@ export async function searchPages(query) {
30
41
  }
31
42
  }
32
43
  };
33
- walk(PATHS.wikiPages, "");
44
+ walk(root, "");
34
45
  return results;
35
46
  }
36
47
  //# sourceMappingURL=search.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "heyio",
3
- "version": "1.2.4",
3
+ "version": "1.4.0",
4
4
  "description": "IO — a personal AI assistant daemon built on the GitHub Copilot SDK",
5
5
  "bin": {
6
6
  "io": "dist/index.js"
@@ -0,0 +1,6 @@
1
+ import{q as W,t as X,x as Z,z as a,o as d,l as o,s as A,K as N,a as V,r as S,J as l,Y as f,T,F as h,E as w,V as F,n as b,D as n,h as L,k as _,v as ee}from"./index-CiZnRvN4.js";/**
2
+ * @license lucide-vue-next v0.474.0 - ISC
3
+ *
4
+ * This source code is licensed under the ISC license.
5
+ * See the LICENSE file in the root directory of this source tree.
6
+ */const te=W("FilterIcon",[["polygon",{points:"22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3",key:"1yg77f"}]]),oe={class:"p-6"},se={class:"flex items-center justify-between mb-6"},ae={class:"text-2xl font-bold flex items-center gap-2"},de={class:"text-sm text-muted-foreground"},le={class:"border border-border rounded-lg p-4 mb-6 space-y-3"},re={class:"flex items-center gap-2 text-sm font-medium text-muted-foreground mb-1"},ne={class:"grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-3"},ue=["value"],ie=["value"],ce=["value"],pe={key:0,class:"text-muted-foreground"},ve={key:1,class:"text-center py-12 text-muted-foreground"},me={key:2,class:"space-y-1"},ge=["onClick"],xe={class:"flex-1 min-w-0"},fe={class:"flex flex-wrap items-center gap-2 mb-1"},be={key:0,class:"text-xs bg-secondary text-secondary-foreground px-2 py-0.5 rounded-full"},_e={key:1,class:"text-xs bg-secondary text-secondary-foreground px-2 py-0.5 rounded-full"},ye={class:"text-sm truncate"},ke={class:"text-xs text-muted-foreground mt-0.5"},he={class:"text-xs text-muted-foreground mt-1 shrink-0"},we={key:0,class:"border-t border-border px-4 py-3"},qe={key:0,class:"text-xs text-muted-foreground mb-2"},Ce={class:"font-mono"},Pe={class:"text-xs bg-muted rounded p-3 overflow-x-auto whitespace-pre-wrap break-words"},Ae={key:3,class:"flex items-center justify-between mt-4"},Ne=["disabled"],Se={class:"text-xs text-muted-foreground"},Te=["disabled"],u=50,Me=X({__name:"AuditLogView",setup(Ve){const q=n([]),c=n(0),C=n(!0),y=n(null),P=n([]),k=n([]),i=n(""),p=n(""),v=n(""),m=n(""),g=n(""),r=n(0),M=["message_received","task_delegated","task_completed","task_failed","shell_command","squad_created","squad_meeting"],I={message_received:"bg-blue-500/20 text-blue-400",task_delegated:"bg-purple-500/20 text-purple-400",task_completed:"bg-green-500/20 text-green-400",task_failed:"bg-red-500/20 text-red-400",shell_command:"bg-yellow-500/20 text-yellow-400",squad_created:"bg-indigo-500/20 text-indigo-400",squad_meeting:"bg-teal-500/20 text-teal-400"};function O(s){return I[s]??"bg-secondary text-secondary-foreground"}async function x(){C.value=!0;try{const s=new URLSearchParams;i.value&&s.set("squad_id",i.value),p.value&&s.set("agent_id",p.value),v.value&&s.set("action_type",v.value),m.value&&s.set("from",m.value),g.value&&s.set("to",g.value),s.set("limit",String(u)),s.set("offset",String(r.value));const t=await L(`/audit-log?${s.toString()}`);q.value=t.entries,c.value=t.total}finally{C.value=!1}}async function U(){const s=await L("/squads");P.value=s.squads,k.value=s.agents}function D(){r.value=0,x()}function $(){i.value="",p.value="",v.value="",m.value="",g.value="",r.value=0,x()}function E(){r.value>0&&(r.value=Math.max(0,r.value-u),x())}function B(){r.value+u<c.value&&(r.value=r.value+u,x())}function J(s){y.value=y.value===s?null:s}function R(s){try{return JSON.stringify(JSON.parse(s.payload),null,2)}catch{return s.payload}}function Y(s){var t;return s?((t=P.value.find(e=>e.id===s))==null?void 0:t.name)??s.slice(0,8):""}function j(s){if(!s)return"";const t=k.value.find(e=>e.id===s);return t?`${t.character_name} (${t.role_title})`:s.slice(0,8)}const z=_(()=>r.value+u<c.value),G=_(()=>r.value>0),K=_(()=>Math.floor(r.value/u)+1),H=_(()=>Math.max(1,Math.ceil(c.value/u))),Q=_(()=>i.value?k.value.filter(s=>s.squad_id===i.value):k.value);return Z(async()=>{await Promise.all([x(),U()])}),(s,t)=>(a(),d("div",oe,[o("div",se,[o("h1",ae,[A(N(V),{class:"w-6 h-6"}),t[6]||(t[6]=S(" Audit Log ",-1))]),o("span",de,l(c.value)+" entries",1)]),o("div",le,[o("div",re,[A(N(te),{class:"w-4 h-4"}),t[7]||(t[7]=S(" Filters ",-1))]),o("div",ne,[f(o("select",{"onUpdate:modelValue":t[0]||(t[0]=e=>i.value=e),class:"px-2 py-1.5 text-sm rounded-md border border-border bg-background",onChange:t[1]||(t[1]=e=>p.value="")},[t[8]||(t[8]=o("option",{value:""},"All squads",-1)),(a(!0),d(h,null,w(P.value,e=>(a(),d("option",{key:e.id,value:e.id},l(e.name),9,ue))),128))],544),[[T,i.value]]),f(o("select",{"onUpdate:modelValue":t[2]||(t[2]=e=>p.value=e),class:"px-2 py-1.5 text-sm rounded-md border border-border bg-background"},[t[9]||(t[9]=o("option",{value:""},"All agents",-1)),(a(!0),d(h,null,w(Q.value,e=>(a(),d("option",{key:e.id,value:e.id},l(e.character_name),9,ie))),128))],512),[[T,p.value]]),f(o("select",{"onUpdate:modelValue":t[3]||(t[3]=e=>v.value=e),class:"px-2 py-1.5 text-sm rounded-md border border-border bg-background"},[t[10]||(t[10]=o("option",{value:""},"All action types",-1)),(a(),d(h,null,w(M,e=>o("option",{key:e,value:e},l(e),9,ce)),64))],512),[[T,v.value]]),f(o("input",{"onUpdate:modelValue":t[4]||(t[4]=e=>m.value=e),type:"datetime-local",placeholder:"From",class:"px-2 py-1.5 text-sm rounded-md border border-border bg-background"},null,512),[[F,m.value]]),f(o("input",{"onUpdate:modelValue":t[5]||(t[5]=e=>g.value=e),type:"datetime-local",placeholder:"To",class:"px-2 py-1.5 text-sm rounded-md border border-border bg-background"},null,512),[[F,g.value]])]),o("div",{class:"flex gap-2"},[o("button",{onClick:D,class:"px-3 py-1.5 text-xs rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors"}," Apply "),o("button",{onClick:$,class:"px-3 py-1.5 text-xs rounded-md border border-border text-muted-foreground hover:text-foreground transition-colors"}," Reset ")])]),C.value?(a(),d("div",pe,"Loading...")):q.value.length===0?(a(),d("div",ve,[A(N(V),{class:"w-12 h-12 mx-auto mb-3 opacity-50"}),t[11]||(t[11]=o("p",null,"No audit log entries found.",-1))])):(a(),d("div",me,[(a(!0),d(h,null,w(q.value,e=>(a(),d("div",{key:e.id,class:"border border-border rounded-lg overflow-hidden"},[o("div",{onClick:Fe=>J(e.id),class:"flex items-start gap-3 px-4 py-3 cursor-pointer hover:bg-muted/50 transition-colors"},[o("div",xe,[o("div",fe,[o("span",{class:ee(["text-xs px-2 py-0.5 rounded-full font-mono",O(e.action_type)])},l(e.action_type),3),e.squad_id?(a(),d("span",be,l(Y(e.squad_id)),1)):b("",!0),e.agent_id?(a(),d("span",_e,l(j(e.agent_id)),1)):b("",!0)]),o("p",ye,l(e.summary),1),o("p",ke,l(e.created_at),1)]),o("span",he,l(y.value===e.id?"▲":"▼"),1)],8,ge),y.value===e.id?(a(),d("div",we,[e.task_id?(a(),d("div",qe,[t[12]||(t[12]=S(" Task ID: ",-1)),o("code",Ce,l(e.task_id),1)])):b("",!0),o("pre",Pe,l(R(e)),1)])):b("",!0)]))),128))])),c.value>u?(a(),d("div",Ae,[o("button",{disabled:!G.value,onClick:E,class:"px-3 py-1.5 text-xs rounded-md border border-border text-muted-foreground disabled:opacity-40 hover:text-foreground transition-colors"}," ← Previous ",8,Ne),o("span",Se," Page "+l(K.value)+" of "+l(H.value),1),o("button",{disabled:!z.value,onClick:B,class:"px-3 py-1.5 text-xs rounded-md border border-border text-muted-foreground disabled:opacity-40 hover:text-foreground transition-colors"}," Next → ",8,Te)])):b("",!0)]))}});export{Me as default};