heyio 0.42.0 → 1.0.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 (100) hide show
  1. package/README.md +40 -52
  2. package/dist/api/auth.js +35 -38
  3. package/dist/api/server.js +157 -1139
  4. package/dist/config.js +49 -32
  5. package/dist/copilot/agents.js +72 -1055
  6. package/dist/copilot/client.js +6 -17
  7. package/dist/copilot/io-scheduler.js +55 -139
  8. package/dist/copilot/model-router.js +100 -72
  9. package/dist/copilot/orchestrator.js +91 -515
  10. package/dist/copilot/scheduler.js +67 -189
  11. package/dist/copilot/skills.js +41 -366
  12. package/dist/copilot/system-message.js +40 -200
  13. package/dist/copilot/tools.js +191 -2042
  14. package/dist/daemon.js +54 -201
  15. package/dist/index.js +15 -133
  16. package/dist/mcp/config.js +23 -31
  17. package/dist/mcp/index.js +2 -3
  18. package/dist/mcp/registry.js +33 -88
  19. package/dist/notify.js +18 -100
  20. package/dist/paths.js +13 -24
  21. package/dist/setup.js +35 -0
  22. package/dist/store/db.js +111 -297
  23. package/dist/store/feed.js +29 -97
  24. package/dist/store/instances.js +56 -121
  25. package/dist/store/schedules.js +21 -73
  26. package/dist/store/squads.js +35 -186
  27. package/dist/store/tasks.js +25 -168
  28. package/dist/telegram/bot.js +20 -312
  29. package/dist/telegram/handlers.js +39 -3
  30. package/dist/watchdog.js +31 -45
  31. package/dist/wiki/fs.js +38 -155
  32. package/dist/wiki/search.js +31 -44
  33. package/package.json +5 -8
  34. package/web-dist/assets/ChatView-EFFiln1H.js +11 -0
  35. package/web-dist/assets/FeedView-bN4NMOL7.js +6 -0
  36. package/web-dist/assets/LoginView-CNtasq3n.js +1 -0
  37. package/web-dist/assets/McpView-C2CHiwsi.js +1 -0
  38. package/web-dist/assets/SchedulesView-CyilLban.js +1 -0
  39. package/web-dist/assets/SettingsView-1wLXKEF4.js +1 -0
  40. package/web-dist/assets/SkillsView-BLsD-0u0.js +1 -0
  41. package/web-dist/assets/SquadDetailView-CsCw2ZLp.js +21 -0
  42. package/web-dist/assets/SquadsView-DQ3vFlyO.js +6 -0
  43. package/web-dist/assets/WikiView-19M3oqnq.js +21 -0
  44. package/web-dist/assets/api-WGvTsXaE.js +1 -0
  45. package/web-dist/assets/index-D7M5O-_l.css +1 -0
  46. package/web-dist/assets/index-DZOS9syn.js +95 -0
  47. package/web-dist/assets/plus-BOvyX1BC.js +6 -0
  48. package/web-dist/assets/trash-2-DHoetkC4.js +6 -0
  49. package/web-dist/favicon.svg +4 -1
  50. package/web-dist/index.html +7 -10
  51. package/dist/api/logout.test.js +0 -129
  52. package/dist/api/mcp.test.js +0 -285
  53. package/dist/api/wiki.test.js +0 -283
  54. package/dist/auth/session-logic.js +0 -79
  55. package/dist/auth/session-logic.test.js +0 -201
  56. package/dist/copilot/auto-complete-instance.test.js +0 -104
  57. package/dist/copilot/cron.js +0 -136
  58. package/dist/copilot/event-summary.js +0 -286
  59. package/dist/copilot/instance-deactivate.test.js +0 -119
  60. package/dist/copilot/model-router.test.js +0 -71
  61. package/dist/copilot/review-backfill.js +0 -57
  62. package/dist/copilot/session-timeout.js +0 -112
  63. package/dist/copilot/session-timeout.test.js +0 -372
  64. package/dist/copilot/skills.test.js +0 -55
  65. package/dist/copilot/universes.js +0 -469
  66. package/dist/instance-watchdog.js +0 -104
  67. package/dist/instance-watchdog.test.js +0 -183
  68. package/dist/mcp/client.js +0 -109
  69. package/dist/mcp/client.test.js +0 -99
  70. package/dist/mcp/config.test.js +0 -49
  71. package/dist/mcp/registry.test.js +0 -79
  72. package/dist/notify.test.js +0 -232
  73. package/dist/store/feed.test.js +0 -279
  74. package/dist/store/instances.test.js +0 -310
  75. package/dist/store/io-schedules.js +0 -63
  76. package/dist/store/notifications.js +0 -79
  77. package/dist/store/notifications.test.js +0 -197
  78. package/dist/store/schedule-runs.js +0 -46
  79. package/dist/store/squads.test.js +0 -405
  80. package/dist/store/tasks.test.js +0 -150
  81. package/dist/store/worktrees.js +0 -83
  82. package/dist/tui/index.js +0 -286
  83. package/dist/update.js +0 -81
  84. package/dist/watchdog.test.js +0 -83
  85. package/dist/wiki/wiki-squad.test.js +0 -54
  86. package/web-dist/assets/AgentActivityView-CedxxE6K.js +0 -1
  87. package/web-dist/assets/ChatView-DMkYQo_V.js +0 -4
  88. package/web-dist/assets/FeedView-BH4q-31V.js +0 -1
  89. package/web-dist/assets/InboxView-BVwVP4EW.js +0 -1
  90. package/web-dist/assets/LoginView-DRPDhnwu.js +0 -1
  91. package/web-dist/assets/McpView-D8yWz-lq.js +0 -1
  92. package/web-dist/assets/SchedulesView-BzzyncGF.js +0 -1
  93. package/web-dist/assets/SettingsTabs.vue_vue_type_script_setup_true_lang-oW3ySu7Y.js +0 -1
  94. package/web-dist/assets/SkillsView-oxpYuhx7.js +0 -1
  95. package/web-dist/assets/SquadsView-CaKUIKlq.js +0 -1
  96. package/web-dist/assets/StatusIndicator.vue_vue_type_script_setup_true_lang-8U15Qp_Q.js +0 -1
  97. package/web-dist/assets/WikiView-C5jXUlfW.js +0 -1
  98. package/web-dist/assets/index-BrWzNw-N.css +0 -10
  99. package/web-dist/assets/index-f67odrrt.js +0 -81
  100. package/web-dist/icons.svg +0 -24
package/dist/notify.js CHANGED
@@ -1,106 +1,24 @@
1
- import { config } from "./config.js";
2
- import { createFeedEntry } from "./store/feed.js";
3
- const HEARTBEAT_PATTERNS = [
4
- /^no active tasks?\.?$/i,
5
- /^nothing to report\.?$/i,
6
- /^all clear\.?$/i,
7
- /^no updates?\.?$/i,
8
- /^no changes?\.?$/i,
9
- /^idle\.?$/i,
10
- /^heartbeat\.?$/i,
11
- /^ok\.?$/i,
12
- ];
13
- export function isMeaningfulOutput(text) {
14
- const trimmed = (text ?? "").trim();
15
- if (trimmed.length < 20)
16
- return false;
17
- const firstLine = trimmed.split("\n").map((l) => l.trim()).find((l) => l.length > 0) ?? "";
18
- if (HEARTBEAT_PATTERNS.some((re) => re.test(firstLine)))
19
- return false;
20
- return true;
21
- }
22
- let telegramSender;
23
- let tuiSender;
24
- let sseBroadcaster;
25
- export function setTelegramSender(fn) {
26
- telegramSender = fn;
27
- }
28
- export function setTuiSender(fn) {
29
- tuiSender = fn;
30
- }
31
- export function setSseBroadcaster(fn) {
32
- sseBroadcaster = fn;
33
- }
34
- export function _resetNotifySendersForTests() {
35
- telegramSender = undefined;
36
- tuiSender = undefined;
37
- sseBroadcaster = undefined;
38
- }
39
- export async function notifyBackground(input) {
40
- const dispatched = { telegram: false, tui: false, sse: false };
41
- const text = (input.text ?? "").trim();
42
- if (text.length === 0)
43
- return { dispatched, skipped: "empty" };
44
- const mode = config.backgroundNotifyMode ?? "meaningful";
45
- if (mode === "off")
46
- return { dispatched, skipped: "off" };
47
- if (mode === "meaningful" && !isMeaningfulOutput(text)) {
48
- return { dispatched, skipped: "not-meaningful" };
49
- }
50
- const { source, title } = input;
51
- const sourceRefJson = JSON.stringify(stripType(source));
52
- let row;
53
- try {
54
- row = createFeedEntry({
55
- type: "notification",
56
- title,
57
- body: text,
58
- source_type: source.type,
59
- source_ref: sourceRefJson === "{}" ? null : sourceRefJson,
60
- });
61
- }
62
- catch (err) {
63
- console.error("[notify] failed to persist notification:", err);
64
- return { dispatched };
65
- }
66
- if (sseBroadcaster) {
1
+ import { loadConfig } from "./config.js";
2
+ import { postFeedItem } from "./store/feed.js";
3
+ export async function notify(title, content, source) {
4
+ const config = loadConfig();
5
+ // Always post to feed
6
+ postFeedItem(source, title, content);
7
+ // Notify via Telegram if enabled
8
+ if (config.backgroundNotifyTelegram && config.telegramEnabled) {
67
9
  try {
68
- sseBroadcaster({
69
- id: row.id,
70
- source: { type: source.type, ...stripType(source) },
71
- title,
72
- text,
73
- createdAt: row.created_at,
74
- });
75
- dispatched.sse = true;
10
+ const { getBot } = await import("./telegram/bot.js");
11
+ const bot = getBot();
12
+ if (bot && config.authorizedUserId) {
13
+ const msg = `📬 *${title}*\n\n${content.slice(0, 1000)}`;
14
+ await bot.api.sendMessage(config.authorizedUserId, msg, {
15
+ parse_mode: "Markdown",
16
+ });
17
+ }
76
18
  }
77
- catch (err) {
78
- console.error("[notify] sse broadcast failed:", err);
19
+ catch {
20
+ // Telegram notification failed silently
79
21
  }
80
22
  }
81
- if (tuiSender && (config.backgroundNotifyTui ?? true)) {
82
- try {
83
- tuiSender({ title, text });
84
- dispatched.tui = true;
85
- }
86
- catch (err) {
87
- console.error("[notify] tui send failed:", err);
88
- }
89
- }
90
- if (telegramSender && (config.backgroundNotifyTelegram ?? true)) {
91
- try {
92
- await telegramSender({ title, text });
93
- dispatched.telegram = true;
94
- }
95
- catch (err) {
96
- console.error("[notify] telegram send failed:", err);
97
- }
98
- }
99
- return { id: row.id, dispatched };
100
- }
101
- function stripType(s) {
102
- const { type: _t, ...rest } = s;
103
- void _t;
104
- return rest;
105
23
  }
106
24
  //# sourceMappingURL=notify.js.map
package/dist/paths.js CHANGED
@@ -1,25 +1,14 @@
1
- import { homedir } from "os";
2
- import { join, dirname } from "path";
3
- import { fileURLToPath } from "url";
4
- import { readFileSync } from "fs";
5
- export const IO_HOME = join(homedir(), ".io");
6
- export const CONFIG_PATH = join(IO_HOME, "config.json");
7
- export const DB_PATH = join(IO_HOME, "io.db");
8
- export const WIKI_DIR = join(IO_HOME, "wiki");
9
- export const SKILLS_DIR = join(IO_HOME, "skills");
10
- export const SESSIONS_DIR = join(IO_HOME, "sessions");
11
- export const LOGS_DIR = join(IO_HOME, "logs");
12
- function resolveVersion() {
13
- const __dirname = dirname(fileURLToPath(import.meta.url));
14
- for (const rel of ["..", join("..", "..")]) {
15
- try {
16
- const pkg = JSON.parse(readFileSync(join(__dirname, rel, "package.json"), "utf-8"));
17
- if (pkg.version)
18
- return pkg.version;
19
- }
20
- catch { /* try next */ }
21
- }
22
- return "0.0.0";
23
- }
24
- export const IO_VERSION = resolveVersion();
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ const IO_HOME = join(homedir(), ".io");
4
+ export const PATHS = {
5
+ home: IO_HOME,
6
+ config: join(IO_HOME, "config.json"),
7
+ db: join(IO_HOME, "io.db"),
8
+ wiki: join(IO_HOME, "wiki"),
9
+ wikiPages: join(IO_HOME, "wiki", "pages"),
10
+ skills: join(IO_HOME, "skills"),
11
+ mcpConfig: join(IO_HOME, "mcp.json"),
12
+ sessions: join(IO_HOME, "sessions"),
13
+ };
25
14
  //# sourceMappingURL=paths.js.map
package/dist/setup.js ADDED
@@ -0,0 +1,35 @@
1
+ import { createInterface } from "node:readline";
2
+ import { loadConfig, saveConfig } from "./config.js";
3
+ import { PATHS } from "./paths.js";
4
+ import { mkdirSync, existsSync } from "node:fs";
5
+ export async function runSetup() {
6
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
7
+ const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
8
+ console.log("\n🤖 IO Setup\n");
9
+ if (!existsSync(PATHS.home))
10
+ mkdirSync(PATHS.home, { recursive: true });
11
+ const config = loadConfig();
12
+ // Supabase (required for API auth)
13
+ console.log("— Supabase Auth (required for web/API access) —");
14
+ const supabaseUrl = await ask(`Supabase URL [${config.supabaseUrl ?? ""}]: `);
15
+ const supabaseAnonKey = await ask(`Supabase Anon Key [${config.supabaseAnonKey ? "****" : ""}]: `);
16
+ const authorizedEmail = await ask(`Authorized Email [${config.authorizedEmail ?? ""}]: `);
17
+ // Telegram (optional)
18
+ console.log("\n— Telegram Bot (optional) —");
19
+ const telegramBotToken = await ask(`Bot Token [${config.telegramBotToken ? "****" : ""}]: `);
20
+ const authorizedUserId = await ask(`Your Telegram User ID [${config.authorizedUserId ?? ""}]: `);
21
+ const telegramEnabled = await ask(`Enable Telegram? (y/n) [${config.telegramEnabled ? "y" : "n"}]: `);
22
+ saveConfig({
23
+ supabaseUrl: supabaseUrl || config.supabaseUrl,
24
+ supabaseAnonKey: supabaseAnonKey || config.supabaseAnonKey,
25
+ authorizedEmail: authorizedEmail || config.authorizedEmail,
26
+ telegramBotToken: telegramBotToken || config.telegramBotToken,
27
+ authorizedUserId: authorizedUserId
28
+ ? parseInt(authorizedUserId, 10)
29
+ : config.authorizedUserId,
30
+ telegramEnabled: telegramEnabled === "y" ? true : telegramEnabled === "n" ? false : config.telegramEnabled,
31
+ });
32
+ console.log(`\n✅ Configuration saved to ${PATHS.config}\n`);
33
+ rl.close();
34
+ }
35
+ //# sourceMappingURL=setup.js.map
package/dist/store/db.js CHANGED
@@ -1,318 +1,132 @@
1
1
  import Database from "better-sqlite3";
2
- import { mkdirSync } from "fs";
3
- import { dirname } from "path";
4
- import { DB_PATH, IO_HOME } from "../paths.js";
5
- let db = null;
6
- let insertCount = 0;
7
- let dbPathOverride = null;
8
- /**
9
- * Override the DB path for tests. Closes the existing connection (if any)
10
- * so the next getDb() call opens a fresh DB at the given path.
11
- * Never call this in production code.
12
- */
13
- export function setDbPathForTests(path) {
14
- if (db) {
15
- db.close();
16
- db = null;
17
- }
18
- dbPathOverride = path;
19
- }
2
+ import { existsSync, mkdirSync } from "node:fs";
3
+ import { dirname } from "node:path";
4
+ import { PATHS } from "../paths.js";
5
+ let db;
20
6
  export function getDb() {
21
7
  if (db)
22
8
  return db;
23
- const resolvedPath = dbPathOverride ?? DB_PATH;
24
- const resolvedHome = dbPathOverride ? dirname(resolvedPath) : IO_HOME;
25
- mkdirSync(resolvedHome, { recursive: true });
26
- db = new Database(resolvedPath);
9
+ const dir = dirname(PATHS.db);
10
+ if (!existsSync(dir))
11
+ mkdirSync(dir, { recursive: true });
12
+ db = new Database(PATHS.db);
27
13
  db.pragma("journal_mode = WAL");
14
+ db.pragma("foreign_keys = ON");
15
+ runMigrations(db);
16
+ return db;
17
+ }
18
+ function runMigrations(db) {
28
19
  db.exec(`
29
- CREATE TABLE IF NOT EXISTS io_state (
20
+ CREATE TABLE IF NOT EXISTS meta (
30
21
  key TEXT PRIMARY KEY,
31
22
  value TEXT NOT NULL
32
23
  );
24
+ `);
25
+ const version = getSchemaVersion(db);
26
+ if (version < 1) {
27
+ db.exec(`
28
+ CREATE TABLE IF NOT EXISTS squads (
29
+ id TEXT PRIMARY KEY,
30
+ name TEXT NOT NULL,
31
+ universe TEXT NOT NULL,
32
+ repo_url TEXT,
33
+ rules TEXT DEFAULT '',
34
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
35
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
36
+ );
33
37
 
34
- CREATE TABLE IF NOT EXISTS squads (
35
- id INTEGER PRIMARY KEY AUTOINCREMENT,
36
- slug TEXT UNIQUE NOT NULL,
37
- name TEXT NOT NULL,
38
- project_path TEXT NOT NULL,
39
- copilot_session_id TEXT,
40
- model TEXT,
41
- status TEXT NOT NULL DEFAULT 'idle',
42
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
43
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
44
- );
38
+ CREATE TABLE IF NOT EXISTS agents (
39
+ id TEXT PRIMARY KEY,
40
+ squad_id TEXT NOT NULL REFERENCES squads(id) ON DELETE CASCADE,
41
+ character_name TEXT NOT NULL,
42
+ role_title TEXT NOT NULL,
43
+ persona TEXT DEFAULT '',
44
+ is_lead INTEGER NOT NULL DEFAULT 0,
45
+ is_qa INTEGER NOT NULL DEFAULT 0,
46
+ is_test INTEGER NOT NULL DEFAULT 0,
47
+ status TEXT NOT NULL DEFAULT 'idle',
48
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
49
+ );
45
50
 
46
- CREATE TABLE IF NOT EXISTS squad_decisions (
47
- id INTEGER PRIMARY KEY AUTOINCREMENT,
48
- squad_slug TEXT NOT NULL,
49
- decision TEXT NOT NULL,
50
- context TEXT,
51
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP
52
- );
51
+ CREATE TABLE IF NOT EXISTS tasks (
52
+ id TEXT PRIMARY KEY,
53
+ squad_id TEXT NOT NULL REFERENCES squads(id) ON DELETE CASCADE,
54
+ instance_id TEXT,
55
+ agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,
56
+ description TEXT NOT NULL,
57
+ status TEXT NOT NULL DEFAULT 'pending',
58
+ result TEXT,
59
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
60
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
61
+ );
53
62
 
54
- CREATE TABLE IF NOT EXISTS conversation_log (
55
- id INTEGER PRIMARY KEY AUTOINCREMENT,
56
- role TEXT NOT NULL CHECK(role IN ('user', 'assistant', 'system')),
57
- content TEXT NOT NULL,
58
- source TEXT NOT NULL DEFAULT 'unknown',
59
- ts DATETIME DEFAULT CURRENT_TIMESTAMP
60
- );
63
+ CREATE TABLE IF NOT EXISTS instances (
64
+ id TEXT PRIMARY KEY,
65
+ squad_id TEXT NOT NULL REFERENCES squads(id) ON DELETE CASCADE,
66
+ branch TEXT NOT NULL,
67
+ worktree_path TEXT NOT NULL,
68
+ status TEXT NOT NULL DEFAULT 'active',
69
+ last_activity TEXT NOT NULL DEFAULT (datetime('now')),
70
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
71
+ );
61
72
 
62
- CREATE TABLE IF NOT EXISTS agent_tasks (
63
- task_id TEXT PRIMARY KEY,
64
- agent_slug TEXT NOT NULL,
65
- description TEXT NOT NULL,
66
- status TEXT NOT NULL DEFAULT 'running',
67
- result TEXT,
68
- origin_channel TEXT,
69
- started_at DATETIME DEFAULT CURRENT_TIMESTAMP,
70
- completed_at DATETIME
71
- );
72
- `);
73
- // Migrations for existing databases
74
- const migrations = [
75
- `ALTER TABLE squads ADD COLUMN model TEXT`,
76
- `ALTER TABLE squads ADD COLUMN universe TEXT`,
77
- `CREATE TABLE IF NOT EXISTS squad_agents (
78
- id INTEGER PRIMARY KEY AUTOINCREMENT,
79
- squad_slug TEXT NOT NULL,
80
- character_name TEXT NOT NULL,
81
- role_title TEXT NOT NULL,
82
- charter TEXT,
83
- model_tier TEXT NOT NULL DEFAULT 'medium',
84
- personality TEXT,
85
- copilot_session_id TEXT,
86
- status TEXT NOT NULL DEFAULT 'idle',
87
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
88
- UNIQUE(squad_slug, character_name)
89
- )`,
90
- `ALTER TABLE squad_agents ADD COLUMN is_lead INTEGER NOT NULL DEFAULT 0`,
91
- `ALTER TABLE squad_agents ADD COLUMN is_qa INTEGER NOT NULL DEFAULT 0`,
92
- `CREATE TABLE IF NOT EXISTS squad_task_reviews (
93
- id INTEGER PRIMARY KEY AUTOINCREMENT,
94
- task_id TEXT NOT NULL,
95
- squad_slug TEXT NOT NULL,
96
- reviewer_character TEXT NOT NULL,
97
- approved INTEGER NOT NULL DEFAULT 0,
98
- comments TEXT,
99
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP
100
- )`,
101
- `CREATE TABLE IF NOT EXISTS squad_schedules (
102
- id INTEGER PRIMARY KEY AUTOINCREMENT,
103
- squad_slug TEXT NOT NULL,
104
- name TEXT NOT NULL,
105
- cron_expr TEXT NOT NULL,
106
- agenda TEXT NOT NULL,
107
- notes TEXT,
108
- enabled INTEGER NOT NULL DEFAULT 1,
109
- last_run_at DATETIME,
110
- next_run_at DATETIME,
111
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP
112
- )`,
113
- `CREATE INDEX IF NOT EXISTS idx_squad_schedules_due
114
- ON squad_schedules (enabled, next_run_at)`,
115
- `CREATE TABLE IF NOT EXISTS io_schedules (
116
- id INTEGER PRIMARY KEY AUTOINCREMENT,
117
- name TEXT NOT NULL,
118
- cron_expr TEXT NOT NULL,
119
- prompt TEXT NOT NULL,
120
- notes TEXT,
121
- enabled INTEGER NOT NULL DEFAULT 1,
122
- last_run_at DATETIME,
123
- next_run_at DATETIME,
124
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP
125
- )`,
126
- `CREATE INDEX IF NOT EXISTS idx_io_schedules_due
127
- ON io_schedules (enabled, next_run_at)`,
128
- `CREATE VIEW IF NOT EXISTS agent_stats AS
129
- SELECT agent_slug,
130
- COUNT(*) AS task_count,
131
- MAX(started_at) AS last_delegated_at
132
- FROM agent_tasks
133
- GROUP BY agent_slug`,
134
- `CREATE TABLE IF NOT EXISTS background_notifications (
135
- id INTEGER PRIMARY KEY AUTOINCREMENT,
136
- source_type TEXT NOT NULL,
137
- source_ref TEXT,
138
- title TEXT NOT NULL,
139
- text TEXT NOT NULL,
140
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
141
- read_at DATETIME
142
- )`,
143
- `CREATE INDEX IF NOT EXISTS idx_bg_notifications_unread ON background_notifications(read_at, created_at)`,
144
- `CREATE TABLE IF NOT EXISTS schedule_runs (
145
- id INTEGER PRIMARY KEY AUTOINCREMENT,
146
- schedule_type TEXT NOT NULL,
147
- schedule_id INTEGER NOT NULL,
148
- schedule_name TEXT NOT NULL,
149
- squad_slug TEXT,
150
- status TEXT NOT NULL DEFAULT 'running',
151
- error_text TEXT,
152
- notification_id INTEGER,
153
- started_at DATETIME DEFAULT CURRENT_TIMESTAMP,
154
- completed_at DATETIME
155
- )`,
156
- `CREATE INDEX IF NOT EXISTS idx_schedule_runs_lookup ON schedule_runs(schedule_type, schedule_id, started_at)`,
157
- `CREATE TABLE IF NOT EXISTS inbox_entries (
158
- id INTEGER PRIMARY KEY AUTOINCREMENT,
159
- title TEXT NOT NULL,
160
- body TEXT NOT NULL,
161
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
162
- )`,
163
- `CREATE TABLE IF NOT EXISTS unified_feed (
164
- id INTEGER PRIMARY KEY AUTOINCREMENT,
165
- type TEXT NOT NULL CHECK(type IN ('inbox', 'notification')),
166
- title TEXT NOT NULL,
167
- body TEXT NOT NULL,
168
- source_type TEXT,
169
- source_ref TEXT,
170
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
171
- read_at DATETIME
172
- )`,
173
- `CREATE INDEX IF NOT EXISTS idx_unified_feed_type ON unified_feed(type, created_at)`,
174
- `CREATE INDEX IF NOT EXISTS idx_unified_feed_unread ON unified_feed(read_at, created_at)`,
175
- `ALTER TABLE squads ADD COLUMN color TEXT`,
176
- `CREATE TABLE IF NOT EXISTS squad_instances (
177
- id TEXT PRIMARY KEY,
178
- master_squad_slug TEXT NOT NULL,
179
- issue_ref TEXT,
180
- worktree_path TEXT NOT NULL,
181
- branch_name TEXT NOT NULL,
182
- status TEXT NOT NULL DEFAULT 'pending',
183
- context_snapshot TEXT,
184
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
185
- completed_at DATETIME
186
- )`,
187
- `CREATE TABLE IF NOT EXISTS instance_decisions (
188
- id INTEGER PRIMARY KEY AUTOINCREMENT,
189
- instance_id TEXT NOT NULL,
190
- decision TEXT NOT NULL,
191
- context TEXT,
192
- merged_to_master INTEGER DEFAULT 0,
193
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP
194
- )`,
195
- `ALTER TABLE agent_tasks ADD COLUMN instance_id TEXT`,
196
- `CREATE INDEX IF NOT EXISTS idx_instance_decisions_instance ON instance_decisions(instance_id, merged_to_master)`,
197
- `ALTER TABLE unified_feed RENAME TO unified_feed_old`,
198
- `CREATE TABLE unified_feed (
199
- id INTEGER PRIMARY KEY AUTOINCREMENT,
200
- type TEXT NOT NULL CHECK(type IN ('inbox', 'notification')),
201
- title TEXT NOT NULL,
202
- body TEXT NOT NULL,
203
- source_type TEXT,
204
- source_ref TEXT,
205
- squad_slug TEXT,
206
- instance_id TEXT,
207
- task_id TEXT,
208
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
209
- read_at DATETIME
210
- )`,
211
- `INSERT INTO unified_feed (id, type, title, body, source_type, source_ref, created_at, read_at)
212
- SELECT id, CASE WHEN type='deliverable' THEN 'inbox' ELSE type END, title, body, source_type, source_ref, created_at, read_at
213
- FROM unified_feed_old`,
214
- `DROP TABLE unified_feed_old`,
215
- `CREATE INDEX IF NOT EXISTS idx_unified_feed_type ON unified_feed(type, created_at)`,
216
- `CREATE INDEX IF NOT EXISTS idx_unified_feed_unread ON unified_feed(read_at, created_at)`,
217
- `ALTER TABLE squads ADD COLUMN color TEXT`,
218
- ];
219
- for (const migration of migrations) {
220
- try {
221
- db.exec(migration);
222
- }
223
- catch {
224
- // Already applied — ignore
225
- }
226
- }
227
- // One-time data migration: copy inbox_entries + background_notifications → unified_feed
228
- try {
229
- const migrated = db.prepare("SELECT value FROM io_state WHERE key = 'unified_feed_migrated'").get();
230
- if (!migrated) {
231
- db.exec(`
232
- INSERT OR IGNORE INTO unified_feed (type, title, body, source_type, source_ref, created_at, read_at)
233
- SELECT 'deliverable', title, body, NULL, NULL, created_at, NULL
234
- FROM inbox_entries
235
- `);
236
- db.exec(`
237
- INSERT OR IGNORE INTO unified_feed (type, title, body, source_type, source_ref, created_at, read_at)
238
- SELECT 'notification', title, text, source_type, source_ref, created_at, read_at
239
- FROM background_notifications
240
- `);
241
- db.prepare("INSERT OR REPLACE INTO io_state (key, value) VALUES ('unified_feed_migrated', '1')").run();
242
- }
243
- }
244
- catch {
245
- // Migration failed (e.g. old tables don't exist yet on a fresh install) — safe to ignore
246
- }
247
- // One-time migration: assign colors to existing squads that have none
248
- try {
249
- const colorMigrated = db.prepare("SELECT value FROM io_state WHERE key = 'squad_colors_migrated'").get();
250
- if (!colorMigrated) {
251
- const palette = [
252
- "#ff6b35", "#ffd000", "#5fff87", "#c4a7ff", "#00d9ff",
253
- "#ff9800", "#9c27b0", "#2196f3", "#e91e63", "#00bcd4",
254
- "#8bc34a", "#ff5722",
255
- ];
256
- const uncolored = db.prepare("SELECT slug FROM squads WHERE color IS NULL ORDER BY id ASC").all();
257
- const usedColors = new Set(db.prepare("SELECT color FROM squads WHERE color IS NOT NULL").all().map(r => r.color));
258
- const update = db.prepare("UPDATE squads SET color = ? WHERE slug = ?");
259
- let idx = 0;
260
- for (const { slug } of uncolored) {
261
- const available = palette.filter(c => !usedColors.has(c));
262
- const color = available.length > 0 ? available[0] : palette[idx % palette.length];
263
- update.run(color, slug);
264
- usedColors.add(color);
265
- idx++;
266
- }
267
- db.prepare("INSERT OR REPLACE INTO io_state (key, value) VALUES ('squad_colors_migrated', '1')").run();
268
- }
269
- }
270
- catch {
271
- // Safe to ignore on fresh install
73
+ CREATE TABLE IF NOT EXISTS feed_items (
74
+ id TEXT PRIMARY KEY,
75
+ source TEXT NOT NULL,
76
+ title TEXT NOT NULL,
77
+ content TEXT NOT NULL DEFAULT '',
78
+ read INTEGER NOT NULL DEFAULT 0,
79
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
80
+ );
81
+
82
+ CREATE TABLE IF NOT EXISTS schedules (
83
+ id TEXT PRIMARY KEY,
84
+ type TEXT NOT NULL CHECK(type IN ('squad', 'io')),
85
+ squad_id TEXT REFERENCES squads(id) ON DELETE CASCADE,
86
+ cron TEXT NOT NULL,
87
+ agenda TEXT NOT NULL DEFAULT '',
88
+ prompt TEXT NOT NULL DEFAULT '',
89
+ enabled INTEGER NOT NULL DEFAULT 1,
90
+ last_run TEXT,
91
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
92
+ );
93
+
94
+ CREATE TABLE IF NOT EXISTS session_state (
95
+ key TEXT PRIMARY KEY,
96
+ value TEXT NOT NULL
97
+ );
98
+
99
+ CREATE TABLE IF NOT EXISTS mcp_servers (
100
+ id TEXT PRIMARY KEY,
101
+ name TEXT NOT NULL,
102
+ type TEXT NOT NULL CHECK(type IN ('stdio', 'http')),
103
+ config TEXT NOT NULL DEFAULT '{}',
104
+ enabled INTEGER NOT NULL DEFAULT 1,
105
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
106
+ );
107
+
108
+ CREATE TABLE IF NOT EXISTS mcp_tools (
109
+ id TEXT PRIMARY KEY,
110
+ server_id TEXT NOT NULL REFERENCES mcp_servers(id) ON DELETE CASCADE,
111
+ name TEXT NOT NULL,
112
+ description TEXT DEFAULT '',
113
+ enabled INTEGER NOT NULL DEFAULT 1
114
+ );
115
+ `);
116
+ setSchemaVersion(db, 1);
272
117
  }
273
- return db;
118
+ }
119
+ function getSchemaVersion(db) {
120
+ const row = db.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get();
121
+ return row ? parseInt(row.value, 10) : 0;
122
+ }
123
+ function setSchemaVersion(db, version) {
124
+ db.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES ('schema_version', ?)").run(String(version));
274
125
  }
275
126
  export function closeDb() {
276
127
  if (db) {
277
128
  db.close();
278
- db = null;
129
+ db = undefined;
279
130
  }
280
131
  }
281
- export function getState(key) {
282
- const row = getDb()
283
- .prepare("SELECT value FROM io_state WHERE key = ?")
284
- .get(key);
285
- return row?.value;
286
- }
287
- export function setState(key, value) {
288
- getDb()
289
- .prepare("INSERT INTO io_state (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value")
290
- .run(key, value);
291
- }
292
- export function deleteState(key) {
293
- getDb().prepare("DELETE FROM io_state WHERE key = ?").run(key);
294
- }
295
- export function logConversation(role, content, source) {
296
- const database = getDb();
297
- database
298
- .prepare("INSERT INTO conversation_log (role, content, source) VALUES (?, ?, ?)")
299
- .run(role, content, source);
300
- insertCount++;
301
- if (insertCount % 50 === 0) {
302
- database
303
- .prepare("DELETE FROM conversation_log WHERE id NOT IN (SELECT id FROM conversation_log ORDER BY id DESC LIMIT 1000)")
304
- .run();
305
- }
306
- }
307
- export function getRecentConversation(limit = 50) {
308
- const rows = getDb()
309
- .prepare("SELECT role, content, source, ts FROM conversation_log ORDER BY id DESC LIMIT ?")
310
- .all(limit);
311
- return rows.reverse().map((row) => ({
312
- ...row,
313
- content: row.content.length > 1500
314
- ? row.content.slice(0, 1500) + "…"
315
- : row.content,
316
- }));
317
- }
318
132
  //# sourceMappingURL=db.js.map