From e34380656c4c086f7c9a2ddceed560b62197000c Mon Sep 17 00:00:00 2001 From: "exe.dev user" Date: Sun, 3 May 2026 12:38:30 +0000 Subject: [PATCH 1/7] feat(setup): headless-aware Claude sign-in pre-message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pre-message printed by setup/register-claude-token.sh used to say "A browser window will open for you to sign in with your Claude account." Accurate on a laptop or desktop, but a lie on headless devices (Pi, SSH'd-into Linux server, CI) where the browser auto-open never lands and the user actually has to copy the URL `claude setup-token` prints to another device. Add a small bash isHeadless check (mirrors `isHeadless()` in setup/platform.ts: Linux without DISPLAY / WAYLAND_DISPLAY) and swap the heredoc accordingly: - Headless: "A sign-in link will appear for you to sign in with your Claude account. When you finish, we'll save the token to your OneCLI vault automatically." - GUI: existing "A browser window will open…" copy, unchanged. The trailing "Press Enter to continue, or edit the command first." line and the actual `claude setup-token` invocation are unchanged — only the leading sentence flips. --- setup/register-claude-token.sh | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/setup/register-claude-token.sh b/setup/register-claude-token.sh index e0adfc6..324b1be 100755 --- a/setup/register-claude-token.sh +++ b/setup/register-claude-token.sh @@ -51,13 +51,34 @@ command -v script >/dev/null \ tmpfile=$(mktemp -t claude-setup-token.XXXXXX) trap 'rm -f "$tmpfile"' EXIT -cat <<'EOF' +# Detect headless. Mirrors `isHeadless()` in setup/platform.ts: on Linux +# with neither DISPLAY nor WAYLAND_DISPLAY set, no graphical session +# exists, so `claude setup-token` won't be able to auto-open a browser +# and the user will need to copy the printed sign-in URL by hand. The +# pre-message copy below is swapped accordingly so we don't promise a +# browser pop that will never happen. +is_headless=0 +if [ "$(uname -s)" = "Linux" ] && [ -z "${DISPLAY:-}" ] && [ -z "${WAYLAND_DISPLAY:-}" ]; then + is_headless=1 +fi + +if [ "$is_headless" = "1" ]; then + cat <<'EOF' +A sign-in link will appear for you to sign in with your Claude account. +When you finish, we'll save the token to your OneCLI vault automatically. + +Press Enter to continue, or edit the command first. + +EOF +else + cat <<'EOF' A browser window will open for you to sign in with your Claude account. When you finish, we'll save the token to your OneCLI vault automatically. Press Enter to continue, or edit the command first. EOF +fi cmd="claude setup-token" if [ "${BASH_VERSINFO[0]:-0}" -ge 4 ]; then From 6a05e41afe620c5b8a2bdef5a75f90426ab16a38 Mon Sep 17 00:00:00 2001 From: Koshkoshinsk Date: Sun, 3 May 2026 14:47:52 +0300 Subject: [PATCH 2/7] fix: require interactive terminal for migrate-v2.sh The migration script has interactive prompts and streams progress output that gets collapsed when run via Claude Code's Bash tool. Add a TTY guard that exits early with instructions to use the ! prefix instead. Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 4 ++-- migrate-v2.sh | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e65515a..3104b32 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,8 +6,8 @@ This is NanoClaw v2, a ground-up rewrite with breaking changes throughout. It ca **Do this instead:** 1. Abort the merge: `git merge --abort` (or `git reset --hard ORIG_HEAD` if the merge already completed). -2. Tell the user, verbatim: *"This is the v2 rewrite — it can't be merged into your existing install. Run `bash migrate-v2.sh` to migrate instead."* -3. Wait for the user to confirm before doing anything else. Do not attempt the migration yourself. +2. Tell the user, verbatim: *"This is the v2 rewrite — it can't be merged into your existing install. Run `! bash migrate-v2.sh` to migrate instead."* +3. Wait for the user to confirm before doing anything else. Do not run the migration script yourself — it requires an interactive terminal and must be run by the user with the `!` prefix. If you are a fresh install (you ran `git clone`, not `git pull`) and there are no conflicts, ignore this banner and continue below. diff --git a/migrate-v2.sh b/migrate-v2.sh index eb5a381..ac9f4e0 100644 --- a/migrate-v2.sh +++ b/migrate-v2.sh @@ -5,6 +5,9 @@ # Run from the v2 directory: # bash migrate-v2.sh # +# From Claude Code, use the ! prefix so it runs in your terminal: +# ! bash migrate-v2.sh +# # Finds v1 automatically (sibling directory, or $NANOCLAW_V1_PATH). # Installs prerequisites (Node, pnpm, deps) via the existing setup.sh # bootstrap, then runs the migration steps. @@ -17,6 +20,19 @@ set -uo pipefail PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$PROJECT_ROOT" +# This script has interactive prompts (channel selection, service switchover) +# and streams progress output — it must run in a real terminal, not inside +# a tool subprocess (e.g. Claude Code's Bash tool, which collapses output). +if ! [ -t 0 ] || ! [ -t 1 ]; then + echo "This script requires an interactive terminal." + echo "" + echo "If you're in Claude Code, run it directly with the ! prefix:" + echo " ! bash migrate-v2.sh" + echo "" + echo "Or run it in a separate terminal session." + exit 1 +fi + LOGS_DIR="$PROJECT_ROOT/logs" STEPS_DIR="$LOGS_DIR/migrate-steps" MIGRATE_LOG="$LOGS_DIR/migrate-v2.log" From d88b0807e60dd63b787432fc37288a0d962264ab Mon Sep 17 00:00:00 2001 From: Koshkoshinsk Date: Sun, 3 May 2026 13:02:34 +0000 Subject: [PATCH 3/7] fix: retire legacy v1 service file after migration switchover MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After migration keeps v2, the old unslugged `nanoclaw.service` (or `com.nanoclaw.plist`) was only disabled — the unit file stayed on disk. A `systemctl --user restart nanoclaw` would start v1 instead of v2. Now the migration removes the old file and symlinks it to the v2 unit, so the legacy name transparently starts v2. Handles systemd (Linux/WSL) and launchd (macOS). Idempotent — skips if the symlink already exists. Co-Authored-By: Claude Opus 4.6 --- migrate-v2.sh | 51 +++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/migrate-v2.sh b/migrate-v2.sh index ac9f4e0..aedbf0f 100644 --- a/migrate-v2.sh +++ b/migrate-v2.sh @@ -563,6 +563,51 @@ echo echo "$(bold 'Service switchover')" echo +# Retire the legacy v1 service file and alias it to the v2 unit. +# Called after the user confirms "keep v2", or when v1 wasn't running. +# Idempotent — safe to call multiple times. +retire_v1_service() { + if [ -z "$V2_SERVICE" ]; then + return + fi + + if [ "$PLATFORM_SERVICE" = "systemd" ]; then + local unit_dir="$HOME/.config/systemd/user" + local v1_file="$unit_dir/${V1_SERVICE}.service" + local v2_file="${V2_SERVICE}.service" + + # Already a correct symlink — nothing to do + if [ -L "$v1_file" ] && [ "$(readlink "$v1_file")" = "$v2_file" ]; then + return + fi + + # Only retire if the file exists (as a regular file or stale symlink) + if [ -f "$v1_file" ] || [ -L "$v1_file" ]; then + systemctl --user stop "$V1_SERVICE" 2>/dev/null || true + systemctl --user disable "$V1_SERVICE" 2>/dev/null || true + rm -f "$v1_file" + ln -s "$v2_file" "$v1_file" + systemctl --user daemon-reload 2>/dev/null || true + step_ok "Aliased $V1_SERVICE → $V2_SERVICE" + fi + + elif [ "$PLATFORM_SERVICE" = "launchd" ]; then + local v1_plist="$HOME/Library/LaunchAgents/${V1_SERVICE}.plist" + local v2_plist="${V2_SERVICE}.plist" + + if [ -L "$v1_plist" ] && [ "$(readlink "$v1_plist")" = "$v2_plist" ]; then + return + fi + + if [ -f "$v1_plist" ] || [ -L "$v1_plist" ]; then + launchctl unload "$v1_plist" 2>/dev/null || true + rm -f "$v1_plist" + ln -s "$v2_plist" "$v1_plist" + step_ok "Aliased $V1_SERVICE → $V2_SERVICE" + fi + fi +} + # Detect platform and service names V1_SERVICE="" V2_SERVICE="" @@ -651,16 +696,14 @@ if [ "$V1_RUNNING" = "true" ]; then SERVICE_SWITCHED=false else step_ok "Keeping v2 service" - # Disable v1 from auto-starting - if [ "$PLATFORM_SERVICE" = "systemd" ]; then - systemctl --user disable "$V1_SERVICE" 2>/dev/null || true - fi + retire_v1_service fi else step_skip "Service switchover skipped" fi else step_skip "v1 service not running — nothing to switch" + retire_v1_service fi echo From 58e4df44e24bb05e051001e68cd4c6c0ed14a361 Mon Sep 17 00:00:00 2001 From: Koshkoshinsk Date: Sun, 3 May 2026 16:23:52 +0300 Subject: [PATCH 4/7] fix: add hint to channel multiselect in migration Co-Authored-By: Claude Opus 4.6 --- setup/migrate-v2/select-channels.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup/migrate-v2/select-channels.ts b/setup/migrate-v2/select-channels.ts index eecf1ab..a2c8b21 100644 --- a/setup/migrate-v2/select-channels.ts +++ b/setup/migrate-v2/select-channels.ts @@ -12,6 +12,7 @@ import fs from 'fs'; import * as p from '@clack/prompts'; +import { styleText } from 'node:util'; const CHANNELS = [ { value: 'telegram', label: 'Telegram' }, @@ -47,7 +48,7 @@ async function main(): Promise { } const selected = await p.multiselect({ - message: 'Which channels do you want to set up?', + message: 'Which channels do you want to set up?\n' + styleText('dim', ' space to select, enter to confirm') + '\n', options: CHANNELS, required: false, }); From 6daa1a3ffe5d04d3cc6a9483305e8b9c4590b66a Mon Sep 17 00:00:00 2001 From: Koshkoshinsk Date: Sun, 3 May 2026 17:09:31 +0300 Subject: [PATCH 5/7] fix: preserve v1 service file for rollback instead of symlinking The previous approach deleted the v1 unit file and symlinked it to v2, making rollback impossible. Now we just disable v1 and leave the file on disk so users can switch back with a single command. Also adds rollback instructions to the migration summary output. Co-Authored-By: Claude Opus 4.6 --- migrate-v2.sh | 51 ++++++++++++++++++--------------------------------- 1 file changed, 18 insertions(+), 33 deletions(-) diff --git a/migrate-v2.sh b/migrate-v2.sh index aedbf0f..dec497f 100644 --- a/migrate-v2.sh +++ b/migrate-v2.sh @@ -563,47 +563,22 @@ echo echo "$(bold 'Service switchover')" echo -# Retire the legacy v1 service file and alias it to the v2 unit. -# Called after the user confirms "keep v2", or when v1 wasn't running. +# Disable the v1 service so it doesn't auto-start, but leave the unit file +# on disk so the user can rollback with: systemctl --user start nanoclaw # Idempotent — safe to call multiple times. -retire_v1_service() { - if [ -z "$V2_SERVICE" ]; then - return - fi - +disable_v1_service() { if [ "$PLATFORM_SERVICE" = "systemd" ]; then - local unit_dir="$HOME/.config/systemd/user" - local v1_file="$unit_dir/${V1_SERVICE}.service" - local v2_file="${V2_SERVICE}.service" - - # Already a correct symlink — nothing to do - if [ -L "$v1_file" ] && [ "$(readlink "$v1_file")" = "$v2_file" ]; then - return - fi - - # Only retire if the file exists (as a regular file or stale symlink) + local v1_file="$HOME/.config/systemd/user/${V1_SERVICE}.service" if [ -f "$v1_file" ] || [ -L "$v1_file" ]; then systemctl --user stop "$V1_SERVICE" 2>/dev/null || true systemctl --user disable "$V1_SERVICE" 2>/dev/null || true - rm -f "$v1_file" - ln -s "$v2_file" "$v1_file" - systemctl --user daemon-reload 2>/dev/null || true - step_ok "Aliased $V1_SERVICE → $V2_SERVICE" + step_ok "Disabled $V1_SERVICE (unit file kept for rollback)" fi - elif [ "$PLATFORM_SERVICE" = "launchd" ]; then local v1_plist="$HOME/Library/LaunchAgents/${V1_SERVICE}.plist" - local v2_plist="${V2_SERVICE}.plist" - - if [ -L "$v1_plist" ] && [ "$(readlink "$v1_plist")" = "$v2_plist" ]; then - return - fi - if [ -f "$v1_plist" ] || [ -L "$v1_plist" ]; then launchctl unload "$v1_plist" 2>/dev/null || true - rm -f "$v1_plist" - ln -s "$v2_plist" "$v1_plist" - step_ok "Aliased $V1_SERVICE → $V2_SERVICE" + step_ok "Unloaded $V1_SERVICE (plist kept for rollback)" fi fi } @@ -696,14 +671,14 @@ if [ "$V1_RUNNING" = "true" ]; then SERVICE_SWITCHED=false else step_ok "Keeping v2 service" - retire_v1_service + disable_v1_service fi else step_skip "Service switchover skipped" fi else step_skip "v1 service not running — nothing to switch" - retire_v1_service + disable_v1_service fi echo @@ -735,6 +710,16 @@ echo " $(green '✓') Channels installed: ${SELECTED_CHANNELS[*]}" fi echo " $(green '✓') Container skills copied" echo " $(green '✓') Container image built" +if [ "$SERVICE_SWITCHED" = "true" ] && [ -n "$V2_SERVICE" ]; then +echo " $(green '✓') Service switched to v2 $(dim "($V2_SERVICE)")" +echo +echo " $(bold 'Rollback to v1:')" +if [ "$PLATFORM_SERVICE" = "systemd" ]; then +echo " $(dim '$') systemctl --user stop $V2_SERVICE && systemctl --user start $V1_SERVICE" +elif [ "$PLATFORM_SERVICE" = "launchd" ]; then +echo " $(dim '$') launchctl unload ~/Library/LaunchAgents/${V2_SERVICE}.plist && launchctl load ~/Library/LaunchAgents/${V1_SERVICE}.plist" +fi +fi echo echo " $(bold 'What still needs a human:')" if [ "$ONECLI_OK" = "false" ]; then From 5deccc44eaa47fedf1a1c483f1864909116101b4 Mon Sep 17 00:00:00 2001 From: Koshkoshinsk Date: Sun, 3 May 2026 17:26:46 +0300 Subject: [PATCH 6/7] fix: direct users to exit Claude Code for migration instead of using ! prefix Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 4 ++-- migrate-v2.sh | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3104b32..c17001b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,8 +6,8 @@ This is NanoClaw v2, a ground-up rewrite with breaking changes throughout. It ca **Do this instead:** 1. Abort the merge: `git merge --abort` (or `git reset --hard ORIG_HEAD` if the merge already completed). -2. Tell the user, verbatim: *"This is the v2 rewrite — it can't be merged into your existing install. Run `! bash migrate-v2.sh` to migrate instead."* -3. Wait for the user to confirm before doing anything else. Do not run the migration script yourself — it requires an interactive terminal and must be run by the user with the `!` prefix. +2. Tell the user, verbatim: *"This is the v2 rewrite — it can't be merged into your existing install. Exit Claude Code (or open a separate terminal) and run `bash migrate-v2.sh` from the shell."* +3. Wait for the user to confirm before doing anything else. Do not run the migration script yourself — it requires an interactive terminal and cannot be run from within Claude Code. If you are a fresh install (you ran `git clone`, not `git pull`) and there are no conflicts, ignore this banner and continue below. diff --git a/migrate-v2.sh b/migrate-v2.sh index dec497f..f06a548 100644 --- a/migrate-v2.sh +++ b/migrate-v2.sh @@ -5,8 +5,7 @@ # Run from the v2 directory: # bash migrate-v2.sh # -# From Claude Code, use the ! prefix so it runs in your terminal: -# ! bash migrate-v2.sh +# If you're in Claude Code, exit first or open a separate terminal. # # Finds v1 automatically (sibling directory, or $NANOCLAW_V1_PATH). # Installs prerequisites (Node, pnpm, deps) via the existing setup.sh @@ -26,10 +25,10 @@ cd "$PROJECT_ROOT" if ! [ -t 0 ] || ! [ -t 1 ]; then echo "This script requires an interactive terminal." echo "" - echo "If you're in Claude Code, run it directly with the ! prefix:" - echo " ! bash migrate-v2.sh" + echo "If you're in Claude Code, exit first or open a separate terminal," + echo "then run:" + echo " bash migrate-v2.sh" echo "" - echo "Or run it in a separate terminal session." exit 1 fi From 37d6335ebc2ead019a8e0d9675d786011bd98772 Mon Sep 17 00:00:00 2001 From: Koshkoshinsk Date: Sun, 3 May 2026 20:12:37 +0000 Subject: [PATCH 7/7] fix(setup): clean up legacy OneCLI containers before installer runs The OneCLI installer (curl onecli.sh/install | sh) doesn't pass --remove-orphans to docker compose up. After the upstream service rename (app -> onecli), the legacy onecli-app-1 container keeps :10254 bound and crashes the new bring-up. This breaks /migrate-v2.sh on any host that has a pre-rename OneCLI installed. Workaround: before invoking the installer, remove containers in the "onecli" compose project whose service name isn't in the v2 set ({onecli, postgres}). Label-keyed and no-op on fresh installs. Filed upstream; remove this once the installer adds --remove-orphans. Co-Authored-By: Claude Opus 4.7 (1M context) --- setup/onecli.ts | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/setup/onecli.ts b/setup/onecli.ts index fbf76a9..8f758bb 100644 --- a/setup/onecli.ts +++ b/setup/onecli.ts @@ -115,9 +115,43 @@ function installOnecliCliOnly(): { stdout: string; ok: boolean } { return { stdout: upstream.stdout + (upstream.stderr ?? '') + '\n' + fallback.stdout, ok: fallback.ok }; } +// Remove containers in the "onecli" compose project whose service name isn't +// in the v2 set. Pre-v2 OneCLI used service "app" (container onecli-app-1); +// v2 uses "onecli". Compose flags the old container as an orphan but won't +// stop it without --remove-orphans, leaving port 10254 bound and crashing +// the new bring-up. Filed upstream; this is the downstream workaround. +function removeLegacyOnecliContainers(): string { + const out: string[] = []; + let list = ''; + try { + list = execSync( + `docker ps -a --filter "label=com.docker.compose.project=onecli" --format '{{.Names}}|{{.Label "com.docker.compose.service"}}'`, + { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] }, + ).trim(); + } catch { + return ''; + } + if (!list) return ''; + const v2Services = new Set(['onecli', 'postgres']); + for (const line of list.split('\n')) { + const [name, service] = line.split('|'); + if (!name || !service || v2Services.has(service)) continue; + out.push(`Removing legacy OneCLI container: ${name} (service=${service})`); + try { + execSync(`docker rm -f ${JSON.stringify(name)}`, { stdio: ['ignore', 'pipe', 'pipe'] }); + } catch (err) { + out.push(` rm failed (continuing): ${(err as Error).message}`); + } + } + return out.join('\n'); +} + function installOnecli(): { stdout: string; ok: boolean } { let stdout = ''; + const cleanup = removeLegacyOnecliContainers(); + if (cleanup) stdout += cleanup + '\n'; + // Gateway install (docker-compose based, no rate-limit concerns). const gw = runInstall('curl -fsSL onecli.sh/install | sh'); stdout += gw.stdout;