heyio 3.0.2 → 3.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/server.js +1 -1
- package/dist/api/server.js.map +1 -1
- package/dist/logging/logger.d.ts.map +1 -1
- package/dist/logging/logger.js +13 -1
- package/dist/logging/logger.js.map +1 -1
- package/node_modules/@io/shared/package.json +1 -1
- package/package.json +7 -2
- package/public/assets/index-2RY89H3W.js +336 -0
- package/public/assets/index-2RY89H3W.js.map +1 -0
- package/public/assets/index-D3cGfBsj.css +1 -0
- package/public/index.html +14 -0
- package/src/api/middleware/auth.ts +0 -76
- package/src/api/notifications.ts +0 -122
- package/src/api/routes/activity.ts +0 -29
- package/src/api/routes/attachments.ts +0 -93
- package/src/api/routes/config.ts +0 -115
- package/src/api/routes/conversations.ts +0 -87
- package/src/api/routes/health.ts +0 -18
- package/src/api/routes/inbox.ts +0 -98
- package/src/api/routes/schedules.ts +0 -121
- package/src/api/routes/skills.ts +0 -105
- package/src/api/routes/squads.ts +0 -145
- package/src/api/routes/usage.ts +0 -57
- package/src/api/routes/wiki.ts +0 -49
- package/src/api/server.ts +0 -186
- package/src/config.ts +0 -3
- package/src/copilot/client.ts +0 -42
- package/src/copilot/health-monitor.ts +0 -85
- package/src/copilot/orchestrator.ts +0 -222
- package/src/copilot/tools.ts +0 -707
- package/src/index.ts +0 -113
- package/src/logging/logger.ts +0 -26
- package/src/models/index.ts +0 -11
- package/src/models/pricing.ts +0 -121
- package/src/models/registry.ts +0 -131
- package/src/models/token-tracker.ts +0 -151
- package/src/scheduler/engine.ts +0 -146
- package/src/skills/index.ts +0 -13
- package/src/skills/store.ts +0 -188
- package/src/squad/agent.ts +0 -326
- package/src/squad/autonomy.ts +0 -78
- package/src/squad/event-bus.ts +0 -71
- package/src/squad/execution/index.ts +0 -17
- package/src/squad/execution/instance.ts +0 -186
- package/src/squad/execution/meeting.ts +0 -191
- package/src/squad/execution/pr.ts +0 -127
- package/src/squad/execution/runner.ts +0 -97
- package/src/squad/execution/tasks.ts +0 -111
- package/src/squad/execution/worktree.ts +0 -138
- package/src/squad/hiring.ts +0 -222
- package/src/squad/index.ts +0 -17
- package/src/squad/manager.ts +0 -337
- package/src/squad/name-generator.ts +0 -135
- package/src/squad/roles/templates.ts +0 -104
- package/src/squad/skill-parser.ts +0 -120
- package/src/squad/source-resolver.ts +0 -57
- package/src/store/activity.ts +0 -176
- package/src/store/db.ts +0 -237
- package/src/store/inbox.ts +0 -199
- package/src/store/schedules.ts +0 -199
- package/src/wiki/index.ts +0 -12
- package/src/wiki/store.ts +0 -139
- 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
|
-
}
|
package/src/store/activity.ts
DELETED
|
@@ -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
|
-
}
|
package/src/store/inbox.ts
DELETED
|
@@ -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
|
-
}
|