office-core 0.1.2 → 0.1.4
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.
|
@@ -182,14 +182,15 @@ async function processRoomMessages(runtime) {
|
|
|
182
182
|
}
|
|
183
183
|
return;
|
|
184
184
|
}
|
|
185
|
-
const runnableSessions = Array.from(sessions.values()).filter((session) => session.status === "running");
|
|
186
185
|
const project = await getJson(runtime, `/api/projects/${runtime.projectId}`).catch(() => null);
|
|
187
186
|
for (const message of messages) {
|
|
188
187
|
hooks.onRoomMessage?.(message, runtime);
|
|
189
188
|
if (message.author_type === "system") {
|
|
190
189
|
runtime.roomCursorSeq = Math.max(runtime.roomCursorSeq, message.seq);
|
|
190
|
+
await persistRoomCursor(runtime);
|
|
191
191
|
continue;
|
|
192
192
|
}
|
|
193
|
+
const runnableSessions = Array.from(sessions.values()).filter((session) => session.status === "running");
|
|
193
194
|
const targetSessions = runnableSessions.filter((session) => shouldSessionReceiveMessage(runtime, session, message, settings));
|
|
194
195
|
if (targetSessions.length === 0) {
|
|
195
196
|
console.log(`[room] seq=${message.seq} no sessions matched (runnable=${runnableSessions.length}), skipping`);
|
|
@@ -7,14 +7,14 @@ import { getHostConfigPath, getStartupLauncherPath, upsertHostConfig, } from "./
|
|
|
7
7
|
const args = parseArgs(process.argv.slice(2));
|
|
8
8
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
9
9
|
void main().catch((error) => {
|
|
10
|
-
console.error(error);
|
|
10
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
11
11
|
process.exitCode = 1;
|
|
12
12
|
});
|
|
13
13
|
async function main() {
|
|
14
|
-
const baseUrl = (args.baseUrl
|
|
15
|
-
const projectId = args.project
|
|
14
|
+
const baseUrl = normalizeBaseUrlArg(args.baseUrl);
|
|
15
|
+
const projectId = normalizeStringArg(args.project, "--project", "prj_local");
|
|
16
16
|
const requestedHostId = args.hostId;
|
|
17
|
-
const displayName = args.displayName
|
|
17
|
+
const displayName = normalizeStringArg(args.displayName, "--displayName", `${os.hostname()} host`);
|
|
18
18
|
const machineName = os.hostname();
|
|
19
19
|
const enrollSecret = args.enrollSecret ??
|
|
20
20
|
process.env.OFFICE_HOST_ENROLL_SECRET ??
|
|
@@ -37,7 +37,7 @@ async function main() {
|
|
|
37
37
|
host_id: registration.host_id,
|
|
38
38
|
base_url: baseUrl,
|
|
39
39
|
project_id: registration.project_id,
|
|
40
|
-
workdir: args.workdir,
|
|
40
|
+
workdir: normalizeOptionalPathArg(args.workdir, "--workdir"),
|
|
41
41
|
display_name: displayName,
|
|
42
42
|
token: registration.host_token,
|
|
43
43
|
room_cursor_seq: Number(registration.room_cursor_seq ?? 0),
|
|
@@ -55,14 +55,21 @@ async function main() {
|
|
|
55
55
|
console.log(`Auto-start: ${config.auto_start ? "enabled" : "disabled"}`);
|
|
56
56
|
}
|
|
57
57
|
async function registerHost(baseUrl, projectId, body, enrollSecret) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
58
|
+
let response;
|
|
59
|
+
try {
|
|
60
|
+
response = await fetch(`${baseUrl}/api/projects/${projectId}/local-host/register`, {
|
|
61
|
+
method: "POST",
|
|
62
|
+
headers: {
|
|
63
|
+
"content-type": "application/json",
|
|
64
|
+
"x-enroll-secret": enrollSecret,
|
|
65
|
+
},
|
|
66
|
+
body: JSON.stringify(body),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
71
|
+
throw new Error(`Host registration failed: could not reach ${baseUrl} (${message})`);
|
|
72
|
+
}
|
|
66
73
|
if (!response.ok) {
|
|
67
74
|
throw new Error(`Host registration failed: ${response.status} ${await response.text()}`);
|
|
68
75
|
}
|
|
@@ -106,3 +113,42 @@ function parseArgs(argv) {
|
|
|
106
113
|
function escapeBatch(value) {
|
|
107
114
|
return value.replace(/"/g, '""');
|
|
108
115
|
}
|
|
116
|
+
function normalizeBaseUrlArg(value) {
|
|
117
|
+
if (value === "true") {
|
|
118
|
+
throw new Error("Missing value for --baseUrl");
|
|
119
|
+
}
|
|
120
|
+
const raw = (value ?? "http://127.0.0.1:8787").trim().replace(/\/$/, "");
|
|
121
|
+
try {
|
|
122
|
+
const url = new URL(raw);
|
|
123
|
+
if (!/^https?:$/.test(url.protocol)) {
|
|
124
|
+
throw new Error("protocol");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
throw new Error(`Invalid --baseUrl: ${raw}`);
|
|
129
|
+
}
|
|
130
|
+
return raw;
|
|
131
|
+
}
|
|
132
|
+
function normalizeStringArg(value, flagName, fallback) {
|
|
133
|
+
if (value === "true") {
|
|
134
|
+
throw new Error(`Missing value for ${flagName}`);
|
|
135
|
+
}
|
|
136
|
+
const normalized = String(value ?? fallback).trim();
|
|
137
|
+
if (!normalized) {
|
|
138
|
+
throw new Error(`Missing value for ${flagName}`);
|
|
139
|
+
}
|
|
140
|
+
return normalized;
|
|
141
|
+
}
|
|
142
|
+
function normalizeOptionalPathArg(value, flagName) {
|
|
143
|
+
if (value === undefined) {
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
if (value === "true") {
|
|
147
|
+
throw new Error(`Missing value for ${flagName}`);
|
|
148
|
+
}
|
|
149
|
+
const normalized = value.trim();
|
|
150
|
+
if (!normalized) {
|
|
151
|
+
throw new Error(`Missing value for ${flagName}`);
|
|
152
|
+
}
|
|
153
|
+
return normalized;
|
|
154
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
const HOST_CONFIG_VERSION = 1;
|
|
@@ -28,8 +28,14 @@ export async function loadHostConfig() {
|
|
|
28
28
|
if (!filePath) {
|
|
29
29
|
return null;
|
|
30
30
|
}
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
let parsed;
|
|
32
|
+
try {
|
|
33
|
+
const raw = await readFile(filePath, "utf8");
|
|
34
|
+
parsed = parseLooseJson(raw);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
33
39
|
if (!parsed || parsed.version !== HOST_CONFIG_VERSION) {
|
|
34
40
|
return null;
|
|
35
41
|
}
|
|
@@ -163,8 +169,19 @@ function parseLooseJson(raw) {
|
|
|
163
169
|
}
|
|
164
170
|
async function writeAtomicJson(filePath, value) {
|
|
165
171
|
const tempPath = `${filePath}.tmp`;
|
|
166
|
-
|
|
167
|
-
|
|
172
|
+
try {
|
|
173
|
+
await writeFile(tempPath, JSON.stringify(value, null, 2), "utf8");
|
|
174
|
+
await rename(tempPath, filePath);
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
await rm(tempPath, { force: true }).catch(() => undefined);
|
|
178
|
+
const code = error && typeof error === "object" && "code" in error ? String(error.code ?? "") : "";
|
|
179
|
+
if (code === "EPERM" || code === "EACCES" || code === "EBUSY") {
|
|
180
|
+
const fileLabel = path.basename(filePath);
|
|
181
|
+
throw new Error(`Unable to write ${fileLabel} at ${filePath}. Remove read-only protection or close any program using that file.`);
|
|
182
|
+
}
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
168
185
|
}
|
|
169
186
|
function baseAppDataDir() {
|
|
170
187
|
return process.env.APPDATA ?? path.join(process.env.USERPROFILE ?? os.homedir(), "AppData", "Roaming");
|
|
@@ -7,20 +7,39 @@ import { loadHostConfig, upsertHostConfig, getHostConfigPath } from "./lib/host-
|
|
|
7
7
|
import { probeRunnerAvailability, resolveRunnerCommand } from "./lib/local-runner.js";
|
|
8
8
|
import { sessions, hooks, buildRuntimeConfig, startDaemonLoop, stopDaemonLoop, stopSession, buildSpawnContext, spawnInteractiveSession, persistSessions, postRoomMessageUpsert, postJson, getJson, resolveProjectWorkdir, } from "./home-agent-host.js";
|
|
9
9
|
// ─── ANSI Codes ─────────────────────────────────────────────────────────────
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
10
|
+
let R = "\x1b[0m";
|
|
11
|
+
let BOLD = "\x1b[1m";
|
|
12
|
+
let DIM = "\x1b[2m";
|
|
13
|
+
let BMAG = "\x1b[95m";
|
|
14
|
+
let MAG = "\x1b[35m";
|
|
15
|
+
let BBLU = "\x1b[94m";
|
|
16
|
+
let BLU = "\x1b[34m";
|
|
17
|
+
let BCYN = "\x1b[96m";
|
|
18
|
+
let CYN = "\x1b[36m";
|
|
19
|
+
let GRN = "\x1b[32m";
|
|
20
|
+
let YEL = "\x1b[33m";
|
|
21
|
+
let RED = "\x1b[31m";
|
|
22
|
+
let WHT = "\x1b[97m";
|
|
23
|
+
let GRY = "\x1b[90m";
|
|
24
|
+
let USE_UNICODE = true;
|
|
25
|
+
function configureOutputMode(isInteractiveTerminal) {
|
|
26
|
+
const styled = isInteractiveTerminal && !("NO_COLOR" in process.env) && process.env.TERM !== "dumb";
|
|
27
|
+
USE_UNICODE = isInteractiveTerminal && process.env.OFFICE_CORE_ASCII !== "1";
|
|
28
|
+
R = styled ? "\x1b[0m" : "";
|
|
29
|
+
BOLD = styled ? "\x1b[1m" : "";
|
|
30
|
+
DIM = styled ? "\x1b[2m" : "";
|
|
31
|
+
BMAG = styled ? "\x1b[95m" : "";
|
|
32
|
+
MAG = styled ? "\x1b[35m" : "";
|
|
33
|
+
BBLU = styled ? "\x1b[94m" : "";
|
|
34
|
+
BLU = styled ? "\x1b[34m" : "";
|
|
35
|
+
BCYN = styled ? "\x1b[96m" : "";
|
|
36
|
+
CYN = styled ? "\x1b[36m" : "";
|
|
37
|
+
GRN = styled ? "\x1b[32m" : "";
|
|
38
|
+
YEL = styled ? "\x1b[33m" : "";
|
|
39
|
+
RED = styled ? "\x1b[31m" : "";
|
|
40
|
+
WHT = styled ? "\x1b[97m" : "";
|
|
41
|
+
GRY = styled ? "\x1b[90m" : "";
|
|
42
|
+
}
|
|
24
43
|
// ─── ASCII Banner ───────────────────────────────────────────────────────────
|
|
25
44
|
const BANNER = [
|
|
26
45
|
[" ██████ ███████ ███████ ██ ██████ ███████ ", BMAG],
|
|
@@ -78,9 +97,15 @@ function drawAgentBox(label, text, maxW = 74) {
|
|
|
78
97
|
const lines = wordWrap(text.trim(), maxW - 4);
|
|
79
98
|
const inner = Math.max(label.length + 2, ...lines.map((l) => l.length), 20);
|
|
80
99
|
const w = Math.min(inner, maxW - 4);
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
const
|
|
100
|
+
const tl = USE_UNICODE ? "┌" : "+";
|
|
101
|
+
const tr = USE_UNICODE ? "┐" : "+";
|
|
102
|
+
const bl = USE_UNICODE ? "└" : "+";
|
|
103
|
+
const br = USE_UNICODE ? "┘" : "+";
|
|
104
|
+
const h = USE_UNICODE ? "─" : "-";
|
|
105
|
+
const v = USE_UNICODE ? "│" : "|";
|
|
106
|
+
const top = `${CYN}${tl}${h} ${R}${BOLD}${BCYN}${label}${R}${CYN} ${h.repeat(Math.max(0, w - label.length - 1))}${tr}${R}`;
|
|
107
|
+
const bot = `${CYN}${bl}${h.repeat(w + 2)}${br}${R}`;
|
|
108
|
+
const body = lines.map((l) => `${CYN}${v}${R} ${WHT}${l.padEnd(w)}${R} ${CYN}${v}${R}`);
|
|
84
109
|
return [top, ...body, bot].join("\n");
|
|
85
110
|
}
|
|
86
111
|
// ─── Timestamp ──────────────────────────────────────────────────────────────
|
|
@@ -191,6 +216,27 @@ function clearSlashMenu() {
|
|
|
191
216
|
eraseSlashMenu();
|
|
192
217
|
resetSlashMenuState();
|
|
193
218
|
}
|
|
219
|
+
let _menuRefreshPending = false;
|
|
220
|
+
let _queuedPreserveSelection = false;
|
|
221
|
+
function scheduleSlashMenuRefresh(rl, preserveSelection = false) {
|
|
222
|
+
_queuedPreserveSelection ||= preserveSelection;
|
|
223
|
+
if (_menuRefreshPending) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
_menuRefreshPending = true;
|
|
227
|
+
setImmediate(() => {
|
|
228
|
+
_menuRefreshPending = false;
|
|
229
|
+
const keepSelection = _queuedPreserveSelection;
|
|
230
|
+
_queuedPreserveSelection = false;
|
|
231
|
+
const line = rl.line ?? "";
|
|
232
|
+
if (line.startsWith("/") && line.length >= 1) {
|
|
233
|
+
renderSlashMenu(line.slice(1), keepSelection);
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
clearSlashMenu();
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
194
240
|
// ─── Tab Completer ──────────────────────────────────────────────────────────
|
|
195
241
|
function completer(line) {
|
|
196
242
|
if (!line.startsWith("/"))
|
|
@@ -211,6 +257,26 @@ function completer(line) {
|
|
|
211
257
|
function ask(rl, prompt) {
|
|
212
258
|
return new Promise((resolve) => rl.question(prompt, (a) => resolve(a?.trim() ?? "")));
|
|
213
259
|
}
|
|
260
|
+
async function readAllStdin(stream) {
|
|
261
|
+
let buffer = "";
|
|
262
|
+
stream.setEncoding?.("utf8");
|
|
263
|
+
for await (const chunk of stream) {
|
|
264
|
+
buffer += String(chunk);
|
|
265
|
+
}
|
|
266
|
+
return buffer;
|
|
267
|
+
}
|
|
268
|
+
function createNonInteractiveReadline() {
|
|
269
|
+
const noop = () => undefined;
|
|
270
|
+
return {
|
|
271
|
+
close: noop,
|
|
272
|
+
question: (_query, callback) => {
|
|
273
|
+
callback("");
|
|
274
|
+
return undefined;
|
|
275
|
+
},
|
|
276
|
+
prompt: noop,
|
|
277
|
+
setPrompt: noop,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
214
280
|
function safePrompt(ctx, preserveCursor = false) {
|
|
215
281
|
if (!ctx.running || !ctx.isInteractiveTerminal) {
|
|
216
282
|
return;
|
|
@@ -222,23 +288,11 @@ function safePrompt(ctx, preserveCursor = false) {
|
|
|
222
288
|
// Ignore prompt attempts after readline closes during shutdown/tests.
|
|
223
289
|
}
|
|
224
290
|
}
|
|
225
|
-
function
|
|
226
|
-
const internal = rl;
|
|
227
|
-
internal.line = value;
|
|
228
|
-
internal.cursor = value.length;
|
|
229
|
-
internal._refreshLine?.();
|
|
230
|
-
}
|
|
231
|
-
function startDaemonInBackground(ctx) {
|
|
291
|
+
async function startDaemon(ctx) {
|
|
232
292
|
if (!ctx.runtime) {
|
|
233
293
|
return;
|
|
234
294
|
}
|
|
235
|
-
|
|
236
|
-
if (!ctx.running) {
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
console.log(`${RED}Daemon failed: ${error instanceof Error ? error.message : String(error)}${R}`);
|
|
240
|
-
safePrompt(ctx);
|
|
241
|
-
});
|
|
295
|
+
await startDaemonLoop(ctx.runtime);
|
|
242
296
|
}
|
|
243
297
|
function fmtAge(ms) {
|
|
244
298
|
if (ms < 60_000)
|
|
@@ -488,7 +542,7 @@ async function cmdSetup(_a, ctx) {
|
|
|
488
542
|
// Connect immediately
|
|
489
543
|
stopDaemonLoop();
|
|
490
544
|
ctx.runtime = await buildRuntimeConfig();
|
|
491
|
-
|
|
545
|
+
await startDaemon(ctx);
|
|
492
546
|
console.log(`\n ${GRN}Host online.${R}\n`);
|
|
493
547
|
}
|
|
494
548
|
catch (e) {
|
|
@@ -557,9 +611,10 @@ async function cmdClear(_a, _ctx) {
|
|
|
557
611
|
printBanner();
|
|
558
612
|
}
|
|
559
613
|
async function cmdHelp(_a, _ctx) {
|
|
614
|
+
const rule = (USE_UNICODE ? "─" : "-").repeat(56);
|
|
560
615
|
console.log("");
|
|
561
616
|
console.log(` ${BOLD}${WHT}Commands${R}`);
|
|
562
|
-
console.log(` ${
|
|
617
|
+
console.log(` ${rule}`);
|
|
563
618
|
for (const c of CMDS) {
|
|
564
619
|
const a = c.args ? ` ${CYN}${c.args}${R}` : "";
|
|
565
620
|
const al = c.alias ? ` ${GRY}(${c.alias.map((x) => `/${x}`).join(", ")})${R}` : "";
|
|
@@ -570,80 +625,81 @@ async function cmdHelp(_a, _ctx) {
|
|
|
570
625
|
console.log(` ${DIM}Tab to autocomplete slash commands${R}\n`);
|
|
571
626
|
}
|
|
572
627
|
async function cmdQuit(_a, ctx) {
|
|
573
|
-
ctx.running = false;
|
|
574
|
-
stopDaemonLoop();
|
|
575
628
|
ctx.rl.close();
|
|
576
629
|
}
|
|
577
630
|
// ─── Main ───────────────────────────────────────────────────────────────────
|
|
578
631
|
void main().catch((err) => {
|
|
579
|
-
console.error(err);
|
|
632
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
580
633
|
process.exitCode = 1;
|
|
581
634
|
});
|
|
582
635
|
async function main() {
|
|
583
|
-
console.clear();
|
|
584
|
-
printBanner();
|
|
585
636
|
const isInteractiveTerminal = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
}
|
|
637
|
+
configureOutputMode(isInteractiveTerminal);
|
|
638
|
+
const pipedInputPromise = !isInteractiveTerminal ? readAllStdin(process.stdin) : Promise.resolve("");
|
|
639
|
+
if (isInteractiveTerminal) {
|
|
640
|
+
console.clear();
|
|
641
|
+
printBanner();
|
|
642
|
+
}
|
|
643
|
+
const rl = isInteractiveTerminal
|
|
644
|
+
? createInterface({
|
|
645
|
+
input: process.stdin,
|
|
646
|
+
output: process.stdout,
|
|
647
|
+
terminal: true,
|
|
648
|
+
historySize: 0,
|
|
649
|
+
completer,
|
|
650
|
+
})
|
|
651
|
+
: createNonInteractiveReadline();
|
|
592
652
|
const ctx = { runtime: null, rl, running: true, isInteractiveTerminal };
|
|
593
653
|
const sentIds = new Set();
|
|
654
|
+
const startupMessages = [];
|
|
655
|
+
let startupComplete = false;
|
|
656
|
+
let pendingInputs = 0;
|
|
657
|
+
let closeRequested = false;
|
|
658
|
+
let closeFinalized = false;
|
|
594
659
|
let finishStartup;
|
|
595
660
|
const startupReady = new Promise((resolve) => {
|
|
596
661
|
finishStartup = resolve;
|
|
597
662
|
});
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
safePrompt(ctx);
|
|
602
|
-
}
|
|
603
|
-
else {
|
|
604
|
-
try {
|
|
605
|
-
rl.resume();
|
|
663
|
+
const flushStartupMessages = () => {
|
|
664
|
+
if (!startupMessages.length) {
|
|
665
|
+
return;
|
|
606
666
|
}
|
|
607
|
-
|
|
608
|
-
|
|
667
|
+
for (const message of startupMessages.splice(0)) {
|
|
668
|
+
process.stdout.write(`\r\x1b[K`);
|
|
669
|
+
displayMessage(message);
|
|
609
670
|
}
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
renderSlashMenu(line.slice(1), true);
|
|
627
|
-
return;
|
|
671
|
+
};
|
|
672
|
+
const finalizeClose = () => {
|
|
673
|
+
if (closeFinalized) {
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
closeFinalized = true;
|
|
677
|
+
ctx.running = false;
|
|
678
|
+
stopDaemonLoop();
|
|
679
|
+
if (ctx.isInteractiveTerminal) {
|
|
680
|
+
process.stdin.setRawMode?.(false);
|
|
681
|
+
}
|
|
682
|
+
if (ctx.isInteractiveTerminal) {
|
|
683
|
+
const internalRl = rl;
|
|
684
|
+
if (!internalRl.closed) {
|
|
685
|
+
try {
|
|
686
|
+
rl.close();
|
|
628
687
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
replaceInputLine(rl, `/${selected.name} `);
|
|
632
|
-
renderSlashMenu(selected.name, false);
|
|
633
|
-
return;
|
|
688
|
+
catch {
|
|
689
|
+
// Ignore close races during shutdown.
|
|
634
690
|
}
|
|
635
|
-
renderSlashMenu(line.slice(1), false);
|
|
636
691
|
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
rl.on("line", async (raw) => {
|
|
692
|
+
}
|
|
693
|
+
console.log(`\n${DIM}Goodbye.${R}`);
|
|
694
|
+
process.exitCode = 0;
|
|
695
|
+
};
|
|
696
|
+
const handleInput = async (raw) => {
|
|
643
697
|
clearSlashMenu();
|
|
644
698
|
const input = raw.trim();
|
|
645
699
|
if (!input) {
|
|
646
|
-
|
|
700
|
+
if (startupComplete) {
|
|
701
|
+
safePrompt(ctx);
|
|
702
|
+
}
|
|
647
703
|
return;
|
|
648
704
|
}
|
|
649
705
|
await startupReady;
|
|
@@ -672,41 +728,103 @@ async function main() {
|
|
|
672
728
|
}
|
|
673
729
|
}
|
|
674
730
|
}
|
|
731
|
+
else if (!ctx.runtime) {
|
|
732
|
+
console.log(`${RED}Not connected. Run /setup${R}`);
|
|
733
|
+
}
|
|
675
734
|
else {
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
735
|
+
const messageId = `msg_${crypto.randomUUID()}`;
|
|
736
|
+
sentIds.add(messageId);
|
|
737
|
+
console.log(`${GRN}[${ts()}]${R} ${BOLD}You${R}: ${input}`);
|
|
738
|
+
try {
|
|
739
|
+
const result = await postRoomMessageUpsert(ctx.runtime, {
|
|
740
|
+
message_id: messageId,
|
|
741
|
+
author_type: "user",
|
|
742
|
+
author_id: ctx.runtime.hostId,
|
|
743
|
+
author_label: "You",
|
|
744
|
+
text: input,
|
|
745
|
+
});
|
|
746
|
+
if (result?.message_id && result.message_id !== messageId) {
|
|
747
|
+
sentIds.add(String(result.message_id));
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
catch (e) {
|
|
751
|
+
sentIds.delete(messageId);
|
|
752
|
+
console.log(`${RED}Send failed: ${e instanceof Error ? e.message : String(e)}${R}`);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
safePrompt(ctx);
|
|
756
|
+
};
|
|
757
|
+
// ── REPL ──
|
|
758
|
+
if (ctx.isInteractiveTerminal) {
|
|
759
|
+
rl.setPrompt(`${BMAG}>${R} `);
|
|
760
|
+
}
|
|
761
|
+
// Live slash menu on keypress
|
|
762
|
+
if (ctx.isInteractiveTerminal) {
|
|
763
|
+
emitKeypressEvents(process.stdin, rl);
|
|
764
|
+
process.stdin.setRawMode?.(true);
|
|
765
|
+
process.stdin.on("keypress", (chunk, key) => {
|
|
766
|
+
if (!ctx.running || !startupComplete)
|
|
767
|
+
return;
|
|
768
|
+
const line = rl.line ?? "";
|
|
769
|
+
if (line.startsWith("/") && line.length >= 1) {
|
|
770
|
+
const hits = getSlashMatches(line.slice(1));
|
|
771
|
+
const isTab = key?.name === "tab" || chunk === "\t";
|
|
772
|
+
if (hits.length > 0 && (key?.name === "up" || key?.name === "down")) {
|
|
773
|
+
_menuSelection =
|
|
774
|
+
key.name === "up"
|
|
775
|
+
? (_menuSelection + hits.length - 1) % hits.length
|
|
776
|
+
: (_menuSelection + 1) % hits.length;
|
|
777
|
+
scheduleSlashMenuRefresh(rl, true);
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
if (!isTab) {
|
|
781
|
+
scheduleSlashMenuRefresh(rl, false);
|
|
782
|
+
}
|
|
679
783
|
}
|
|
680
784
|
else {
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
785
|
+
clearSlashMenu();
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
if (ctx.isInteractiveTerminal) {
|
|
790
|
+
rl.on("line", async (raw) => {
|
|
791
|
+
pendingInputs += 1;
|
|
792
|
+
try {
|
|
793
|
+
await handleInput(raw);
|
|
794
|
+
}
|
|
795
|
+
finally {
|
|
796
|
+
pendingInputs = Math.max(0, pendingInputs - 1);
|
|
797
|
+
if (closeRequested && pendingInputs === 0) {
|
|
798
|
+
finalizeClose();
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
});
|
|
802
|
+
rl.on("close", () => {
|
|
803
|
+
closeRequested = true;
|
|
804
|
+
if (pendingInputs === 0) {
|
|
805
|
+
finalizeClose();
|
|
806
|
+
}
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
if (!ctx.isInteractiveTerminal) {
|
|
810
|
+
void (async () => {
|
|
811
|
+
const pipedInput = await pipedInputPromise;
|
|
812
|
+
const lines = pipedInput.split(/\r?\n/).filter((line) => line.trim().length > 0);
|
|
813
|
+
for (const line of lines) {
|
|
814
|
+
pendingInputs += 1;
|
|
684
815
|
try {
|
|
685
|
-
await
|
|
686
|
-
message_id: messageId,
|
|
687
|
-
author_type: "user",
|
|
688
|
-
author_id: ctx.runtime.hostId,
|
|
689
|
-
author_label: "You",
|
|
690
|
-
text: input,
|
|
691
|
-
});
|
|
816
|
+
await handleInput(line);
|
|
692
817
|
}
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
console.log(`${RED}Send failed: ${e instanceof Error ? e.message : String(e)}${R}`);
|
|
818
|
+
finally {
|
|
819
|
+
pendingInputs = Math.max(0, pendingInputs - 1);
|
|
696
820
|
}
|
|
697
821
|
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
if (ctx.isInteractiveTerminal) {
|
|
705
|
-
process.stdin.setRawMode?.(false);
|
|
706
|
-
}
|
|
707
|
-
console.log(`\n${DIM}Goodbye.${R}`);
|
|
708
|
-
process.exit(0);
|
|
709
|
-
});
|
|
822
|
+
closeRequested = true;
|
|
823
|
+
if (pendingInputs === 0) {
|
|
824
|
+
finalizeClose();
|
|
825
|
+
}
|
|
826
|
+
})();
|
|
827
|
+
}
|
|
710
828
|
// ── Try to connect ──
|
|
711
829
|
const cfg = await loadHostConfig();
|
|
712
830
|
if (cfg) {
|
|
@@ -724,6 +842,10 @@ async function main() {
|
|
|
724
842
|
sentIds.delete(msg.message_id);
|
|
725
843
|
return;
|
|
726
844
|
}
|
|
845
|
+
if (!startupComplete) {
|
|
846
|
+
startupMessages.push(msg);
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
727
849
|
process.stdout.write(`\r\x1b[K`);
|
|
728
850
|
displayMessage(msg);
|
|
729
851
|
safePrompt(ctx, true);
|
|
@@ -731,16 +853,17 @@ async function main() {
|
|
|
731
853
|
hooks.onSessionChange = () => {
|
|
732
854
|
// Session list changed - could update status bar
|
|
733
855
|
};
|
|
734
|
-
|
|
856
|
+
await startDaemon(ctx);
|
|
735
857
|
console.log(` ${GRN}Host online.${R} Type ${GRN}/help${R} for commands.\n`);
|
|
736
|
-
|
|
858
|
+
flushStartupMessages();
|
|
737
859
|
}
|
|
738
860
|
catch (e) {
|
|
739
861
|
console.log(` ${RED}Connection failed: ${e instanceof Error ? e.message : String(e)}${R}`);
|
|
740
862
|
console.log(` ${DIM}Run /setup to configure or /doctor to diagnose.${R}\n`);
|
|
741
|
-
safePrompt(ctx);
|
|
742
863
|
}
|
|
743
864
|
finally {
|
|
865
|
+
startupComplete = true;
|
|
866
|
+
safePrompt(ctx);
|
|
744
867
|
finishStartup();
|
|
745
868
|
}
|
|
746
869
|
}
|
|
@@ -748,6 +871,7 @@ async function main() {
|
|
|
748
871
|
printHeader(os.hostname());
|
|
749
872
|
console.log(` ${YEL}No host config found.${R}`);
|
|
750
873
|
console.log(` ${DIM}Run /setup to get started or /doctor to check prerequisites.${R}\n`);
|
|
874
|
+
startupComplete = true;
|
|
751
875
|
safePrompt(ctx);
|
|
752
876
|
finishStartup();
|
|
753
877
|
}
|