heyio 0.6.0 → 0.9.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/dist/api/server.js +156 -1
- package/dist/config.js +3 -0
- package/dist/copilot/io-scheduler.js +26 -3
- package/dist/copilot/scheduler.js +24 -2
- package/dist/copilot/session-timeout.test.js +372 -0
- package/dist/copilot/skills.js +112 -15
- package/dist/copilot/tools.js +2 -2
- package/dist/daemon.js +35 -2
- package/dist/notify.js +105 -0
- package/dist/notify.test.js +232 -0
- package/dist/store/db.js +41 -2
- package/dist/store/notifications.js +79 -0
- package/dist/store/notifications.test.js +197 -0
- package/dist/store/schedule-runs.js +46 -0
- package/dist/telegram/bot.js +14 -0
- package/dist/tui/index.js +73 -0
- package/package.json +3 -2
- package/web-dist/assets/index-CikqmObM.js +78 -0
- package/web-dist/assets/index-Tc2XV93a.css +1 -0
- package/web-dist/index.html +2 -2
- package/web-dist/assets/index-BlZDeDCS.js +0 -74
- package/web-dist/assets/index-DMKRXYjX.css +0 -1
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { getDb } from "./db.js";
|
|
2
|
+
/**
|
|
3
|
+
* Insert a new background notification. Returns the inserted row including
|
|
4
|
+
* the autoincrement id and DB-assigned created_at timestamp. source_ref
|
|
5
|
+
* should be a JSON string or null.
|
|
6
|
+
*/
|
|
7
|
+
export function insertNotification(input) {
|
|
8
|
+
const db = getDb();
|
|
9
|
+
const info = db
|
|
10
|
+
.prepare(`INSERT INTO background_notifications (source_type, source_ref, title, text)
|
|
11
|
+
VALUES (?, ?, ?, ?)`)
|
|
12
|
+
.run(input.source_type, input.source_ref, input.title, input.text);
|
|
13
|
+
return db
|
|
14
|
+
.prepare("SELECT * FROM background_notifications WHERE id = ?")
|
|
15
|
+
.get(info.lastInsertRowid);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* List the most recent notifications, newest first. Default limit 50.
|
|
19
|
+
*/
|
|
20
|
+
export function listRecentNotifications(limit = 50) {
|
|
21
|
+
return getDb()
|
|
22
|
+
.prepare("SELECT * FROM background_notifications ORDER BY created_at DESC, id DESC LIMIT ?")
|
|
23
|
+
.all(limit);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* List unread notifications (read_at IS NULL), newest first.
|
|
27
|
+
*/
|
|
28
|
+
export function listUnreadNotifications() {
|
|
29
|
+
return getDb()
|
|
30
|
+
.prepare("SELECT * FROM background_notifications WHERE read_at IS NULL ORDER BY created_at DESC, id DESC")
|
|
31
|
+
.all();
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Count unread notifications. Cheap — uses COUNT(*).
|
|
35
|
+
*/
|
|
36
|
+
export function countUnreadNotifications() {
|
|
37
|
+
const row = getDb()
|
|
38
|
+
.prepare("SELECT COUNT(*) AS n FROM background_notifications WHERE read_at IS NULL")
|
|
39
|
+
.get();
|
|
40
|
+
return row.n;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Mark a single notification read. Returns true if the row exists (whether
|
|
44
|
+
* it was already read or just now marked), false if no such id exists.
|
|
45
|
+
*/
|
|
46
|
+
export function markNotificationRead(id) {
|
|
47
|
+
const db = getDb();
|
|
48
|
+
const info = db
|
|
49
|
+
.prepare("UPDATE background_notifications SET read_at = CURRENT_TIMESTAMP WHERE id = ? AND read_at IS NULL")
|
|
50
|
+
.run(id);
|
|
51
|
+
if (info.changes > 0)
|
|
52
|
+
return true;
|
|
53
|
+
// Already read — verify the row exists at all
|
|
54
|
+
const exists = db
|
|
55
|
+
.prepare("SELECT id FROM background_notifications WHERE id = ?")
|
|
56
|
+
.get(id);
|
|
57
|
+
return exists !== undefined;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Mark every unread notification read. Returns the number of rows affected.
|
|
61
|
+
*/
|
|
62
|
+
export function markAllNotificationsRead() {
|
|
63
|
+
const info = getDb()
|
|
64
|
+
.prepare("UPDATE background_notifications SET read_at = CURRENT_TIMESTAMP WHERE read_at IS NULL")
|
|
65
|
+
.run();
|
|
66
|
+
return info.changes;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Delete notifications older than `olderThanDays` days. Returns rows deleted.
|
|
70
|
+
* Used by a future retention sweep.
|
|
71
|
+
*/
|
|
72
|
+
export function pruneOldNotifications(olderThanDays) {
|
|
73
|
+
const info = getDb()
|
|
74
|
+
.prepare(`DELETE FROM background_notifications
|
|
75
|
+
WHERE created_at < datetime('now', ? || ' days')`)
|
|
76
|
+
.run(`-${olderThanDays}`);
|
|
77
|
+
return info.changes;
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=notifications.js.map
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for src/store/notifications.ts — SQLite CRUD helpers.
|
|
3
|
+
*
|
|
4
|
+
* DB isolation: setDbPathForTests() redirects the SQLite singleton to a
|
|
5
|
+
* fresh tmp file, ensuring these tests never touch ~/.io/io.db.
|
|
6
|
+
*/
|
|
7
|
+
import { before, after, beforeEach, describe, it } from "node:test";
|
|
8
|
+
import assert from "node:assert/strict";
|
|
9
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { setDbPathForTests, closeDb, getDb } from "./db.js";
|
|
13
|
+
import { insertNotification, listRecentNotifications, listUnreadNotifications, countUnreadNotifications, markNotificationRead, markAllNotificationsRead, pruneOldNotifications, } from "./notifications.js";
|
|
14
|
+
// ── DB isolation ────────────────────────────────────────────────────────────
|
|
15
|
+
let tmpDir;
|
|
16
|
+
before(() => {
|
|
17
|
+
tmpDir = mkdtempSync(join(tmpdir(), "io-notifs-test-"));
|
|
18
|
+
setDbPathForTests(join(tmpDir, "io.db"));
|
|
19
|
+
});
|
|
20
|
+
// Wipe all notifications between tests for a clean slate.
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
getDb().prepare("DELETE FROM background_notifications").run();
|
|
23
|
+
});
|
|
24
|
+
after(() => {
|
|
25
|
+
closeDb();
|
|
26
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
27
|
+
});
|
|
28
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
29
|
+
function makeNotif(overrides = {}) {
|
|
30
|
+
return insertNotification({
|
|
31
|
+
source_type: "io-schedule",
|
|
32
|
+
source_ref: JSON.stringify({ scheduleId: 1 }),
|
|
33
|
+
title: "Test Notification",
|
|
34
|
+
text: "This is the notification body.",
|
|
35
|
+
...overrides,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
// ── insertNotification ───────────────────────────────────────────────────────
|
|
39
|
+
describe("insertNotification", () => {
|
|
40
|
+
it("returns a row with autoincrement id and created_at timestamp", () => {
|
|
41
|
+
const row = makeNotif();
|
|
42
|
+
assert.ok(typeof row.id === "number" && row.id > 0, "id should be a positive integer");
|
|
43
|
+
assert.ok(row.created_at, "created_at should be set");
|
|
44
|
+
assert.equal(row.title, "Test Notification");
|
|
45
|
+
assert.equal(row.text, "This is the notification body.");
|
|
46
|
+
assert.equal(row.read_at, null);
|
|
47
|
+
});
|
|
48
|
+
it("ids are autoincremented across inserts", () => {
|
|
49
|
+
const a = makeNotif();
|
|
50
|
+
const b = makeNotif();
|
|
51
|
+
assert.ok(b.id > a.id, "second id should be greater than first");
|
|
52
|
+
});
|
|
53
|
+
it("accepts source_ref: null", () => {
|
|
54
|
+
const row = makeNotif({ source_ref: null });
|
|
55
|
+
assert.equal(row.source_ref, null);
|
|
56
|
+
});
|
|
57
|
+
it("stores source_type correctly", () => {
|
|
58
|
+
const row = makeNotif({ source_type: "squad-schedule" });
|
|
59
|
+
assert.equal(row.source_type, "squad-schedule");
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
// ── listRecentNotifications ──────────────────────────────────────────────────
|
|
63
|
+
describe("listRecentNotifications", () => {
|
|
64
|
+
it("returns newest first", () => {
|
|
65
|
+
const a = makeNotif({ title: "First" });
|
|
66
|
+
const b = makeNotif({ title: "Second" });
|
|
67
|
+
const c = makeNotif({ title: "Third" });
|
|
68
|
+
const rows = listRecentNotifications();
|
|
69
|
+
assert.equal(rows[0].id, c.id, "newest should be first");
|
|
70
|
+
assert.equal(rows[rows.length - 1].id, a.id, "oldest should be last");
|
|
71
|
+
});
|
|
72
|
+
it("default limit is 50", () => {
|
|
73
|
+
for (let i = 0; i < 55; i++)
|
|
74
|
+
makeNotif({ title: `N${i}` });
|
|
75
|
+
const rows = listRecentNotifications();
|
|
76
|
+
assert.equal(rows.length, 50);
|
|
77
|
+
});
|
|
78
|
+
it("explicit limit is honored", () => {
|
|
79
|
+
for (let i = 0; i < 10; i++)
|
|
80
|
+
makeNotif({ title: `N${i}` });
|
|
81
|
+
const rows = listRecentNotifications(3);
|
|
82
|
+
assert.equal(rows.length, 3);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
// ── countUnreadNotifications ─────────────────────────────────────────────────
|
|
86
|
+
describe("countUnreadNotifications", () => {
|
|
87
|
+
it("starts at zero on a clean DB", () => {
|
|
88
|
+
assert.equal(countUnreadNotifications(), 0);
|
|
89
|
+
});
|
|
90
|
+
it("increases on insert", () => {
|
|
91
|
+
makeNotif();
|
|
92
|
+
assert.equal(countUnreadNotifications(), 1);
|
|
93
|
+
makeNotif();
|
|
94
|
+
assert.equal(countUnreadNotifications(), 2);
|
|
95
|
+
});
|
|
96
|
+
it("decreases when a notification is marked read", () => {
|
|
97
|
+
const a = makeNotif();
|
|
98
|
+
makeNotif();
|
|
99
|
+
assert.equal(countUnreadNotifications(), 2);
|
|
100
|
+
markNotificationRead(a.id);
|
|
101
|
+
assert.equal(countUnreadNotifications(), 1);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
// ── markNotificationRead ─────────────────────────────────────────────────────
|
|
105
|
+
describe("markNotificationRead", () => {
|
|
106
|
+
it("returns false on a non-existent id", () => {
|
|
107
|
+
assert.equal(markNotificationRead(999999), false);
|
|
108
|
+
});
|
|
109
|
+
it("returns true on an existing unread notification", () => {
|
|
110
|
+
const row = makeNotif();
|
|
111
|
+
assert.equal(markNotificationRead(row.id), true);
|
|
112
|
+
});
|
|
113
|
+
it("is idempotent — returns true even if already read", () => {
|
|
114
|
+
const row = makeNotif();
|
|
115
|
+
assert.equal(markNotificationRead(row.id), true);
|
|
116
|
+
assert.equal(markNotificationRead(row.id), true, "second call should still return true");
|
|
117
|
+
});
|
|
118
|
+
it("sets read_at on the row", () => {
|
|
119
|
+
const row = makeNotif();
|
|
120
|
+
markNotificationRead(row.id);
|
|
121
|
+
const updated = getDb()
|
|
122
|
+
.prepare("SELECT read_at FROM background_notifications WHERE id = ?")
|
|
123
|
+
.get(row.id);
|
|
124
|
+
assert.ok(updated.read_at, "read_at should be set after marking read");
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
// ── markAllNotificationsRead ─────────────────────────────────────────────────
|
|
128
|
+
describe("markAllNotificationsRead", () => {
|
|
129
|
+
it("returns the count of newly-marked rows", () => {
|
|
130
|
+
makeNotif();
|
|
131
|
+
makeNotif();
|
|
132
|
+
makeNotif();
|
|
133
|
+
assert.equal(markAllNotificationsRead(), 3);
|
|
134
|
+
});
|
|
135
|
+
it("subsequent call returns 0 (all already read)", () => {
|
|
136
|
+
makeNotif();
|
|
137
|
+
makeNotif();
|
|
138
|
+
markAllNotificationsRead();
|
|
139
|
+
assert.equal(markAllNotificationsRead(), 0);
|
|
140
|
+
});
|
|
141
|
+
it("only marks unread rows — pre-read rows not re-touched", () => {
|
|
142
|
+
const a = makeNotif();
|
|
143
|
+
makeNotif();
|
|
144
|
+
markNotificationRead(a.id); // mark one manually first
|
|
145
|
+
const count = markAllNotificationsRead(); // should only mark the remaining 1
|
|
146
|
+
assert.equal(count, 1);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
// ── listUnreadNotifications ──────────────────────────────────────────────────
|
|
150
|
+
describe("listUnreadNotifications", () => {
|
|
151
|
+
it("excludes read rows", () => {
|
|
152
|
+
const a = makeNotif({ title: "A" });
|
|
153
|
+
const b = makeNotif({ title: "B" });
|
|
154
|
+
markNotificationRead(a.id);
|
|
155
|
+
const unread = listUnreadNotifications();
|
|
156
|
+
assert.ok(!unread.some((r) => r.id === a.id), "read row should not appear");
|
|
157
|
+
assert.ok(unread.some((r) => r.id === b.id), "unread row should appear");
|
|
158
|
+
});
|
|
159
|
+
it("returns newest first", () => {
|
|
160
|
+
const a = makeNotif({ title: "A" });
|
|
161
|
+
const b = makeNotif({ title: "B" });
|
|
162
|
+
const rows = listUnreadNotifications();
|
|
163
|
+
assert.equal(rows[0].id, b.id);
|
|
164
|
+
assert.equal(rows[1].id, a.id);
|
|
165
|
+
});
|
|
166
|
+
it("returns empty array when all are read", () => {
|
|
167
|
+
makeNotif();
|
|
168
|
+
makeNotif();
|
|
169
|
+
markAllNotificationsRead();
|
|
170
|
+
assert.deepEqual(listUnreadNotifications(), []);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
// ── pruneOldNotifications ─────────────────────────────────────────────────────
|
|
174
|
+
describe("pruneOldNotifications", () => {
|
|
175
|
+
it("deletes rows older than the threshold and returns the count", () => {
|
|
176
|
+
const old = makeNotif({ title: "Old" });
|
|
177
|
+
makeNotif({ title: "Recent" }); // stays untouched
|
|
178
|
+
// Back-date the 'old' row to 10 days ago
|
|
179
|
+
getDb()
|
|
180
|
+
.prepare("UPDATE background_notifications SET created_at = datetime('now', '-10 days') WHERE id = ?")
|
|
181
|
+
.run(old.id);
|
|
182
|
+
const deleted = pruneOldNotifications(7); // prune rows older than 7 days
|
|
183
|
+
assert.equal(deleted, 1, "should delete exactly the back-dated row");
|
|
184
|
+
const remaining = listRecentNotifications();
|
|
185
|
+
assert.ok(!remaining.some((r) => r.id === old.id), "old row should be gone");
|
|
186
|
+
assert.ok(remaining.some((r) => r.title === "Recent"), "recent row should remain");
|
|
187
|
+
});
|
|
188
|
+
it("returns 0 when nothing is old enough to prune", () => {
|
|
189
|
+
makeNotif();
|
|
190
|
+
makeNotif();
|
|
191
|
+
assert.equal(pruneOldNotifications(7), 0);
|
|
192
|
+
});
|
|
193
|
+
it("is safe on an empty table", () => {
|
|
194
|
+
assert.equal(pruneOldNotifications(1), 0);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
//# sourceMappingURL=notifications.test.js.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { getDb } from "./db.js";
|
|
2
|
+
/** Create a new run in 'running' status. Returns the row. */
|
|
3
|
+
export function startScheduleRun(input) {
|
|
4
|
+
const db = getDb();
|
|
5
|
+
const info = db
|
|
6
|
+
.prepare(`INSERT INTO schedule_runs (schedule_type, schedule_id, schedule_name, squad_slug)
|
|
7
|
+
VALUES (?, ?, ?, ?)`)
|
|
8
|
+
.run(input.schedule_type, input.schedule_id, input.schedule_name, input.squad_slug ?? null);
|
|
9
|
+
return db
|
|
10
|
+
.prepare("SELECT * FROM schedule_runs WHERE id = ?")
|
|
11
|
+
.get(info.lastInsertRowid);
|
|
12
|
+
}
|
|
13
|
+
/** Mark a run complete (success). Optionally link a notification_id. */
|
|
14
|
+
export function completeScheduleRun(id, notificationId) {
|
|
15
|
+
getDb()
|
|
16
|
+
.prepare(`UPDATE schedule_runs
|
|
17
|
+
SET status = 'done', completed_at = CURRENT_TIMESTAMP, notification_id = ?
|
|
18
|
+
WHERE id = ?`)
|
|
19
|
+
.run(notificationId ?? null, id);
|
|
20
|
+
}
|
|
21
|
+
/** Mark a run failed with an error message. */
|
|
22
|
+
export function failScheduleRun(id, errorText) {
|
|
23
|
+
getDb()
|
|
24
|
+
.prepare(`UPDATE schedule_runs
|
|
25
|
+
SET status = 'failed', completed_at = CURRENT_TIMESTAMP, error_text = ?
|
|
26
|
+
WHERE id = ?`)
|
|
27
|
+
.run(errorText, id);
|
|
28
|
+
}
|
|
29
|
+
/** Get last N runs for a specific schedule. Newest first. */
|
|
30
|
+
export function getScheduleRuns(scheduleType, scheduleId, limit = 10) {
|
|
31
|
+
return getDb()
|
|
32
|
+
.prepare(`SELECT * FROM schedule_runs
|
|
33
|
+
WHERE schedule_type = ? AND schedule_id = ?
|
|
34
|
+
ORDER BY started_at DESC
|
|
35
|
+
LIMIT ?`)
|
|
36
|
+
.all(scheduleType, scheduleId, limit);
|
|
37
|
+
}
|
|
38
|
+
/** Prune runs older than the given number of days. Returns rows deleted. */
|
|
39
|
+
export function pruneOldScheduleRuns(olderThanDays) {
|
|
40
|
+
const info = getDb()
|
|
41
|
+
.prepare(`DELETE FROM schedule_runs
|
|
42
|
+
WHERE started_at < datetime('now', '-' || ? || ' days')`)
|
|
43
|
+
.run(olderThanDays);
|
|
44
|
+
return info.changes;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=schedule-runs.js.map
|
package/dist/telegram/bot.js
CHANGED
|
@@ -141,4 +141,18 @@ export async function sendProactiveMessage(text) {
|
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* Send a background schedule result as a Telegram notification.
|
|
146
|
+
* Delegates to sendProactiveMessage for chunking, bot-not-configured guards,
|
|
147
|
+
* and authorized-user checks — no extra logic needed here.
|
|
148
|
+
*
|
|
149
|
+
* Format (plain text, no parse_mode):
|
|
150
|
+
* 🔔 Background update — <title>
|
|
151
|
+
*
|
|
152
|
+
* <text>
|
|
153
|
+
*/
|
|
154
|
+
export async function sendBackgroundNotification(opts) {
|
|
155
|
+
const formatted = `\ud83d\udd14 Background update \u2014 ${opts.title}\n\n${opts.text}`;
|
|
156
|
+
await sendProactiveMessage(formatted);
|
|
157
|
+
}
|
|
144
158
|
//# sourceMappingURL=bot.js.map
|
package/dist/tui/index.js
CHANGED
|
@@ -17,6 +17,76 @@ Type a message to chat. Commands:
|
|
|
17
17
|
/quit — exit
|
|
18
18
|
`;
|
|
19
19
|
let verbose = false;
|
|
20
|
+
// Held so printBackgroundNotification can redraw the prompt around notifications.
|
|
21
|
+
// Assigned inside startTui(); undefined when running headless (no TUI).
|
|
22
|
+
let activeInterface;
|
|
23
|
+
const NOTIFICATION_MAX_LINES = 6;
|
|
24
|
+
const NOTIFICATION_WRAP_WIDTH = 78;
|
|
25
|
+
/** Hard-wrap a single line to at most `width` visible chars, returning segments. */
|
|
26
|
+
function wrapLine(line, width) {
|
|
27
|
+
const segments = [];
|
|
28
|
+
while (line.length > width) {
|
|
29
|
+
segments.push(line.slice(0, width));
|
|
30
|
+
line = line.slice(width);
|
|
31
|
+
}
|
|
32
|
+
segments.push(line);
|
|
33
|
+
return segments;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Print a background notification in a bordered block above the prompt,
|
|
37
|
+
* preserving any in-progress readline input the user has typed.
|
|
38
|
+
*
|
|
39
|
+
* Format:
|
|
40
|
+
* ╭─🔔 Background update: <title>
|
|
41
|
+
* │ <line1>
|
|
42
|
+
* │ […N more lines — see /notifications] (if truncated)
|
|
43
|
+
* ╰─
|
|
44
|
+
*
|
|
45
|
+
* Safe to call at any time, even before startTui(). Never throws.
|
|
46
|
+
*/
|
|
47
|
+
export function printBackgroundNotification(opts) {
|
|
48
|
+
try {
|
|
49
|
+
// Build display lines from text, hard-wrapping long lines.
|
|
50
|
+
const rawLines = opts.text.split(/\r?\n/);
|
|
51
|
+
const displayLines = [];
|
|
52
|
+
for (const raw of rawLines) {
|
|
53
|
+
for (const seg of wrapLine(raw, NOTIFICATION_WRAP_WIDTH)) {
|
|
54
|
+
displayLines.push(seg);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const truncated = displayLines.length > NOTIFICATION_MAX_LINES;
|
|
58
|
+
const visible = truncated ? displayLines.slice(0, NOTIFICATION_MAX_LINES) : displayLines;
|
|
59
|
+
const extra = displayLines.length - NOTIFICATION_MAX_LINES;
|
|
60
|
+
const top = "\u256d\u2500\ud83d\udd14 Background update: " + opts.title + "\n";
|
|
61
|
+
const body = visible.map((l) => "\u2502 " + l).join("\n");
|
|
62
|
+
const overflow = truncated
|
|
63
|
+
? "\n\u2502 [\u2026" + extra + " more line" + (extra === 1 ? "" : "s") + " \u2014 see /notifications]"
|
|
64
|
+
: "";
|
|
65
|
+
const bottom = "\n\u2570\u2500\n";
|
|
66
|
+
const block = top + body + overflow + bottom;
|
|
67
|
+
if (!activeInterface) {
|
|
68
|
+
// Headless daemon — no readline interface live; plain log is fine.
|
|
69
|
+
process.stdout.write(block);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// Capture whatever the user has typed so far.
|
|
73
|
+
// readline stores the pending input in `rl.line` (stable internal property).
|
|
74
|
+
const currentLine = activeInterface.line ?? "";
|
|
75
|
+
// Clear the current prompt+input line, print the notification, then
|
|
76
|
+
// redraw the prompt with the user's buffer.
|
|
77
|
+
process.stdout.write("\r\x1b[K");
|
|
78
|
+
process.stdout.write(block);
|
|
79
|
+
if (currentLine === "") {
|
|
80
|
+
activeInterface.prompt(true);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
process.stdout.write("io> " + currentLine);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
console.error("[io] printBackgroundNotification failed:", err instanceof Error ? err.message : String(err));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
20
90
|
function renderActivity(taskIdArg) {
|
|
21
91
|
const recent = listRecentTasks(20);
|
|
22
92
|
let task = undefined;
|
|
@@ -62,6 +132,9 @@ export async function startTui() {
|
|
|
62
132
|
input: process.stdin,
|
|
63
133
|
output: process.stdout,
|
|
64
134
|
});
|
|
135
|
+
// Keep a module-level reference so printBackgroundNotification can redraw
|
|
136
|
+
// the prompt without disturbing the user's in-progress input.
|
|
137
|
+
activeInterface = rl;
|
|
65
138
|
console.log(WELCOME_BANNER);
|
|
66
139
|
rl.setPrompt("io> ");
|
|
67
140
|
rl.prompt();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "heyio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "IO — a personal AI assistant built on the GitHub Copilot SDK",
|
|
5
5
|
"bin": {
|
|
6
6
|
"io": "dist/index.js"
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"daemon": "tsx src/daemon.ts",
|
|
19
19
|
"tui": "tsx src/tui/index.ts",
|
|
20
20
|
"dev": "tsx --watch src/daemon.ts",
|
|
21
|
-
"prepublishOnly": "npm run build:all"
|
|
21
|
+
"prepublishOnly": "npm run build:all",
|
|
22
|
+
"test": "node --import tsx --test 'src/**/*.test.ts'"
|
|
22
23
|
},
|
|
23
24
|
"engines": {
|
|
24
25
|
"node": ">=22"
|