Merge branch 'main' into setup-headless-auth-message
This commit is contained in:
@@ -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. 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.
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
# Run from the v2 directory:
|
||||
# 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
|
||||
# bootstrap, then runs the migration steps.
|
||||
@@ -17,6 +19,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, exit first or open a separate terminal,"
|
||||
echo "then run:"
|
||||
echo " bash migrate-v2.sh"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
LOGS_DIR="$PROJECT_ROOT/logs"
|
||||
STEPS_DIR="$LOGS_DIR/migrate-steps"
|
||||
MIGRATE_LOG="$LOGS_DIR/migrate-v2.log"
|
||||
@@ -547,6 +562,26 @@ echo
|
||||
echo "$(bold 'Service switchover')"
|
||||
echo
|
||||
|
||||
# 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.
|
||||
disable_v1_service() {
|
||||
if [ "$PLATFORM_SERVICE" = "systemd" ]; then
|
||||
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
|
||||
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"
|
||||
if [ -f "$v1_plist" ] || [ -L "$v1_plist" ]; then
|
||||
launchctl unload "$v1_plist" 2>/dev/null || true
|
||||
step_ok "Unloaded $V1_SERVICE (plist kept for rollback)"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Detect platform and service names
|
||||
V1_SERVICE=""
|
||||
V2_SERVICE=""
|
||||
@@ -635,16 +670,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
|
||||
disable_v1_service
|
||||
fi
|
||||
else
|
||||
step_skip "Service switchover skipped"
|
||||
fi
|
||||
else
|
||||
step_skip "v1 service not running — nothing to switch"
|
||||
disable_v1_service
|
||||
fi
|
||||
|
||||
echo
|
||||
@@ -676,6 +709,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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "nanoclaw",
|
||||
"version": "2.0.27",
|
||||
"version": "2.0.28",
|
||||
"description": "Personal Claude assistant. Lightweight, secure, customizable.",
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@10.33.0",
|
||||
|
||||
@@ -84,21 +84,28 @@ describe('credentials detection', () => {
|
||||
const content =
|
||||
'SOME_KEY=value\nANTHROPIC_API_KEY=sk-ant-test123\nOTHER=foo';
|
||||
const hasCredentials =
|
||||
/^(CLAUDE_CODE_OAUTH_TOKEN|ANTHROPIC_API_KEY)=/m.test(content);
|
||||
/^(CLAUDE_CODE_OAUTH_TOKEN|ANTHROPIC_API_KEY|ANTHROPIC_AUTH_TOKEN|ONECLI_URL)=/m.test(content);
|
||||
expect(hasCredentials).toBe(true);
|
||||
});
|
||||
|
||||
it('detects CLAUDE_CODE_OAUTH_TOKEN in env content', () => {
|
||||
const content = 'CLAUDE_CODE_OAUTH_TOKEN=token123';
|
||||
const hasCredentials =
|
||||
/^(CLAUDE_CODE_OAUTH_TOKEN|ANTHROPIC_API_KEY)=/m.test(content);
|
||||
/^(CLAUDE_CODE_OAUTH_TOKEN|ANTHROPIC_API_KEY|ANTHROPIC_AUTH_TOKEN|ONECLI_URL)=/m.test(content);
|
||||
expect(hasCredentials).toBe(true);
|
||||
});
|
||||
|
||||
it('detects ANTHROPIC_AUTH_TOKEN in env content', () => {
|
||||
const content = 'ANTHROPIC_AUTH_TOKEN=token123\nANTHROPIC_BASE_URL=http://localhost:8080';
|
||||
const hasCredentials =
|
||||
/^(CLAUDE_CODE_OAUTH_TOKEN|ANTHROPIC_API_KEY|ANTHROPIC_AUTH_TOKEN|ONECLI_URL)=/m.test(content);
|
||||
expect(hasCredentials).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when no credentials', () => {
|
||||
const content = 'ASSISTANT_NAME="Andy"\nOTHER=foo';
|
||||
const hasCredentials =
|
||||
/^(CLAUDE_CODE_OAUTH_TOKEN|ANTHROPIC_API_KEY)=/m.test(content);
|
||||
/^(CLAUDE_CODE_OAUTH_TOKEN|ANTHROPIC_API_KEY|ANTHROPIC_AUTH_TOKEN|ONECLI_URL)=/m.test(content);
|
||||
expect(hasCredentials).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<void> {
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -139,7 +139,7 @@ export async function run(_args: string[]): Promise<void> {
|
||||
const envFile = path.join(projectRoot, '.env');
|
||||
if (fs.existsSync(envFile)) {
|
||||
const envContent = fs.readFileSync(envFile, 'utf-8');
|
||||
if (/^(CLAUDE_CODE_OAUTH_TOKEN|ANTHROPIC_API_KEY|ONECLI_URL)=/m.test(envContent)) {
|
||||
if (/^(CLAUDE_CODE_OAUTH_TOKEN|ANTHROPIC_API_KEY|ANTHROPIC_AUTH_TOKEN|ONECLI_URL)=/m.test(envContent)) {
|
||||
credentials = 'configured';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,12 +253,12 @@ export function createChatSdkBridge(config: ChatSdkBridgeConfig): ChannelAdapter
|
||||
// Chat SDK dispatch (handling-events.mdx §"Handler dispatch order") is
|
||||
// exclusive: subscribed → onSubscribedMessage; unsubscribed+mention →
|
||||
// onNewMention; unsubscribed+pattern-match → onNewMessage. Registering
|
||||
// with `/./` lets the router see every plain message on every
|
||||
// unsubscribed thread the bot can see. The router short-circuits via
|
||||
// with `/[\s\S]*/` lets the router see every plain message (including
|
||||
// media-only messages with empty text) on every unsubscribed thread the
|
||||
// getMessagingGroupWithAgentCount (~1 DB read) for unwired channels,
|
||||
// so forwarding every one is cheap enough to not need a bridge-side
|
||||
// flood gate.
|
||||
chat.onNewMessage(/./, async (thread, message) => {
|
||||
chat.onNewMessage(/[\s\S]*/, async (thread, message) => {
|
||||
const channelId = adapter.channelIdFromThreadId(thread.id);
|
||||
await setupConfig.onInbound(channelId, thread.id, await messageToInbound(message, false, true));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user