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:**
|
**Do this instead:**
|
||||||
1. Abort the merge: `git merge --abort` (or `git reset --hard ORIG_HEAD` if the merge already completed).
|
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."*
|
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 attempt the migration yourself.
|
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.
|
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:
|
# Run from the v2 directory:
|
||||||
# bash migrate-v2.sh
|
# 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).
|
# Finds v1 automatically (sibling directory, or $NANOCLAW_V1_PATH).
|
||||||
# Installs prerequisites (Node, pnpm, deps) via the existing setup.sh
|
# Installs prerequisites (Node, pnpm, deps) via the existing setup.sh
|
||||||
# bootstrap, then runs the migration steps.
|
# bootstrap, then runs the migration steps.
|
||||||
@@ -17,6 +19,19 @@ set -uo pipefail
|
|||||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
cd "$PROJECT_ROOT"
|
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"
|
LOGS_DIR="$PROJECT_ROOT/logs"
|
||||||
STEPS_DIR="$LOGS_DIR/migrate-steps"
|
STEPS_DIR="$LOGS_DIR/migrate-steps"
|
||||||
MIGRATE_LOG="$LOGS_DIR/migrate-v2.log"
|
MIGRATE_LOG="$LOGS_DIR/migrate-v2.log"
|
||||||
@@ -547,6 +562,26 @@ echo
|
|||||||
echo "$(bold 'Service switchover')"
|
echo "$(bold 'Service switchover')"
|
||||||
echo
|
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
|
# Detect platform and service names
|
||||||
V1_SERVICE=""
|
V1_SERVICE=""
|
||||||
V2_SERVICE=""
|
V2_SERVICE=""
|
||||||
@@ -635,16 +670,14 @@ if [ "$V1_RUNNING" = "true" ]; then
|
|||||||
SERVICE_SWITCHED=false
|
SERVICE_SWITCHED=false
|
||||||
else
|
else
|
||||||
step_ok "Keeping v2 service"
|
step_ok "Keeping v2 service"
|
||||||
# Disable v1 from auto-starting
|
disable_v1_service
|
||||||
if [ "$PLATFORM_SERVICE" = "systemd" ]; then
|
|
||||||
systemctl --user disable "$V1_SERVICE" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
step_skip "Service switchover skipped"
|
step_skip "Service switchover skipped"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
step_skip "v1 service not running — nothing to switch"
|
step_skip "v1 service not running — nothing to switch"
|
||||||
|
disable_v1_service
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo
|
echo
|
||||||
@@ -676,6 +709,16 @@ echo " $(green '✓') Channels installed: ${SELECTED_CHANNELS[*]}"
|
|||||||
fi
|
fi
|
||||||
echo " $(green '✓') Container skills copied"
|
echo " $(green '✓') Container skills copied"
|
||||||
echo " $(green '✓') Container image built"
|
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
|
||||||
echo " $(bold 'What still needs a human:')"
|
echo " $(bold 'What still needs a human:')"
|
||||||
if [ "$ONECLI_OK" = "false" ]; then
|
if [ "$ONECLI_OK" = "false" ]; then
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "nanoclaw",
|
"name": "nanoclaw",
|
||||||
"version": "2.0.27",
|
"version": "2.0.28",
|
||||||
"description": "Personal Claude assistant. Lightweight, secure, customizable.",
|
"description": "Personal Claude assistant. Lightweight, secure, customizable.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "pnpm@10.33.0",
|
"packageManager": "pnpm@10.33.0",
|
||||||
|
|||||||
@@ -84,21 +84,28 @@ describe('credentials detection', () => {
|
|||||||
const content =
|
const content =
|
||||||
'SOME_KEY=value\nANTHROPIC_API_KEY=sk-ant-test123\nOTHER=foo';
|
'SOME_KEY=value\nANTHROPIC_API_KEY=sk-ant-test123\nOTHER=foo';
|
||||||
const hasCredentials =
|
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);
|
expect(hasCredentials).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('detects CLAUDE_CODE_OAUTH_TOKEN in env content', () => {
|
it('detects CLAUDE_CODE_OAUTH_TOKEN in env content', () => {
|
||||||
const content = 'CLAUDE_CODE_OAUTH_TOKEN=token123';
|
const content = 'CLAUDE_CODE_OAUTH_TOKEN=token123';
|
||||||
const hasCredentials =
|
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);
|
expect(hasCredentials).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns false when no credentials', () => {
|
it('returns false when no credentials', () => {
|
||||||
const content = 'ASSISTANT_NAME="Andy"\nOTHER=foo';
|
const content = 'ASSISTANT_NAME="Andy"\nOTHER=foo';
|
||||||
const hasCredentials =
|
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);
|
expect(hasCredentials).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
|
||||||
import * as p from '@clack/prompts';
|
import * as p from '@clack/prompts';
|
||||||
|
import { styleText } from 'node:util';
|
||||||
|
|
||||||
const CHANNELS = [
|
const CHANNELS = [
|
||||||
{ value: 'telegram', label: 'Telegram' },
|
{ value: 'telegram', label: 'Telegram' },
|
||||||
@@ -47,7 +48,7 @@ async function main(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const selected = await p.multiselect({
|
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,
|
options: CHANNELS,
|
||||||
required: false,
|
required: false,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -115,9 +115,43 @@ function installOnecliCliOnly(): { stdout: string; ok: boolean } {
|
|||||||
return { stdout: upstream.stdout + (upstream.stderr ?? '') + '\n' + fallback.stdout, ok: fallback.ok };
|
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 } {
|
function installOnecli(): { stdout: string; ok: boolean } {
|
||||||
let stdout = '';
|
let stdout = '';
|
||||||
|
|
||||||
|
const cleanup = removeLegacyOnecliContainers();
|
||||||
|
if (cleanup) stdout += cleanup + '\n';
|
||||||
|
|
||||||
// Gateway install (docker-compose based, no rate-limit concerns).
|
// Gateway install (docker-compose based, no rate-limit concerns).
|
||||||
const gw = runInstall('curl -fsSL onecli.sh/install | sh');
|
const gw = runInstall('curl -fsSL onecli.sh/install | sh');
|
||||||
stdout += gw.stdout;
|
stdout += gw.stdout;
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ export async function run(_args: string[]): Promise<void> {
|
|||||||
const envFile = path.join(projectRoot, '.env');
|
const envFile = path.join(projectRoot, '.env');
|
||||||
if (fs.existsSync(envFile)) {
|
if (fs.existsSync(envFile)) {
|
||||||
const envContent = fs.readFileSync(envFile, 'utf-8');
|
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';
|
credentials = 'configured';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -253,12 +253,12 @@ export function createChatSdkBridge(config: ChatSdkBridgeConfig): ChannelAdapter
|
|||||||
// Chat SDK dispatch (handling-events.mdx §"Handler dispatch order") is
|
// Chat SDK dispatch (handling-events.mdx §"Handler dispatch order") is
|
||||||
// exclusive: subscribed → onSubscribedMessage; unsubscribed+mention →
|
// exclusive: subscribed → onSubscribedMessage; unsubscribed+mention →
|
||||||
// onNewMention; unsubscribed+pattern-match → onNewMessage. Registering
|
// onNewMention; unsubscribed+pattern-match → onNewMessage. Registering
|
||||||
// with `/./` lets the router see every plain message on every
|
// with `/[\s\S]*/` lets the router see every plain message (including
|
||||||
// unsubscribed thread the bot can see. The router short-circuits via
|
// media-only messages with empty text) on every unsubscribed thread the
|
||||||
// getMessagingGroupWithAgentCount (~1 DB read) for unwired channels,
|
// getMessagingGroupWithAgentCount (~1 DB read) for unwired channels,
|
||||||
// so forwarding every one is cheap enough to not need a bridge-side
|
// so forwarding every one is cheap enough to not need a bridge-side
|
||||||
// flood gate.
|
// flood gate.
|
||||||
chat.onNewMessage(/./, async (thread, message) => {
|
chat.onNewMessage(/[\s\S]*/, async (thread, message) => {
|
||||||
const channelId = adapter.channelIdFromThreadId(thread.id);
|
const channelId = adapter.channelIdFromThreadId(thread.id);
|
||||||
await setupConfig.onInbound(channelId, thread.id, await messageToInbound(message, false, true));
|
await setupConfig.onInbound(channelId, thread.id, await messageToInbound(message, false, true));
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user