office-core 0.1.4 → 0.2.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.
@@ -3,43 +3,25 @@ import { spawn } from "node:child_process";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import process from "node:process";
6
- import { loadHostConfig, upsertHostConfig, getHostConfigPath } from "./lib/host-config.js";
7
- import { probeRunnerAvailability, resolveRunnerCommand } from "./lib/local-runner.js";
6
+ import { loadHostConfig, upsertHostConfig, getHostConfigPath, getHostLogPath, getSupportBundlesDir } from "./lib/host-config.js";
7
+ import { assertRunnerAvailable, probeRunnerAvailability } 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
+ import { createSupportBundle, openExternal, readRecentLines } from "./lib/cli-support.js";
9
10
  // ─── ANSI Codes ─────────────────────────────────────────────────────────────
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
- }
11
+ const R = "\x1b[0m";
12
+ const BOLD = "\x1b[1m";
13
+ const DIM = "\x1b[2m";
14
+ const BMAG = "\x1b[95m";
15
+ const MAG = "\x1b[35m";
16
+ const BBLU = "\x1b[94m";
17
+ const BLU = "\x1b[34m";
18
+ const BCYN = "\x1b[96m";
19
+ const CYN = "\x1b[36m";
20
+ const GRN = "\x1b[32m";
21
+ const YEL = "\x1b[33m";
22
+ const RED = "\x1b[31m";
23
+ const WHT = "\x1b[97m";
24
+ const GRY = "\x1b[90m";
43
25
  // ─── ASCII Banner ───────────────────────────────────────────────────────────
44
26
  const BANNER = [
45
27
  [" ██████ ███████ ███████ ██ ██████ ███████ ", BMAG],
@@ -97,15 +79,9 @@ function drawAgentBox(label, text, maxW = 74) {
97
79
  const lines = wordWrap(text.trim(), maxW - 4);
98
80
  const inner = Math.max(label.length + 2, ...lines.map((l) => l.length), 20);
99
81
  const w = Math.min(inner, maxW - 4);
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}`);
82
+ const top = `${CYN}┌─ ${R}${BOLD}${BCYN}${label}${R}${CYN} ${"".repeat(Math.max(0, w - label.length - 1))}┐${R}`;
83
+ const bot = `${CYN}└${"".repeat(w + 2)}┘${R}`;
84
+ const body = lines.map((l) => `${CYN}│${R} ${WHT}${l.padEnd(w)}${R} ${CYN}│${R}`);
109
85
  return [top, ...body, bot].join("\n");
110
86
  }
111
87
  // ─── Timestamp ──────────────────────────────────────────────────────────────
@@ -136,6 +112,8 @@ const CMDS = [
136
112
  { name: "setup", desc: "Run first-time setup wizard", fn: cmdSetup },
137
113
  { name: "login", args: "<codex|claude>", desc: "Authenticate a runner", fn: cmdLogin },
138
114
  { name: "open", desc: "Open dashboard in browser", fn: cmdOpen },
115
+ { name: "logs", args: "[tail]", desc: "Show recent host logs", fn: cmdLogs },
116
+ { name: "support", alias: ["bundle"], desc: "Export a support bundle", fn: cmdSupport },
139
117
  { name: "project", args: "[id]", desc: "Show or switch project", fn: cmdProject },
140
118
  { name: "clear", alias: ["cls"], desc: "Clear screen", fn: cmdClear },
141
119
  { name: "help", alias: ["h", "?"], desc: "Show available commands", fn: cmdHelp },
@@ -257,26 +235,6 @@ function completer(line) {
257
235
  function ask(rl, prompt) {
258
236
  return new Promise((resolve) => rl.question(prompt, (a) => resolve(a?.trim() ?? "")));
259
237
  }
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
- }
280
238
  function safePrompt(ctx, preserveCursor = false) {
281
239
  if (!ctx.running || !ctx.isInteractiveTerminal) {
282
240
  return;
@@ -288,11 +246,23 @@ function safePrompt(ctx, preserveCursor = false) {
288
246
  // Ignore prompt attempts after readline closes during shutdown/tests.
289
247
  }
290
248
  }
291
- async function startDaemon(ctx) {
249
+ function replaceInputLine(rl, value) {
250
+ const internal = rl;
251
+ internal.line = value;
252
+ internal.cursor = value.length;
253
+ internal._refreshLine?.();
254
+ }
255
+ function startDaemonInBackground(ctx) {
292
256
  if (!ctx.runtime) {
293
257
  return;
294
258
  }
295
- await startDaemonLoop(ctx.runtime);
259
+ void startDaemonLoop(ctx.runtime).catch((error) => {
260
+ if (!ctx.running) {
261
+ return;
262
+ }
263
+ console.log(`${RED}Daemon failed: ${error instanceof Error ? error.message : String(error)}${R}`);
264
+ safePrompt(ctx);
265
+ });
296
266
  }
297
267
  function fmtAge(ms) {
298
268
  if (ms < 60_000)
@@ -542,7 +512,7 @@ async function cmdSetup(_a, ctx) {
542
512
  // Connect immediately
543
513
  stopDaemonLoop();
544
514
  ctx.runtime = await buildRuntimeConfig();
545
- await startDaemon(ctx);
515
+ startDaemonInBackground(ctx);
546
516
  console.log(`\n ${GRN}Host online.${R}\n`);
547
517
  }
548
518
  catch (e) {
@@ -555,7 +525,7 @@ async function cmdLogin(a, _ctx) {
555
525
  console.log(`${RED}Usage: /login <codex|claude>${R}`);
556
526
  return;
557
527
  }
558
- const cmd = resolveRunnerCommand(runner);
528
+ const cmd = assertRunnerAvailable(runner);
559
529
  const args = runner === "codex" ? ["login"] : ["auth"];
560
530
  console.log(`${DIM}Running ${runner} auth...${R}`);
561
531
  await new Promise((resolve) => {
@@ -571,9 +541,37 @@ async function cmdOpen(_a, ctx) {
571
541
  return;
572
542
  }
573
543
  const url = `${cfg.base_url}/?projectId=${encodeURIComponent(cfg.project_id)}`;
574
- spawn("cmd.exe", ["/c", "start", "", url], { detached: true, stdio: "ignore", windowsHide: true }).unref();
544
+ await openExternal(url);
575
545
  console.log(`${DIM}Opened ${url}${R}`);
576
546
  }
547
+ async function cmdLogs(a, _ctx) {
548
+ const tail = Math.max(20, Number.parseInt(a.trim() || '120', 10) || 120);
549
+ const lines = await readRecentLines(getHostLogPath(), tail);
550
+ console.log(`
551
+ ${BOLD}Logs${R} ${DIM}${getHostLogPath()}${R}`);
552
+ if (!lines.length) {
553
+ console.log(` ${DIM}No host logs yet.${R}
554
+ `);
555
+ return;
556
+ }
557
+ for (const line of lines) {
558
+ console.log(` ${line}`);
559
+ }
560
+ console.log('');
561
+ }
562
+ async function cmdSupport(_a, _ctx) {
563
+ const bundle = await createSupportBundle({
564
+ outputRoot: getSupportBundlesDir(),
565
+ bundleNamePrefix: 'office-core-support',
566
+ includePaths: [{ source: getHostConfigPath(), target: 'config/host-config.json' }, { source: getHostLogPath(), target: 'logs/office-host.log' }],
567
+ metadata: { generated_at: new Date().toISOString(), cwd: process.cwd() },
568
+ });
569
+ console.log(`
570
+ ${BOLD}Support bundle${R}`);
571
+ console.log(` ${bundle.outputPath}`);
572
+ console.log(` ${DIM}${bundle.mode}${R}
573
+ `);
574
+ }
577
575
  async function cmdProject(a, ctx) {
578
576
  if (!ctx.runtime) {
579
577
  console.log(`${RED}Not connected.${R}`);
@@ -611,10 +609,9 @@ async function cmdClear(_a, _ctx) {
611
609
  printBanner();
612
610
  }
613
611
  async function cmdHelp(_a, _ctx) {
614
- const rule = (USE_UNICODE ? "─" : "-").repeat(56);
615
612
  console.log("");
616
613
  console.log(` ${BOLD}${WHT}Commands${R}`);
617
- console.log(` ${rule}`);
614
+ console.log(` ${"─".repeat(56)}`);
618
615
  for (const c of CMDS) {
619
616
  const a = c.args ? ` ${CYN}${c.args}${R}` : "";
620
617
  const al = c.alias ? ` ${GRY}(${c.alias.map((x) => `/${x}`).join(", ")})${R}` : "";
@@ -625,81 +622,80 @@ async function cmdHelp(_a, _ctx) {
625
622
  console.log(` ${DIM}Tab to autocomplete slash commands${R}\n`);
626
623
  }
627
624
  async function cmdQuit(_a, ctx) {
625
+ ctx.running = false;
626
+ stopDaemonLoop();
628
627
  ctx.rl.close();
629
628
  }
630
629
  // ─── Main ───────────────────────────────────────────────────────────────────
631
630
  void main().catch((err) => {
632
- console.error(err instanceof Error ? err.message : String(err));
631
+ console.error(err);
633
632
  process.exitCode = 1;
634
633
  });
635
634
  async function main() {
635
+ console.clear();
636
+ printBanner();
636
637
  const isInteractiveTerminal = Boolean(process.stdin.isTTY && process.stdout.isTTY);
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();
638
+ const rl = createInterface({
639
+ input: process.stdin,
640
+ output: process.stdout,
641
+ terminal: isInteractiveTerminal,
642
+ historySize: 0,
643
+ });
652
644
  const ctx = { runtime: null, rl, running: true, isInteractiveTerminal };
653
645
  const sentIds = new Set();
654
- const startupMessages = [];
655
- let startupComplete = false;
656
- let pendingInputs = 0;
657
- let closeRequested = false;
658
- let closeFinalized = false;
659
646
  let finishStartup;
660
647
  const startupReady = new Promise((resolve) => {
661
648
  finishStartup = resolve;
662
649
  });
663
- const flushStartupMessages = () => {
664
- if (!startupMessages.length) {
665
- return;
666
- }
667
- for (const message of startupMessages.splice(0)) {
668
- process.stdout.write(`\r\x1b[K`);
669
- displayMessage(message);
670
- }
671
- };
672
- const finalizeClose = () => {
673
- if (closeFinalized) {
674
- return;
650
+ // ── REPL ──
651
+ if (ctx.isInteractiveTerminal) {
652
+ rl.setPrompt(`${BMAG}>${R} `);
653
+ safePrompt(ctx);
654
+ }
655
+ else {
656
+ try {
657
+ rl.resume();
675
658
  }
676
- closeFinalized = true;
677
- ctx.running = false;
678
- stopDaemonLoop();
679
- if (ctx.isInteractiveTerminal) {
680
- process.stdin.setRawMode?.(false);
659
+ catch {
660
+ // stdin may already be closed in redirected test harnesses
681
661
  }
682
- if (ctx.isInteractiveTerminal) {
683
- const internalRl = rl;
684
- if (!internalRl.closed) {
685
- try {
686
- rl.close();
662
+ }
663
+ // Live slash menu on keypress
664
+ if (ctx.isInteractiveTerminal) {
665
+ emitKeypressEvents(process.stdin, rl);
666
+ process.stdin.setRawMode?.(true);
667
+ process.stdin.on("keypress", (_chunk, key) => {
668
+ if (!ctx.running)
669
+ return;
670
+ const line = rl.line ?? "";
671
+ if (line.startsWith("/") && line.length >= 1) {
672
+ const hits = getSlashMatches(line.slice(1));
673
+ if (hits.length > 0 && (key?.name === "up" || key?.name === "down")) {
674
+ _menuSelection =
675
+ key.name === "up"
676
+ ? (_menuSelection + hits.length - 1) % hits.length
677
+ : (_menuSelection + 1) % hits.length;
678
+ scheduleSlashMenuRefresh(rl, true);
679
+ return;
687
680
  }
688
- catch {
689
- // Ignore close races during shutdown.
681
+ if (hits.length > 0 && key?.name === "tab") {
682
+ const selected = hits[_menuSelection] ?? hits[0];
683
+ replaceInputLine(rl, `/${selected.name} `);
684
+ scheduleSlashMenuRefresh(rl, false);
685
+ return;
690
686
  }
687
+ scheduleSlashMenuRefresh(rl, false);
691
688
  }
692
- }
693
- console.log(`\n${DIM}Goodbye.${R}`);
694
- process.exitCode = 0;
695
- };
696
- const handleInput = async (raw) => {
689
+ else {
690
+ clearSlashMenu();
691
+ }
692
+ });
693
+ }
694
+ rl.on("line", async (raw) => {
697
695
  clearSlashMenu();
698
696
  const input = raw.trim();
699
697
  if (!input) {
700
- if (startupComplete) {
701
- safePrompt(ctx);
702
- }
698
+ safePrompt(ctx);
703
699
  return;
704
700
  }
705
701
  await startupReady;
@@ -728,103 +724,44 @@ async function main() {
728
724
  }
729
725
  }
730
726
  }
731
- else if (!ctx.runtime) {
732
- console.log(`${RED}Not connected. Run /setup${R}`);
733
- }
734
727
  else {
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
- }
728
+ // Send room message
729
+ if (!ctx.runtime) {
730
+ console.log(`${RED}Not connected. Run /setup${R}`);
783
731
  }
784
732
  else {
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;
733
+ const messageId = `msg_${crypto.randomUUID()}`;
734
+ sentIds.add(messageId);
735
+ console.log(`${GRN}[${ts()}]${R} ${BOLD}You${R}: ${input}`);
815
736
  try {
816
- await handleInput(line);
737
+ const result = await postRoomMessageUpsert(ctx.runtime, {
738
+ message_id: messageId,
739
+ author_type: "user",
740
+ author_id: ctx.runtime.hostId,
741
+ author_label: "You",
742
+ text: input,
743
+ });
744
+ if (result?.message_id && result.message_id !== messageId) {
745
+ sentIds.add(String(result.message_id));
746
+ }
817
747
  }
818
- finally {
819
- pendingInputs = Math.max(0, pendingInputs - 1);
748
+ catch (e) {
749
+ sentIds.delete(messageId);
750
+ console.log(`${RED}Send failed: ${e instanceof Error ? e.message : String(e)}${R}`);
820
751
  }
821
752
  }
822
- closeRequested = true;
823
- if (pendingInputs === 0) {
824
- finalizeClose();
825
- }
826
- })();
827
- }
753
+ }
754
+ safePrompt(ctx);
755
+ });
756
+ rl.on("close", () => {
757
+ ctx.running = false;
758
+ stopDaemonLoop();
759
+ if (ctx.isInteractiveTerminal) {
760
+ process.stdin.setRawMode?.(false);
761
+ }
762
+ console.log(`\n${DIM}Goodbye.${R}`);
763
+ process.exit(0);
764
+ });
828
765
  // ── Try to connect ──
829
766
  const cfg = await loadHostConfig();
830
767
  if (cfg) {
@@ -842,10 +779,6 @@ async function main() {
842
779
  sentIds.delete(msg.message_id);
843
780
  return;
844
781
  }
845
- if (!startupComplete) {
846
- startupMessages.push(msg);
847
- return;
848
- }
849
782
  process.stdout.write(`\r\x1b[K`);
850
783
  displayMessage(msg);
851
784
  safePrompt(ctx, true);
@@ -853,17 +786,16 @@ async function main() {
853
786
  hooks.onSessionChange = () => {
854
787
  // Session list changed - could update status bar
855
788
  };
856
- await startDaemon(ctx);
789
+ startDaemonInBackground(ctx);
857
790
  console.log(` ${GRN}Host online.${R} Type ${GRN}/help${R} for commands.\n`);
858
- flushStartupMessages();
791
+ safePrompt(ctx);
859
792
  }
860
793
  catch (e) {
861
794
  console.log(` ${RED}Connection failed: ${e instanceof Error ? e.message : String(e)}${R}`);
862
795
  console.log(` ${DIM}Run /setup to configure or /doctor to diagnose.${R}\n`);
796
+ safePrompt(ctx);
863
797
  }
864
798
  finally {
865
- startupComplete = true;
866
- safePrompt(ctx);
867
799
  finishStartup();
868
800
  }
869
801
  }
@@ -871,7 +803,6 @@ async function main() {
871
803
  printHeader(os.hostname());
872
804
  console.log(` ${YEL}No host config found.${R}`);
873
805
  console.log(` ${DIM}Run /setup to get started or /doctor to check prerequisites.${R}\n`);
874
- startupComplete = true;
875
806
  safePrompt(ctx);
876
807
  finishStartup();
877
808
  }