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.
- package/LICENSE +21 -0
- package/README.md +212 -0
- package/dist/api/server.js +70 -0
- package/dist/config.js +28 -0
- package/dist/copilot/agents.js +267 -0
- package/dist/copilot/client.js +29 -0
- package/dist/copilot/orchestrator.js +354 -0
- package/dist/copilot/skills.js +132 -0
- package/dist/copilot/system-message.js +90 -0
- package/dist/copilot/tools.js +239 -0
- package/dist/daemon.js +145 -0
- package/dist/index.js +139 -0
- package/dist/paths.js +10 -0
- package/dist/store/db.js +101 -0
- package/dist/store/squads.js +52 -0
- package/dist/store/tasks.js +32 -0
- package/dist/telegram/bot.js +141 -0
- package/dist/telegram/handlers.js +4 -0
- package/dist/tui/index.js +79 -0
- package/dist/update.js +53 -0
- package/dist/wiki/fs.js +152 -0
- package/dist/wiki/search.js +49 -0
- package/package.json +54 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { getDb } from "./db.js";
|
|
2
|
+
export function createTask(taskId, agentSlug, description, originChannel) {
|
|
3
|
+
const db = getDb();
|
|
4
|
+
db.prepare("INSERT INTO agent_tasks (task_id, agent_slug, description, origin_channel) VALUES (?, ?, ?, ?)").run(taskId, agentSlug, description, originChannel ?? null);
|
|
5
|
+
return getTask(taskId);
|
|
6
|
+
}
|
|
7
|
+
export function getTask(taskId) {
|
|
8
|
+
return getDb()
|
|
9
|
+
.prepare("SELECT * FROM agent_tasks WHERE task_id = ?")
|
|
10
|
+
.get(taskId);
|
|
11
|
+
}
|
|
12
|
+
export function getActiveTasks() {
|
|
13
|
+
return getDb()
|
|
14
|
+
.prepare("SELECT * FROM agent_tasks WHERE status = 'running' ORDER BY started_at DESC")
|
|
15
|
+
.all();
|
|
16
|
+
}
|
|
17
|
+
export function completeTask(taskId, result) {
|
|
18
|
+
getDb()
|
|
19
|
+
.prepare("UPDATE agent_tasks SET status = 'done', result = ?, completed_at = CURRENT_TIMESTAMP WHERE task_id = ?")
|
|
20
|
+
.run(result, taskId);
|
|
21
|
+
}
|
|
22
|
+
export function failTask(taskId, error) {
|
|
23
|
+
getDb()
|
|
24
|
+
.prepare("UPDATE agent_tasks SET status = 'failed', result = ?, completed_at = CURRENT_TIMESTAMP WHERE task_id = ?")
|
|
25
|
+
.run(error, taskId);
|
|
26
|
+
}
|
|
27
|
+
export function clearStaleTasks() {
|
|
28
|
+
getDb()
|
|
29
|
+
.prepare("UPDATE agent_tasks SET status = 'failed', result = 'Marked stale on startup', completed_at = CURRENT_TIMESTAMP WHERE status = 'running'")
|
|
30
|
+
.run();
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=tasks.js.map
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { Bot } from "grammy";
|
|
2
|
+
import { config } from "../config.js";
|
|
3
|
+
const TELEGRAM_MAX_LENGTH = 4096;
|
|
4
|
+
const EDIT_DEBOUNCE_MS = 500;
|
|
5
|
+
let bot;
|
|
6
|
+
let messageHandler;
|
|
7
|
+
export function setMessageHandler(handler) {
|
|
8
|
+
messageHandler = handler;
|
|
9
|
+
}
|
|
10
|
+
export function createBot() {
|
|
11
|
+
if (!config.telegramBotToken) {
|
|
12
|
+
console.error("[io] Telegram bot token not configured");
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
bot = new Bot(config.telegramBotToken);
|
|
16
|
+
bot.on("message:text", async (ctx) => {
|
|
17
|
+
const userId = ctx.from?.id;
|
|
18
|
+
if (config.authorizedUserId && userId !== config.authorizedUserId) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (!messageHandler) {
|
|
22
|
+
console.error("[io] No message handler registered");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const text = ctx.message.text;
|
|
26
|
+
const chatId = ctx.chat.id;
|
|
27
|
+
const messageId = ctx.message.message_id;
|
|
28
|
+
await ctx.replyWithChatAction("typing");
|
|
29
|
+
const placeholder = await ctx.reply("…");
|
|
30
|
+
let accumulated = "";
|
|
31
|
+
let lastEditTime = 0;
|
|
32
|
+
let pendingEdit;
|
|
33
|
+
const editReply = async (content) => {
|
|
34
|
+
try {
|
|
35
|
+
await ctx.api.editMessageText(chatId, placeholder.message_id, content);
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
39
|
+
if (!message.includes("message is not modified")) {
|
|
40
|
+
console.error("[io] Failed to edit message:", message);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
try {
|
|
45
|
+
await messageHandler(text, chatId, messageId, (chunk, done) => {
|
|
46
|
+
accumulated += chunk;
|
|
47
|
+
if (done) {
|
|
48
|
+
if (pendingEdit) {
|
|
49
|
+
clearTimeout(pendingEdit);
|
|
50
|
+
pendingEdit = undefined;
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
const timeSinceLastEdit = now - lastEditTime;
|
|
56
|
+
if (timeSinceLastEdit >= EDIT_DEBOUNCE_MS) {
|
|
57
|
+
lastEditTime = now;
|
|
58
|
+
void editReply(accumulated);
|
|
59
|
+
}
|
|
60
|
+
else if (!pendingEdit) {
|
|
61
|
+
pendingEdit = setTimeout(() => {
|
|
62
|
+
pendingEdit = undefined;
|
|
63
|
+
lastEditTime = Date.now();
|
|
64
|
+
void editReply(accumulated);
|
|
65
|
+
}, EDIT_DEBOUNCE_MS - timeSinceLastEdit);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
if (pendingEdit) {
|
|
69
|
+
clearTimeout(pendingEdit);
|
|
70
|
+
}
|
|
71
|
+
// Send final message
|
|
72
|
+
if (accumulated.length > 0) {
|
|
73
|
+
await editReply(accumulated);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
78
|
+
console.error("[io] Error handling message:", message);
|
|
79
|
+
await editReply("An error occurred while processing your message.");
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
bot.catch((err) => {
|
|
83
|
+
console.error("[io] Grammy bot error:", err.message);
|
|
84
|
+
});
|
|
85
|
+
console.log("[io] Telegram bot created");
|
|
86
|
+
}
|
|
87
|
+
export async function startBot() {
|
|
88
|
+
if (!bot) {
|
|
89
|
+
console.error("[io] Bot not created — call createBot() first");
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
console.log("[io] Starting Telegram bot polling…");
|
|
94
|
+
void bot.start({
|
|
95
|
+
onStart: () => console.log("[io] Telegram bot polling started"),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
100
|
+
console.error("[io] Failed to start Telegram bot:", message);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
export async function stopBot() {
|
|
104
|
+
if (!bot) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
await bot.stop();
|
|
109
|
+
console.log("[io] Telegram bot stopped");
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
113
|
+
console.error("[io] Error stopping Telegram bot:", message);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export async function sendProactiveMessage(text) {
|
|
117
|
+
if (!bot) {
|
|
118
|
+
console.error("[io] Bot not created — cannot send proactive message");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (!config.authorizedUserId) {
|
|
122
|
+
console.error("[io] No authorized user ID configured");
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const chunks = [];
|
|
126
|
+
let remaining = text;
|
|
127
|
+
while (remaining.length > 0) {
|
|
128
|
+
chunks.push(remaining.slice(0, TELEGRAM_MAX_LENGTH));
|
|
129
|
+
remaining = remaining.slice(TELEGRAM_MAX_LENGTH);
|
|
130
|
+
}
|
|
131
|
+
for (const chunk of chunks) {
|
|
132
|
+
try {
|
|
133
|
+
await bot.api.sendMessage(config.authorizedUserId, chunk);
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
137
|
+
console.error("[io] Failed to send proactive message:", message);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=bot.js.map
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { createInterface } from "readline";
|
|
2
|
+
let messageHandler;
|
|
3
|
+
export function setMessageHandler(handler) {
|
|
4
|
+
messageHandler = handler;
|
|
5
|
+
}
|
|
6
|
+
const WELCOME_BANNER = `
|
|
7
|
+
╔══════════════════════════════════════╗
|
|
8
|
+
║ IO — AI Assistant ║
|
|
9
|
+
╚══════════════════════════════════════╝
|
|
10
|
+
Type a message to chat. Commands:
|
|
11
|
+
/status — show status
|
|
12
|
+
/quit — exit
|
|
13
|
+
`;
|
|
14
|
+
function clearLine() {
|
|
15
|
+
process.stdout.write("\r\x1b[K");
|
|
16
|
+
}
|
|
17
|
+
export async function startTui() {
|
|
18
|
+
const rl = createInterface({
|
|
19
|
+
input: process.stdin,
|
|
20
|
+
output: process.stdout,
|
|
21
|
+
});
|
|
22
|
+
console.log(WELCOME_BANNER);
|
|
23
|
+
rl.setPrompt("io> ");
|
|
24
|
+
rl.prompt();
|
|
25
|
+
rl.on("line", async (input) => {
|
|
26
|
+
const trimmed = input.trim();
|
|
27
|
+
if (!trimmed) {
|
|
28
|
+
rl.prompt();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (trimmed === "/quit") {
|
|
32
|
+
console.log("[io] Goodbye!");
|
|
33
|
+
rl.close();
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
if (trimmed === "/status") {
|
|
37
|
+
console.log(`[io] Uptime: ${Math.floor(process.uptime())}s`);
|
|
38
|
+
rl.prompt();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (!messageHandler) {
|
|
42
|
+
console.log("[io] No message handler registered.");
|
|
43
|
+
rl.prompt();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
// Show typing indicator
|
|
47
|
+
process.stdout.write("...");
|
|
48
|
+
let accumulated = "";
|
|
49
|
+
let firstChunk = true;
|
|
50
|
+
try {
|
|
51
|
+
await messageHandler(trimmed, (text, done) => {
|
|
52
|
+
if (firstChunk) {
|
|
53
|
+
clearLine();
|
|
54
|
+
firstChunk = false;
|
|
55
|
+
}
|
|
56
|
+
if (done) {
|
|
57
|
+
clearLine();
|
|
58
|
+
process.stdout.write(accumulated + "\n");
|
|
59
|
+
rl.prompt();
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
accumulated += text;
|
|
63
|
+
clearLine();
|
|
64
|
+
process.stdout.write(accumulated);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
clearLine();
|
|
70
|
+
console.error(`[io] Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
71
|
+
rl.prompt();
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
rl.on("close", () => {
|
|
75
|
+
console.log("\n[io] Goodbye!");
|
|
76
|
+
process.exit(0);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=index.js.map
|
package/dist/update.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
const PACKAGE_NAME = "heyio";
|
|
3
|
+
export async function checkForUpdate() {
|
|
4
|
+
try {
|
|
5
|
+
const packageJson = await import("../package.json", { with: { type: "json" } }).catch(() => null);
|
|
6
|
+
const current = packageJson?.default?.version ?? "0.0.0";
|
|
7
|
+
const latest = execSync(`npm view ${PACKAGE_NAME} version 2>/dev/null`, {
|
|
8
|
+
encoding: "utf-8",
|
|
9
|
+
timeout: 10_000,
|
|
10
|
+
}).trim();
|
|
11
|
+
if (!latest)
|
|
12
|
+
return { updateAvailable: false, current, latest: current };
|
|
13
|
+
const updateAvailable = latest !== current && compareSemver(current, latest) < 0;
|
|
14
|
+
return { updateAvailable, current, latest };
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return { updateAvailable: false, current: "unknown", latest: "unknown" };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Auto-update: install the latest version globally and return true if updated.
|
|
22
|
+
* Returns false if already up-to-date or if the update fails.
|
|
23
|
+
*/
|
|
24
|
+
export async function autoUpdate() {
|
|
25
|
+
const info = await checkForUpdate();
|
|
26
|
+
if (!info.updateAvailable)
|
|
27
|
+
return false;
|
|
28
|
+
console.log(`[io] ⬆ Updating: v${info.current} → v${info.latest}...`);
|
|
29
|
+
try {
|
|
30
|
+
execSync(`npm install -g ${PACKAGE_NAME}@latest`, {
|
|
31
|
+
encoding: "utf-8",
|
|
32
|
+
timeout: 60_000,
|
|
33
|
+
stdio: "pipe",
|
|
34
|
+
});
|
|
35
|
+
console.log(`[io] ✓ Updated to v${info.latest}. Restarting...`);
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
console.error(`[io] ⚠ Auto-update failed:`, err instanceof Error ? err.message : err);
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function compareSemver(a, b) {
|
|
44
|
+
const pa = a.split(".").map(Number);
|
|
45
|
+
const pb = b.split(".").map(Number);
|
|
46
|
+
for (let i = 0; i < 3; i++) {
|
|
47
|
+
const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
48
|
+
if (diff !== 0)
|
|
49
|
+
return diff;
|
|
50
|
+
}
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=update.js.map
|
package/dist/wiki/fs.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync, openSync, fsyncSync, closeSync, } from "fs";
|
|
2
|
+
import { join, resolve, relative, dirname, basename, extname } from "path";
|
|
3
|
+
import { randomBytes } from "crypto";
|
|
4
|
+
import { WIKI_DIR } from "../paths.js";
|
|
5
|
+
export const PAGES_DIR = join(WIKI_DIR, "pages");
|
|
6
|
+
export const SOURCES_DIR = join(WIKI_DIR, "sources");
|
|
7
|
+
const INDEX_PATH = join(WIKI_DIR, "index.md");
|
|
8
|
+
const LOG_PATH = join(WIKI_DIR, "log.md");
|
|
9
|
+
const PAGE_CATEGORIES = ["preferences", "projects", "people", "general"];
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Internal helpers
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
function resolvePath(relativePath) {
|
|
14
|
+
const full = resolve(WIKI_DIR, relativePath);
|
|
15
|
+
const rel = relative(WIKI_DIR, full);
|
|
16
|
+
if (rel.startsWith("..") || resolve(full) !== full) {
|
|
17
|
+
throw new Error(`Path traversal detected: ${relativePath}`);
|
|
18
|
+
}
|
|
19
|
+
return full;
|
|
20
|
+
}
|
|
21
|
+
function walkDir(dir) {
|
|
22
|
+
if (!existsSync(dir))
|
|
23
|
+
return [];
|
|
24
|
+
const results = [];
|
|
25
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
26
|
+
const fullPath = join(dir, entry.name);
|
|
27
|
+
if (entry.isDirectory()) {
|
|
28
|
+
results.push(...walkDir(fullPath));
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
results.push(fullPath);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return results;
|
|
35
|
+
}
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Public API
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
export function writeFileAtomic(fullPath, content) {
|
|
40
|
+
const dir = dirname(fullPath);
|
|
41
|
+
mkdirSync(dir, { recursive: true });
|
|
42
|
+
const tmp = join(dir, `.tmp-${randomBytes(6).toString("hex")}`);
|
|
43
|
+
const fd = openSync(tmp, "w");
|
|
44
|
+
try {
|
|
45
|
+
writeFileSync(fd, content, "utf-8");
|
|
46
|
+
fsyncSync(fd);
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
closeSync(fd);
|
|
50
|
+
}
|
|
51
|
+
renameSync(tmp, fullPath);
|
|
52
|
+
}
|
|
53
|
+
export function assertPagePath(relativePath) {
|
|
54
|
+
if (!relativePath.startsWith("pages/") && !relativePath.startsWith("pages\\")) {
|
|
55
|
+
throw new Error(`Page path must be under pages/: ${relativePath}`);
|
|
56
|
+
}
|
|
57
|
+
if (extname(relativePath) !== ".md") {
|
|
58
|
+
throw new Error(`Page path must end in .md: ${relativePath}`);
|
|
59
|
+
}
|
|
60
|
+
const full = resolve(WIKI_DIR, relativePath);
|
|
61
|
+
const rel = relative(WIKI_DIR, full);
|
|
62
|
+
if (rel.startsWith("..") || !rel.startsWith("pages")) {
|
|
63
|
+
throw new Error(`Path traversal detected: ${relativePath}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export function ensureWikiStructure() {
|
|
67
|
+
const created = !existsSync(WIKI_DIR);
|
|
68
|
+
mkdirSync(WIKI_DIR, { recursive: true });
|
|
69
|
+
mkdirSync(PAGES_DIR, { recursive: true });
|
|
70
|
+
mkdirSync(SOURCES_DIR, { recursive: true });
|
|
71
|
+
for (const cat of PAGE_CATEGORIES) {
|
|
72
|
+
mkdirSync(join(PAGES_DIR, cat), { recursive: true });
|
|
73
|
+
}
|
|
74
|
+
if (!existsSync(INDEX_PATH)) {
|
|
75
|
+
writeFileAtomic(INDEX_PATH, "# Wiki Index\n\n_No pages yet._\n");
|
|
76
|
+
}
|
|
77
|
+
if (!existsSync(LOG_PATH)) {
|
|
78
|
+
writeFileAtomic(LOG_PATH, "# Wiki Log\n\n_No operations recorded._\n");
|
|
79
|
+
}
|
|
80
|
+
return created;
|
|
81
|
+
}
|
|
82
|
+
export function readPage(relativePath) {
|
|
83
|
+
const full = resolvePath(relativePath);
|
|
84
|
+
if (!existsSync(full))
|
|
85
|
+
return undefined;
|
|
86
|
+
return readFileSync(full, "utf-8");
|
|
87
|
+
}
|
|
88
|
+
export function writePage(relativePath, content) {
|
|
89
|
+
assertPagePath(relativePath);
|
|
90
|
+
const full = resolvePath(relativePath);
|
|
91
|
+
writeFileAtomic(full, content);
|
|
92
|
+
}
|
|
93
|
+
export function deletePage(relativePath) {
|
|
94
|
+
assertPagePath(relativePath);
|
|
95
|
+
const full = resolvePath(relativePath);
|
|
96
|
+
if (!existsSync(full))
|
|
97
|
+
return false;
|
|
98
|
+
unlinkSync(full);
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
export function pageExists(relativePath) {
|
|
102
|
+
assertPagePath(relativePath);
|
|
103
|
+
const full = resolvePath(relativePath);
|
|
104
|
+
return existsSync(full);
|
|
105
|
+
}
|
|
106
|
+
export function listPages() {
|
|
107
|
+
return walkDir(PAGES_DIR)
|
|
108
|
+
.filter((f) => extname(f) === ".md")
|
|
109
|
+
.map((f) => relative(WIKI_DIR, f).replace(/\\/g, "/"));
|
|
110
|
+
}
|
|
111
|
+
export function writeRawSource(name, content) {
|
|
112
|
+
const sanitized = basename(name).replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
113
|
+
if (!sanitized)
|
|
114
|
+
throw new Error(`Invalid source name: ${name}`);
|
|
115
|
+
const full = join(SOURCES_DIR, sanitized);
|
|
116
|
+
writeFileAtomic(full, content);
|
|
117
|
+
}
|
|
118
|
+
export function readRawSource(name) {
|
|
119
|
+
const sanitized = basename(name).replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
120
|
+
const full = join(SOURCES_DIR, sanitized);
|
|
121
|
+
if (!existsSync(full))
|
|
122
|
+
return undefined;
|
|
123
|
+
return readFileSync(full, "utf-8");
|
|
124
|
+
}
|
|
125
|
+
export function listSources() {
|
|
126
|
+
if (!existsSync(SOURCES_DIR))
|
|
127
|
+
return [];
|
|
128
|
+
return readdirSync(SOURCES_DIR).filter((f) => {
|
|
129
|
+
const full = join(SOURCES_DIR, f);
|
|
130
|
+
return statSync(full).isFile();
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
export function readIndexFile() {
|
|
134
|
+
if (!existsSync(INDEX_PATH))
|
|
135
|
+
return "";
|
|
136
|
+
return readFileSync(INDEX_PATH, "utf-8");
|
|
137
|
+
}
|
|
138
|
+
export function writeIndexFile(content) {
|
|
139
|
+
writeFileAtomic(INDEX_PATH, content);
|
|
140
|
+
}
|
|
141
|
+
export function readLogFile() {
|
|
142
|
+
if (!existsSync(LOG_PATH))
|
|
143
|
+
return "";
|
|
144
|
+
return readFileSync(LOG_PATH, "utf-8");
|
|
145
|
+
}
|
|
146
|
+
export function writeLogFile(content) {
|
|
147
|
+
writeFileAtomic(LOG_PATH, content);
|
|
148
|
+
}
|
|
149
|
+
export function getWikiDir() {
|
|
150
|
+
return WIKI_DIR;
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=fs.js.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { readPage, listPages } from "./fs.js";
|
|
2
|
+
export function searchWiki(query) {
|
|
3
|
+
if (!query.trim())
|
|
4
|
+
return [];
|
|
5
|
+
const lowerQuery = query.toLowerCase();
|
|
6
|
+
const pages = listPages();
|
|
7
|
+
const results = [];
|
|
8
|
+
for (const pagePath of pages) {
|
|
9
|
+
const content = readPage(pagePath);
|
|
10
|
+
if (!content)
|
|
11
|
+
continue;
|
|
12
|
+
const lowerContent = content.toLowerCase();
|
|
13
|
+
const idx = lowerContent.indexOf(lowerQuery);
|
|
14
|
+
if (idx === -1)
|
|
15
|
+
continue;
|
|
16
|
+
const title = extractTitle(pagePath, content);
|
|
17
|
+
const snippetStart = Math.max(0, idx - 100);
|
|
18
|
+
const snippetEnd = Math.min(content.length, idx + query.length + 100);
|
|
19
|
+
let snippet = content.slice(snippetStart, snippetEnd).trim();
|
|
20
|
+
if (snippetStart > 0)
|
|
21
|
+
snippet = `…${snippet}`;
|
|
22
|
+
if (snippetEnd < content.length)
|
|
23
|
+
snippet = `${snippet}…`;
|
|
24
|
+
results.push({ path: pagePath, title, snippet });
|
|
25
|
+
}
|
|
26
|
+
return results;
|
|
27
|
+
}
|
|
28
|
+
export function getWikiSummary() {
|
|
29
|
+
const pages = listPages();
|
|
30
|
+
if (pages.length === 0)
|
|
31
|
+
return "Wiki is empty — no pages yet.";
|
|
32
|
+
const lines = ["Wiki pages:"];
|
|
33
|
+
for (const pagePath of pages) {
|
|
34
|
+
const content = readPage(pagePath);
|
|
35
|
+
const title = content ? extractTitle(pagePath, content) : pagePath;
|
|
36
|
+
lines.push(`- ${pagePath}: ${title}`);
|
|
37
|
+
}
|
|
38
|
+
return lines.join("\n");
|
|
39
|
+
}
|
|
40
|
+
function extractTitle(pagePath, content) {
|
|
41
|
+
const firstLine = content.split("\n").find((l) => l.trim().length > 0);
|
|
42
|
+
if (firstLine) {
|
|
43
|
+
const heading = firstLine.replace(/^#+\s*/, "").trim();
|
|
44
|
+
if (heading)
|
|
45
|
+
return heading;
|
|
46
|
+
}
|
|
47
|
+
return pagePath.replace(/.*\//, "").replace(/\.md$/, "");
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=search.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "heyio",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "IO — a personal AI assistant built on the GitHub Copilot SDK",
|
|
5
|
+
"bin": {
|
|
6
|
+
"io": "dist/index.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"dist/**/*.js",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"daemon": "tsx src/daemon.ts",
|
|
15
|
+
"tui": "tsx src/tui/index.ts",
|
|
16
|
+
"dev": "tsx --watch src/daemon.ts",
|
|
17
|
+
"prepublishOnly": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=18"
|
|
21
|
+
},
|
|
22
|
+
"type": "module",
|
|
23
|
+
"keywords": [
|
|
24
|
+
"copilot",
|
|
25
|
+
"telegram",
|
|
26
|
+
"orchestrator",
|
|
27
|
+
"ai",
|
|
28
|
+
"cli",
|
|
29
|
+
"assistant"
|
|
30
|
+
],
|
|
31
|
+
"author": "Michael Jolley",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/michaeljolley/io.git"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/michaeljolley/io#readme",
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@github/copilot-sdk": "^0.2.2",
|
|
40
|
+
"better-sqlite3": "^12.6.2",
|
|
41
|
+
"commander": "^14.0.0",
|
|
42
|
+
"dotenv": "^17.3.1",
|
|
43
|
+
"express": "^5.2.1",
|
|
44
|
+
"grammy": "^1.40.0",
|
|
45
|
+
"zod": "^4.3.6"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
49
|
+
"@types/express": "^5.0.6",
|
|
50
|
+
"@types/node": "^25.3.0",
|
|
51
|
+
"tsx": "^4.21.0",
|
|
52
|
+
"typescript": "^5.9.3"
|
|
53
|
+
}
|
|
54
|
+
}
|