heyio 0.1.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.
@@ -0,0 +1,239 @@
1
+ import { defineTool } from "@github/copilot-sdk";
2
+ import { z } from "zod";
3
+ import { execSync } from "child_process";
4
+ import { readFileSync, writeFileSync, readdirSync, statSync, existsSync, mkdirSync } from "fs";
5
+ import { join, dirname, resolve } from "path";
6
+ export function createTools(deps) {
7
+ const wikiRead = defineTool("wiki_read", {
8
+ description: "Read a page from IO's knowledge base wiki. Path is relative to the wiki root (e.g., 'pages/preferences/editor.md').",
9
+ parameters: z.object({
10
+ path: z.string().describe("Relative path to the wiki page"),
11
+ }),
12
+ handler: async ({ path }) => {
13
+ const content = deps.wikiRead(path);
14
+ if (!content)
15
+ return `Page not found: ${path}`;
16
+ return content;
17
+ },
18
+ });
19
+ const wikiWrite = defineTool("wiki_write", {
20
+ description: "Write or update a page in IO's knowledge base. Use this to remember preferences, project details, and important facts. Path must be under pages/ and end in .md.",
21
+ parameters: z.object({
22
+ path: z.string().describe("Relative path under pages/ (e.g., 'pages/preferences/clone-location.md')"),
23
+ content: z.string().describe("Markdown content to write"),
24
+ }),
25
+ handler: async ({ path, content }) => {
26
+ try {
27
+ deps.wikiAssertPagePath(path);
28
+ deps.wikiWrite(path, content);
29
+ return `Written: ${path}`;
30
+ }
31
+ catch (err) {
32
+ return `Error: ${err instanceof Error ? err.message : String(err)}`;
33
+ }
34
+ },
35
+ });
36
+ const wikiSearch = defineTool("wiki_search", {
37
+ description: "Search IO's knowledge base for matching pages.",
38
+ parameters: z.object({
39
+ query: z.string().describe("Search query"),
40
+ }),
41
+ handler: async ({ query }) => {
42
+ const results = deps.wikiSearch(query);
43
+ if (results.length === 0)
44
+ return "No matching pages found.";
45
+ return results
46
+ .map((r) => `**${r.title}** (${r.path})\n${r.snippet}`)
47
+ .join("\n\n");
48
+ },
49
+ });
50
+ const squadCreate = defineTool("squad_create", {
51
+ description: "Create a persistent project squad. Squads remember decisions and context for a specific codebase.",
52
+ parameters: z.object({
53
+ slug: z.string().describe("Unique identifier (e.g., 'michaeljolley-io')"),
54
+ name: z.string().describe("Display name (e.g., 'IO Assistant')"),
55
+ project_path: z.string().describe("Path to the project directory"),
56
+ }),
57
+ handler: async ({ slug, name, project_path }) => {
58
+ try {
59
+ deps.createSquad(slug, name, project_path);
60
+ return `Squad "${name}" created for ${project_path}`;
61
+ }
62
+ catch (err) {
63
+ return `Error creating squad: ${err instanceof Error ? err.message : String(err)}`;
64
+ }
65
+ },
66
+ });
67
+ const squadRecall = defineTool("squad_recall", {
68
+ description: "Recall a squad's context and past decisions. Use this before working on a project to load relevant history.",
69
+ parameters: z.object({
70
+ slug: z.string().describe("Squad slug"),
71
+ }),
72
+ handler: async ({ slug }) => {
73
+ const squad = deps.getSquad(slug);
74
+ if (!squad)
75
+ return `Squad not found: ${slug}`;
76
+ const decisions = deps.getDecisionsSummary(slug);
77
+ return `**Squad: ${squad.name}**\nProject: ${squad.projectPath}\nStatus: ${squad.status}\n\n${decisions}`;
78
+ },
79
+ });
80
+ const squadStatus = defineTool("squad_status", {
81
+ description: "List all squads and their status.",
82
+ parameters: z.object({}),
83
+ handler: async () => {
84
+ const squads = deps.listSquads();
85
+ if (squads.length === 0)
86
+ return "No squads created yet.";
87
+ return squads
88
+ .map((s) => `- **${s.name}** (\`${s.slug}\`) — ${s.status} — ${s.projectPath}`)
89
+ .join("\n");
90
+ },
91
+ });
92
+ const squadLogDecision = defineTool("squad_log_decision", {
93
+ description: "Log a decision for a squad. Use this to record important choices made during project work.",
94
+ parameters: z.object({
95
+ slug: z.string().describe("Squad slug"),
96
+ decision: z.string().describe("The decision made"),
97
+ context: z.string().optional().describe("Context or reasoning"),
98
+ }),
99
+ handler: async ({ slug, decision, context }) => {
100
+ try {
101
+ deps.logDecision(slug, decision, context);
102
+ return `Decision logged for squad ${slug}`;
103
+ }
104
+ catch (err) {
105
+ return `Error: ${err instanceof Error ? err.message : String(err)}`;
106
+ }
107
+ },
108
+ });
109
+ const shell = defineTool("shell", {
110
+ description: "Run a shell command on the user's machine. Use for git, build tools, file operations, etc.",
111
+ parameters: z.object({
112
+ command: z.string().describe("The command to run"),
113
+ timeout_secs: z.number().optional().describe("Timeout in seconds (default: 60)"),
114
+ working_dir: z.string().optional().describe("Working directory for the command"),
115
+ }),
116
+ handler: async ({ command, timeout_secs, working_dir }) => {
117
+ try {
118
+ const result = execSync(command, {
119
+ encoding: "utf-8",
120
+ timeout: (timeout_secs ?? 60) * 1000,
121
+ maxBuffer: 1024 * 1024,
122
+ cwd: working_dir,
123
+ });
124
+ const output = result.trim();
125
+ if (output.length > 8000) {
126
+ return output.slice(0, 8000) + "\n\n[…truncated]";
127
+ }
128
+ return output || "(no output)";
129
+ }
130
+ catch (err) {
131
+ const execErr = err;
132
+ const stderr = execErr.stderr?.trim() ?? "";
133
+ const stdout = execErr.stdout?.trim() ?? "";
134
+ const msg = stderr || stdout || execErr.message || "Command failed";
135
+ if (msg.length > 4000) {
136
+ return `Error:\n${msg.slice(0, 4000)}\n[…truncated]`;
137
+ }
138
+ return `Error:\n${msg}`;
139
+ }
140
+ },
141
+ });
142
+ const webFetch = defineTool("web_fetch", {
143
+ description: "Fetch a URL and return its content as text.",
144
+ parameters: z.object({
145
+ url: z.string().describe("URL to fetch"),
146
+ max_length: z.number().optional().describe("Max chars to return (default: 5000)"),
147
+ }),
148
+ handler: async ({ url, max_length }) => {
149
+ try {
150
+ const response = await fetch(url, {
151
+ headers: { "User-Agent": "IO-Assistant/1.0" },
152
+ signal: AbortSignal.timeout(15000),
153
+ });
154
+ if (!response.ok) {
155
+ return `HTTP ${response.status}: ${response.statusText}`;
156
+ }
157
+ const text = await response.text();
158
+ const limit = max_length ?? 5000;
159
+ if (text.length > limit) {
160
+ return text.slice(0, limit) + "\n\n[…truncated]";
161
+ }
162
+ return text;
163
+ }
164
+ catch (err) {
165
+ return `Fetch error: ${err instanceof Error ? err.message : String(err)}`;
166
+ }
167
+ },
168
+ });
169
+ const fileOps = defineTool("file_ops", {
170
+ description: "Read, write, or list files on the local filesystem.",
171
+ parameters: z.object({
172
+ operation: z.enum(["read", "write", "list"]).describe("Operation to perform"),
173
+ path: z.string().describe("File or directory path"),
174
+ content: z.string().optional().describe("Content to write (for write operation)"),
175
+ recursive: z.boolean().optional().describe("Recurse into subdirectories (for list)"),
176
+ }),
177
+ handler: async ({ operation, path: filePath, content, recursive }) => {
178
+ try {
179
+ const resolved = resolve(filePath);
180
+ if (operation === "read") {
181
+ if (!existsSync(resolved))
182
+ return `File not found: ${filePath}`;
183
+ const text = readFileSync(resolved, "utf-8");
184
+ if (text.length > 8000) {
185
+ return text.slice(0, 8000) + "\n\n[…truncated]";
186
+ }
187
+ return text;
188
+ }
189
+ if (operation === "write") {
190
+ if (!content)
191
+ return "Error: content is required for write operation";
192
+ mkdirSync(dirname(resolved), { recursive: true });
193
+ writeFileSync(resolved, content, "utf-8");
194
+ return `Written: ${filePath}`;
195
+ }
196
+ if (operation === "list") {
197
+ if (!existsSync(resolved))
198
+ return `Directory not found: ${filePath}`;
199
+ if (recursive) {
200
+ const files = walkDirectory(resolved);
201
+ return files.join("\n") || "(empty directory)";
202
+ }
203
+ const entries = readdirSync(resolved);
204
+ return entries
205
+ .map((e) => {
206
+ const full = join(resolved, e);
207
+ const isDir = statSync(full).isDirectory();
208
+ return isDir ? `${e}/` : e;
209
+ })
210
+ .join("\n") || "(empty directory)";
211
+ }
212
+ return `Unknown operation: ${operation}`;
213
+ }
214
+ catch (err) {
215
+ return `Error: ${err instanceof Error ? err.message : String(err)}`;
216
+ }
217
+ },
218
+ });
219
+ return [wikiRead, wikiWrite, wikiSearch, squadCreate, squadRecall, squadStatus, squadLogDecision, shell, webFetch, fileOps];
220
+ }
221
+ function walkDirectory(dir, maxDepth = 3, depth = 0) {
222
+ if (depth >= maxDepth)
223
+ return [];
224
+ const results = [];
225
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
226
+ if (entry.name.startsWith("."))
227
+ continue;
228
+ const full = join(dir, entry.name);
229
+ if (entry.isDirectory()) {
230
+ results.push(`${entry.name}/`);
231
+ results.push(...walkDirectory(full, maxDepth, depth + 1).map((f) => ` ${entry.name}/${f}`));
232
+ }
233
+ else {
234
+ results.push(entry.name);
235
+ }
236
+ }
237
+ return results;
238
+ }
239
+ //# sourceMappingURL=tools.js.map
package/dist/daemon.js ADDED
@@ -0,0 +1,145 @@
1
+ import { getClient, stopClient } from "./copilot/client.js";
2
+ import { initOrchestrator, sendToOrchestrator, shutdownOrchestrator } from "./copilot/orchestrator.js";
3
+ import { startApiServer, setMessageHandler as setApiHandler } from "./api/server.js";
4
+ import { createBot, startBot, stopBot, sendProactiveMessage, setMessageHandler as setTelegramHandler } from "./telegram/bot.js";
5
+ import { getDb, closeDb } from "./store/db.js";
6
+ import { clearStaleTasks } from "./store/tasks.js";
7
+ import { config } from "./config.js";
8
+ import { ensureWikiStructure } from "./wiki/fs.js";
9
+ import { autoUpdate } from "./update.js";
10
+ import { readdirSync, statSync, rmSync } from "fs";
11
+ import { join } from "path";
12
+ import { SESSIONS_DIR } from "./paths.js";
13
+ const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
14
+ function pruneOldSessions() {
15
+ try {
16
+ const sessionDir = join(SESSIONS_DIR, "session-state");
17
+ let entries;
18
+ try {
19
+ entries = readdirSync(sessionDir);
20
+ }
21
+ catch {
22
+ return;
23
+ }
24
+ const cutoff = Date.now() - SEVEN_DAYS_MS;
25
+ let pruned = 0;
26
+ for (const entry of entries) {
27
+ const fullPath = join(sessionDir, entry);
28
+ try {
29
+ const stat = statSync(fullPath);
30
+ if (stat.isDirectory() && stat.mtimeMs < cutoff) {
31
+ rmSync(fullPath, { recursive: true, force: true });
32
+ pruned++;
33
+ }
34
+ }
35
+ catch { /* skip */ }
36
+ }
37
+ if (pruned > 0) {
38
+ console.log(`[io] Pruned ${pruned} orphaned session folder(s)`);
39
+ }
40
+ }
41
+ catch (err) {
42
+ console.error("[io] Session pruning failed (non-fatal):", err instanceof Error ? err.message : err);
43
+ }
44
+ }
45
+ export async function startDaemon() {
46
+ console.log("[io] Starting IO daemon...");
47
+ // Auto-update on startup
48
+ const updated = await autoUpdate();
49
+ if (updated) {
50
+ // Re-exec the process with the new version
51
+ const { spawn } = await import("child_process");
52
+ const child = spawn(process.execPath, [...process.execArgv, ...process.argv.slice(1)], {
53
+ detached: true,
54
+ stdio: "inherit",
55
+ env: { ...process.env, IO_RESTARTED: "1" },
56
+ });
57
+ child.unref();
58
+ process.exit(0);
59
+ }
60
+ if (config.selfEditEnabled) {
61
+ console.log("[io] ⚠ Self-edit mode enabled");
62
+ }
63
+ // Initialize database
64
+ getDb();
65
+ console.log("[io] Database initialized");
66
+ // Initialize wiki
67
+ const wikiIsNew = ensureWikiStructure();
68
+ if (wikiIsNew) {
69
+ console.log("[io] Created wiki at ~/.io/wiki/");
70
+ }
71
+ // Clear stale tasks from previous run
72
+ clearStaleTasks();
73
+ // Prune old sessions
74
+ pruneOldSessions();
75
+ // Start Copilot SDK client
76
+ console.log("[io] Starting Copilot SDK client...");
77
+ const client = await getClient();
78
+ console.log("[io] Copilot SDK client ready");
79
+ // Initialize orchestrator
80
+ console.log("[io] Creating orchestrator session...");
81
+ await initOrchestrator(client);
82
+ console.log("[io] Orchestrator session ready");
83
+ // Wire up API message handler
84
+ setApiHandler(async (text, connectionId, callback) => {
85
+ await sendToOrchestrator(text, { type: "tui", connectionId }, callback);
86
+ });
87
+ // Start HTTP API
88
+ await startApiServer();
89
+ // Wire up Telegram handler
90
+ if (config.telegramEnabled) {
91
+ setTelegramHandler(async (text, chatId, messageId, callback) => {
92
+ await sendToOrchestrator(text, { type: "telegram", chatId, messageId }, callback);
93
+ });
94
+ createBot();
95
+ await startBot();
96
+ }
97
+ else {
98
+ console.log("[io] Telegram not configured — skipping bot. Set telegramBotToken in ~/.io/config.json");
99
+ }
100
+ console.log("[io] IO is fully operational.");
101
+ // Notify Telegram if restarting
102
+ if (config.telegramEnabled && process.env.IO_RESTARTED === "1") {
103
+ await sendProactiveMessage("I'm back online 🟢").catch(() => { });
104
+ delete process.env.IO_RESTARTED;
105
+ }
106
+ }
107
+ // Graceful shutdown
108
+ let shuttingDown = false;
109
+ async function shutdown() {
110
+ if (shuttingDown) {
111
+ console.log("\n[io] Forced exit.");
112
+ process.exit(1);
113
+ }
114
+ shuttingDown = true;
115
+ console.log("\n[io] Shutting down... (Ctrl+C again to force)");
116
+ const forceTimer = setTimeout(() => {
117
+ console.log("[io] Shutdown timed out — forcing exit.");
118
+ process.exit(1);
119
+ }, 5000);
120
+ forceTimer.unref();
121
+ if (config.telegramEnabled) {
122
+ try {
123
+ await stopBot();
124
+ }
125
+ catch { /* best effort */ }
126
+ }
127
+ await shutdownOrchestrator();
128
+ try {
129
+ await stopClient();
130
+ }
131
+ catch { /* best effort */ }
132
+ closeDb();
133
+ console.log("[io] Goodbye.");
134
+ process.exit(0);
135
+ }
136
+ process.on("SIGINT", shutdown);
137
+ process.on("SIGTERM", shutdown);
138
+ process.on("unhandledRejection", (reason) => {
139
+ console.error("[io] Unhandled rejection (kept alive):", reason);
140
+ });
141
+ process.on("uncaughtException", (err) => {
142
+ console.error("[io] Uncaught exception — shutting down:", err);
143
+ process.exit(1);
144
+ });
145
+ //# sourceMappingURL=daemon.js.map
package/dist/index.js ADDED
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { startDaemon } from "./daemon.js";
4
+ import { startTui, setMessageHandler as setTuiHandler } from "./tui/index.js";
5
+ import { sendToOrchestrator, initOrchestrator } from "./copilot/orchestrator.js";
6
+ import { getClient } from "./copilot/client.js";
7
+ import { getDb } from "./store/db.js";
8
+ import { ensureWikiStructure } from "./wiki/fs.js";
9
+ import { startApiServer, setMessageHandler as setApiHandler } from "./api/server.js";
10
+ import { listSkills, installSkill, removeSkill, searchSkillsRegistry } from "./copilot/skills.js";
11
+ import { config, saveConfig } from "./config.js";
12
+ import { createInterface } from "readline";
13
+ const program = new Command();
14
+ program
15
+ .name("io")
16
+ .description("IO — personal AI assistant daemon")
17
+ .version("1.0.0");
18
+ program
19
+ .option("--daemon", "Run as a background daemon")
20
+ .option("--self-edit", "Allow IO to modify its own source code")
21
+ .action(async (opts) => {
22
+ if (opts.selfEdit) {
23
+ config.selfEditEnabled = true;
24
+ }
25
+ if (opts.daemon) {
26
+ await startDaemon();
27
+ }
28
+ else {
29
+ // TUI mode — start minimal services and launch interactive chat
30
+ console.log("[io] Starting IO in interactive mode...");
31
+ getDb();
32
+ ensureWikiStructure();
33
+ const client = await getClient();
34
+ await initOrchestrator(client);
35
+ // Wire up API handler for TUI bridge
36
+ setApiHandler(async (text, connectionId, callback) => {
37
+ await sendToOrchestrator(text, { type: "tui", connectionId }, callback);
38
+ });
39
+ await startApiServer();
40
+ // Wire up TUI handler
41
+ setTuiHandler(async (text, callback) => {
42
+ await sendToOrchestrator(text, { type: "tui", connectionId: "tui-main" }, callback);
43
+ });
44
+ await startTui();
45
+ }
46
+ });
47
+ // Skill management commands
48
+ const skillCmd = program.command("skill").description("Manage IO skills");
49
+ skillCmd
50
+ .command("list")
51
+ .description("List installed skills")
52
+ .action(() => {
53
+ const skills = listSkills();
54
+ if (skills.length === 0) {
55
+ console.log("No skills installed.");
56
+ return;
57
+ }
58
+ for (const s of skills) {
59
+ console.log(` ${s.name} (${s.slug})`);
60
+ if (s.description)
61
+ console.log(` ${s.description}`);
62
+ }
63
+ });
64
+ skillCmd
65
+ .command("add <repo-url>")
66
+ .description("Install a skill from a git repository")
67
+ .action(async (repoUrl) => {
68
+ try {
69
+ console.log(`Installing skill from ${repoUrl}...`);
70
+ const skill = await installSkill(repoUrl);
71
+ console.log(`✓ Installed: ${skill.name} (${skill.slug})`);
72
+ }
73
+ catch (err) {
74
+ console.error(`✗ ${err instanceof Error ? err.message : String(err)}`);
75
+ process.exit(1);
76
+ }
77
+ });
78
+ skillCmd
79
+ .command("remove <slug>")
80
+ .description("Remove an installed skill")
81
+ .action((slug) => {
82
+ const removed = removeSkill(slug);
83
+ if (removed) {
84
+ console.log(`✓ Removed: ${slug}`);
85
+ }
86
+ else {
87
+ console.log(`Skill not found: ${slug}`);
88
+ }
89
+ });
90
+ skillCmd
91
+ .command("search <query>")
92
+ .description("Search the skills registry")
93
+ .action(async (query) => {
94
+ const results = await searchSkillsRegistry(query);
95
+ if (results.length === 0) {
96
+ console.log("No skills found.");
97
+ return;
98
+ }
99
+ for (const r of results) {
100
+ console.log(` ${r.name}`);
101
+ console.log(` ${r.description}`);
102
+ console.log(` ${r.repoUrl}`);
103
+ }
104
+ });
105
+ // Setup command
106
+ program
107
+ .command("setup")
108
+ .description("Configure IO (Telegram bot token, user ID, etc.)")
109
+ .action(async () => {
110
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
111
+ const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
112
+ console.log("\n🔧 IO Setup\n");
113
+ const token = await ask(`Telegram Bot Token${config.telegramBotToken ? " (press Enter to keep current)" : ""}: `);
114
+ if (token.trim()) {
115
+ config.telegramBotToken = token.trim();
116
+ }
117
+ const userId = await ask(`Telegram User ID${config.authorizedUserId ? ` (current: ${config.authorizedUserId})` : ""}: `);
118
+ if (userId.trim()) {
119
+ const parsed = parseInt(userId.trim(), 10);
120
+ if (!isNaN(parsed))
121
+ config.authorizedUserId = parsed;
122
+ }
123
+ config.telegramEnabled = !!(config.telegramBotToken && config.authorizedUserId);
124
+ saveConfig({
125
+ telegramBotToken: config.telegramBotToken,
126
+ authorizedUserId: config.authorizedUserId,
127
+ telegramEnabled: config.telegramEnabled,
128
+ });
129
+ console.log("\n✓ Configuration saved to ~/.io/config.json");
130
+ if (config.telegramEnabled) {
131
+ console.log(" Telegram: enabled");
132
+ }
133
+ else {
134
+ console.log(" Telegram: disabled (need both bot token and user ID)");
135
+ }
136
+ rl.close();
137
+ });
138
+ program.parse();
139
+ //# sourceMappingURL=index.js.map
package/dist/paths.js ADDED
@@ -0,0 +1,10 @@
1
+ import { homedir } from "os";
2
+ import { join } from "path";
3
+ export const IO_HOME = join(homedir(), ".io");
4
+ export const CONFIG_PATH = join(IO_HOME, "config.json");
5
+ export const DB_PATH = join(IO_HOME, "io.db");
6
+ export const WIKI_DIR = join(IO_HOME, "wiki");
7
+ export const SKILLS_DIR = join(IO_HOME, "skills");
8
+ export const SESSIONS_DIR = join(IO_HOME, "sessions");
9
+ export const LOGS_DIR = join(IO_HOME, "logs");
10
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1,101 @@
1
+ import Database from "better-sqlite3";
2
+ import { mkdirSync } from "fs";
3
+ import { DB_PATH, IO_HOME } from "../paths.js";
4
+ let db = null;
5
+ let insertCount = 0;
6
+ export function getDb() {
7
+ if (db)
8
+ return db;
9
+ mkdirSync(IO_HOME, { recursive: true });
10
+ db = new Database(DB_PATH);
11
+ db.pragma("journal_mode = WAL");
12
+ db.exec(`
13
+ CREATE TABLE IF NOT EXISTS io_state (
14
+ key TEXT PRIMARY KEY,
15
+ value TEXT NOT NULL
16
+ );
17
+
18
+ CREATE TABLE IF NOT EXISTS squads (
19
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
20
+ slug TEXT UNIQUE NOT NULL,
21
+ name TEXT NOT NULL,
22
+ project_path TEXT NOT NULL,
23
+ copilot_session_id TEXT,
24
+ status TEXT NOT NULL DEFAULT 'idle',
25
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
26
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
27
+ );
28
+
29
+ CREATE TABLE IF NOT EXISTS squad_decisions (
30
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
31
+ squad_slug TEXT NOT NULL,
32
+ decision TEXT NOT NULL,
33
+ context TEXT,
34
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
35
+ );
36
+
37
+ CREATE TABLE IF NOT EXISTS conversation_log (
38
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
39
+ role TEXT NOT NULL CHECK(role IN ('user', 'assistant', 'system')),
40
+ content TEXT NOT NULL,
41
+ source TEXT NOT NULL DEFAULT 'unknown',
42
+ ts DATETIME DEFAULT CURRENT_TIMESTAMP
43
+ );
44
+
45
+ CREATE TABLE IF NOT EXISTS agent_tasks (
46
+ task_id TEXT PRIMARY KEY,
47
+ agent_slug TEXT NOT NULL,
48
+ description TEXT NOT NULL,
49
+ status TEXT NOT NULL DEFAULT 'running',
50
+ result TEXT,
51
+ origin_channel TEXT,
52
+ started_at DATETIME DEFAULT CURRENT_TIMESTAMP,
53
+ completed_at DATETIME
54
+ );
55
+ `);
56
+ return db;
57
+ }
58
+ export function closeDb() {
59
+ if (db) {
60
+ db.close();
61
+ db = null;
62
+ }
63
+ }
64
+ export function getState(key) {
65
+ const row = getDb()
66
+ .prepare("SELECT value FROM io_state WHERE key = ?")
67
+ .get(key);
68
+ return row?.value;
69
+ }
70
+ export function setState(key, value) {
71
+ getDb()
72
+ .prepare("INSERT INTO io_state (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value")
73
+ .run(key, value);
74
+ }
75
+ export function deleteState(key) {
76
+ getDb().prepare("DELETE FROM io_state WHERE key = ?").run(key);
77
+ }
78
+ export function logConversation(role, content, source) {
79
+ const database = getDb();
80
+ database
81
+ .prepare("INSERT INTO conversation_log (role, content, source) VALUES (?, ?, ?)")
82
+ .run(role, content, source);
83
+ insertCount++;
84
+ if (insertCount % 50 === 0) {
85
+ database
86
+ .prepare("DELETE FROM conversation_log WHERE id NOT IN (SELECT id FROM conversation_log ORDER BY id DESC LIMIT 1000)")
87
+ .run();
88
+ }
89
+ }
90
+ export function getRecentConversation(limit = 50) {
91
+ const rows = getDb()
92
+ .prepare("SELECT role, content, source, ts FROM conversation_log ORDER BY id DESC LIMIT ?")
93
+ .all(limit);
94
+ return rows.reverse().map((row) => ({
95
+ ...row,
96
+ content: row.content.length > 1500
97
+ ? row.content.slice(0, 1500) + "…"
98
+ : row.content,
99
+ }));
100
+ }
101
+ //# sourceMappingURL=db.js.map
@@ -0,0 +1,52 @@
1
+ import { getDb } from "./db.js";
2
+ export function createSquad(slug, name, projectPath) {
3
+ const db = getDb();
4
+ db.prepare("INSERT INTO squads (slug, name, project_path) VALUES (?, ?, ?)").run(slug, name, projectPath);
5
+ return getSquad(slug);
6
+ }
7
+ export function getSquad(slug) {
8
+ return getDb()
9
+ .prepare("SELECT * FROM squads WHERE slug = ?")
10
+ .get(slug);
11
+ }
12
+ export function listSquads() {
13
+ return getDb().prepare("SELECT * FROM squads ORDER BY created_at DESC").all();
14
+ }
15
+ export function updateSquadSession(slug, sessionId) {
16
+ getDb()
17
+ .prepare("UPDATE squads SET copilot_session_id = ?, updated_at = CURRENT_TIMESTAMP WHERE slug = ?")
18
+ .run(sessionId, slug);
19
+ }
20
+ export function updateSquadStatus(slug, status) {
21
+ getDb()
22
+ .prepare("UPDATE squads SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE slug = ?")
23
+ .run(status, slug);
24
+ }
25
+ export function deleteSquad(slug) {
26
+ const db = getDb();
27
+ db.prepare("DELETE FROM squad_decisions WHERE squad_slug = ?").run(slug);
28
+ db.prepare("DELETE FROM squads WHERE slug = ?").run(slug);
29
+ }
30
+ export function logDecision(squadSlug, decision, context) {
31
+ getDb()
32
+ .prepare("INSERT INTO squad_decisions (squad_slug, decision, context) VALUES (?, ?, ?)")
33
+ .run(squadSlug, decision, context ?? null);
34
+ }
35
+ export function getDecisions(squadSlug, limit = 20) {
36
+ return getDb()
37
+ .prepare("SELECT * FROM squad_decisions WHERE squad_slug = ? ORDER BY created_at DESC LIMIT ?")
38
+ .all(squadSlug, limit);
39
+ }
40
+ export function getDecisionsSummary(squadSlug) {
41
+ const decisions = getDecisions(squadSlug);
42
+ if (decisions.length === 0)
43
+ return "No decisions recorded.";
44
+ return decisions
45
+ .reverse()
46
+ .map((d) => {
47
+ const ctx = d.context ? ` (${d.context})` : "";
48
+ return `- [${d.created_at}] ${d.decision}${ctx}`;
49
+ })
50
+ .join("\n");
51
+ }
52
+ //# sourceMappingURL=squads.js.map