UNPKG

@arbidocs/tui

Version:

Interactive terminal UI for ARBI — chat-style RAG assistant with streaming responses

2,835 lines • 102 kB
#!/usr/bin/env node
'use strict';

var sdk = require('@arbidocs/sdk');
var commander = require('commander');
var piTui = require('@earendil-works/pi-tui');
var chalk = require('chalk');
require('fake-indexeddb/auto');
var client = require('@arbidocs/client');
var prompts = require('@inquirer/prompts');
var child_process = require('child_process');
var fs = require('fs');
var os = require('os');
var path = require('path');

function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }

var chalk__default = /*#__PURE__*/_interopDefault(chalk);

var __defProp = Object.defineProperty;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __esm = (fn, res) => function __init() {
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
};
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};

// src/tui-helpers.ts
var tui_helpers_exports = {};
__export(tui_helpers_exports, {
  applyWorkspaceSelection: () => applyWorkspaceSelection,
  runInteractiveFlow: () => runInteractiveFlow,
  showMessage: () => showMessage,
  switchWorkspace: () => switchWorkspace
});
function showMessage(tui, message, level) {
  tui.chatLog.addSystem(message, level);
  tui.requestRender();
}
async function switchWorkspace(tui, workspaceId) {
  if (!tui.authContext) return null;
  try {
    const ws = await sdk.selectWorkspaceById(
      tui.authContext.arbi,
      workspaceId,
      tui.authContext.loginResult.serverSessionKey,
      tui.store.requireCredentials().signingPrivateKeyBase64
    );
    tui.state.workspaceId = ws.external_id;
    tui.state.workspaceName = ws.name;
    tui.state.conversationMessageId = null;
    tui.store.clearChatSession();
    tui.store.updateConfig({ selectedWorkspaceId: ws.external_id });
    await tui.refreshWorkspaceContext();
    return ws;
  } catch (err) {
    showMessage(tui, `Failed to switch workspace: ${sdk.getErrorMessage(err)}`, "error");
    return null;
  }
}
async function applyWorkspaceSelection(tui, workspaceId, workspaceName) {
  if (!workspaceId) return;
  const wsCtx = await sdk.resolveWorkspace(tui.store, workspaceId);
  tui.setWorkspaceContext(wsCtx);
  tui.state.workspaceName = workspaceName ?? null;
}
async function runInteractiveFlow(tui, pauseMessage, action, errorPrefix) {
  showMessage(tui, pauseMessage);
  tui.stopTui();
  try {
    const result = await action();
    tui.restartTui();
    return result;
  } catch (err) {
    tui.restartTui();
    showMessage(tui, `${errorPrefix}: ${sdk.getErrorMessage(err)}`, "error");
    return null;
  }
}
var init_tui_helpers = __esm({
  "src/tui-helpers.ts"() {
  }
});
var colors = {
  /** Primary accent — teal/cyan used for headings, prompts, highlights */
  accent: chalk__default.default.cyan,
  accentBold: chalk__default.default.bold.cyan,
  /** Secondary accent — blue for links and info */
  secondary: chalk__default.default.blue,
  /** Muted text — dim gray for borders, metadata */
  muted: chalk__default.default.gray,
  mutedDim: chalk__default.default.dim.gray,
  /** Success — green for completed states */
  success: chalk__default.default.green,
  /** Warning — yellow for pending states, code */
  warning: chalk__default.default.yellow,
  /** Error — red for error messages */
  error: chalk__default.default.red,
  errorBold: chalk__default.default.bold.red,
  /** Text — default and bold */
  text: chalk__default.default.white,
  textBold: chalk__default.default.bold.white,
  /** User message styling */
  userLabel: chalk__default.default.bold.green,
  userText: chalk__default.default.white,
  /** Assistant message styling */
  assistantLabel: chalk__default.default.bold.cyan,
  /** System message styling */
  systemText: chalk__default.default.dim,
  systemInfo: chalk__default.default.dim.cyan,
  systemError: chalk__default.default.dim.red,
  systemWarning: chalk__default.default.dim.yellow,
  /** Agent step styling */
  stepPending: chalk__default.default.dim.yellow,
  stepComplete: chalk__default.default.dim.green
};
var selectListTheme = {
  selectedPrefix: (s) => colors.accent(s),
  selectedText: (s) => colors.text(s),
  description: (s) => colors.muted(s),
  scrollInfo: (s) => colors.muted(s),
  noMatch: (s) => colors.muted(s)
};
var editorTheme = {
  borderColor: (s) => colors.accent(s),
  selectList: selectListTheme
};
var markdownTheme = {
  heading: (s) => colors.accentBold(s),
  link: (s) => chalk__default.default.underline.blue(s),
  linkUrl: (s) => colors.muted(s),
  code: (s) => colors.warning(s),
  codeBlock: (s) => colors.text(s),
  codeBlockBorder: (s) => colors.muted(s),
  quote: (s) => chalk__default.default.italic.gray(s),
  quoteBorder: (s) => colors.muted(s),
  hr: (s) => colors.muted(s),
  listBullet: (s) => colors.accent(s),
  bold: (s) => chalk__default.default.bold(s),
  italic: (s) => chalk__default.default.italic(s),
  strikethrough: (s) => chalk__default.default.strikethrough(s),
  underline: (s) => chalk__default.default.underline(s)
};
function formatHeader(workspaceName, status) {
  const app = colors.accentBold("ARBI");
  const ws = workspaceName ? colors.muted(` | ${workspaceName}`) : "";
  const st = colors.muted(` | ${status}`);
  return `${app}${ws}${st}`;
}
function highlightCode(code, lang) {
  const lines = code.split("\n");
  return lines.map((line) => {
    let styled = line.replace(/(["'])(?:(?=(\\?))\2.)*?\1/g, (m) => chalk__default.default.green(m)).replace(/\b(\d+\.?\d*)\b/g, (m) => chalk__default.default.yellow(m)).replace(/(\/\/.*|#.*)$/g, (m) => chalk__default.default.gray(m));
    const keywords = /\b(const|let|var|function|return|if|else|for|while|import|export|from|class|extends|new|async|await|try|catch|throw|type|interface|enum|def|self|None|True|False|print)\b/g;
    styled = styled.replace(keywords, (m) => chalk__default.default.cyan(m));
    return styled;
  });
}

// src/components/assistant-message.ts
var assistantMarkdownTheme = {
  ...markdownTheme,
  highlightCode
};
var AssistantMessage = class extends piTui.Container {
  markdown;
  label;
  constructor() {
    super();
    this.label = new piTui.Text(colors.assistantLabel("ARBI"), 1, 0);
    this.markdown = new piTui.Markdown("", 1, 0, assistantMarkdownTheme);
    this.addChild(this.label);
    this.addChild(this.markdown);
  }
  /** Update the response text (called as tokens stream in). */
  setText(text) {
    this.markdown.setText(text);
  }
};
var UserMessage = class extends piTui.Container {
  constructor(text) {
    super();
    const label = new piTui.Text(colors.userLabel("You"), 1, 0);
    const content = new piTui.Markdown(text, 1, 0, markdownTheme);
    this.addChild(label);
    this.addChild(content);
  }
};
var SystemMessage = class extends piTui.Text {
  constructor(message, level = "info") {
    const styleFn = level === "error" ? colors.systemError : level === "warning" ? colors.systemWarning : colors.systemInfo;
    super(styleFn(message), 1, 0);
  }
};
var RightAlignedText = class {
  text;
  styleFn;
  constructor(text, styleFn) {
    this.text = text;
    this.styleFn = styleFn;
  }
  invalidate() {
  }
  render(width) {
    if (!this.text || this.text.trim() === "") return [];
    const styled = this.styleFn(this.text);
    const textWidth = piTui.visibleWidth(styled);
    const padding = Math.max(0, width - textWidth);
    return [" ".repeat(padding) + styled];
  }
};
var GLYPH_LIFECYCLE = ">";
var GLYPH_TOOL = "\u25B8";
var GLYPH_RESULT = "\u2192";
var AgentStep = class extends piTui.Text {
  completed = false;
  constructor(label, details) {
    super(buildText(label, details), 2, 0);
  }
  /** Mark this step as completed. Renderer is unchanged for now —
   *  see the file-header comment for the planned focusable-collapse
   *  upgrade path. */
  complete() {
    if (this.completed) return;
    this.completed = true;
  }
};
function buildText(label, details) {
  const hasDetails = !!(details?.tool || details?.args || details?.result);
  const glyph = hasDetails ? GLYPH_TOOL : GLYPH_LIFECYCLE;
  const header = `${colors.stepPending(glyph)} ${hasDetails ? colors.accent(label) : colors.muted(label)}`;
  if (!hasDetails) return header;
  const lines = [header];
  if (details?.tool && details.tool !== label) {
    lines.push(`  ${colors.muted("tool:")} ${details.tool}`);
  }
  if (details?.args) {
    lines.push(`  ${colors.muted("args:")} ${details.args}`);
  }
  if (details?.result) {
    lines.push(`  ${colors.muted(GLYPH_RESULT)} ${details.result}`);
  }
  return lines.join("\n");
}

// src/components/chat-log.ts
var ChatLog = class extends piTui.Container {
  /** Currently streaming assistant message (null when idle). */
  activeAssistant = null;
  /** Agent steps associated with the current streaming run. */
  activeSteps = [];
  /** Add a user message to the log. */
  addUser(text) {
    this.addChild(new UserMessage(text));
    this.addChild(new piTui.Spacer(1));
  }
  /** Add a complete assistant message (non-streaming, e.g. for history restoration). */
  addAssistant(text) {
    const msg = new AssistantMessage();
    msg.setText(text);
    this.addChild(msg);
    this.addChild(new piTui.Spacer(1));
  }
  /** Add a system/info/error message. */
  addSystem(message, level = "info") {
    this.addChild(new SystemMessage(message, level));
    this.addChild(new piTui.Spacer(1));
  }
  /** Add a right-aligned summary line (e.g. stream stats). */
  addSummary(text) {
    this.addChild(new RightAlignedText(text, colors.systemInfo));
    this.addChild(new piTui.Spacer(1));
  }
  /** Add a received DM message (cyan label with sender email). */
  addDm(senderEmail, text) {
    const container = new piTui.Container();
    const label = new piTui.Text(colors.accent(senderEmail), 1, 0);
    const content = new piTui.Text(colors.text(text), 1, 0);
    container.addChild(label);
    container.addChild(content);
    this.addChild(container);
    this.addChild(new piTui.Spacer(1));
  }
  /** Remove all messages from the chat log. */
  clearMessages() {
    this.activeAssistant = null;
    this.activeSteps = [];
    while (this.children.length > 0) {
      this.removeChild(this.children[this.children.length - 1]);
    }
  }
  /** Start a new assistant streaming response. */
  startAssistant() {
    this.activeAssistant = new AssistantMessage();
    this.activeSteps = [];
    this.addChild(this.activeAssistant);
  }
  /** Update the active assistant message with accumulated text. */
  updateAssistant(text) {
    this.activeAssistant?.setText(text);
  }
  /** Add an agent step to the current streaming run. Optional
   *  ``details`` enriches the rendered block with ``tool`` / ``args``
   *  / ``result`` lines beneath the header — claude-code-style
   *  tool-use expansion. Callers that don't have structured detail
   *  can omit the second arg and get the original flat behaviour. */
  addAgentStep(label, details) {
    if (!this.activeAssistant) return;
    const step = new AgentStep(label, details);
    this.activeSteps.push(step);
    this.addChild(step);
  }
  /** Remove all active agent steps from the chat log (e.g. when streaming starts). */
  clearActiveSteps() {
    for (const step of this.activeSteps) {
      this.removeChild(step);
    }
    this.activeSteps = [];
  }
  /** Finalize the current assistant response. */
  finalizeAssistant() {
    this.clearActiveSteps();
    this.activeAssistant = null;
    this.addChild(new piTui.Spacer(1));
  }
};

// src/event-log.ts
var WsEventLog = class {
  entries = [];
  capacity;
  /** ``capacity`` defaults to 200, the same order-of-magnitude as
   *  a typical long chat session. Old entries roll off when the
   *  buffer fills — same shape as openclaw's session log. */
  constructor(capacity = 200) {
    this.capacity = capacity;
  }
  /** Append a new entry. Drops the oldest if at capacity. */
  add(entry) {
    this.entries.push({
      at: entry.at ?? /* @__PURE__ */ new Date(),
      text: entry.text,
      level: entry.level,
      msgType: entry.msgType
    });
    while (this.entries.length > this.capacity) this.entries.shift();
  }
  /** Read the last ``limit`` entries (most recent last). Optionally
   *  filter by level — useful for ``/events --errors``. */
  getAll(options) {
    let view = this.entries;
    if (options?.level) view = view.filter((e) => e.level === options.level);
    if (options?.limit != null) view = view.slice(-options.limit);
    return view;
  }
  /** Number of entries currently retained (after eviction). */
  get size() {
    return this.entries.length;
  }
};
function formatEventLine(entry) {
  const h = String(entry.at.getHours()).padStart(2, "0");
  const m = String(entry.at.getMinutes()).padStart(2, "0");
  const s = String(entry.at.getSeconds()).padStart(2, "0");
  const tag = entry.level.padEnd(7);
  return `[${h}:${m}:${s}] ${tag} ${entry.text}`;
}
var SourcesIndex = class {
  // Keyed by docId so successive turns add to the same entry rather
  // than duplicating it.
  entries = /* @__PURE__ */ new Map();
  /** Fold every citation in ``metadata`` into the index. Idempotent
   *  on the same payload — adding the same metadata twice doesn't
   *  inflate counts. */
  recordCitations(metadata) {
    if (!metadata) return;
    this.recordResolved(sdk.resolveCitations(metadata));
  }
  /** The underlying fold step, separated so tests can drive it with
   *  hand-rolled ``ResolvedCitation`` fixtures rather than mocking
   *  the full ``MessageMetadataPayload.tools.*`` graph that
   *  ``resolveCitations`` traverses. */
  recordResolved(resolved) {
    for (const c of resolved) {
      for (const chunk of c.chunks) {
        const docId = chunk.metadata?.doc_ext_id;
        if (!docId) continue;
        const title = chunk.metadata?.doc_title ?? docId;
        const page = chunk.metadata?.page_number;
        const existing = this.entries.get(docId);
        if (existing) {
          if (typeof page === "number" && !existing.pages.includes(page)) {
            existing.pages.push(page);
            existing.pages.sort((a, b) => a - b);
          }
          if (!existing.citationNums.includes(c.citationNum)) {
            existing.citationNums.push(c.citationNum);
            existing.citationNums.sort();
          }
          if (existing.title === docId && title !== docId) existing.title = title;
        } else {
          this.entries.set(docId, {
            docId,
            title,
            pages: typeof page === "number" ? [page] : [],
            citationNums: [c.citationNum]
          });
        }
      }
    }
  }
  /** All entries in registration order — first-cited first. The
   *  iterator order of ``Map`` is insertion order, which is what
   *  we want for ``/sources`` (oldest at top, newest at bottom). */
  list() {
    return Array.from(this.entries.values());
  }
  /** Clear the index. Wired to ``/new`` so a fresh conversation
   *  starts with no inherited sources. */
  clear() {
    this.entries.clear();
  }
  /** Number of unique documents tracked. */
  get size() {
    return this.entries.size;
  }
};
var ArbiEditor = class extends piTui.Editor {
  /** Callback when Escape is pressed (abort streaming). */
  onEscape;
  /** Callback when Ctrl+C is pressed (clear or exit). */
  onCtrlC;
  /** Callback when Ctrl+D is pressed (exit). */
  onCtrlD;
  /** Callback when Ctrl+W is pressed (workspace selector). */
  onCtrlW;
  /** Callback when Ctrl+N is pressed (new conversation). */
  onCtrlN;
  /**
   * Callback fired after every keystroke that mutates the buffer.
   *
   * The TUI uses this to power the **pre-flight skill hint**: when
   * the user types ``/<slug>`` we surface the skill's description in
   * the chat log so they see what they're about to invoke before
   * pressing Enter. The handler is responsible for its own
   * deduplication (e.g. only show the hint when the parsed slug
   * changes), since this callback fires on *every* mutating key.
   *
   * Receives the buffer text *after* the base ``handleInput`` has
   * applied the change. Read-only — handlers must not mutate the
   * editor synchronously or it'll loop.
   */
  onBufferChange;
  /** Track Ctrl+C presses for double-tap exit. */
  lastCtrlCTime = 0;
  /** Snapshot of the last text we emitted, so we don't fire on
   *  non-mutating keys (cursor moves, redraws). */
  lastEmittedText = "";
  constructor(tui) {
    super(tui, editorTheme, { paddingX: 1 });
  }
  handleInput(data) {
    if (piTui.matchesKey(data, piTui.Key.escape)) {
      this.onEscape?.();
      return;
    }
    if (piTui.matchesKey(data, piTui.Key.ctrl("c"))) {
      const now = Date.now();
      if (this.getText().trim()) {
        this.setText("");
        this.lastCtrlCTime = now;
      } else if (now - this.lastCtrlCTime < 1e3) {
        this.onCtrlC?.();
      } else {
        this.lastCtrlCTime = now;
        this.onCtrlC?.();
      }
      return;
    }
    if (piTui.matchesKey(data, piTui.Key.ctrl("d"))) {
      this.onCtrlD?.();
      return;
    }
    if (piTui.matchesKey(data, piTui.Key.ctrl("w"))) {
      this.onCtrlW?.();
      return;
    }
    if (piTui.matchesKey(data, piTui.Key.ctrl("n"))) {
      this.onCtrlN?.();
      return;
    }
    super.handleInput(data);
    if (this.onBufferChange) {
      const current = this.getText();
      if (current !== this.lastEmittedText) {
        this.lastEmittedText = current;
        try {
          this.onBufferChange(current);
        } catch {
        }
      }
    }
  }
};
var LEVEL_STYLE = {
  info: chalk__default.default.dim.cyan,
  success: chalk__default.default.dim.green,
  warning: chalk__default.default.dim.yellow,
  error: chalk__default.default.dim.red
};
function formatTimestamp() {
  const now = /* @__PURE__ */ new Date();
  const hh = String(now.getHours()).padStart(2, "0");
  const mm = String(now.getMinutes()).padStart(2, "0");
  const ss = String(now.getSeconds()).padStart(2, "0");
  return `${hh}:${mm}:${ss}`;
}
var ToastContainer = class extends piTui.Container {
  activeToasts = [];
  renderCallback = null;
  /** Set the callback used to trigger a TUI re-render after toast changes. */
  setRenderCallback(cb) {
    this.renderCallback = cb;
  }
  /** Show a toast notification that auto-dismisses after `durationMs`. */
  show(message, level = "info", durationMs = 5e3) {
    const styleFn = LEVEL_STYLE[level];
    const formatted = styleFn(`[${formatTimestamp()}] ${message}`);
    const text = new piTui.Text(formatted, 1, 0);
    const timer = setTimeout(() => {
      this.dismiss(text);
    }, durationMs);
    this.activeToasts.push({ text, timer });
    this.addChild(text);
    this.renderCallback?.();
  }
  /** Remove all active toasts immediately. */
  clear() {
    for (const toast of this.activeToasts) {
      clearTimeout(toast.timer);
      this.removeChild(toast.text);
    }
    this.activeToasts = [];
    this.renderCallback?.();
  }
  dismiss(text) {
    const idx = this.activeToasts.findIndex((t) => t.text === text);
    if (idx !== -1) {
      clearTimeout(this.activeToasts[idx].timer);
      this.activeToasts.splice(idx, 1);
      this.removeChild(text);
      this.renderCallback?.();
    }
  }
};

// src/command-registry.ts
init_tui_helpers();
var registry = /* @__PURE__ */ new Map();
var KNOWN_SERVER_COMMANDS = /* @__PURE__ */ new Set([
  "pa",
  "setup",
  "cold-start",
  "create-skill"
]);
function registerCommand(def) {
  registry.set(def.name, def);
}
function registerCommands(defs) {
  for (const def of defs) {
    registerCommand(def);
  }
}
function toSlashCommands() {
  const cmds = [];
  for (const def of registry.values()) {
    if (def.hidden) continue;
    cmds.push({ name: def.name, description: def.description });
  }
  return cmds;
}
function toSlashCommandsWithSkills(skills, builtins = []) {
  const out = toSlashCommands();
  const taken = new Set(out.map((c) => c.name));
  for (const b of builtins) {
    if (taken.has(b.name)) continue;
    taken.add(b.name);
    out.push({ name: b.name, description: b.description });
  }
  for (const s of skills) {
    if (taken.has(s.slug)) continue;
    out.push({ name: s.slug, description: s.description });
  }
  return out;
}
function formatHelpText() {
  const lines = [];
  for (const def of registry.values()) {
    if (def.hidden) continue;
    const hint = def.argHint ? ` <${def.argHint}>` : "";
    lines.push(`  /${def.name}${hint} \u2014 ${def.description}`);
  }
  return [
    "Available commands:",
    "",
    ...lines,
    "",
    "Direct messages:",
    "  @name  \u2014 Switch to DM channel with a contact",
    "  @arbi  \u2014 Switch back to AI chat",
    "",
    "Keyboard shortcuts:",
    "  Ctrl+N    \u2014 New conversation",
    "  Ctrl+W    \u2014 Switch workspace",
    "  Ctrl+D    \u2014 Exit",
    "  Escape    \u2014 Abort streaming",
    "",
    "Skills: any workspace SKILL.md with `user-invocable: true` becomes a",
    "  ``/<slug>`` command \u2014 type it like a regular slash command and the",
    "  agent handles it. ``/skills`` lists what is available."
  ].join("\n");
}
async function dispatchCommand(tui, input2) {
  const parsed = sdk.parseSlashCommand(input2);
  if (!parsed) return false;
  const cmdName = parsed.slug;
  const args = parsed.args.length > 0 ? parsed.args.split(/\s+/) : [];
  const trimmed = input2.trim();
  const def = registry.get(cmdName);
  if (!def) {
    const skill = tui.skillsCache?.find(
      (s) => s.slug === cmdName || s.name.toLowerCase() === cmdName
    );
    if (skill) {
      const meta = skill.arg_hint ? `Skill: ${skill.name} ${skill.arg_hint} \u2014 ${skill.description}` : `Skill: ${skill.name} \u2014 ${skill.description}`;
      showMessage(tui, meta, "info");
      return false;
    }
    const isBuiltinServerCommand = tui.commandsCache?.some((c) => c.name === cmdName) || KNOWN_SERVER_COMMANDS.has(cmdName);
    if (isBuiltinServerCommand) {
      return false;
    }
    showMessage(tui, `Unknown command: /${cmdName}. Type /help for available commands.`, "warning");
    return true;
  }
  if (def.minArgs && args.length < def.minArgs) {
    const hint = def.argHint ? ` <${def.argHint}>` : "";
    showMessage(tui, `Usage: /${def.name}${hint}`, "warning");
    return true;
  }
  let ctx;
  if (def.requires === "none") {
    ctx = { requires: "none", args, rawInput: trimmed, tui };
  } else if (def.requires === "auth") {
    if (!tui.authContext) {
      showMessage(tui, "Not authenticated. Use /login first.", "error");
      return true;
    }
    ctx = {
      requires: "auth",
      args,
      rawInput: trimmed,
      tui,
      arbi: tui.authContext.arbi,
      authContext: tui.authContext
    };
  } else {
    if (!tui.authContext) {
      showMessage(tui, "Not authenticated. Use /login first.", "error");
      return true;
    }
    if (!tui.wsContext) {
      showMessage(tui, "No workspace selected. Use /workspace <id> first.", "warning");
      return true;
    }
    ctx = {
      requires: "workspace",
      args,
      rawInput: trimmed,
      tui,
      arbi: tui.wsContext.arbi,
      authContext: tui.authContext,
      wsContext: tui.wsContext,
      authHeaders: {
        baseUrl: tui.wsContext.config.baseUrl,
        accessToken: tui.wsContext.accessToken
      }
    };
  }
  try {
    const result = await def.run(ctx);
    if (typeof result === "string") {
      showMessage(tui, result);
    } else if (Array.isArray(result)) {
      showMessage(tui, result.join("\n"));
    }
  } catch (err) {
    showMessage(tui, `Command /${def.name} failed: ${sdk.getErrorMessage(err)}`, "error");
  }
  return true;
}

// src/commands/general.ts
var generalCommands = [
  {
    name: "help",
    description: "Show available commands",
    requires: "none",
    run: () => formatHelpText()
  },
  {
    name: "status",
    description: "Show auth/workspace/connection status",
    requires: "none",
    run: (ctx) => {
      const { tui } = ctx;
      const { state } = tui;
      return [
        `Authenticated: ${state.isAuthenticated ? colors.success("yes") : colors.error("no")}`,
        `Workspace: ${state.workspaceName ? colors.accent(state.workspaceName) : colors.muted("none")}`,
        `Workspace ID: ${state.workspaceId ?? colors.muted("none")}`,
        `Conversation: ${state.conversationMessageId ? colors.muted(state.conversationMessageId) : colors.muted("new")}`,
        `Status: ${state.activityStatus}`,
        `WebSocket: ${tui.wsConnected ? colors.success("connected") : colors.muted("disconnected")}`
      ];
    }
  },
  {
    name: "new",
    description: "Start fresh conversation (clear threading)",
    requires: "none",
    run: (ctx) => {
      const { tui } = ctx;
      tui.state.conversationMessageId = null;
      tui.lastMetadata = null;
      tui.sourcesIndex.clear();
      tui.store.clearChatSession();
      return "Started new conversation.";
    }
  },
  {
    name: "exit",
    description: "Exit TUI",
    requires: "none",
    run: (ctx) => {
      const { tui } = ctx;
      tui.shutdown();
    }
  },
  {
    name: "quit",
    description: "Exit TUI",
    requires: "none",
    hidden: true,
    run: (ctx) => {
      const { tui } = ctx;
      tui.shutdown();
    }
  }
];
async function promptSelect(message, choices) {
  return prompts.select({ message, choices });
}
async function promptInput(message, required = true) {
  return prompts.input({
    message,
    validate: required ? (v) => v.trim() ? true : "Required" : void 0
  });
}
async function promptPassword(message) {
  return prompts.password({
    message,
    mask: "*",
    validate: (v) => v ? true : "Required"
  });
}
async function promptConfirm(message, defaultValue = true) {
  return prompts.confirm({ message, default: defaultValue });
}

// src/auth.ts
async function interactiveLogin(store) {
  const config = store.requireConfig();
  const email = await promptInput("Email");
  const pw = await promptPassword("Password");
  const authContext = await sdk.performPasswordLogin(config, email, pw, store);
  console.info(`Logged in as ${email}`);
  const result = await selectOrCreateWorkspace(authContext, store);
  return { authContext, ...result };
}
async function interactiveRegister(store) {
  const config = store.requireConfig();
  const email = await promptInput("Email");
  const arbi = client.createArbiClient({
    baseUrl: config.baseUrl,
    deploymentDomain: config.deploymentDomain,
    credentials: "omit"
  });
  await arbi.crypto.initSodium();
  const codeMethod = await promptSelect("Verification method", [
    { name: "I have an invitation code", value: "code" },
    { name: "Send me a verification email", value: "email" }
  ]);
  let verificationCode;
  if (codeMethod === "code") {
    verificationCode = await promptInput("Invitation code");
  } else {
    console.info("Sending verification email...");
    const verifyResponse = await arbi.fetch.POST("/v1/user/verify-email", {
      body: { email }
    });
    if (verifyResponse.error) {
      throw new Error(`Failed to send verification email: ${JSON.stringify(verifyResponse.error)}`);
    }
    console.info("Verification email sent. Check your inbox.");
    verificationCode = await promptInput("Verification code");
  }
  const pw = await promptPassword("Password");
  const confirmPw = await promptPassword("Confirm password");
  if (pw !== confirmPw) {
    throw new Error("Passwords do not match.");
  }
  const firstName = await promptInput("First name (optional)", false) || "User";
  const lastName = await promptInput("Last name (optional)", false) || "";
  await arbi.auth.register({
    email,
    password: pw,
    verificationCode,
    firstName,
    lastName
  });
  console.info(`
Registered successfully as ${email}`);
  console.info("Logging in...");
  const authContext = await sdk.performPasswordLogin(config, email, pw, store);
  console.info(`Logged in as ${email}`);
  const result = await selectOrCreateWorkspace(authContext, store);
  return { authContext, ...result };
}
async function selectOrCreateWorkspace(authContext, store) {
  const wsList = await sdk.workspaces.listWorkspaces(authContext.arbi);
  if (wsList.length === 1) {
    store.updateConfig({ selectedWorkspaceId: wsList[0].external_id });
    console.info(`Workspace: ${wsList[0].name}`);
    return { selectedWorkspaceId: wsList[0].external_id, selectedWorkspaceName: wsList[0].name };
  }
  if (wsList.length > 1) {
    const choices = sdk.formatWorkspaceChoices(wsList);
    const selectedWorkspaceId = await promptSelect("Select workspace", choices);
    const ws = wsList.find((w) => w.external_id === selectedWorkspaceId);
    store.updateConfig({ selectedWorkspaceId });
    console.info(`Workspace: ${ws?.name}`);
    return { selectedWorkspaceId, selectedWorkspaceName: ws?.name };
  }
  console.info("No workspaces found.");
  const shouldCreate = await promptConfirm("Create a new workspace?");
  if (shouldCreate) {
    const name = await promptInput("Workspace name");
    const userProjects = await sdk.projects.listProjects(authContext.arbi);
    const defaultProjectExtId = userProjects[0]?.external_id;
    if (!defaultProjectExtId) throw new Error("No projects found for user");
    const encryptedKey = await sdk.generateNewWorkspaceKey(
      authContext.arbi,
      authContext.loginResult.serverSessionKey
    );
    const ws = await sdk.workspaces.createWorkspace(
      authContext.arbi,
      name.trim(),
      encryptedKey,
      defaultProjectExtId
    );
    store.updateConfig({ selectedWorkspaceId: ws.external_id });
    console.info(`Created workspace: ${ws.name}`);
    return { selectedWorkspaceId: ws.external_id, selectedWorkspaceName: ws.name };
  }
  return {};
}
async function ensureAuthenticated(store) {
  try {
    const authContext = await sdk.resolveAuth(store);
    const config = store.requireConfig();
    return {
      authContext,
      selectedWorkspaceId: config.selectedWorkspaceId
    };
  } catch {
    console.info("Not authenticated.\n");
  }
  while (true) {
    const action = await promptSelect("What would you like to do?", [
      { name: "Log in", value: "login" },
      { name: "Register a new account", value: "register" },
      { name: "Exit", value: "exit" }
    ]);
    if (action === "exit") {
      process.exit(0);
    }
    try {
      if (action === "register") {
        return await interactiveRegister(store);
      }
      return await interactiveLogin(store);
    } catch (err) {
      console.error(`
${sdk.getErrorMessage(err)}
`);
    }
  }
}

// src/commands/auth.ts
init_tui_helpers();
var authCommands = [
  {
    name: "login",
    description: "Log in (re-authenticate)",
    requires: "none",
    run: async (ctx) => {
      const { tui } = ctx;
      const result = await runInteractiveFlow(
        tui,
        "Pausing TUI for login...",
        () => interactiveLogin(tui.store),
        "Login failed"
      );
      if (result) {
        tui.setAuthContext(result.authContext);
        await applyWorkspaceSelection(tui, result.selectedWorkspaceId, result.selectedWorkspaceName);
        showMessage(tui, "Logged in successfully.");
      }
    }
  },
  {
    name: "register",
    description: "Register a new account",
    requires: "none",
    run: async (ctx) => {
      const { tui } = ctx;
      const result = await runInteractiveFlow(
        tui,
        "Pausing TUI for registration...",
        () => interactiveRegister(tui.store),
        "Registration failed"
      );
      if (result) {
        tui.setAuthContext(result.authContext);
        await applyWorkspaceSelection(tui, result.selectedWorkspaceId, result.selectedWorkspaceName);
        showMessage(tui, "Registered and logged in.");
      }
    }
  },
  {
    name: "logout",
    description: "Log out and clear credentials",
    requires: "none",
    run: (ctx) => {
      const { tui } = ctx;
      tui.store.deleteCredentials();
      tui.store.updateConfig({ selectedWorkspaceId: void 0 });
      tui.store.clearChatSession();
      tui.authContext = null;
      tui.state.isAuthenticated = false;
      tui.state.workspaceId = null;
      tui.state.workspaceName = null;
      tui.state.conversationMessageId = null;
      return "Logged out. Use /login to authenticate again.";
    }
  }
];
init_tui_helpers();
var workspaceCommands = [
  {
    name: "workspaces",
    description: "List all workspaces",
    requires: "auth",
    run: async (ctx) => {
      const { arbi, tui } = ctx;
      const wsList = await sdk.workspaces.listWorkspaces(arbi);
      const lines = wsList.map((ws) => {
        const current = ws.external_id === tui.state.workspaceId ? colors.accent(" (current)") : "";
        return `  ${ws.external_id}  ${ws.name}${current}`;
      });
      return ["Workspaces:", "", ...lines];
    }
  },
  {
    name: "workspace",
    description: "Switch workspace",
    argHint: "id",
    requires: "auth",
    run: async (ctx) => {
      const { args, arbi, tui } = ctx;
      if (!args[0]) {
        const wsList = await sdk.workspaces.listWorkspaces(arbi);
        const lines = wsList.map((ws2) => {
          const current = ws2.external_id === tui.state.workspaceId ? colors.accent(" (current)") : "";
          return `  ${ws2.external_id}  ${ws2.name}${current}`;
        });
        showMessage(tui, ["Workspaces:", "", ...lines].join("\n"));
        return "Use /workspace <id> to switch.";
      }
      showMessage(tui, `Switching to workspace ${args[0]}...`);
      const ws = await switchWorkspace(tui, args[0]);
      if (ws) {
        return `Switched to workspace: ${colors.accentBold(ws.name)}`;
      }
    }
  },
  {
    name: "create",
    description: "Create a new workspace",
    argHint: "name",
    minArgs: 1,
    requires: "auth",
    run: async (ctx) => {
      const { args, arbi, authContext, tui } = ctx;
      const name = args.join(" ").trim();
      showMessage(tui, `Creating workspace "${name}"...`);
      const userProjects = await sdk.projects.listProjects(arbi);
      const defaultProjectExtId = userProjects[0]?.external_id;
      if (!defaultProjectExtId) throw new Error("No projects found");
      const encryptedKey = await sdk.generateNewWorkspaceKey(
        arbi,
        authContext.loginResult.serverSessionKey
      );
      const ws = await sdk.workspaces.createWorkspace(arbi, name, encryptedKey, defaultProjectExtId);
      const selected = await switchWorkspace(tui, ws.external_id);
      if (selected) {
        return `Created and switched to workspace: ${colors.accentBold(selected.name)}`;
      }
    }
  },
  {
    name: "ws-delete",
    description: "Delete a workspace",
    argHint: "id",
    minArgs: 1,
    requires: "auth",
    run: async (ctx) => {
      const { args, arbi } = ctx;
      await sdk.workspaces.deleteWorkspaces(arbi, [args[0]]);
      return `${colors.success("Deleted")} workspace ${args[0]}`;
    }
  },
  {
    name: "ws-users",
    description: "List users in current workspace",
    requires: "workspace",
    run: async (ctx) => {
      const { arbi } = ctx;
      const users = await sdk.workspaces.listWorkspaceUsers(arbi);
      if (users.length === 0) return "No users in this workspace.";
      const lines = users.map((u) => {
        const name = sdk.formatUserName(u.user);
        const nameStr = name ? colors.muted(` (${name})`) : "";
        const role = u.role ? colors.muted(` [${u.role}]`) : "";
        return `  ${u.user.email}${nameStr}${role}`;
      });
      return [`Workspace users (${users.length}):`, "", ...lines];
    }
  },
  {
    name: "ws-add-user",
    description: "Add a user to current workspace",
    argHint: "email",
    minArgs: 1,
    requires: "workspace",
    run: async (ctx) => {
      const { args, arbi } = ctx;
      const email = args[0].trim();
      await sdk.workspaces.addWorkspaceUsers(arbi, [email]);
      return `${colors.success("Added")} ${email} to workspace.`;
    }
  },
  {
    name: "ws-remove-user",
    description: "Remove a user from current workspace",
    argHint: "email",
    minArgs: 1,
    requires: "workspace",
    run: async (ctx) => {
      const { args, arbi } = ctx;
      const email = args[0].trim();
      const users = await sdk.workspaces.listWorkspaceUsers(arbi);
      const user = users.find((u) => u.user.email === email);
      if (!user) return `User ${email} not found in this workspace.`;
      const userId = user.user.external_id;
      if (!userId) return `Could not resolve user ID for ${email}.`;
      await sdk.workspaces.removeWorkspaceUsers(arbi, [userId]);
      return `${colors.success("Removed")} ${email} from workspace.`;
    }
  }
];
init_tui_helpers();
var documentCommands = [
  {
    name: "docs",
    description: "List documents in current workspace",
    requires: "workspace",
    run: async (ctx) => {
      const { arbi } = ctx;
      const docs = await sdk.documents.listDocuments(arbi);
      if (docs.length === 0) return "No documents in this workspace.";
      const lines = docs.map((d) => `  ${d.external_id}  ${d.file_name ?? "(unnamed)"}`);
      return [`Documents (${docs.length}):`, "", ...lines];
    }
  },
  {
    name: "doc",
    description: "Show document details",
    argHint: "id",
    minArgs: 1,
    requires: "workspace",
    run: async (ctx) => {
      const { args, arbi } = ctx;
      const docs = await sdk.documents.getDocuments(arbi, [args[0]]);
      if (docs.length === 0) return `Document ${args[0]} not found.`;
      const doc = docs[0];
      const lines = [
        `Document: ${colors.accent(doc.file_name ?? "(unnamed)")}`,
        "",
        `  ID:       ${doc.external_id}`,
        `  Status:   ${doc.status ?? colors.muted("unknown")}`,
        `  Size:     ${doc.file_size != null ? `${doc.file_size} bytes` : colors.muted("unknown")}`,
        `  Created:  ${doc.created_at ?? colors.muted("unknown")}`
      ];
      return lines;
    }
  },
  {
    name: "upload",
    description: "Upload a file",
    argHint: "path",
    minArgs: 1,
    requires: "workspace",
    run: async (ctx) => {
      const { args, tui, wsContext } = ctx;
      const filePath = args.join(" ").trim();
      showMessage(tui, `Uploading ${filePath}...`);
      const result = await sdk.documentsNode.uploadLocalFile(
        {
          baseUrl: wsContext.config.baseUrl,
          accessToken: wsContext.accessToken
        },
        filePath
      );
      const ids = (result.doc_ext_ids ?? []).join(", ");
      showMessage(tui, `${colors.success("Uploaded")} ${result.fileName} \u2014 doc ID(s): ${ids}`);
      if (result.skipped && result.skipped.length > 0) {
        showMessage(
          tui,
          `Skipped: ${result.skipped.map((s) => `${s.file_name} (${s.reason})`).join(", ")}`,
          "warning"
        );
      }
    }
  },
  {
    name: "upload-url",
    description: "Upload a document from URL",
    argHint: "url",
    minArgs: 1,
    requires: "workspace",
    run: async (ctx) => {
      const { args, arbi, wsContext, tui } = ctx;
      const url = args[0].trim();
      showMessage(tui, `Uploading from ${url}...`);
      const result = await sdk.documents.uploadUrl(arbi, [url], {
        workspaceExtId: wsContext.workspaceId
      });
      const ids = (result.doc_ext_ids ?? []).join(", ");
      const lines = [`${colors.success("Uploaded")} from URL \u2014 doc ID(s): ${ids}`];
      if (result.skipped && result.skipped.length > 0) {
        lines.push(
          `Skipped: ${result.skipped.map((s) => `${s.file_name} (${s.reason})`).join(", ")}`
        );
      }
      return lines;
    }
  },
  {
    name: "delete",
    description: "Delete a document",
    argHint: "doc-id",
    minArgs: 1,
    requires: "workspace",
    run: async (ctx) => {
      const { args, arbi, tui } = ctx;
      const docId = args[0].trim();
      showMessage(tui, `Deleting document ${docId}...`);
      await sdk.documents.deleteDocuments(arbi, [docId]);
      return `${colors.success("Deleted")} document ${docId}`;
    }
  },
  {
    name: "parsed",
    description: "Show parsed content of a document",
    argHint: "doc-id",
    minArgs: 1,
    requires: "workspace",
    run: async (ctx) => {
      const { args, authHeaders } = ctx;
      const docId = args[0].trim();
      const data = await sdk.documents.getParsedContent(authHeaders, docId, "content");
      const content = data.content;
      if (!content) return `No parsed content available for document ${docId}.`;
      const maxLen = 3e3;
      const truncated = content.length > maxLen ? content.slice(0, maxLen) + "\n... (truncated)" : content;
      return [`Parsed content for ${docId}:`, "", truncated];
    }
  }
];
var conversationCommands = [
  {
    name: "conversations",
    description: "List conversations",
    requires: "workspace",
    run: async (ctx) => {
      const { arbi, wsContext } = ctx;
      const convs = await sdk.conversations.listConversations(arbi, wsContext.workspaceId);
      if (convs.length === 0) return "No conversations in this workspace.";
      const lines = convs.map((c) => `  ${c.external_id}  ${c.title ?? "(untitled)"}`);
      return [`Conversations (${convs.length}):`, "", ...lines];
    }
  },
  {
    name: "conv-delete",
    description: "Delete a conversation",
    argHint: "id",
    minArgs: 1,
    requires: "workspace",
    run: async (ctx) => {
      const { args, arbi } = ctx;
      await sdk.conversations.deleteConversation(arbi, args[0]);
      return `${colors.success("Deleted")} conversation ${args[0]}`;
    }
  },
  {
    name: "conv-title",
    description: "Rename a conversation",
    argHint: "id title",
    minArgs: 2,
    requires: "workspace",
    run: async (ctx) => {
      const { args, arbi } = ctx;
      const [id, ...titleParts] = args;
      const title = titleParts.join(" ");
      await sdk.work.updateConversation(arbi, id, { title });
      return `Renamed conversation ${id} to: ${colors.accent(title)}`;
    }
  },
  {
    name: "conv-share",
    description: "Share a conversation",
    argHint: "id",
    minArgs: 1,
    requires: "workspace",
    run: async (ctx) => {
      const { args, arbi } = ctx;
      await sdk.work.updateConversation(arbi, args[0], { shared: true });
      return `${colors.success("Shared")} conversation ${args[0]} with the workspace`;
    }
  },
  {
    name: "conv-unshare",
    description: "Make a shared conversation private again",
    argHint: "id",
    minArgs: 1,
    requires: "workspace",
    run: async (ctx) => {
      const { args, arbi } = ctx;
      await sdk.work.updateConversation(arbi, args[0], { shared: false });
      return `${colors.success("Private")} again: conversation ${args[0]}`;
    }
  }
];
var tagCommands = [
  {
    name: "tags",
    description: "List tags in workspace",
    requires: "workspace",
    run: async (ctx) => {
      const { arbi } = ctx;
      const data = await sdk.tags.listTags(arbi);
      if (data.length === 0) return "No tags in this workspace.";
      const lines = data.map((t) => `  ${t.external_id}  ${t.name}`);
      return [`Tags (${data.length}):`, "", ...lines];
    }
  },
  {
    name: "tag-create",
    description: "Create a tag",
    argHint: "name",
    minArgs: 1,
    requires: "workspace",
    run: async (ctx) => {
      const { args, arbi } = ctx;
      const name = args.join(" ").trim();
      const tag = await sdk.tags.createTag(arbi, { name });
      return `${colors.success("Created")} tag "${tag.name}" \u2014 ID: ${tag.external_id}`;
    }
  },
  {
    name: "tag-delete",
    description: "Delete a tag",
    argHint: "id",
    minArgs: 1,
    requires: "workspace",
    run: async (ctx) => {
      const { args, arbi } = ctx;
      await sdk.tags.deleteTag(arbi, args[0]);
      return `${colors.success("Deleted")} tag ${args[0]}`;
    }
  }
];
var miscCommands = [
  {
    name: "contacts",
    description: "List contacts (type @name to DM)",
    requires: "auth",
    run: async (ctx) => {
      const { arbi } = ctx;
      const contactList = await sdk.contacts.listContacts(arbi);
      const { registered, pending, cards } = sdk.contacts.groupContactsByStatus(contactList);
      if (registered.length === 0 && pending.length === 0 && cards.length === 0) {
        return "No contacts. Use the web app to add contacts.";
      }
      const lines = [];
      if (registered.length > 0) {
        lines.push(`Contacts (${registered.length}):`, "");
        for (const c of registered) {
          const name = sdk.formatUserName(c.user);
          const nameStr = name ? colors.muted(` (${name})`) : "";
          lines.push(`  ${c.email}${nameStr}`);
        }
      }
      if (pending.length > 0) {
        if (lines.length > 0) lines.push("");
        lines.push(`Pending invitations (${pending.length}):`);
        for (const c of pending) {
          lines.push(`  ${c.email} \u2014 ${colors.muted(c.status)}`);
        }
      }
      if (cards.length > 0) {
        if (lines.length > 0) lines.push("");
        lines.push(`Address book cards (${cards.length}):`);
        for (const c of cards) {
          const label = c.card?.name || c.card?.emails?.[0]?.email || colors.muted("(locked card)");
          lines.push(`  ${label}`);
        }
      }
      lines.push("", colors.muted("Type @name to start a DM with a registered contact."));
      return lines;
    }
  },
  {
    name: "invite",
    description: "Invite a contact",
    argHint: "email",
    minArgs: 1,
    requires: "auth",
    run: async (ctx) => {
      const { args, arbi, tui } = ctx;
      const email = args[0].trim();
      const { showMessage: showMessage2 } = await Promise.resolve().then(() => (init_tui_helpers(), tui_helpers_exports));
      showMessage2(tui, `Inviting ${email}...`);
      const result = await sdk.contacts.inviteContacts(arbi, [email]);
      const contact = result[0];
      if (!contact || contact.status === "failed") {
        return `${colors.error("Not invited")} ${email} \u2014 ${contact?.outcome_detail ?? "the invitation could not be sent"}`;
      }
      if (contact.status === "registered") {
        return `${colors.success("Added")} ${email} \u2014 already registered. You can DM them with @${email.split("@")[0]}`;
      }
      return `${colors.success("Invited")} ${email} \u2014 invitation sent. They'll appear in /contacts once they register.`;
    }
  },
  {
    name: "models",
    description: "List available AI models",
    requires: "auth",
    run: async (ctx) => {
      const { arbi } = ctx;
      const data = await sdk.health.getHealthModels(arbi);
      const models = Array.isArray(data) ? data : data.data ?? [];
      if (models.length === 0) return "No models available.";
      const lines = [`Models (${models.length}):`, ""];
      for (const m of models) {
        const model = m;
        const name = model.model_name ?? model.name ?? "unknown";
        const provider = model.provider ?? "";
        const apiType = model.api_type ?? "";
        const parts = [colors.accent(name)];
        if (provider) parts.push(`provider: ${provider}`);
        if (apiType) parts.push(`api: ${apiType}`);
        lines.push(`  ${parts.join("  ")}`);
      }
      return lines;
    }
  },
  {
    name: "health",
    description: "Show system health status",
    requires: "auth",
    run: async (ctx) => {
      const { arbi } = ctx;
      const data = await sdk.health.getHealth(arbi);
      const lines = ["System Health:", ""];
      const status = data.status;
      if (status) {
        const statusColor = status === "healthy" ? colors.success(status) : colors.warning(status);
        lines.push(`  Status: ${statusColor}`);
      }
      const services = data.services;
      if (services) {
        lines.push("", "  Services:");
        for (const [name, info] of Object.entries(services)) {
          const svc = info;
          const svcStatus = svc.status;
          const statusStr = svcStatus ? svcStatus === "healthy" ? colors.success(svcStatus) : colors.error(svcStatus) : colors.muted("unknown");
          lines.push(`    ${name}: ${statusStr}`);
        }
      }
      return lines;
    }
  },
  {
    name: "settings",
    description: "Show or update user settings",
    argHint: "get | set <key> <value>",
    requires: "auth",
    run: async (ctx) => {
      const { args, arbi } = ctx;
      const sub = args[0]?.toLowerCase();
      if (sub === "set") {
        const key = args[1];
        const rawValue = args.slice(2).join(" ").trim();
        if (!key || rawValue.length === 0) {
          return `${colors.error("Usage:")} /settings set <key> <value>  (e.g. /settings set show_pa_mode true)`;
        }
        const value = coerceSettingValue(rawValue);
        await sdk.settings.updateSettings(arbi, {
          [key]: value
        });
        return `${colors.success("Updated")} ${colors.accent(key)} \u2192 ${String(value)}`;
      }
      if (sub && sub !== "get") {
        return `${colors.error("Unknown subcommand:")} /settings ${sub} \u2014 use /settings, /settings get, or /settings set <key> <value>`;
      }
      const data = await sdk.settings.getSettings(arbi);
      const entries = Object.entries(data);
      if (entries.length === 0) return "No settings configured.";
      const lines = ["User Settings:", ""];
      for (const [key, value] of entries) {
        const display = typeof value === "object" ? JSON.stringify(value) : String(value);
        lines.push(`  ${colors.accent(key)}: ${display}`);
      }
      return lines;
    }
  }
];
function coerceSettingValue(raw) {
  const lower = raw.toLowerCase();
  if (lower === "true" || lower === "yes" || lower === "on") return true;
  if (lower === "false" || lower === "no" || lower === "off") return false;
  if (lower === "null") return null;
  if (/^-?\d+$/.test(raw)) return Number(raw);
  if (/^-?\d+\.\d+$/.test(raw)) return Number(raw);
  return raw;
}
var MAX_PASSAGE_CHARS = 2e3;
var CitationPanel = class extends piTui.Box {
  constructor(citation, passageIndex = 0) {
    super(1, 1);
    const chunk = citation.chunks[passageIndex];
    const docTitle = chunk?.metadata?.doc_title ?? "Unknown document";
    const page = chunk?.metadata?.page_number;
    const headerParts = [
      `${colors.accentBold(`[Citation ${citation.citationNum}]`)} ${colors.textBold(docTitle)}`
    ];
    if (page != null) {
      headerParts.push(colors.muted(`  Page ${page}`));
    }
    if (citation.chunks.length > 1) {
      headerParts.push(colors.muted(`  Passage ${passageIndex + 1}/${citation.chunks.length}`));
    }
    this.addChild(new piTui.Text(headerParts.join(""), 0, 0));
    this.addChild(new piTui.Text("", 0, 0));
    this.addChild(new piTui.Text(colors.textBold("Statement:"), 0, 0));
    this.addChild(new piTui.Text(`  ${citation.citationData.statement}`, 0, 0));
    this.addChild(new piTui.Text("", 0, 0));
    this.addChild(new piTui.Text(colors.textBold("Passage:"), 0, 0));
    if (chunk) {
      let content = chunk.content;
      if (content.length > MAX_PASSAGE_CHARS) {
        content = content.slice(0, MAX_PASSAGE_CHARS) + "\n...(truncated)";
      }
      this.addChild(new piTui.Markdown(content, 0, 0, markdownTheme));
    } else {
      this.addChild(new piTui.Text(colors.muted("  (no passage data available)"), 0, 0));
    }
    this.addChild(new piTui.Text("", 0, 0));
    const hints = [];
    if (citation.chunks.length > 1) {
      hints.push(
        `/cite ${citation.citationNum} <1-${citation.chunks.length}> to view other passages`
      );
    }
    hints.push("Press Escape to close");
    this.addChild(new piTui.Text(colors.muted(hints.join("  \xB7  ")), 0, 0));
  }
};

// src/commands/citation.ts
function handleCite(ctx) {
  const { tui, args } = ctx;
  if (!tui.lastMetadata) {
    return "No citation data available. Ask a question first.";
  }
  const count = sdk.countCitations(tui.lastMetadata);
  if (count === 0) {
    return "The last response contained no citations.";
  }
  const resolved = sdk.resolveCitations(tui.lastMetadata);
  if (args.length >= 1) {
    const citationNum = args[0];
    const citation = resolved.find((r) => r.citationNum === citationNum);
    if (!citation) {
      return `Citation [${citationNum}] not found. Use /cite to list all citations.`;
    }
    let passageIndex = 0;
    if (args.length >= 2) {
      const p = parseInt(args[1], 10);
      if (isNaN(p) || p < 1 || p > citation.chunks.length) {
        return `Passage must be between 1 and ${citation.chunks.length}.`;
      }
      passageIndex = p - 1;
    }
    const panel = new CitationPanel(citation, passageIndex);
    tui.showCitationOverlay(panel);
    return;
  }
  const summaries = sdk.summarizeCitations(resolved);
  const lines = [`Citations (${summaries.length}):
`];
  for (const s of summaries) {
    const page = s.pageNumber != null ? `, p${s.pageNumber}` : "";
    const chunks = s.chunkCount > 1 ? ` (${s.chunkCount} passages)` : "";
    const truncated = s.statement.length > 100 ? s.statement.slice(0, 100).replace(/\n/g, " ").trim() + "..." : s.statement.replace(/\n/g, " ").trim();
    lines.push(
      `  ${colors.accent(`[${s.citationNum}]`)} ${colors.textBold(s.docTitle)}${page}${chunks}`
    );
    lines.push(`      ${colors.muted(truncated)}`);
  }
  lines.push(colors.muted("\nUse /cite <N> to view full passage."));
  return lines;
}
var citationCommands = [
  {
    name: "cite",
    description: "Browse citations from last response",
    requires: "none",
    argHint: "[N] [passage]",
    run: (ctx) => handleCite(ctx)
  },
  {
    name: "refs",
    description: "Browse citations from last response",
    requires: "none",
    hidden: true,
    run: (ctx) => handleCite(ctx)
  }
];

// src/commands/events.ts
var DEFAULT_LIMIT = 20;
function parseEventsArg(arg) {
  if (!arg) return { limit: DEFAULT_LIMIT };
  const lower = arg.trim().toLowerCase();
  if (lower === "all") return {};
  if (lower === "errors" || lower === "error") {
    return { limit: DEFAULT_LIMIT, level: "error" };
  }
  if (lower === "warnings" || lower === "warning") {
    return { limit: DEFAULT_LIMIT, level: "warning" };
  }
  const n = Number.parseInt(lower, 10);
  if (Number.isFinite(n) && n > 0) return { limit: n };
  return { limit: DEFAULT_LIMIT };
}
var eventCommands = [
  {
    name: "events",
    description: "Show recent WebSocket events (replaces toasts that scroll off)",
    argHint: "count|all|errors",
    requires: "none",
    run: (ctx) => {
      const { tui, args } = ctx;
      const query = parseEventsArg(args[0]);
      const entries = tui.eventLog.getAll(query);
      if (entries.length === 0) {
        if (query.level) {
          return `No ${query.level} events recorded yet.`;
        }
        return "No WebSocket events recorded yet.";
      }
      const lines = entries.map((e) => {
        const line = formatEventLine(e);
        switch (e.level) {
          case "error":
            return colors.error(line);
          case "warning":
            return colors.warning(line);
          case "success":
            return colors.success(line);
          default:
            return colors.muted(line);
        }
      });
      const header = query.level != null ? `Events (${entries.length}, level=${query.level}):` : `Events (${entries.length} of ${tui.eventLog.size} retained):`;
      return [header, "", ...lines];
    }
  }
];
var MAX_CHARS_PER_PAGE = 2e3;
function splitIntoPages(content) {
  if (!content) return [];
  const trimmed = content.trim();
  if (!trimmed) return [];
  if (trimmed.includes("\n---\n")) {
    return trimmed.split(/\n---\n/).map((p) => p.trim()).filter((p) => p.length > 0);
  }
  if (trimmed.includes("\f")) {
    return trimmed.split("\f").map((p) => p.trim()).filter((p) => p.length > 0);
  }
  return [trimmed];
}
function indexCitationsByPage(docId, citations) {
  const byPage = /* @__PURE__ */ new Map();
  for (const c of citations) {
    for (const chunk of c.chunks) {
      if (chunk.metadata?.doc_ext_id !== docId) continue;
      const page = chunk.metadata?.page_number ?? 1;
      const list = byPage.get(page) ?? [];
      if (!list.includes(c.citationNum)) list.push(c.citationNum);
      byPage.set(page, list);
    }
  }
  return byPage;
}
function renderDocView(opts) {
  const { docId, title, fileName, pages, citationsByPage } = opts;
  const totalCitations = Array.from(citationsByPage.values()).reduce((n, ids) => n + ids.length, 0);
  const lines = [];
  lines.push(colors.textBold(title));
  if (fileName && fileName !== title) lines.push(colors.muted(fileName));
  lines.push(colors.muted(`${docId} \u2014 ${pages.length} page${pages.length === 1 ? "" : "s"}`));
  if (totalCitations > 0) {
    lines.push(
      colors.muted(
        `${totalCitations} citation${totalCitations === 1 ? "" : "s"} from the current conversation`
      )
    );
  }
  lines.push("");
  for (let i = 0; i < pages.length; i++) {
    const pageNum = i + 1;
    const cites = citationsByPage.get(pageNum);
    const citeBadge = cites && cites.length > 0 ? colors.accent(` [${cites.map((n) => `[${n}]`).join(" ")}]`) : "";
    lines.push(colors.accentBold(`\u2500\u2500 page ${pageNum} \u2500\u2500`) + citeBadge);
    const body = pages[i];
    if (body.length > MAX_CHARS_PER_PAGE) {
      lines.push(body.slice(0, MAX_CHARS_PER_PAGE));
      lines.push(
        colors.muted(
          `\u2026 (${body.length - MAX_CHARS_PER_PAGE} more chars \u2014 use /parsed ${docId} for the raw stream)`
        )
      );
    } else {
      lines.push(body);
    }
    lines.push("");
  }
  return lines;
}
var viewCommands = [
  {
    name: "view",
    description: "Read a document in the chat log, with citation hints per page",
    argHint: "doc-id",
    minArgs: 1,
    requires: "workspace",
    run: async (ctx) => {
      const { args, arbi, authHeaders, tui } = ctx;
      const docId = args[0].trim();
      const [docs, parsed] = await Promise.all([
        sdk.documents.getDocuments(arbi, [docId]),
        sdk.documents.getParsedContent(authHeaders, docId, "content")
      ]);
      const doc = docs[0];
      if (!doc) return `Document ${docId} not found.`;
      const meta = doc.doc_metadata;
      const title = meta?.title ?? doc.file_name ?? docId;
      const content = parsed.content ?? "";
      if (!content) return `No parsed content available for ${docId}.`;
      const pages = splitIntoPages(content);
      const citationsByPage = tui.lastMetadata ? indexCitationsByPage(docId, sdk.resolveCitations(tui.lastMetadata)) : /* @__PURE__ */ new Map();
      return renderDocView({
        docId,
        title,
        fileName: doc.file_name ?? null,
        pages,
        citationsByPage
      });
    }
  }
];

// src/commands/sources.ts
function renderSourcesList(entries) {
  if (entries.length === 0) {
    return "No sources cited yet in this conversation. Ask a question first.";
  }
  const lines = [`Sources in this conversation (${entries.length}):`, ""];
  for (const e of entries) {
    const cites = e.citationNums.map((n) => `[${n}]`).join(" ");
    const pages = e.pages.length > 0 ? ` p.${e.pages.join(", p.")}` : "";
    lines.push(`  ${colors.accent(cites)} ${colors.textBold(e.title)}${pages}`);
    lines.push(`      ${colors.muted(e.docId)}`);
  }
  lines.push("");
  lines.push(
    colors.muted("Use /view <doc-id> to read a source, or /cite N for a specific passage.")
  );
  return lines;
}
var sourcesCommands = [
  {
    name: "sources",
    description: "List documents cited in this conversation",
    requires: "none",
    run: (ctx) => {
      const { tui } = ctx;
      return renderSourcesList(tui.sourcesIndex.list());
    }
  }
];
var DEFAULT_LIMIT2 = 10;
function formatHistoryLine(row) {
  const title = row.title?.trim() || colors.muted("(untitled)");
  const count = typeof row.message_count === "number" ? ` \xB7 ${row.message_count} msg` : "";
  const when = row.updated_at ? ` \xB7 ${formatRelativeTime(row.updated_at)}` : "";
  return `  ${colors.accent(row.external_id)}  ${title}${colors.muted(count + when)}`;
}
function formatRelativeTime(iso, now = /* @__PURE__ */ new Date()) {
  const t = new Date(iso);
  if (Number.isNaN(t.getTime())) return iso;
  const seconds = Math.floor((now.getTime() - t.getTime()) / 1e3);
  if (seconds < 60) return "just now";
  if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
  if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
  if (seconds < 86400 * 2) return "yesterday";
  if (seconds < 86400 * 7) return `${Math.floor(seconds / 86400)}d ago`;
  return t.toLocaleDateString(void 0, { month: "short", day: "numeric" });
}
var historyCommands = [
  {
    name: "history",
    description: "Browse past conversations",
    argHint: "count|all",
    requires: "workspace",
    run: async (ctx) => {
      const { arbi, args, wsContext } = ctx;
      const arg = args[0]?.trim().toLowerCase();
      const convs = await sdk.conversations.listConversations(
        arbi,
        wsContext.workspaceId
      );
      if (convs.length === 0) {
        return "No conversations yet in this workspace. Ask a question to start one.";
      }
      const slice = arg === "all" ? convs : convs.slice(0, parseLimit(arg) ?? DEFAULT_LIMIT2);
      const lines = [
        `Conversations (${slice.length}${slice.length < convs.length ? ` of ${convs.length}` : ""}):`,
        ""
      ];
      for (const row of slice) lines.push(formatHistoryLine(row));
      lines.push("");
      lines.push(colors.muted("Use /resume <conv-id> to switch to one of these."));
      return lines;
    }
  },
  {
    name: "resume",
    description: "Switch to a past conversation by ID",
    argHint: "conv-id",
    minArgs: 1,
    requires: "workspace",
    run: async (ctx) => {
      const { args, arbi, tui } = ctx;
      const convId = args[0].trim();
      const data = await sdk.conversations.getConversationThreads(arbi, convId);
      const threadList = data.threads ?? [];
      const primary = threadList[0];
      if (!primary?.leaf_message_ext_id) {
        return `Conversation ${convId} not found or has no messages.`;
      }
      tui.chatLog.clearMessages();
      for (const msg of primary.history ?? []) {
        if (msg.role === "user") tui.chatLog.addUser(msg.content);
        else if (msg.role === "assistant") tui.chatLog.addAssistant(msg.content);
      }
      tui.chatLog.addSystem(`--- resumed conversation ${convId} ---`, "info");
      tui.state.conversationMessageId = primary.leaf_message_ext_id;
      tui.store.updateChatSession({
        lastMessageExtId: primary.leaf_message_ext_id,
        conversationExtId: convId
      });
      tui.sourcesIndex.clear();
      tui.lastMetadata = null;
      tui.requestRender();
      return `Resumed conversation ${convId}.`;
    }
  }
];
function parseLimit(arg) {
  if (!arg) return void 0;
  const n = Number.parseInt(arg, 10);
  return Number.isFinite(n) && n > 0 ? n : void 0;
}
var workflowCommands = [
  {
    name: "automations",
    description: "List workspace automations",
    requires: "workspace",
    run: async (ctx) => {
      const { arbi } = ctx;
      const rows = await sdk.workflows.listWorkflows(arbi);
      return rows.length ? rows.map(
        (row) => `${row.external_id}  ${row.name}  ${row.active ? "Active" : "Paused"}  ${row.action?.type ?? "Chat"}  ${row.next_fire_at ?? "Run now / notification"}`
      ) : "No automations in this workspace.";
    }
  },
  {
    name: "automation",
    description: "Manage an automation: view | run | runs | pause | resume | delete <id>; create <JSON>; update <id> <JSON>",
    argHint: "subcommand id",
    minArgs: 2,
    requires: "workspace",
    run: async (ctx) => {
      const { arbi, args, wsContext, rawInput } = ctx;
      const [sub, id, ...rest] = args;
      switch (sub) {
        case "view":
          return JSON.stringify(await sdk.workflows.getWorkflow(arbi, id), null, 2);
        case "run": {
          const result = await sdk.workflows.runWorkflow(arbi, id);
          if (result.error) throw new Error(result.error);
          return `${result.started ? "Started" : "Submitted"} ${id}${result.execution_id ? ` \u2014 execution ${result.execution_id}` : ""}${result.conversation_ext_id ? ` \u2014 thread ${result.conversation_ext_id}` : ""}`;
        }
        case "runs": {
          const rows = await sdk.workflows.listWorkflowRuns(arbi, id);
          return rows.length ? rows.map((row) => JSON.stringify(row)) : "This automation has not run yet.";
        }
        case "pause":
        case "resume":
          await sdk.workflows.updateWorkflow(arbi, id, { active: sub === "resume" });
          return `${sub === "pause" ? "Paused" : "Resumed"} ${id}`;
        case "delete":
          await sdk.workflows.deleteWorkflow(arbi, id);
          return `Deleted ${id}; existing threads and results are preserved.`;
        case "create": {
          const body = JSON.parse(
            rawInput?.slice(rawInput.indexOf("{")) || [id, ...rest].join(" ")
          );
          body.workspace_ext_id ??= wsContext.workspaceId;
          const result = await sdk.workflows.createWorkflow(arbi, body);
          return `Created ${result.name} (${result.external_id})`;
        }
        case "update":
          await sdk.workflows.updateWorkflow(
            arbi,
            id,
            JSON.parse(
              rawInput?.slice(rawInput.indexOf("{")) || rest.join(" ")
            )
          );
          return `Updated ${id}`;
        default:
          return "Use /automation view|run|runs|pause|resume|delete <wfl-id>, create <JSON>, or update <wfl-id> <JSON>.";
      }
    }
  }
];
init_tui_helpers();
var skillCommands = [
  {
    name: "skills",
    description: "List user-invocable skills in the active workspace",
    requires: "workspace",
    run: async (ctx) => {
      const { arbi, tui, wsContext } = ctx;
      const list = await sdk.assistant.listSkills(arbi, { workspaceExtId: wsContext.workspaceId });
      tui.skillsCache = list;
      if (list.length === 0) {
        return [
          "No skills in this workspace yet.",
          colors.muted(
            "Skills are documents with wp_type=skill. Upload a SKILL.md with frontmatter (name, description, user-invocable: true) to add one."
          )
        ];
      }
      const lines = [`Skills (${list.length}):`, ""];
      for (const s of list) {
        const argHint = s.arg_hint ? colors.muted(` ${s.arg_hint}`) : "";
        const typeBadge = s.skill_type && s.skill_type !== "markdown" ? colors.muted(` [${s.skill_type}]`) : "";
        lines.push(`  ${colors.accent("/" + s.slug)}${argHint}${typeBadge}`);
        if (s.description) {
          lines.push(`      ${colors.muted(s.description)}`);
        }
      }
      lines.push("");
      lines.push(
        colors.muted("Use /skill view <slug> to read a skill, /skill source <slug> for its doc-id.")
      );
      return lines;
    }
  },
  {
    name: "skill",
    description: "Inspect or edit a skill (view | source | edit) <slug>",
    argHint: "subcommand slug",
    minArgs: 2,
    requires: "workspace",
    run: async (ctx) => {
      const { args, arbi, tui } = ctx;
      const [sub, slug, ...rest] = args;
      if (sub !== "view" && sub !== "source" && sub !== "edit") {
        return `Unknown /skill subcommand "${sub}". Use /skill view <slug>, /skill source <slug>, or /skill edit <slug>.`;
      }
      const list = tui.skillsCache?.length ? tui.skillsCache : await sdk.assistant.listSkills(arbi, {
        includeHidden: true,
        workspaceExtId: ctx.wsContext.workspaceId
      });
      const skill = list.find((s) => s.slug === slug || s.name === slug);
      if (!skill) {
        return `No skill matching "${slug}" in this workspace. Use /skills to list available skills.`;
      }
      if (!skill.doc_ext_id) {
        if (sub !== "view") {
          return `/${skill.slug} is a built-in skill (it ships with ARBI) and cannot be edited. Save a skill of your own with the same name to replace it.`;
        }
        const lines2 = [colors.textBold(`${skill.name} (${skill.slug})`)];
        if (skill.description) lines2.push(colors.muted(skill.description));
        if (skill.arg_hint) lines2.push(colors.muted(`args: ${skill.arg_hint}`));
        lines2.push(colors.muted(`type: ${skill.skill_type}  built-in (ships with ARBI)`));
        lines2.push("");
        lines2.push(skill.content ?? "");
        return lines2;
      }
      const docExtId = skill.doc_ext_id;
      if (sub === "source") {
        return [
          colors.textBold(`${skill.name} (${skill.slug})`),
          colors.muted(`doc-id: ${skill.doc_ext_id}`),
          colors.muted(`workspace: ${skill.workspace_ext_id}`),
          colors.muted(
            "Use `arbi upload --folder skills/<slug>` to replace the SKILL.md, or edit it via the web app."
          )
        ];
      }
      const { authHeaders } = ctx;
      if (sub === "edit") {
        return await editSkill(tui, authHeaders, { ...skill, doc_ext_id: docExtId });
      }
      const dlRes = await sdk.documents.downloadDocument(authHeaders, docExtId);
      const body = dlRes.ok ? await dlRes.text() : "";
      const lines = [colors.textBold(`${skill.name} (${skill.slug})`)];
      if (skill.description) lines.push(colors.muted(skill.description));
      if (skill.arg_hint) lines.push(colors.muted(`args: ${skill.arg_hint}`));
      lines.push(colors.muted(`type: ${skill.skill_type}  doc: ${skill.doc_ext_id}`));
      lines.push("");
      lines.push(body);
      return lines;
    }
  }
];
async function editSkill(tui, authHeaders, skill) {
  const dlRes = await sdk.documents.downloadDocument(authHeaders, skill.doc_ext_id);
  if (!dlRes.ok) {
    return `Failed to fetch skill body (HTTP ${dlRes.status}). Edit aborted.`;
  }
  const original = await dlRes.text();
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), `arbi-skill-${skill.slug}-`));
  const path$1 = path.join(dir, "SKILL.md");
  fs.writeFileSync(path$1, original, "utf8");
  const mtimeBefore = fs.statSync(path$1).mtimeMs;
  const editor = process.env.VISUAL || process.env.EDITOR || "vi";
  const result = await runInteractiveFlow(
    tui,
    colors.muted(`Opening ${skill.slug}/SKILL.md in ${editor}\u2026`),
    async () => {
      const child = child_process.spawnSync(editor, [path$1], { stdio: "inherit" });
      if (child.error) throw child.error;
      if (child.status !== 0 && child.status !== null) {
        throw new Error(`${editor} exited with status ${child.status}`);
      }
      return fs.readFileSync(path$1, "utf8");
    },
    `Skill edit (${skill.slug}) failed`
  );
  const mtimeAfter = (() => {
    try {
      return fs.statSync(path$1).mtimeMs;
    } catch {
      return mtimeBefore;
    }
  })();
  const updated = result ?? original;
  fs.rmSync(dir, { recursive: true, force: true });
  if (result === null) {
    return [];
  }
  if (mtimeAfter === mtimeBefore || updated === original) {
    return colors.muted(`No changes to ${skill.slug}/SKILL.md \u2014 skill unchanged.`);
  }
  await sdk.assistant.uploadSkill(authHeaders, skill.slug, updated, { shared: sdk.isSharedSkill(skill) });
  await tui.refreshSkillsCache();
  return [
    colors.textBold(`Updated ${skill.name} (${skill.slug})`),
    colors.muted("Skill cache refreshed \u2014 new description and args take effect immediately.")
  ];
}

// src/commands/index.ts
function registerAllCommands() {
  registerCommands(generalCommands);
  registerCommands(authCommands);
  registerCommands(workspaceCommands);
  registerCommands(documentCommands);
  registerCommands(conversationCommands);
  registerCommands(tagCommands);
  registerCommands(miscCommands);
  registerCommands(citationCommands);
  registerCommands(eventCommands);
  registerCommands(viewCommands);
  registerCommands(sourcesCommands);
  registerCommands(historyCommands);
  registerCommands(skillCommands);
  registerCommands(workflowCommands);
}
async function streamResponse(tui, response, docNames) {
  tui.chatLog.startAssistant();
  tui.state.activityStatus = "streaming";
  tui.requestRender();
  let accumulated = "";
  let elapsedTime = null;
  let firstToken = true;
  const updateAnswer = (content) => {
    if (!content) return;
    if (firstToken) {
      firstToken = false;
      tui.chatLog.clearActiveSteps();
    }
    accumulated = content;
    tui.chatLog.updateAssistant(accumulated);
    tui.requestRender();
  };
  let lastItemId = null;
  let lastStepLabel = "";
  const callbacks = {
    onStreamStart: (data) => {
      if (data.assistant_message_ext_id) {
        tui.state.conversationMessageId = data.assistant_message_ext_id;
      }
    },
    onToken: (content) => {
      updateAnswer(accumulated + content);
    },
    onTextDone: updateAnswer,
    onOutputItemDone: (ev) => {
      const item = ev.item;
      if (item.type !== "function_call" && item.type !== "reasoning") return;
      const label = sdk.formatItemLabel(item, docNames);
      if (label) {
        if (item.id === lastItemId && label === lastStepLabel) return;
        lastItemId = item.id ?? null;
        lastStepLabel = label;
        tui.chatLog.addAgentStep(label);
        tui.requestRender();
      }
    },
    onElapsedTime: (t) => {
      elapsedTime = t;
    },
    onError: (message) => {
      tui.chatLog.addSystem(`Stream error: ${message}`, "error");
      tui.requestRender();
    }
  };
  try {
    const result = await sdk.streamSSE(response, callbacks);
    if (result.text && result.text !== accumulated) updateAnswer(result.text);
    tui.chatLog.finalizeAssistant();
    tui.lastMetadata = result.metadata ?? null;
    tui.sourcesIndex.recordCitations(result.metadata);
    const summary = sdk.formatStreamSummary(result, elapsedTime);
    if (summary) {
      const refs = sdk.countCitations(result.metadata ?? null);
      const refSuffix = refs > 0 ? ` \xB7 ${refs} ref${refs === 1 ? "" : "s"}` : "";
      tui.chatLog.addSummary(`[${summary}${refSuffix}]`);
    }
    tui.state.activityStatus = "idle";
    tui.requestRender();
    return result;
  } catch (err) {
    tui.chatLog.finalizeAssistant();
    tui.state.activityStatus = "error";
    tui.chatLog.addSystem(`Streaming failed: ${sdk.getErrorMessage(err)}`, "error");
    tui.requestRender();
    throw err;
  }
}
init_tui_helpers();
function chatCrypto(tui) {
  const userExtId = tui.authContext?.loginResult.userExtId;
  if (!tui.encryptionKeys || !userExtId) return null;
  return { encryptionKeypair: tui.encryptionKeys, userExtId };
}
async function ensureChannelChat(tui, channel) {
  if (channel.ring && channel.chatExtId) return channel.ring;
  const crypto = chatCrypto(tui);
  if (!tui.authContext || !crypto) throw new Error("Not authenticated");
  const chat = await sdk.chats.openDirectChat(tui.authContext.arbi, channel.user.external_id);
  channel.chatExtId = chat.external_id;
  channel.ring = await sdk.chats.openChatKeyRing(chat, crypto);
  return channel.ring;
}
function deriveEncryptionKeypair(signingPrivateKeyBase64) {
  const signingPrivateKey = client.base64ToBytes(signingPrivateKeyBase64);
  return client.encryptionKeypairFromSigningKey(signingPrivateKey);
}
async function resolveRecipient(tui, query) {
  if (!tui.authContext) {
    showMessage(tui, "Not authenticated. Use /login first.", "error");
    return null;
  }
  const queryLower = query.toLowerCase();
  try {
    const contactList = await sdk.contacts.listContacts(tui.authContext.arbi);
    const registered = contactList.filter(
      (c) => c.status === "registered" && c.user?.encryption_public_key
    );
    const matches = registered.filter((c) => {
      const emailPrefix = (c.email ?? "").split("@")[0]?.toLowerCase();
      const givenName = c.user?.given_name?.toLowerCase();
      return emailPrefix === queryLower || givenName === queryLower;
    });
    if (matches.length === 0) {
      showMessage(
        tui,
        `No registered contact matching "${query}". Use /contacts to see available contacts.`,
        "warning"
      );
      return null;
    }
    if (matches.length > 1) {
      const names = matches.map((c) => `  ${c.email} (${c.user?.given_name ?? ""} ${c.user?.family_name ?? ""})`).join("\n");
      showMessage(tui, `Ambiguous match for "${query}". Be more specific:
${names}`, "warning");
      return null;
    }
    const contact = matches[0];
    const user = contact.user;
    return {
      external_id: user.external_id,
      email: user.email,
      encryption_public_key: user.encryption_public_key,
      given_name: user.given_name,
      family_name: user.family_name
    };
  } catch (err) {
    showMessage(tui, `Failed to resolve contact: ${sdk.getErrorMessage(err)}`, "error");
    return null;
  }
}
async function handleChannelSwitch(tui, input2) {
  const query = input2.slice(1).trim();
  if (!query) {
    showMessage(tui, "Usage: @username to switch to DM, @arbi to switch back.", "warning");
    return;
  }
  if (query.toLowerCase() === "arbi") {
    switchToArbi(tui);
    return;
  }
  const user = await resolveRecipient(tui, query);
  if (!user) return;
  await switchToDmChannel(tui, user);
}
async function switchToDmChannel(tui, user) {
  if (!tui.authContext || !tui.encryptionKeys) {
    showMessage(tui, "Not authenticated or encryption not initialized.", "error");
    return;
  }
  tui.currentDmChannel = { user };
  tui.updateHeader();
  tui.chatLog.clearMessages();
  showMessage(tui, `Switched to DM with ${colors.accent(user.email)}. Type @arbi to switch back.`);
  await loadDmHistory(tui, user);
}
function switchToArbi(tui) {
  if (!tui.currentDmChannel) {
    showMessage(tui, "Already in AI chat mode.", "info");
    return;
  }
  tui.currentDmChannel = null;
  tui.updateHeader();
  tui.chatLog.clearMessages();
  showMessage(tui, "Switched back to AI chat.");
}
async function loadDmHistory(tui, user) {
  const channel = tui.currentDmChannel;
  if (!tui.authContext || !channel) return;
  const myExtId = tui.authContext.loginResult.userExtId;
  try {
    const ring = await ensureChannelChat(tui, channel);
    const raw = await sdk.chats.listChatMessages(tui.authContext.arbi, ring.chatExtId, { limit: 100 });
    const conversation = (await sdk.chats.decryptChatMessages(ring, raw)).reverse();
    if (conversation.length > 0) {
      sdk.chats.markChatRead(tui.authContext.arbi, ring.chatExtId).catch(() => {
      });
    }
    for (const msg of conversation) {
      if (!msg.content) continue;
      displayDmMessage(tui, msg.content, msg.sender_ext_id === myExtId, user.email);
    }
    tui.requestRender();
  } catch (err) {
    showMessage(tui, `Failed to load DM history: ${sdk.getErrorMessage(err)}`, "error");
  }
}
async function sendEncryptedDm(tui, plaintext) {
  const channel = tui.currentDmChannel;
  if (!tui.authContext || !tui.encryptionKeys || !channel) {
    showMessage(tui, "Cannot send DM \u2014 not in a DM channel.", "error");
    return;
  }
  try {
    const ring = await ensureChannelChat(tui, channel);
    await sdk.chats.sendChatMessage(tui.authContext.arbi, ring, plaintext);
    displayDmMessage(tui, plaintext, true, channel.user.email);
    tui.requestRender();
  } catch (err) {
    showMessage(tui, `Failed to send DM: ${sdk.getErrorMessage(err)}`, "error");
  }
}
async function handleIncomingDm(tui, event) {
  const channel = tui.currentDmChannel;
  if (!channel || !tui.encryptionKeys || !tui.authContext) return false;
  const msg = event.message;
  if (msg.sender_ext_id !== channel.user.external_id) return false;
  let ring;
  try {
    ring = await ensureChannelChat(tui, channel);
  } catch {
    return false;
  }
  if (event.chat_ext_id !== ring.chatExtId) return false;
  const decrypted = await sdk.chats.decryptChatMessage(ring, msg);
  const text = decrypted.decryptFailed ? "[decryption failed]" : decrypted.content;
  if (text) {
    displayDmMessage(tui, text, false, channel.user.email);
    tui.requestRender();
    sdk.chats.markChatRead(tui.authContext.arbi, ring.chatExtId, msg.external_id).catch(() => {
    });
  }
  return true;
}
function displayDmMessage(tui, text, isSent, senderEmail) {
  if (isSent) {
    tui.chatLog.addUser(text);
  } else {
    tui.chatLog.addDm(senderEmail, text);
  }
}

// src/ws-handler.ts
var DURATION_SHORT = 3e3;
var DURATION_DEFAULT = 5e3;
var DURATION_LONG = 8e3;
var DURATION_BY_LEVEL = {
  info: DURATION_SHORT,
  success: DURATION_DEFAULT,
  error: DURATION_LONG,
  warning: DURATION_DEFAULT
};
function isChatMessage(msg) {
  return msg.type === "chat.message";
}
function shouldToast(level, msgType) {
  if (level === "error" || level === "warning" || level === "success") return true;
  if (msgType === "response_complete" || msgType === "batch_complete") return true;
  return false;
}
async function handleMessage(msg, toasts, eventLog, tui) {
  if (isChatMessage(msg) && tui) {
    const handled = await handleIncomingDm(tui, msg);
    if (handled) return;
  }
  const { text, level } = sdk.formatWsMessage(msg);
  const msgType = msg.type;
  eventLog.add({ text, level, msgType });
  if (shouldToast(level, msgType)) {
    const duration = DURATION_BY_LEVEL[level] ?? DURATION_DEFAULT;
    toasts.show(text, level, duration);
  }
}
async function connectTuiWebSocket(options) {
  const { baseUrl, accessToken, toasts, eventLog, tui } = options;
  const recordTransport = (text, level) => {
    eventLog.add({ text, level });
    const duration = DURATION_BY_LEVEL[level] ?? DURATION_DEFAULT;
    toasts.show(text, level, duration);
  };
  try {
    const connection = await sdk.connectWithReconnect({
      baseUrl,
      accessToken,
      onMessage: (msg) => handleMessage(msg, toasts, eventLog, tui ?? null),
      onClose: () => recordTransport("WebSocket disconnected", "warning"),
      onReconnecting: (attempt, maxRetries) => recordTransport(`Reconnecting... (${attempt}/${maxRetries})`, "warning"),
      onReconnected: () => recordTransport("WebSocket reconnected", "success"),
      onReconnectFailed: () => recordTransport("WebSocket reconnection failed", "error")
    });
    return connection;
  } catch {
    recordTransport("WebSocket connection failed", "warning");
    return null;
  }
}

// src/tui.ts
init_tui_helpers();
var ArbiTui = class {
  tui;
  header;
  /** The chat message log — public so command handlers can add messages. */
  chatLog;
  /** Toast notifications from WebSocket events. */
  toastContainer;
  /** The input editor. */
  editor;
  /** Application state — public so handlers can read/write it. */
  state;
  /** Auth context from @arbidocs/sdk — set during init. */
  authContext = null;
  /** Workspace context with tokens/headers — set when workspace is selected. */
  workspaceContext = null;
  /** Config store for persistence. */
  store;
  /** Active WebSocket connection (null when not connected). */
  wsConnection = null;
  /** X25519 encryption keypair derived from signing key (null until login). */
  encryptionKeyPair = null;
  /** Current DM channel (null = AI chat mode). */
  dmChannel = null;
  /** Last response metadata — used by /cite for citation browsing. */
  lastMetadata = null;
  /** Persistent log of WebSocket events for this session. Toasts are
   *  ephemeral; this is the "what happened?" scrollback that
   *  ``/events`` reads from. Public so ws-handler can append and the
   *  events command can read. */
  eventLog = new WsEventLog();
  /** Running tally of documents cited in the current conversation.
   *  Cleared on ``/new``; appended to after every assistant response
   *  that carries citation metadata. Drives ``/sources`` and (later)
   *  the persistent right-side sources panel. */
  sourcesIndex = new SourcesIndex();
  /** Cached list of user-invocable skills in the active workspace.
   *  Refreshed on workspace switch (``setWorkspaceContext``) and
   *  invalidated to ``[]`` when leaving a workspace. Drives the
   *  autocomplete provider and the ``/skills`` / ``/skill view``
   *  commands. ``null`` means "haven't fetched yet"; ``[]`` means
   *  "fetched, no skills exist". */
  skillsCache = [];
  /** Cached built-in slash commands (``GET /v1/assistant/commands``) available
   *  to the caller — the shipped ``/image`` / ``/compact`` / ``/goal`` … set,
   *  gated per-user. Refreshed alongside ``skillsCache`` on workspace switch.
   *  Drives autocomplete and the "known server command" passthrough so the TUI
   *  never hardcodes (and drifts from) the backend's command vocabulary. */
  commandsCache = [];
  /** Active citation overlay handle (null when not showing). */
  citationOverlay = null;
  /** Input listener ID for overlay dismiss (null when no overlay). */
  overlayInputListener = null;
  constructor(store) {
    this.store = store;
    this.state = {
      workspaceId: null,
      workspaceName: null,
      conversationMessageId: null,
      activityStatus: "idle",
      isAuthenticated: false
    };
    registerAllCommands();
    this.tui = new piTui.TUI(new piTui.ProcessTerminal());
    this.header = new piTui.Text(formatHeader(null, "starting..."), 1, 0);
    this.chatLog = new ChatLog();
    this.toastContainer = new ToastContainer();
    this.toastContainer.setRenderCallback(() => this.tui.requestRender());
    this.editor = this.createEditor();
    this.tui.addChild(this.header);
    this.tui.addChild(new piTui.Spacer(1));
    this.tui.addChild(this.toastContainer);
    this.tui.addChild(this.chatLog);
    this.tui.addChild(this.editor);
    this.tui.setFocus(this.editor);
  }
  // ── Lifecycle ──────────────────────────────────────────────────────────
  /** Start the TUI event loop (clears screen for fullscreen layout). */
  start() {
    this.tui.start();
    this.updateHeader();
    this.tui.requestRender(true);
  }
  /** Gracefully shut down the TUI and exit. */
  shutdown() {
    this.wsConnection?.close();
    this.wsConnection = null;
    this.toastContainer.clear();
    this.tui.stop();
    process.exit(0);
  }
  /**
   * Temporarily stop the TUI (releases terminal raw mode).
   * Used before interactive prompts that need stdin (login, register).
   */
  stopTui() {
    this.tui.stop();
  }
  /**
   * Restart the TUI after a stopTui() call.
   * Rebuilds the component tree with a fresh TUI instance since
   * pi-tui doesn't support stop/start cycling on the same instance.
   */
  restartTui() {
    this.tui = new piTui.TUI(new piTui.ProcessTerminal());
    this.tui.addChild(this.header);
    this.tui.addChild(new piTui.Spacer(1));
    this.tui.addChild(this.toastContainer);
    this.tui.addChild(this.chatLog);
    this.editor = this.createEditor();
    this.tui.addChild(this.editor);
    this.tui.setFocus(this.editor);
    this.tui.start();
    this.updateHeader();
    this.tui.requestRender(true);
  }
  /** Create and wire a new editor instance. */
  createEditor() {
    const editor = new ArbiEditor(this.tui);
    editor.setAutocompleteProvider(
      new piTui.CombinedAutocompleteProvider(toSlashCommands(), process.cwd())
    );
    editor.onSubmit = (text) => this.handleSubmit(text);
    editor.onEscape = () => this.handleAbort();
    editor.onCtrlC = () => this.shutdown();
    editor.onCtrlD = () => this.shutdown();
    editor.onCtrlW = () => this.handleSubmit("/workspaces");
    editor.onCtrlN = () => this.handleSubmit("/new");
    editor.onBufferChange = (text) => this.handleBufferChange(text);
    return editor;
  }
  /** The slug we last surfaced as a pre-flight hint. ``null`` once
   *  the user has navigated away from the skill prefix (e.g. typed a
   *  non-slash char first, or deleted past the slash). Tracking this
   *  here — not in the editor — keeps the editor pure: it just emits
   *  text, the TUI decides what's interesting. */
  lastHintedSlug = null;
  /**
   * Pre-flight skill hint dispatcher.
   *
   * Watches the editor buffer; when it starts with ``/<token>`` where
   * ``<token>`` is a slug in ``skillsCache`` *and* not also a static
   * command (those have their own ``--help`` style help), we drop a
   * one-line description into the chat log so the user sees what
   * they're about to invoke before pressing Enter. Equivalent of
   * claude-code's slash-command palette preview, minus the modal.
   *
   * Dedup rule: only emit when the matched slug *changes*. Typing
   * "/foo" → "/foob" → "/foo" emits once for ``foo``, never for the
   * intermediate ``foob`` (no match), and not again when the user
   * lands back on ``foo``. The state resets whenever the buffer no
   * longer matches anything, so a fresh ``/foo`` after a clear emits
   * the hint again.
   */
  handleBufferChange(text) {
    const parsed = sdk.parseSlashCommand(text);
    if (!parsed) {
      this.lastHintedSlug = null;
      return;
    }
    const slug = parsed.slug;
    if (this.skillsCache.length === 0) return;
    const skill = this.skillsCache.find((s) => s.slug === slug);
    if (!skill) {
      this.lastHintedSlug = null;
      return;
    }
    if (this.lastHintedSlug === slug) return;
    this.lastHintedSlug = slug;
    const argHint = skill.arg_hint ? ` ${skill.arg_hint}` : "";
    showMessage(this, `Skill: /${skill.slug}${argHint} \u2014 ${skill.description}`, "info");
  }
  /** Request a render update. */
  requestRender() {
    this.tui.requestRender();
  }
  // ── Auth / Workspace ───────────────────────────────────────────────────
  /** Set authentication context after login. */
  setAuthContext(ctx) {
    this.authContext = ctx;
    this.state.isAuthenticated = true;
    this.encryptionKeyPair = deriveEncryptionKeypair(
      this.store.requireCredentials().signingPrivateKeyBase64
    );
    this.updateHeader();
  }
  /** Set workspace context after workspace selection. */
  setWorkspaceContext(ctx) {
    this.workspaceContext = ctx;
    this.state.workspaceId = ctx.workspaceId;
    this.updateHeader();
    this.connectWebSocket();
    void this.refreshSkillsCache();
    void this.refreshCommandsCache();
  }
  /** Refresh the cached skills list for the current workspace. Called
   *  on workspace switch and from ``/skills`` itself so a freshly
   *  uploaded skill becomes invocable without a TUI restart. */
  async refreshSkillsCache() {
    if (!this.workspaceContext) {
      this.skillsCache = [];
      this.rebuildAutocomplete();
      return;
    }
    try {
      this.skillsCache = await sdk.assistant.listSkills(this.workspaceContext.arbi, {
        workspaceExtId: this.workspaceContext.workspaceId
      });
    } catch {
      this.skillsCache = [];
    }
    this.rebuildAutocomplete();
  }
  /** Refresh the cached built-in commands for the current workspace. The
   *  endpoint resolves via WorkspaceContext (like ``/skills``) and applies
   *  per-user gates, so it's refreshed on workspace switch. Failures are
   *  swallowed — older backends without ``GET /v1/assistant/commands`` fall
   *  back to the static ``KNOWN_SERVER_COMMANDS`` passthrough set. */
  async refreshCommandsCache() {
    if (!this.workspaceContext) {
      this.commandsCache = [];
      this.rebuildAutocomplete();
      return;
    }
    try {
      this.commandsCache = await sdk.assistant.listCommands(this.workspaceContext.arbi);
    } catch {
      this.commandsCache = [];
    }
    this.rebuildAutocomplete();
  }
  /** Re-feed the editor's autocomplete provider with the merged
   *  static-commands + skills list. Called on workspace switch and
   *  after every ``refreshSkillsCache`` so a newly uploaded skill
   *  appears in tab-complete without a TUI restart. */
  rebuildAutocomplete() {
    if (!this.editor) return;
    const merged = toSlashCommandsWithSkills(
      this.skillsCache.map((s) => ({ slug: s.slug, description: s.description })),
      this.commandsCache.map((c) => ({ name: c.name, description: c.description }))
    );
    this.editor.setAutocompleteProvider(new piTui.CombinedAutocompleteProvider(merged, process.cwd()));
  }
  /** Refresh workspace context (after switching workspaces). */
  async refreshWorkspaceContext() {
    if (!this.state.workspaceId || !this.authContext) return;
    const { resolveWorkspace: resolveWorkspace3 } = await import('@arbidocs/sdk');
    const ctx = await resolveWorkspace3(this.store, this.state.workspaceId);
    this.workspaceContext = ctx;
    this.updateHeader();
  }
  /** Public accessor for workspace context (used by command handlers). */
  get wsContext() {
    return this.workspaceContext;
  }
  /** Whether the WebSocket is currently connected. */
  get wsConnected() {
    return this.wsConnection !== null;
  }
  /** Connect (or reconnect) WebSocket for real-time notifications. */
  connectWebSocket() {
    if (!this.workspaceContext) return;
    this.wsConnection?.close();
    this.wsConnection = null;
    const { config, accessToken } = this.workspaceContext;
    connectTuiWebSocket({
      baseUrl: config.baseUrl,
      accessToken,
      toasts: this.toastContainer,
      eventLog: this.eventLog,
      tui: this
    }).then((conn) => {
      this.wsConnection = conn;
    });
  }
  // ── Input handling ─────────────────────────────────────────────────────
  async handleSubmit(text) {
    const trimmed = text.trim();
    if (!trimmed) return;
    if (trimmed.startsWith("@")) {
      await handleChannelSwitch(this, trimmed);
      return;
    }
    if (trimmed.startsWith("/")) {
      const handled = await dispatchCommand(this, trimmed);
      if (handled) return;
    }
    if (this.dmChannel) {
      await sendEncryptedDm(this, trimmed);
    } else {
      await this.sendMessage(trimmed);
    }
  }
  handleAbort() {
    if (this.state.activityStatus === "streaming") {
      this.chatLog.addSystem("Abort requested (stream will complete current chunk).", "warning");
      this.requestRender();
    }
  }
  // ── Messaging ──────────────────────────────────────────────────────────
  async sendMessage(question) {
    if (!this.workspaceContext) {
      this.chatLog.addSystem(
        "No workspace selected. Use /workspace <id> or /workspaces to choose one.",
        "warning"
      );
      this.requestRender();
      return;
    }
    this.chatLog.addUser(question);
    this.state.activityStatus = "sending";
    this.updateHeader();
    this.requestRender();
    try {
      const docs = await sdk.documents.listDocuments(this.workspaceContext.arbi);
      const docIds = docs.map((d) => d.external_id);
      const docNames = sdk.buildDocNameMap(docs);
      const session = this.store.getChatSession();
      const previousResponseId = this.state.conversationMessageId ?? session.lastMessageExtId;
      const response = await sdk.assistant.queryAssistant({
        baseUrl: this.workspaceContext.config.baseUrl,
        accessToken: this.workspaceContext.accessToken,
        workspaceId: this.workspaceContext.workspaceId,
        question,
        docIds,
        previousResponseId
      });
      const result = await streamResponse(this, response, docNames);
      if (this.lastMetadata) {
        const n = sdk.countCitations(this.lastMetadata);
        if (n > 0) {
          const plural = n === 1 ? "" : "s";
          this.chatLog.addSystem(
            `${n} citation${plural} \u2014 type /cite to browse, /cite N to view one`,
            "info"
          );
        }
      }
      if (result.assistantMessageExtId) {
        this.state.conversationMessageId = result.assistantMessageExtId;
        const sessionUpdate = {
          lastMessageExtId: result.assistantMessageExtId
        };
        const conversationExtId = result.userMessage?.conversation_ext_id ?? result.metadata?.conversation_ext_id;
        if (conversationExtId) {
          sessionUpdate.conversationExtId = conversationExtId;
        }
        this.store.updateChatSession(sessionUpdate);
      }
    } catch (err) {
      this.state.activityStatus = "error";
      this.chatLog.addSystem(`Error: ${sdk.getErrorMessage(err)}`, "error");
    }
    this.state.activityStatus = "idle";
    this.updateHeader();
    this.requestRender();
  }
  // ── UI Updates ─────────────────────────────────────────────────────────
  /** Update the header bar — public so dm-handler can call it. */
  updateHeader() {
    const statusText = this.state.activityStatus === "idle" ? colors.success("ready") : this.state.activityStatus === "streaming" ? colors.warning("streaming...") : this.state.activityStatus === "sending" ? colors.warning("sending...") : colors.error("error");
    if (this.dmChannel) {
      this.header.setText(formatHeader(`DM: ${this.dmChannel.user.email}`, statusText));
    } else {
      this.header.setText(formatHeader(this.state.workspaceName, statusText));
    }
  }
  // ── Citation overlay ───────────────────────────────────────────────────
  /** Show a component as a centered overlay (used by /cite). */
  showCitationOverlay(component) {
    this.dismissCitationOverlay();
    this.citationOverlay = this.tui.showOverlay(component, {
      anchor: "center",
      width: "80%",
      maxHeight: "80%",
      margin: 2
    });
    this.overlayInputListener = this.tui.addInputListener((data) => {
      if (piTui.matchesKey(data, piTui.Key.escape)) {
        this.dismissCitationOverlay();
        return { consume: true };
      }
      return void 0;
    });
    this.tui.requestRender();
  }
  /** Dismiss the active citation overlay. */
  dismissCitationOverlay() {
    if (this.citationOverlay) {
      this.citationOverlay.hide();
      this.citationOverlay = null;
    }
    if (this.overlayInputListener) {
      this.overlayInputListener();
      this.overlayInputListener = null;
    }
    this.tui.requestRender();
  }
  // ── DM channel accessors (used by dm-handler) ────────────────────────────
  /** Current DM channel (null = AI chat mode). */
  get currentDmChannel() {
    return this.dmChannel;
  }
  set currentDmChannel(ch) {
    this.dmChannel = ch;
  }
  /** Encryption keypair for E2E DM encryption. */
  get encryptionKeys() {
    return this.encryptionKeyPair;
  }
};

// src/index.ts
console.debug = () => {
};
var _origInfo = console.info;
console.info = (...args) => {
  if (typeof args[0] === "string" && args[0].startsWith("[API]")) return;
  _origInfo(...args);
};
var program = new commander.Command();
program.name("arbi-tui").description("Interactive terminal UI for ARBI \u2014 chat with the RAG assistant").version("0.1.0").option("-w, --workspace <id>", "Workspace ID to use").action(async (opts) => {
  const store = new sdk.FileConfigStore();
  try {
    store.requireConfig();
  } catch {
    console.error("Not configured. Run `arbi config set-url <url>` first.");
    process.exit(1);
  }
  const { authContext, selectedWorkspaceId, selectedWorkspaceName } = await ensureAuthenticated(store);
  const workspaceId = opts.workspace || selectedWorkspaceId;
  const tui = new ArbiTui(store);
  tui.setAuthContext(authContext);
  if (workspaceId) {
    try {
      const wsCtx = await sdk.resolveWorkspace(store, workspaceId);
      tui.setWorkspaceContext(wsCtx);
      if (selectedWorkspaceName && workspaceId === selectedWorkspaceId) {
        tui.state.workspaceName = selectedWorkspaceName;
      } else {
        const wsList = await sdk.workspaces.listWorkspaces(authContext.arbi);
        const ws = wsList.find((w) => w.external_id === workspaceId);
        if (ws) {
          tui.state.workspaceName = ws.name;
        }
      }
    } catch (err) {
      tui.chatLog.addSystem(`Failed to load workspace: ${sdk.getErrorMessage(err)}`, "warning");
      tui.chatLog.addSystem("Use /workspaces to list and /workspace <id> to select.");
    }
  } else {
    tui.chatLog.addSystem(
      "No workspace selected. Use /workspaces to list and /workspace <id> to select."
    );
  }
  const session = store.getChatSession();
  if (session.lastMessageExtId) {
    tui.state.conversationMessageId = session.lastMessageExtId;
  }
  if (session.conversationExtId && session.lastMessageExtId && tui.authContext) {
    try {
      const threads = await sdk.conversations.getConversationThreads(
        tui.authContext.arbi,
        session.conversationExtId
      );
      const threadList = threads.threads ?? [];
      const thread = threadList.find(
        (t) => t.leaf_message_ext_id === session.lastMessageExtId
      );
      if (thread?.history && thread.history.length > 0) {
        for (const msg of thread.history) {
          if (msg.role === "user") {
            tui.chatLog.addUser(msg.content);
          } else if (msg.role === "assistant") {
            tui.chatLog.addAssistant(msg.content);
          }
        }
        tui.chatLog.addSystem("--- restored from previous session ---");
      }
    } catch {
    }
  }
  tui.chatLog.addSystem(
    "Type a question to chat with the ARBI assistant. Use /help for commands."
  );
  tui.start();
});
program.parse();
//# sourceMappingURL=index.js.map
//# sourceMappingURL=index.js.map