heyio 3.0.2 → 3.0.4

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 +1 -1
  2. package/dist/api/server.js.map +1 -1
  3. package/dist/index.js +8 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/logging/logger.d.ts.map +1 -1
  6. package/dist/logging/logger.js +13 -1
  7. package/dist/logging/logger.js.map +1 -1
  8. package/node_modules/@io/shared/package.json +1 -1
  9. package/package.json +7 -2
  10. package/public/assets/index-2RY89H3W.js +336 -0
  11. package/public/assets/index-2RY89H3W.js.map +1 -0
  12. package/public/assets/index-D3cGfBsj.css +1 -0
  13. package/public/index.html +14 -0
  14. package/src/api/middleware/auth.ts +0 -76
  15. package/src/api/notifications.ts +0 -122
  16. package/src/api/routes/activity.ts +0 -29
  17. package/src/api/routes/attachments.ts +0 -93
  18. package/src/api/routes/config.ts +0 -115
  19. package/src/api/routes/conversations.ts +0 -87
  20. package/src/api/routes/health.ts +0 -18
  21. package/src/api/routes/inbox.ts +0 -98
  22. package/src/api/routes/schedules.ts +0 -121
  23. package/src/api/routes/skills.ts +0 -105
  24. package/src/api/routes/squads.ts +0 -145
  25. package/src/api/routes/usage.ts +0 -57
  26. package/src/api/routes/wiki.ts +0 -49
  27. package/src/api/server.ts +0 -186
  28. package/src/config.ts +0 -3
  29. package/src/copilot/client.ts +0 -42
  30. package/src/copilot/health-monitor.ts +0 -85
  31. package/src/copilot/orchestrator.ts +0 -222
  32. package/src/copilot/tools.ts +0 -707
  33. package/src/index.ts +0 -113
  34. package/src/logging/logger.ts +0 -26
  35. package/src/models/index.ts +0 -11
  36. package/src/models/pricing.ts +0 -121
  37. package/src/models/registry.ts +0 -131
  38. package/src/models/token-tracker.ts +0 -151
  39. package/src/scheduler/engine.ts +0 -146
  40. package/src/skills/index.ts +0 -13
  41. package/src/skills/store.ts +0 -188
  42. package/src/squad/agent.ts +0 -326
  43. package/src/squad/autonomy.ts +0 -78
  44. package/src/squad/event-bus.ts +0 -71
  45. package/src/squad/execution/index.ts +0 -17
  46. package/src/squad/execution/instance.ts +0 -186
  47. package/src/squad/execution/meeting.ts +0 -191
  48. package/src/squad/execution/pr.ts +0 -127
  49. package/src/squad/execution/runner.ts +0 -97
  50. package/src/squad/execution/tasks.ts +0 -111
  51. package/src/squad/execution/worktree.ts +0 -138
  52. package/src/squad/hiring.ts +0 -222
  53. package/src/squad/index.ts +0 -17
  54. package/src/squad/manager.ts +0 -337
  55. package/src/squad/name-generator.ts +0 -135
  56. package/src/squad/roles/templates.ts +0 -104
  57. package/src/squad/skill-parser.ts +0 -120
  58. package/src/squad/source-resolver.ts +0 -57
  59. package/src/store/activity.ts +0 -176
  60. package/src/store/db.ts +0 -237
  61. package/src/store/inbox.ts +0 -199
  62. package/src/store/schedules.ts +0 -199
  63. package/src/wiki/index.ts +0 -12
  64. package/src/wiki/store.ts +0 -139
  65. package/tsconfig.json +0 -9
@@ -1,57 +0,0 @@
1
- import { execSync } from 'node:child_process';
2
- import { existsSync, mkdirSync } from 'node:fs';
3
- import { join } from 'node:path';
4
- import { loadConfig } from '@io/shared';
5
- import { createChildLogger } from '../logging/logger.js';
6
-
7
- const logger = () => createChildLogger('source-resolver');
8
-
9
- /**
10
- * Parse a GitHub URL into owner/repo parts.
11
- * Supports https://github.com/owner/repo and https://github.com/owner/repo.git
12
- */
13
- export function parseGitHubUrl(url: string): { owner: string; repo: string } | null {
14
- const match = url.match(/github\.com\/([^/]+)\/([^/.]+)/);
15
- if (!match) return null;
16
- return { owner: match[1], repo: match[2] };
17
- }
18
-
19
- /**
20
- * Get the local source directory for a given GitHub repo URL.
21
- * Convention: ~/.io/source/{owner}/{repo}
22
- */
23
- export function getSourcePath(repoUrl: string): string | null {
24
- const parsed = parseGitHubUrl(repoUrl);
25
- if (!parsed) return null;
26
- const config = loadConfig();
27
- return join(config.dataDir, 'source', parsed.owner, parsed.repo);
28
- }
29
-
30
- /**
31
- * Ensure a repo is cloned locally. If the directory already exists and contains
32
- * a .git folder, it is assumed valid and left alone. Otherwise, it clones the repo.
33
- * Returns the absolute path to the local clone.
34
- */
35
- export function ensureCloned(repoUrl: string): string {
36
- const log = logger();
37
- const sourcePath = getSourcePath(repoUrl);
38
- if (!sourcePath) {
39
- throw new Error(`Cannot parse GitHub URL: ${repoUrl}`);
40
- }
41
-
42
- if (existsSync(join(sourcePath, '.git'))) {
43
- log.debug({ sourcePath }, 'Repo already cloned');
44
- return sourcePath;
45
- }
46
-
47
- log.info({ repoUrl, sourcePath }, 'Cloning repository');
48
- mkdirSync(sourcePath, { recursive: true });
49
-
50
- execSync(`git clone "${repoUrl}" "${sourcePath}"`, {
51
- stdio: 'pipe',
52
- timeout: 120_000,
53
- });
54
-
55
- log.info({ sourcePath }, 'Clone complete');
56
- return sourcePath;
57
- }
@@ -1,176 +0,0 @@
1
- import type { IOEvent } from '@io/shared';
2
- import { createChildLogger } from '../logging/logger.js';
3
- import { getDatabase } from './db.js';
4
-
5
- const logger = () => createChildLogger('activity-log');
6
-
7
- export type ActivityType =
8
- | 'tool_call'
9
- | 'message'
10
- | 'meeting_contribution'
11
- | 'task_start'
12
- | 'task_complete'
13
- | 'error';
14
-
15
- export interface ActivityEntry {
16
- id: number;
17
- squadId: string | null;
18
- instanceId: string | null;
19
- agentRole: string;
20
- activityType: ActivityType;
21
- modelUsed: string | null;
22
- content: string | null;
23
- tokensUsed: number | null;
24
- timestamp: string;
25
- }
26
-
27
- /**
28
- * Log an activity to the agent_activity table.
29
- */
30
- export async function logActivity(entry: {
31
- squadId?: string;
32
- instanceId?: string;
33
- agentRole: string;
34
- activityType: ActivityType;
35
- modelUsed?: string;
36
- content?: unknown;
37
- tokensUsed?: number;
38
- }): Promise<void> {
39
- const db = getDatabase();
40
- try {
41
- await db.execute({
42
- sql: `INSERT INTO agent_activity (squad_id, instance_id, agent_role, activity_type, model_used, content, tokens_used)
43
- VALUES (?, ?, ?, ?, ?, ?, ?)`,
44
- args: [
45
- entry.squadId ?? null,
46
- entry.instanceId ?? null,
47
- entry.agentRole,
48
- entry.activityType,
49
- entry.modelUsed ?? null,
50
- entry.content ? JSON.stringify(entry.content) : null,
51
- entry.tokensUsed ?? null,
52
- ],
53
- });
54
- } catch (err) {
55
- logger().error({ err }, 'Failed to log activity');
56
- }
57
- }
58
-
59
- /**
60
- * Query activity entries with optional filters.
61
- */
62
- export async function queryActivity(filters: {
63
- squadId?: string;
64
- instanceId?: string;
65
- agentRole?: string;
66
- activityType?: ActivityType;
67
- limit?: number;
68
- offset?: number;
69
- }): Promise<ActivityEntry[]> {
70
- const db = getDatabase();
71
- const conditions: string[] = [];
72
- const args: (string | number)[] = [];
73
-
74
- if (filters.squadId) {
75
- conditions.push('squad_id = ?');
76
- args.push(filters.squadId);
77
- }
78
- if (filters.instanceId) {
79
- conditions.push('instance_id = ?');
80
- args.push(filters.instanceId);
81
- }
82
- if (filters.agentRole) {
83
- conditions.push('agent_role = ?');
84
- args.push(filters.agentRole);
85
- }
86
- if (filters.activityType) {
87
- conditions.push('activity_type = ?');
88
- args.push(filters.activityType);
89
- }
90
-
91
- const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
92
- const limit = filters.limit ?? 50;
93
- const offset = filters.offset ?? 0;
94
-
95
- const result = await db.execute({
96
- sql: `SELECT id, squad_id, instance_id, agent_role, activity_type, model_used, content, tokens_used, timestamp
97
- FROM agent_activity ${where}
98
- ORDER BY timestamp DESC
99
- LIMIT ? OFFSET ?`,
100
- args: [...args, limit, offset],
101
- });
102
-
103
- return result.rows.map((row) => ({
104
- id: row.id as number,
105
- squadId: row.squad_id as string | null,
106
- instanceId: row.instance_id as string | null,
107
- agentRole: row.agent_role as string,
108
- activityType: row.activity_type as ActivityType,
109
- modelUsed: row.model_used as string | null,
110
- content: row.content as string | null,
111
- tokensUsed: row.tokens_used as number | null,
112
- timestamp: row.timestamp as string,
113
- }));
114
- }
115
-
116
- /**
117
- * Subscribe to the event bus and auto-log relevant events.
118
- */
119
- export function initActivityLogger(eventBus: {
120
- onAny: (handler: (event: IOEvent) => void) => () => void;
121
- }): () => void {
122
- return eventBus.onAny((event: IOEvent) => {
123
- const entry = mapEventToActivity(event);
124
- if (entry) {
125
- logActivity(entry);
126
- }
127
- });
128
- }
129
-
130
- function mapEventToActivity(event: IOEvent): Parameters<typeof logActivity>[0] | null {
131
- switch (event.type) {
132
- case 'agent:task_started':
133
- return {
134
- squadId: event.squadId,
135
- instanceId: event.instanceId,
136
- agentRole: event.agentRole,
137
- activityType: 'task_start',
138
- content: event.data,
139
- };
140
- case 'agent:task_completed':
141
- return {
142
- squadId: event.squadId,
143
- instanceId: event.instanceId,
144
- agentRole: event.agentRole,
145
- activityType: 'task_complete',
146
- content: event.data,
147
- };
148
- case 'agent:tool_call':
149
- return {
150
- squadId: event.squadId,
151
- instanceId: event.instanceId,
152
- agentRole: event.agentRole,
153
- activityType: 'tool_call',
154
- modelUsed: event.model,
155
- content: event.data,
156
- };
157
- case 'agent:error':
158
- return {
159
- squadId: event.squadId,
160
- instanceId: event.instanceId,
161
- agentRole: event.agentRole,
162
- activityType: 'error',
163
- content: event.data,
164
- };
165
- case 'meeting:contribution':
166
- return {
167
- squadId: event.squadId,
168
- instanceId: event.instanceId,
169
- agentRole: event.agentRole,
170
- activityType: 'meeting_contribution',
171
- content: { message: event.content },
172
- };
173
- default:
174
- return null;
175
- }
176
- }
package/src/store/db.ts DELETED
@@ -1,237 +0,0 @@
1
- import { mkdirSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- import { type Client, createClient } from '@libsql/client';
4
- import type { Logger } from 'pino';
5
- import { createChildLogger } from '../logging/logger.js';
6
-
7
- let db: Client;
8
- let logger: Logger;
9
-
10
- const MIGRATIONS: { version: number; statements: string[] }[] = [
11
- {
12
- version: 1,
13
- statements: [
14
- `CREATE TABLE IF NOT EXISTS conversations (
15
- id TEXT PRIMARY KEY,
16
- role TEXT NOT NULL,
17
- content TEXT NOT NULL,
18
- source TEXT,
19
- attachments TEXT,
20
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP
21
- )`,
22
- `CREATE TABLE IF NOT EXISTS squads (
23
- id TEXT PRIMARY KEY,
24
- name TEXT NOT NULL UNIQUE,
25
- project_path TEXT NOT NULL,
26
- repo_url TEXT,
27
- autonomy_tier TEXT NOT NULL DEFAULT 'medium',
28
- autonomy_config TEXT,
29
- status TEXT DEFAULT 'active',
30
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP
31
- )`,
32
- `CREATE TABLE IF NOT EXISTS squad_members (
33
- id TEXT PRIMARY KEY,
34
- squad_id TEXT NOT NULL REFERENCES squads(id),
35
- role_name TEXT NOT NULL,
36
- skill_file_path TEXT,
37
- tools_allowed TEXT,
38
- is_veto_member INTEGER DEFAULT 0,
39
- status TEXT DEFAULT 'active',
40
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP
41
- )`,
42
- `CREATE TABLE IF NOT EXISTS squad_instances (
43
- id TEXT PRIMARY KEY,
44
- squad_id TEXT NOT NULL REFERENCES squads(id),
45
- issue_ref TEXT,
46
- worktree_path TEXT,
47
- branch_name TEXT,
48
- status TEXT DEFAULT 'planning',
49
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
50
- completed_at DATETIME
51
- )`,
52
- `CREATE TABLE IF NOT EXISTS decisions (
53
- id TEXT PRIMARY KEY,
54
- squad_id TEXT NOT NULL REFERENCES squads(id),
55
- instance_id TEXT REFERENCES squad_instances(id),
56
- agent_role TEXT NOT NULL,
57
- decision_type TEXT,
58
- content TEXT NOT NULL,
59
- rationale TEXT,
60
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP
61
- )`,
62
- `CREATE TABLE IF NOT EXISTS token_usage (
63
- id INTEGER PRIMARY KEY AUTOINCREMENT,
64
- squad_id TEXT REFERENCES squads(id),
65
- instance_id TEXT REFERENCES squad_instances(id),
66
- agent_role TEXT,
67
- model TEXT NOT NULL,
68
- input_tokens INTEGER NOT NULL,
69
- output_tokens INTEGER NOT NULL,
70
- estimated_cost_usd REAL,
71
- timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
72
- )`,
73
- `CREATE TABLE IF NOT EXISTS model_pricing (
74
- model TEXT PRIMARY KEY,
75
- input_cost_per_1m REAL NOT NULL,
76
- output_cost_per_1m REAL NOT NULL,
77
- tier TEXT,
78
- last_updated DATETIME DEFAULT CURRENT_TIMESTAMP
79
- )`,
80
- `CREATE TABLE IF NOT EXISTS attachments (
81
- id TEXT PRIMARY KEY,
82
- message_id TEXT,
83
- filename TEXT NOT NULL,
84
- mime_type TEXT,
85
- size_bytes INTEGER,
86
- disk_path TEXT NOT NULL,
87
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP
88
- )`,
89
- `CREATE TABLE IF NOT EXISTS agent_activity (
90
- id INTEGER PRIMARY KEY AUTOINCREMENT,
91
- squad_id TEXT REFERENCES squads(id),
92
- instance_id TEXT REFERENCES squad_instances(id),
93
- agent_role TEXT NOT NULL,
94
- activity_type TEXT NOT NULL,
95
- model_used TEXT,
96
- content TEXT,
97
- tokens_used INTEGER,
98
- timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
99
- )`,
100
- `CREATE TABLE IF NOT EXISTS schema_version (
101
- version INTEGER PRIMARY KEY
102
- )`,
103
- `CREATE TABLE IF NOT EXISTS io_state (
104
- key TEXT PRIMARY KEY,
105
- value TEXT NOT NULL
106
- )`,
107
- 'INSERT INTO schema_version (version) VALUES (1)',
108
- ],
109
- },
110
- {
111
- version: 2,
112
- statements: [
113
- `CREATE TABLE IF NOT EXISTS inbox_entries (
114
- id TEXT PRIMARY KEY,
115
- squad_id TEXT NOT NULL REFERENCES squads(id),
116
- instance_id TEXT REFERENCES squad_instances(id),
117
- kind TEXT NOT NULL,
118
- title TEXT NOT NULL,
119
- content TEXT NOT NULL,
120
- status TEXT DEFAULT 'unread',
121
- response TEXT,
122
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
123
- resolved_at DATETIME
124
- )`,
125
- 'CREATE INDEX IF NOT EXISTS idx_inbox_status ON inbox_entries(status)',
126
- 'CREATE INDEX IF NOT EXISTS idx_inbox_squad ON inbox_entries(squad_id)',
127
- 'INSERT OR REPLACE INTO schema_version (version) VALUES (2)',
128
- ],
129
- },
130
- {
131
- version: 3,
132
- statements: [
133
- `CREATE TABLE IF NOT EXISTS schedules (
134
- id TEXT PRIMARY KEY,
135
- name TEXT NOT NULL,
136
- target_type TEXT NOT NULL,
137
- target_id TEXT,
138
- cron TEXT NOT NULL,
139
- prompt TEXT NOT NULL,
140
- enabled INTEGER DEFAULT 1,
141
- last_run DATETIME,
142
- next_run DATETIME,
143
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP
144
- )`,
145
- 'CREATE INDEX IF NOT EXISTS idx_schedules_enabled ON schedules(enabled)',
146
- 'CREATE INDEX IF NOT EXISTS idx_schedules_next_run ON schedules(next_run)',
147
- 'INSERT OR REPLACE INTO schema_version (version) VALUES (3)',
148
- ],
149
- },
150
- {
151
- version: 4,
152
- statements: [
153
- `CREATE TABLE IF NOT EXISTS skill_activations (
154
- skill_name TEXT NOT NULL,
155
- target_type TEXT NOT NULL,
156
- target_id TEXT,
157
- activated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
158
- PRIMARY KEY (skill_name, target_type, target_id)
159
- )`,
160
- 'CREATE INDEX IF NOT EXISTS idx_skill_activations_target ON skill_activations(target_type, target_id)',
161
- 'INSERT OR REPLACE INTO schema_version (version) VALUES (4)',
162
- ],
163
- },
164
- {
165
- version: 5,
166
- statements: [
167
- 'ALTER TABLE squads ADD COLUMN universe TEXT',
168
- 'ALTER TABLE squad_members ADD COLUMN display_name TEXT',
169
- 'INSERT OR REPLACE INTO schema_version (version) VALUES (5)',
170
- ],
171
- },
172
- {
173
- version: 6,
174
- statements: [
175
- 'ALTER TABLE squad_members ADD COLUMN persona TEXT',
176
- 'INSERT OR REPLACE INTO schema_version (version) VALUES (6)',
177
- ],
178
- },
179
- ];
180
-
181
- export async function initDatabase(dataDir: string): Promise<Client> {
182
- logger = createChildLogger('store');
183
- mkdirSync(dataDir, { recursive: true });
184
- const dbPath = join(dataDir, 'io.db');
185
-
186
- db = createClient({
187
- url: `file:${dbPath}`,
188
- });
189
-
190
- await db.execute('PRAGMA journal_mode = WAL');
191
- await db.execute('PRAGMA foreign_keys = ON');
192
-
193
- await runMigrations();
194
-
195
- logger.info({ path: dbPath }, 'Database initialized');
196
- return db;
197
- }
198
-
199
- async function runMigrations(): Promise<void> {
200
- const currentVersion = await getCurrentVersion();
201
-
202
- for (const migration of MIGRATIONS) {
203
- if (migration.version > currentVersion) {
204
- logger.info({ version: migration.version }, 'Running migration');
205
- for (const statement of migration.statements) {
206
- await db.execute(statement);
207
- }
208
- }
209
- }
210
- }
211
-
212
- async function getCurrentVersion(): Promise<number> {
213
- try {
214
- const result = await db.execute(
215
- 'SELECT version FROM schema_version ORDER BY version DESC LIMIT 1',
216
- );
217
- if (result.rows.length > 0) {
218
- return result.rows[0].version as number;
219
- }
220
- return 0;
221
- } catch {
222
- return 0;
223
- }
224
- }
225
-
226
- export function getDatabase(): Client {
227
- if (!db) {
228
- throw new Error('Database not initialized. Call initDatabase() first.');
229
- }
230
- return db;
231
- }
232
-
233
- export function closeDatabase(): void {
234
- if (db) {
235
- db.close();
236
- }
237
- }
@@ -1,199 +0,0 @@
1
- import { createChildLogger } from '../logging/logger.js';
2
- import { getEventBus } from '../squad/event-bus.js';
3
- import { getDatabase } from './db.js';
4
-
5
- const logger = () => createChildLogger('inbox');
6
-
7
- export type InboxKind = 'deliverable' | 'question';
8
- export type InboxStatus = 'unread' | 'read' | 'resolved';
9
-
10
- export interface InboxEntry {
11
- id: string;
12
- squadId: string;
13
- instanceId: string | null;
14
- kind: InboxKind;
15
- title: string;
16
- content: string;
17
- status: InboxStatus;
18
- response: string | null;
19
- createdAt: string;
20
- resolvedAt: string | null;
21
- }
22
-
23
- // Pending question resolvers — keyed by entry ID
24
- const pendingQuestions = new Map<string, (response: string) => void>();
25
-
26
- /**
27
- * Add a new inbox entry. For questions, returns a promise that resolves when the user responds.
28
- */
29
- export async function addInboxEntry(params: {
30
- squadId: string;
31
- instanceId?: string;
32
- kind: InboxKind;
33
- title: string;
34
- content: string;
35
- }): Promise<{ entry: InboxEntry; waitForResponse?: Promise<string> }> {
36
- const db = getDatabase();
37
- const id = crypto.randomUUID();
38
-
39
- await db.execute({
40
- sql: `INSERT INTO inbox_entries (id, squad_id, instance_id, kind, title, content)
41
- VALUES (?, ?, ?, ?, ?, ?)`,
42
- args: [
43
- id,
44
- params.squadId,
45
- params.instanceId ?? null,
46
- params.kind,
47
- params.title,
48
- params.content,
49
- ],
50
- });
51
-
52
- const entry: InboxEntry = {
53
- id,
54
- squadId: params.squadId,
55
- instanceId: params.instanceId ?? null,
56
- kind: params.kind,
57
- title: params.title,
58
- content: params.content,
59
- status: 'unread',
60
- response: null,
61
- createdAt: new Date().toISOString(),
62
- resolvedAt: null,
63
- };
64
-
65
- let waitForResponse: Promise<string> | undefined;
66
-
67
- if (params.kind === 'question') {
68
- waitForResponse = new Promise<string>((resolve) => {
69
- pendingQuestions.set(id, resolve);
70
- });
71
- }
72
-
73
- // Emit event for WebSocket broadcast
74
- getEventBus().emit({
75
- type: 'inbox:new',
76
- id: crypto.randomUUID(),
77
- timestamp: new Date(),
78
- squadId: params.squadId,
79
- instanceId: params.instanceId,
80
- kind: params.kind,
81
- title: params.title,
82
- entryId: id,
83
- });
84
-
85
- logger().info({ id, kind: params.kind, title: params.title }, 'Inbox entry created');
86
- return { entry, waitForResponse };
87
- }
88
-
89
- /**
90
- * List inbox entries with optional filters.
91
- */
92
- export async function listInboxEntries(filters?: {
93
- status?: InboxStatus;
94
- squadId?: string;
95
- kind?: InboxKind;
96
- limit?: number;
97
- }): Promise<InboxEntry[]> {
98
- const db = getDatabase();
99
- const conditions: string[] = [];
100
- const args: (string | number)[] = [];
101
-
102
- if (filters?.status) {
103
- conditions.push('status = ?');
104
- args.push(filters.status);
105
- }
106
- if (filters?.squadId) {
107
- conditions.push('squad_id = ?');
108
- args.push(filters.squadId);
109
- }
110
- if (filters?.kind) {
111
- conditions.push('kind = ?');
112
- args.push(filters.kind);
113
- }
114
-
115
- const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
116
- const limit = filters?.limit ?? 50;
117
-
118
- const result = await db.execute({
119
- sql: `SELECT id, squad_id, instance_id, kind, title, content, status, response, created_at, resolved_at
120
- FROM inbox_entries ${where}
121
- ORDER BY created_at DESC
122
- LIMIT ?`,
123
- args: [...args, limit],
124
- });
125
-
126
- return result.rows.map(rowToEntry);
127
- }
128
-
129
- /**
130
- * Get a single inbox entry by ID.
131
- */
132
- export async function getInboxEntry(id: string): Promise<InboxEntry | null> {
133
- const db = getDatabase();
134
- const result = await db.execute({
135
- sql: 'SELECT id, squad_id, instance_id, kind, title, content, status, response, created_at, resolved_at FROM inbox_entries WHERE id = ?',
136
- args: [id],
137
- });
138
- if (result.rows.length === 0) return null;
139
- return rowToEntry(result.rows[0]);
140
- }
141
-
142
- /**
143
- * Mark an entry as read.
144
- */
145
- export async function markInboxRead(id: string): Promise<void> {
146
- const db = getDatabase();
147
- await db.execute({
148
- sql: "UPDATE inbox_entries SET status = 'read' WHERE id = ? AND status = 'unread'",
149
- args: [id],
150
- });
151
- }
152
-
153
- /**
154
- * Respond to an inbox question. Resolves the blocking promise if the squad is waiting.
155
- */
156
- export async function resolveInboxEntry(id: string, response: string): Promise<boolean> {
157
- const db = getDatabase();
158
- await db.execute({
159
- sql: "UPDATE inbox_entries SET status = 'resolved', response = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?",
160
- args: [response, id],
161
- });
162
-
163
- // Resolve the pending promise if squad is waiting
164
- const resolver = pendingQuestions.get(id);
165
- if (resolver) {
166
- resolver(response);
167
- pendingQuestions.delete(id);
168
- logger().info({ id }, 'Inbox question resolved — squad unblocked');
169
- return true;
170
- }
171
-
172
- return false;
173
- }
174
-
175
- /**
176
- * Get count of unread entries.
177
- */
178
- export async function getUnreadCount(): Promise<number> {
179
- const db = getDatabase();
180
- const result = await db.execute(
181
- "SELECT COUNT(*) as count FROM inbox_entries WHERE status = 'unread'",
182
- );
183
- return (result.rows[0]?.count as number) ?? 0;
184
- }
185
-
186
- function rowToEntry(row: Record<string, unknown>): InboxEntry {
187
- return {
188
- id: row.id as string,
189
- squadId: row.squad_id as string,
190
- instanceId: row.instance_id as string | null,
191
- kind: row.kind as InboxKind,
192
- title: row.title as string,
193
- content: row.content as string,
194
- status: row.status as InboxStatus,
195
- response: row.response as string | null,
196
- createdAt: row.created_at as string,
197
- resolvedAt: row.resolved_at as string | null,
198
- };
199
- }