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 ?? "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 ──────────────────────────────────────────────────────────────
@@ -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 replaceInputLine(rl, value) {
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
- void startDaemonLoop(ctx.runtime).catch((error) => {
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
- startDaemonInBackground(ctx);
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(` ${"─".repeat(56)}`);
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
- const rl = createInterface({
587
- input: process.stdin,
588
- output: process.stdout,
589
- completer,
590
- terminal: isInteractiveTerminal,
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
- // ── REPL ──
599
- if (ctx.isInteractiveTerminal) {
600
- rl.setPrompt(`${BMAG}>${R} `);
601
- safePrompt(ctx);
602
- }
603
- else {
604
- try {
605
- rl.resume();
663
+ const flushStartupMessages = () => {
664
+ if (!startupMessages.length) {
665
+ return;
606
666
  }
607
- catch {
608
- // 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);
609
670
  }
610
- }
611
- // Live slash menu on keypress
612
- if (ctx.isInteractiveTerminal) {
613
- emitKeypressEvents(process.stdin, rl);
614
- process.stdin.setRawMode?.(true);
615
- process.stdin.on("keypress", (_chunk, key) => {
616
- if (!ctx.running)
617
- return;
618
- const line = rl.line ?? "";
619
- if (line.startsWith("/") && line.length >= 1) {
620
- const hits = getSlashMatches(line.slice(1));
621
- if (hits.length > 0 && (key?.name === "up" || key?.name === "down")) {
622
- _menuSelection =
623
- key.name === "up"
624
- ? (_menuSelection + hits.length - 1) % hits.length
625
- : (_menuSelection + 1) % hits.length;
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
- if (hits.length > 0 && key?.name === "tab") {
630
- const selected = hits[_menuSelection] ?? hits[0];
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
- else {
638
- clearSlashMenu();
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
- safePrompt(ctx);
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
- // Send room message
677
- if (!ctx.runtime) {
678
- 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
+ }
679
783
  }
680
784
  else {
681
- const messageId = `msg_${crypto.randomUUID()}`;
682
- sentIds.add(messageId);
683
- 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;
684
815
  try {
685
- await postRoomMessageUpsert(ctx.runtime, {
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
- catch (e) {
694
- sentIds.delete(messageId);
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
- safePrompt(ctx);
700
- });
701
- rl.on("close", () => {
702
- ctx.running = false;
703
- stopDaemonLoop();
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
- startDaemonInBackground(ctx);
856
+ await startDaemon(ctx);
735
857
  console.log(` ${GRN}Host online.${R} Type ${GRN}/help${R} for commands.\n`);
736
- safePrompt(ctx);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "office-core",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Installable CLI and local host runtime for Office Core",
5
5
  "type": "module",
6
6
  "bin": {