office-core 0.1.3 → 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.
@@ -187,6 +187,7 @@ async function processRoomMessages(runtime) {
187
187
  hooks.onRoomMessage?.(message, runtime);
188
188
  if (message.author_type === "system") {
189
189
  runtime.roomCursorSeq = Math.max(runtime.roomCursorSeq, message.seq);
190
+ await persistRoomCursor(runtime);
190
191
  continue;
191
192
  }
192
193
  const runnableSessions = Array.from(sessions.values()).filter((session) => session.status === "running");
@@ -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 ?? "http://127.0.0.1:8787").replace(/\/$/, "");
15
- const projectId = args.project ?? "prj_local";
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 ?? `${os.hostname()} host`;
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
- const response = await fetch(`${baseUrl}/api/projects/${projectId}/local-host/register`, {
59
- method: "POST",
60
- headers: {
61
- "content-type": "application/json",
62
- "x-enroll-secret": enrollSecret,
63
- },
64
- body: JSON.stringify(body),
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
- const raw = await readFile(filePath, "utf8");
32
- const parsed = parseLooseJson(raw);
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
- await writeFile(tempPath, JSON.stringify(value, null, 2), "utf8");
167
- await rename(tempPath, filePath);
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
- const R = "\x1b[0m";
11
- const BOLD = "\x1b[1m";
12
- const DIM = "\x1b[2m";
13
- const BMAG = "\x1b[95m";
14
- const MAG = "\x1b[35m";
15
- const BBLU = "\x1b[94m";
16
- const BLU = "\x1b[34m";
17
- const BCYN = "\x1b[96m";
18
- const CYN = "\x1b[36m";
19
- const GRN = "\x1b[32m";
20
- const YEL = "\x1b[33m";
21
- const RED = "\x1b[31m";
22
- const WHT = "\x1b[97m";
23
- const GRY = "\x1b[90m";
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 top = `${CYN}┌─ ${R}${BOLD}${BCYN}${label}${R}${CYN} ${"".repeat(Math.max(0, w - label.length - 1))}┐${R}`;
82
- const bot = `${CYN}└${"".repeat(w + 2)}┘${R}`;
83
- const body = lines.map((l) => `${CYN}│${R} ${WHT}${l.padEnd(w)}${R} ${CYN}│${R}`);
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 ──────────────────────────────────────────────────────────────
@@ -232,6 +257,26 @@ function completer(line) {
232
257
  function ask(rl, prompt) {
233
258
  return new Promise((resolve) => rl.question(prompt, (a) => resolve(a?.trim() ?? "")));
234
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
+ }
235
280
  function safePrompt(ctx, preserveCursor = false) {
236
281
  if (!ctx.running || !ctx.isInteractiveTerminal) {
237
282
  return;
@@ -243,23 +288,11 @@ function safePrompt(ctx, preserveCursor = false) {
243
288
  // Ignore prompt attempts after readline closes during shutdown/tests.
244
289
  }
245
290
  }
246
- function replaceInputLine(rl, value) {
247
- const internal = rl;
248
- internal.line = value;
249
- internal.cursor = value.length;
250
- internal._refreshLine?.();
251
- }
252
- function startDaemonInBackground(ctx) {
291
+ async function startDaemon(ctx) {
253
292
  if (!ctx.runtime) {
254
293
  return;
255
294
  }
256
- void startDaemonLoop(ctx.runtime).catch((error) => {
257
- if (!ctx.running) {
258
- return;
259
- }
260
- console.log(`${RED}Daemon failed: ${error instanceof Error ? error.message : String(error)}${R}`);
261
- safePrompt(ctx);
262
- });
295
+ await startDaemonLoop(ctx.runtime);
263
296
  }
264
297
  function fmtAge(ms) {
265
298
  if (ms < 60_000)
@@ -509,7 +542,7 @@ async function cmdSetup(_a, ctx) {
509
542
  // Connect immediately
510
543
  stopDaemonLoop();
511
544
  ctx.runtime = await buildRuntimeConfig();
512
- startDaemonInBackground(ctx);
545
+ await startDaemon(ctx);
513
546
  console.log(`\n ${GRN}Host online.${R}\n`);
514
547
  }
515
548
  catch (e) {
@@ -578,9 +611,10 @@ async function cmdClear(_a, _ctx) {
578
611
  printBanner();
579
612
  }
580
613
  async function cmdHelp(_a, _ctx) {
614
+ const rule = (USE_UNICODE ? "─" : "-").repeat(56);
581
615
  console.log("");
582
616
  console.log(` ${BOLD}${WHT}Commands${R}`);
583
- console.log(` ${"─".repeat(56)}`);
617
+ console.log(` ${rule}`);
584
618
  for (const c of CMDS) {
585
619
  const a = c.args ? ` ${CYN}${c.args}${R}` : "";
586
620
  const al = c.alias ? ` ${GRY}(${c.alias.map((x) => `/${x}`).join(", ")})${R}` : "";
@@ -591,80 +625,81 @@ async function cmdHelp(_a, _ctx) {
591
625
  console.log(` ${DIM}Tab to autocomplete slash commands${R}\n`);
592
626
  }
593
627
  async function cmdQuit(_a, ctx) {
594
- ctx.running = false;
595
- stopDaemonLoop();
596
628
  ctx.rl.close();
597
629
  }
598
630
  // ─── Main ───────────────────────────────────────────────────────────────────
599
631
  void main().catch((err) => {
600
- console.error(err);
632
+ console.error(err instanceof Error ? err.message : String(err));
601
633
  process.exitCode = 1;
602
634
  });
603
635
  async function main() {
604
- console.clear();
605
- printBanner();
606
636
  const isInteractiveTerminal = Boolean(process.stdin.isTTY && process.stdout.isTTY);
607
- const rl = createInterface({
608
- input: process.stdin,
609
- output: process.stdout,
610
- terminal: isInteractiveTerminal,
611
- historySize: 0,
612
- });
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();
613
652
  const ctx = { runtime: null, rl, running: true, isInteractiveTerminal };
614
653
  const sentIds = new Set();
654
+ const startupMessages = [];
655
+ let startupComplete = false;
656
+ let pendingInputs = 0;
657
+ let closeRequested = false;
658
+ let closeFinalized = false;
615
659
  let finishStartup;
616
660
  const startupReady = new Promise((resolve) => {
617
661
  finishStartup = resolve;
618
662
  });
619
- // ── REPL ──
620
- if (ctx.isInteractiveTerminal) {
621
- rl.setPrompt(`${BMAG}>${R} `);
622
- safePrompt(ctx);
623
- }
624
- else {
625
- try {
626
- rl.resume();
663
+ const flushStartupMessages = () => {
664
+ if (!startupMessages.length) {
665
+ return;
627
666
  }
628
- catch {
629
- // stdin may already be closed in redirected test harnesses
667
+ for (const message of startupMessages.splice(0)) {
668
+ process.stdout.write(`\r\x1b[K`);
669
+ displayMessage(message);
630
670
  }
631
- }
632
- // Live slash menu on keypress
633
- if (ctx.isInteractiveTerminal) {
634
- emitKeypressEvents(process.stdin, rl);
635
- process.stdin.setRawMode?.(true);
636
- process.stdin.on("keypress", (_chunk, key) => {
637
- if (!ctx.running)
638
- return;
639
- const line = rl.line ?? "";
640
- if (line.startsWith("/") && line.length >= 1) {
641
- const hits = getSlashMatches(line.slice(1));
642
- if (hits.length > 0 && (key?.name === "up" || key?.name === "down")) {
643
- _menuSelection =
644
- key.name === "up"
645
- ? (_menuSelection + hits.length - 1) % hits.length
646
- : (_menuSelection + 1) % hits.length;
647
- scheduleSlashMenuRefresh(rl, true);
648
- 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();
649
687
  }
650
- if (hits.length > 0 && key?.name === "tab") {
651
- const selected = hits[_menuSelection] ?? hits[0];
652
- replaceInputLine(rl, `/${selected.name} `);
653
- scheduleSlashMenuRefresh(rl, false);
654
- return;
688
+ catch {
689
+ // Ignore close races during shutdown.
655
690
  }
656
- scheduleSlashMenuRefresh(rl, false);
657
691
  }
658
- else {
659
- clearSlashMenu();
660
- }
661
- });
662
- }
663
- rl.on("line", async (raw) => {
692
+ }
693
+ console.log(`\n${DIM}Goodbye.${R}`);
694
+ process.exitCode = 0;
695
+ };
696
+ const handleInput = async (raw) => {
664
697
  clearSlashMenu();
665
698
  const input = raw.trim();
666
699
  if (!input) {
667
- safePrompt(ctx);
700
+ if (startupComplete) {
701
+ safePrompt(ctx);
702
+ }
668
703
  return;
669
704
  }
670
705
  await startupReady;
@@ -693,44 +728,103 @@ async function main() {
693
728
  }
694
729
  }
695
730
  }
731
+ else if (!ctx.runtime) {
732
+ console.log(`${RED}Not connected. Run /setup${R}`);
733
+ }
696
734
  else {
697
- // Send room message
698
- if (!ctx.runtime) {
699
- console.log(`${RED}Not connected. Run /setup${R}`);
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
+ }
700
783
  }
701
784
  else {
702
- const messageId = `msg_${crypto.randomUUID()}`;
703
- sentIds.add(messageId);
704
- console.log(`${GRN}[${ts()}]${R} ${BOLD}You${R}: ${input}`);
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;
705
815
  try {
706
- const result = await postRoomMessageUpsert(ctx.runtime, {
707
- message_id: messageId,
708
- author_type: "user",
709
- author_id: ctx.runtime.hostId,
710
- author_label: "You",
711
- text: input,
712
- });
713
- if (result?.message_id && result.message_id !== messageId) {
714
- sentIds.add(String(result.message_id));
715
- }
816
+ await handleInput(line);
716
817
  }
717
- catch (e) {
718
- sentIds.delete(messageId);
719
- console.log(`${RED}Send failed: ${e instanceof Error ? e.message : String(e)}${R}`);
818
+ finally {
819
+ pendingInputs = Math.max(0, pendingInputs - 1);
720
820
  }
721
821
  }
722
- }
723
- safePrompt(ctx);
724
- });
725
- rl.on("close", () => {
726
- ctx.running = false;
727
- stopDaemonLoop();
728
- if (ctx.isInteractiveTerminal) {
729
- process.stdin.setRawMode?.(false);
730
- }
731
- console.log(`\n${DIM}Goodbye.${R}`);
732
- process.exit(0);
733
- });
822
+ closeRequested = true;
823
+ if (pendingInputs === 0) {
824
+ finalizeClose();
825
+ }
826
+ })();
827
+ }
734
828
  // ── Try to connect ──
735
829
  const cfg = await loadHostConfig();
736
830
  if (cfg) {
@@ -748,6 +842,10 @@ async function main() {
748
842
  sentIds.delete(msg.message_id);
749
843
  return;
750
844
  }
845
+ if (!startupComplete) {
846
+ startupMessages.push(msg);
847
+ return;
848
+ }
751
849
  process.stdout.write(`\r\x1b[K`);
752
850
  displayMessage(msg);
753
851
  safePrompt(ctx, true);
@@ -755,16 +853,17 @@ async function main() {
755
853
  hooks.onSessionChange = () => {
756
854
  // Session list changed - could update status bar
757
855
  };
758
- startDaemonInBackground(ctx);
856
+ await startDaemon(ctx);
759
857
  console.log(` ${GRN}Host online.${R} Type ${GRN}/help${R} for commands.\n`);
760
- safePrompt(ctx);
858
+ flushStartupMessages();
761
859
  }
762
860
  catch (e) {
763
861
  console.log(` ${RED}Connection failed: ${e instanceof Error ? e.message : String(e)}${R}`);
764
862
  console.log(` ${DIM}Run /setup to configure or /doctor to diagnose.${R}\n`);
765
- safePrompt(ctx);
766
863
  }
767
864
  finally {
865
+ startupComplete = true;
866
+ safePrompt(ctx);
768
867
  finishStartup();
769
868
  }
770
869
  }
@@ -772,6 +871,7 @@ async function main() {
772
871
  printHeader(os.hostname());
773
872
  console.log(` ${YEL}No host config found.${R}`);
774
873
  console.log(` ${DIM}Run /setup to get started or /doctor to check prerequisites.${R}\n`);
874
+ startupComplete = true;
775
875
  safePrompt(ctx);
776
876
  finishStartup();
777
877
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "office-core",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Installable CLI and local host runtime for Office Core",
5
5
  "type": "module",
6
6
  "bin": {