@agentikos/omega-os 0.19.35 → 0.19.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -75,12 +75,45 @@ record_state_version() {
75
75
  }
76
76
 
77
77
  # run_step <name> <function>
78
+ #
79
+ # In NORMAL mode: prints `:: step <name>` headers + step output flows
80
+ # to the terminal in real time (the v0.19.34 behaviour).
81
+ #
82
+ # In FULL mode (INSTALL_FULL=1, set by `--full`): redirects each step's
83
+ # stdout+stderr to $LOG_FILE and updates a single progress bar line
84
+ # instead. No `:: step xx` chatter. Operator sees:
85
+ #
86
+ # Installing… ████████████░░░░░░░░░░ 14/21 66% 45-claude-plugins
87
+ #
88
+ # When the run finishes, the progress bar clears and the post-install
89
+ # card prints normally.
78
90
  run_step() {
79
91
  local name="$1" fn="$2"
80
92
  if step_done "$name" && [ "${FORCE:-0}" != "1" ]; then
93
+ if [ "${INSTALL_FULL:-0}" = "1" ]; then
94
+ _full_progress "$name" "skip"
95
+ return 0
96
+ fi
81
97
  log "${C_DIM}-- skip $name (already done)${C_RST}"
82
98
  return 0
83
99
  fi
100
+
101
+ if [ "${INSTALL_FULL:-0}" = "1" ]; then
102
+ # Redirect step output to the log; only the progress bar stays on screen.
103
+ _full_progress "$name" "run"
104
+ if "$fn" >>"$LOG_FILE" 2>&1; then
105
+ mark_done "$name"
106
+ _full_progress "$name" "ok"
107
+ else
108
+ # Clear the bar then surface the error inline.
109
+ printf "\r\033[K"
110
+ err "step $name failed — see $LOG_FILE"
111
+ die "step $name failed — fix the cause and re-run install.sh"
112
+ fi
113
+ return 0
114
+ fi
115
+
116
+ # Normal verbose mode (default).
84
117
  info "step $name"
85
118
  if "$fn"; then
86
119
  mark_done "$name"
@@ -90,6 +123,38 @@ run_step() {
90
123
  fi
91
124
  }
92
125
 
126
+ # _full_progress <step_name> <phase=run|ok|skip>
127
+ # Renders one line: "Installing… <bar> N/M % <current step>"
128
+ # Uses \r to overwrite — no scrollback noise.
129
+ _full_progress() {
130
+ local step="$1" phase="$2"
131
+ STEP_COUNT=$((${STEP_COUNT:-0} + 1))
132
+ local total="${STEP_TOTAL:-21}"
133
+ local current="$STEP_COUNT"
134
+ # Decrement displayed count for "skip" so the bar represents real progress
135
+ if [ "$phase" = "skip" ]; then
136
+ : # leave count as-is; skips still count as progress
137
+ fi
138
+ local pct=$((current * 100 / total))
139
+ local bar_width=30
140
+ local filled=$((current * bar_width / total))
141
+ local empty=$((bar_width - filled))
142
+ local bar=""
143
+ if [ "$filled" -gt 0 ]; then
144
+ bar="$(printf '█%.0s' $(seq 1 "$filled"))"
145
+ fi
146
+ if [ "$empty" -gt 0 ]; then
147
+ bar="${bar}$(printf '░%.0s' $(seq 1 "$empty"))"
148
+ fi
149
+ # \033[K clears to end of line so the previous step name doesn't bleed.
150
+ printf "\r ${C_ORANGE}Installing…${C_RST} ${bar} ${C_BOLD}%2d/%d${C_RST} %3d%% ${C_DIM}%-32s${C_RST}\033[K" \
151
+ "$current" "$total" "$pct" "$step"
152
+ # Final newline only when we hit the last step.
153
+ if [ "$current" -ge "$total" ]; then
154
+ printf "\n"
155
+ fi
156
+ }
157
+
93
158
  # --- user resolution ---------------------------------------------------------
94
159
  #
95
160
  # The installer is modular about WHICH user it runs as. Three modes, decided
package/install.sh CHANGED
@@ -25,6 +25,7 @@ source "$REPO_DIR/bootstrap/lib/steps.sh"
25
25
  PROFILE="vps"; MANIFEST=""; NONINTERACTIVE=0; FORCE=0
26
26
  USER_REQUESTED=""; CREATE_USER=0; CREATE_USER_NAME=""
27
27
  PROFILE_EXPLICIT=0; NO_PATH_SETUP=0; DRY_RUN=0
28
+ INSTALL_FULL=0; FULL_WANT_TELEGRAM=""
28
29
  [ "$(uname -s)" = "Darwin" ] && PROFILE="workstation"
29
30
 
30
31
  while [ $# -gt 0 ]; do
@@ -41,6 +42,14 @@ while [ $# -gt 0 ]; do
41
42
  --force) FORCE=1; shift ;;
42
43
  --no-path-setup) NO_PATH_SETUP=1; shift ;;
43
44
  --dry-run) DRY_RUN=1; shift ;;
45
+ # `--full` — silent full-recommended install. One prompt
46
+ # (Telegram yes/no), then progress bar only. Selects every
47
+ # recommended LLM CLI / MCP / Claude plugin / system CLI.
48
+ --full) INSTALL_FULL=1; NONINTERACTIVE=1; shift ;;
49
+ --full-no-telegram) INSTALL_FULL=1; NONINTERACTIVE=1
50
+ FULL_WANT_TELEGRAM=no; shift ;;
51
+ --full-with-telegram) INSTALL_FULL=1; NONINTERACTIVE=1
52
+ FULL_WANT_TELEGRAM=yes; shift ;;
44
53
  --user) USER_REQUESTED="${2:?}"; shift 2 ;;
45
54
  --create-user)
46
55
  CREATE_USER=1
@@ -123,15 +132,41 @@ fi
123
132
 
124
133
  # ───── UX header ─────
125
134
  install_banner
126
- preflight_card
135
+ # In --full mode skip the heavy preflight card; we'll show the progress
136
+ # bar instead. Banner still prints so the user sees "Omega OS vX.Y.Z".
137
+ if [ "${INSTALL_FULL:-0}" != "1" ]; then
138
+ preflight_card
139
+ fi
127
140
 
128
141
  # Interactive profile picker if the operator did not pass --profile.
129
- pick_profile_interactive
142
+ # (Skipped in --full mode — uses the default profile for the OS.)
143
+ if [ "${INSTALL_FULL:-0}" != "1" ]; then
144
+ pick_profile_interactive
145
+ fi
130
146
 
131
147
  # Final pre-run summary (one tight line so the operator knows what they
132
- # confirmed).
133
- log "${C_DIM}::${C_RST} starting install profile=${C_BOLD}$PROFILE${C_RST} os=$OMEGA_OS user=$OMEGA_USER home=$OMEGA_HOME"
134
- echo
148
+ # confirmed). Skipped in --full mode.
149
+ if [ "${INSTALL_FULL:-0}" != "1" ]; then
150
+ log "${C_DIM}::${C_RST} starting install — profile=${C_BOLD}$PROFILE${C_RST} os=$OMEGA_OS user=$OMEGA_USER home=$OMEGA_HOME"
151
+ echo
152
+ fi
153
+
154
+ # In --full mode we ask EXACTLY one question before anything else:
155
+ # "install Telegram?" If they pass --full-with-telegram or
156
+ # --full-no-telegram on the CLI, we skip even this prompt.
157
+ if [ "${INSTALL_FULL:-0}" = "1" ] && [ -z "${FULL_WANT_TELEGRAM:-}" ]; then
158
+ if [ -t 0 ] && [ "${NONINTERACTIVE:-0}" != "1" ]; then
159
+ if prompt_yes_no " Install the Telegram bot?" "no"; then
160
+ FULL_WANT_TELEGRAM=yes
161
+ else
162
+ FULL_WANT_TELEGRAM=no
163
+ fi
164
+ else
165
+ # Truly headless (piped stdin) — default to NO Telegram.
166
+ FULL_WANT_TELEGRAM=no
167
+ fi
168
+ echo
169
+ fi
135
170
 
136
171
  # Define the sequence ONCE so we can count it for the progress header.
137
172
  STEPS=(
@@ -157,7 +192,11 @@ STEPS=(
157
192
  "45-claude-plugins:step_claude_plugins"
158
193
  )
159
194
  if [ "$PROFILE" != "minimal" ]; then
160
- STEPS+=("50-telegram:step_telegram")
195
+ # In --full mode the Telegram step is gated by the single yes/no asked
196
+ # at the very top. Skip the step entirely when the user said no.
197
+ if [ "${INSTALL_FULL:-0}" != "1" ] || [ "${FULL_WANT_TELEGRAM:-}" != "no" ]; then
198
+ STEPS+=("50-telegram:step_telegram")
199
+ fi
161
200
  fi
162
201
  STEPS+=(
163
202
  "55-autonomous:step_autonomous"
@@ -170,25 +209,27 @@ STEPS+=(
170
209
  export STEP_TOTAL="${#STEPS[@]}"
171
210
  export STEP_COUNT=0
172
211
 
173
- # ───── Plan mode ─────
174
- # Show every step BEFORE running anything same shape as Claude Code's
175
- # plan mode: the operator sees the full sequence + can abort cleanly
176
- # with --dry-run, or just hit Enter to proceed.
177
- echo
178
- log "${C_DIM}─── Install plan (${STEP_TOTAL} steps, ~2-3 min total) ───${C_RST}"
179
- _idx=0
180
- for entry in "${STEPS[@]}"; do
181
- _idx=$((_idx + 1))
182
- _name="${entry%%:*}"
183
- log " ${C_DIM}${_idx}/${STEP_TOTAL}${C_RST} ${_name}"
184
- done
185
- log "${C_DIM}─────────────────────────────────────────${C_RST}"
186
- echo
212
+ # ───── Plan mode (verbose only) ─────
213
+ # In NORMAL mode: show every step BEFORE running + ask for confirmation.
214
+ # In --full mode: skip the plan operator already opted in via the flag.
215
+ if [ "${INSTALL_FULL:-0}" != "1" ]; then
216
+ echo
217
+ log "${C_DIM}─── Install plan (${STEP_TOTAL} steps, ~2-3 min total) ───${C_RST}"
218
+ _idx=0
219
+ for entry in "${STEPS[@]}"; do
220
+ _idx=$((_idx + 1))
221
+ _name="${entry%%:*}"
222
+ log " ${C_DIM}${_idx}/${STEP_TOTAL}${C_RST} ${_name}"
223
+ done
224
+ log "${C_DIM}─────────────────────────────────────────${C_RST}"
225
+ echo
226
+ fi
187
227
 
188
228
  if [ "${DRY_RUN:-0}" = "1" ]; then
189
229
  ok "dry-run — exiting without running any step"
190
230
  exit 0
191
231
  fi
232
+
192
233
  if [ "${NONINTERACTIVE:-0}" != "1" ] && [ -t 0 ]; then
193
234
  if ! prompt_yes_no "Proceed with the install?" "yes"; then
194
235
  log "aborted by operator — no changes made"
@@ -196,10 +237,22 @@ if [ "${NONINTERACTIVE:-0}" != "1" ] && [ -t 0 ]; then
196
237
  fi
197
238
  fi
198
239
 
240
+ # In --full mode, print the single header line that introduces the
241
+ # progress bar so the user knows what's happening.
242
+ if [ "${INSTALL_FULL:-0}" = "1" ]; then
243
+ echo
244
+ log " ${C_DIM}${STEP_TOTAL} steps · output piped to ${LOG_FILE} · Ctrl-c to abort${C_RST}"
245
+ echo
246
+ fi
247
+
199
248
  for entry in "${STEPS[@]}"; do
200
249
  name="${entry%%:*}"
201
250
  fn="${entry##*:}"
202
- step_progress_header "$name"
251
+ # The verbose per-step header is suppressed in --full mode (progress
252
+ # bar replaces it).
253
+ if [ "${INSTALL_FULL:-0}" != "1" ]; then
254
+ step_progress_header "$name"
255
+ fi
203
256
  run_step "$name" "$fn"
204
257
  done
205
258
 
@@ -188,7 +188,7 @@ from omega_engine.genesis import (
188
188
  )
189
189
  from omega_engine import plan as plan_v7
190
190
 
191
- __version__ = "0.19.35"
191
+ __version__ = "0.19.37"
192
192
 
193
193
  __all__ = [
194
194
  "__version__",
@@ -3208,6 +3208,17 @@ def cmd_paperclip(args: argparse.Namespace) -> int:
3208
3208
  print(f" start failed: {exc}")
3209
3209
  return 2
3210
3210
 
3211
+ if sub == "url":
3212
+ bind = getattr(args, "bind", None) or "loopback"
3213
+ host = getattr(args, "host", None)
3214
+ url = PB.dashboard_url(bind=bind, host=host)
3215
+ print(f" paperclip dashboard: {url}")
3216
+ return 0
3217
+
3218
+ if sub == "remote-setup":
3219
+ print(PB.remote_setup_help())
3220
+ return 0
3221
+
3211
3222
  if sub == "heartbeat":
3212
3223
  # Drop a heartbeat from the current shell — used by `dispatch-to-
3213
3224
  # session.sh` style scripts so Paperclip sees workers come up + report.
@@ -4527,6 +4538,15 @@ def _build_parser() -> argparse.ArgumentParser:
4527
4538
  help="launch the Paperclip web dashboard (npx onboard)")
4528
4539
  p_pcs.add_argument("--bind", choices=["loopback", "lan", "tailnet"],
4529
4540
  default="loopback")
4541
+ p_pcu = pc_sub.add_parser("url",
4542
+ help="print the URL where the dashboard is reachable")
4543
+ p_pcu.add_argument("--bind", choices=["loopback", "lan", "tailnet"],
4544
+ default="loopback")
4545
+ p_pcu.add_argument("--host", default=None,
4546
+ help="override host (e.g. for a Cloudflare Tunnel URL)")
4547
+ pc_sub.add_parser("remote-setup",
4548
+ help="how to reach a VPS-hosted Paperclip from your Mac/PC "
4549
+ "(SSH port-forward / Tailscale / Cloudflare Tunnel)")
4530
4550
  p_pch = pc_sub.add_parser("heartbeat",
4531
4551
  help="send a heartbeat from a tmux session (script callable)")
4532
4552
  p_pch.add_argument("--agent-id", required=True,
@@ -488,6 +488,82 @@ def status() -> BridgeStatus:
488
488
  # ---------------------------------------------------------------------------
489
489
 
490
490
 
491
+ def dashboard_url(bind: str = "loopback", port: int = 8080,
492
+ host: str | None = None) -> str:
493
+ """Return the URL where the Paperclip dashboard is reachable.
494
+
495
+ Resolution:
496
+ - loopback → http://localhost:<port>
497
+ - lan → http://<lan-ip>:<port> (best-effort interface IP)
498
+ - tailnet → http://<tailscale-ip>:<port> (reads `tailscale ip -4`)
499
+ - host → http://<host>:<port> (explicit override)
500
+ """
501
+ if host:
502
+ return f"http://{host}:{port}"
503
+ if bind == "tailnet":
504
+ try:
505
+ r = subprocess.run(["tailscale", "ip", "-4"],
506
+ capture_output=True, text=True, timeout=5)
507
+ ip = (r.stdout or "").strip().splitlines()
508
+ if ip:
509
+ return f"http://{ip[0]}:{port}"
510
+ except (FileNotFoundError, subprocess.SubprocessError):
511
+ pass
512
+ if bind == "lan":
513
+ # Best-effort: use `hostname -I` (Linux) or `ipconfig getifaddr en0` (Mac).
514
+ for cmd in [["hostname", "-I"],
515
+ ["ipconfig", "getifaddr", "en0"]]:
516
+ try:
517
+ r = subprocess.run(cmd, capture_output=True, text=True, timeout=5)
518
+ ip = (r.stdout or "").strip().split()
519
+ if ip:
520
+ return f"http://{ip[0]}:{port}"
521
+ except (FileNotFoundError, subprocess.SubprocessError):
522
+ continue
523
+ return f"http://localhost:{port}"
524
+
525
+
526
+ def remote_setup_help() -> str:
527
+ """Return setup instructions for accessing a VPS-hosted Paperclip
528
+ dashboard from the operator's Mac/PC.
529
+
530
+ Three paths, from simplest to most secure:
531
+
532
+ 1. SSH port-forward (no extra software needed)
533
+ 2. Tailscale (zero-config VPN, recommended)
534
+ 3. Cloudflare Tunnel (public URL with auth)
535
+ """
536
+ return (
537
+ " Three ways to reach a VPS-hosted Paperclip dashboard from\n"
538
+ " your Mac/PC. Pick what fits your setup:\n"
539
+ "\n"
540
+ " ─── (1) SSH PORT-FORWARD (no extra install) ───\n"
541
+ " From your Mac/PC, run ONCE per session:\n"
542
+ " ssh -p 42820 -N -L 8080:localhost:8080 hacker@<vps-ip>\n"
543
+ " Then open http://localhost:8080 on your Mac browser.\n"
544
+ " Leave the SSH window open while you use the dashboard.\n"
545
+ "\n"
546
+ " ─── (2) TAILSCALE (recommended) ───\n"
547
+ " On the VPS:\n"
548
+ " curl -fsSL https://tailscale.com/install.sh | sh\n"
549
+ " sudo tailscale up\n"
550
+ " omega paperclip start --bind tailnet\n"
551
+ " On your Mac/PC: install Tailscale, log into the same account,\n"
552
+ " open http://<vps-tailscale-name>:8080.\n"
553
+ " Auth handled by Tailscale ACLs; no public exposure.\n"
554
+ "\n"
555
+ " ─── (3) CLOUDFLARE TUNNEL (public URL) ───\n"
556
+ " On the VPS:\n"
557
+ " brew install cloudflared (or apt-get install cloudflared)\n"
558
+ " cloudflared tunnel --url http://localhost:8080\n"
559
+ " cloudflared prints a public *.trycloudflare.com URL.\n"
560
+ " For a stable subdomain, use a named tunnel + DNS.\n"
561
+ " Combine with Cloudflare Access for auth.\n"
562
+ "\n"
563
+ " Currently bound: see `omega paperclip status`\n"
564
+ )
565
+
566
+
491
567
  def start_server(bind: str = "loopback", *, detach: bool = True) -> int:
492
568
  """Launch the Paperclip onboard server.
493
569
 
@@ -493,6 +493,11 @@ def _arrow_menu() -> int:
493
493
  (_label("Stealth scrape", "CloakBrowser"), "scrape:cloak"),
494
494
  (_label("Fast scrape", "Scrapling"), "scrape:scrapling"),
495
495
  ("", "__sep__"),
496
+ (_section("GOVERNANCE (Paperclip L0)"), "__sep__"),
497
+ (_label("Paperclip dashboard","web UI for 14 agents"), "paperclip:dashboard"),
498
+ (_label("Paperclip status", "bridge health"), "paperclip:status"),
499
+ (_label("Paperclip register","(re)sync OmegaOS company"), "paperclip:register"),
500
+ ("", "__sep__"),
496
501
  (_section("EXIT"), "__sep__"),
497
502
  (_label("Detach", "session keeps running"), "detach"),
498
503
  (_label("Quit Omega", "kills the tmux session"), "quit:kill"),
@@ -522,20 +527,27 @@ def _arrow_menu() -> int:
522
527
 
523
528
  while True:
524
529
  items = _build_items()
525
- lines = [disp for disp, _ in items]
530
+ # v0.19.37 bulletproof matching via tab-delimited index column.
531
+ # fzf with --ansi STRIPS escape codes from stdout, which broke
532
+ # the old `display == pick` exact-string match (every pick fell
533
+ # through, menu just reloaded). Now we prefix each line with a
534
+ # stable index (`<i>\t<display>`) and use --with-nth=2.. so fzf
535
+ # only DISPLAYS the second column but RETURNS the full line.
536
+ # We split off the index and look up the action — survives any
537
+ # ANSI/whitespace transformation fzf might apply.
538
+ lines = []
539
+ for i, (disp, key) in enumerate(items):
540
+ lines.append(f"{i}\t{disp}")
526
541
  header = (
527
542
  f"{ORANGE}{BOLD}Ω Omega OS v{__version__}{RST} "
528
543
  f"{MUTED}• ↑↓ navigate • ↵ pick • / search • Esc refresh{RST}"
529
544
  )
530
545
  try:
531
- # Claude Code LIGHT theme (white-paper) — bg cream #FAFAF7,
532
- # fg dark slate #3D3929, accent orange #D97757, muted warm
533
- # gray #88837A, soft border #A8A29E. Reads cleanly under
534
- # both light and dark terminal backgrounds (we override
535
- # fzf's bg explicitly).
536
546
  proc = subprocess.run(
537
547
  ["fzf",
538
548
  "--ansi",
549
+ "--delimiter=\t",
550
+ "--with-nth=2..",
539
551
  "--prompt=Ω › ",
540
552
  f"--header={header}",
541
553
  "--header-first",
@@ -570,12 +582,21 @@ def _arrow_menu() -> int:
570
582
  # Esc → refresh.
571
583
  continue
572
584
  pick = proc.stdout.rstrip("\n")
573
- action = None
574
- for disp, key in items:
575
- if disp == pick and key != "__sep__":
576
- action = key
577
- break
578
- if action is None:
585
+ # Parse the index column. The tab is always the first \t in the
586
+ # picked line (the rest of the display may contain colour codes
587
+ # but no tabs).
588
+ if "\t" not in pick:
589
+ continue
590
+ idx_str, _disp_part = pick.split("\t", 1)
591
+ try:
592
+ idx = int(idx_str)
593
+ except ValueError:
594
+ continue
595
+ if not (0 <= idx < len(items)):
596
+ continue
597
+ action = items[idx][1]
598
+ if action == "__sep__":
599
+ # User picked a section header — silent reroll.
579
600
  continue
580
601
 
581
602
  # === Dispatch ===
@@ -665,6 +686,41 @@ def _arrow_menu() -> int:
665
686
  _run_inline([OMEGA_BIN, "scrape", url, "--engine", "scrapling"])
666
687
  continue
667
688
 
689
+ if action == "paperclip:dashboard":
690
+ # Show status, then offer to start if not running and open
691
+ # the URL. Reuses `omega paperclip start` which handles npx.
692
+ _run_inline([OMEGA_BIN, "paperclip", "status"])
693
+ try:
694
+ ans = input(f" {ORANGE}start the Paperclip dashboard now? "
695
+ f"[y/N]:{RST} ").strip().lower()
696
+ except (EOFError, KeyboardInterrupt):
697
+ ans = ""
698
+ if ans in ("y", "yes"):
699
+ bind = "loopback"
700
+ try:
701
+ bind_ans = input(
702
+ f" {ORANGE}bind mode [loopback/lan/tailnet] "
703
+ f"(default loopback):{RST} "
704
+ ).strip().lower() or "loopback"
705
+ if bind_ans in ("loopback", "lan", "tailnet"):
706
+ bind = bind_ans
707
+ except (EOFError, KeyboardInterrupt):
708
+ pass
709
+ _run_inline([OMEGA_BIN, "paperclip", "start", "--bind", bind])
710
+ # Try to open the browser at the default URL.
711
+ import shutil as _sh
712
+ opener = _sh.which("open") or _sh.which("xdg-open")
713
+ if opener:
714
+ subprocess.run([opener, "http://localhost:8080"],
715
+ capture_output=True)
716
+ continue
717
+ if action == "paperclip:status":
718
+ _run_inline([OMEGA_BIN, "paperclip", "status"])
719
+ continue
720
+ if action == "paperclip:register":
721
+ _run_inline([OMEGA_BIN, "paperclip", "register"])
722
+ continue
723
+
668
724
 
669
725
  def run_tui(prefer_textual: bool = False,
670
726
  force_repl: bool = False) -> int:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "omega-engine"
3
- version = "0.19.35"
3
+ version = "0.19.37"
4
4
  description = "The Omega OS orchestration engine — event-sourced, verified-completion agent graphs."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -100,5 +100,81 @@ class TestProviderStateSharedModule(unittest.TestCase):
100
100
  provider_state.set_active_provider)
101
101
 
102
102
 
103
+ class TestFzfMenuMatching(unittest.TestCase):
104
+ """v0.19.37 — the menu was broken because fzf with --ansi STRIPS
105
+ ANSI codes from its stdout, so the old `display == pick` exact
106
+ match was always False. Every pick fell through and the menu just
107
+ re-rendered. These tests lock in the index-based matching that
108
+ survives any ANSI/whitespace transformation."""
109
+
110
+ def test_fzf_ansi_strips_codes_from_stdout(self):
111
+ """Documents the upstream behaviour we're working around."""
112
+ import shutil
113
+ import subprocess
114
+ if not shutil.which("fzf"):
115
+ self.skipTest("fzf not installed")
116
+ ORANGE = "\033[38;2;217;119;87m"
117
+ RST = "\033[0m"
118
+ input_line = f" {ORANGE}AISB master chat{RST} hint"
119
+ proc = subprocess.run(
120
+ ["fzf", "--ansi", "--filter=AISB"],
121
+ input=input_line, capture_output=True, text=True,
122
+ )
123
+ pick = proc.stdout.rstrip("\n")
124
+ # The stdout has the SAME visible characters but NO escape codes.
125
+ self.assertEqual(pick, " AISB master chat hint")
126
+ self.assertNotEqual(pick, input_line,
127
+ "if these match, fzf preserves ANSI — our workaround was unnecessary "
128
+ "but harmless; if not (current behaviour), the workaround is required")
129
+
130
+ def test_arrow_menu_uses_tab_delimited_index(self):
131
+ """Inspect the source of `_arrow_menu` to confirm it builds
132
+ tab-delimited input + parses the index column. If someone reverts
133
+ to display-equality matching, this test fails LOUDLY."""
134
+ import inspect
135
+ from omega_engine.tui import _arrow_menu
136
+ src = inspect.getsource(_arrow_menu)
137
+ # The fix must use --delimiter=\t + --with-nth=2.. + split("\t", 1).
138
+ self.assertIn("--delimiter=", src,
139
+ "menu must use tab-delimited input to survive fzf's --ansi strip")
140
+ self.assertIn("--with-nth", src,
141
+ "menu must use --with-nth to display only the visual column")
142
+ self.assertIn('split("\\t", 1)', src,
143
+ "menu must parse the index column from the tab-delimited pick")
144
+ self.assertNotIn("if disp == pick", src,
145
+ "REGRESSION: don't bring back display-equality matching — "
146
+ "fzf strips ANSI from stdout, the equality always fails")
147
+
148
+
149
+ class TestPaperclipMenuIntegration(unittest.TestCase):
150
+ """The menu must surface the Paperclip governance actions
151
+ (dashboard / status / register) the user explicitly asked for in
152
+ v0.19.37. Also lock in the URL helper + remote-setup help string."""
153
+
154
+ def test_paperclip_dashboard_url_returns_string(self):
155
+ from omega_engine.paperclip_bridge import dashboard_url
156
+ self.assertTrue(dashboard_url("loopback").startswith("http://"))
157
+ # Explicit host override.
158
+ self.assertEqual(dashboard_url("loopback", host="vps.example.com"),
159
+ "http://vps.example.com:8080")
160
+
161
+ def test_remote_setup_help_lists_three_paths(self):
162
+ from omega_engine.paperclip_bridge import remote_setup_help
163
+ text = remote_setup_help()
164
+ # The three documented paths must all appear.
165
+ for kw in ("SSH PORT-FORWARD", "TAILSCALE", "CLOUDFLARE TUNNEL"):
166
+ self.assertIn(kw, text,
167
+ f"remote_setup_help must document the {kw} path")
168
+
169
+ def test_arrow_menu_contains_paperclip_actions(self):
170
+ import inspect
171
+ from omega_engine.tui import _arrow_menu
172
+ src = inspect.getsource(_arrow_menu)
173
+ for action in ("paperclip:dashboard", "paperclip:status",
174
+ "paperclip:register"):
175
+ self.assertIn(action, src,
176
+ f"arrow menu must wire {action} (user asked for it in v0.19.37)")
177
+
178
+
103
179
  if __name__ == "__main__":
104
180
  unittest.main()
@@ -1 +1 @@
1
- 0.19.35
1
+ 0.19.37
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentikos/omega-os",
3
- "version": "0.19.35",
3
+ "version": "0.19.37",
4
4
  "description": "Omega OS — installable agentic operating system with verified-completion orchestration. Event-sourced engine, 8-block rack, autonomous agents, MCP.",
5
5
  "bin": {
6
6
  "omega-os": "bin/omega-os.js"