From 305b1a39ebc0849da2d93b56c4997c32c091433d Mon Sep 17 00:00:00 2001 From: Alexander Mayo Date: Tue, 14 Apr 2026 16:23:35 -0500 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20iris=20campaign=20create=20?= =?UTF-8?q?=E2=80=94=20interactive=20wizard=20for=20outreach=20campaigns?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Creates board + outreach strategy + schedule + IG session in one flow. Supports 9 verticals (restaurants, venues, dentists, creators, etc.) with custom option. Configures channel (IG DM, email, LinkedIn, both), frequency, message style, and warmup behavior. Also: iris campaign list — shows all campaigns from som-config.js. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../opencode/src/cli/cmd/platform-campaign.ts | 387 ++++++++++++++++++ packages/opencode/src/index.ts | 4 + 2 files changed, 391 insertions(+) create mode 100644 packages/opencode/src/cli/cmd/platform-campaign.ts diff --git a/packages/opencode/src/cli/cmd/platform-campaign.ts b/packages/opencode/src/cli/cmd/platform-campaign.ts new file mode 100644 index 000000000000..7dc406e6e3b8 --- /dev/null +++ b/packages/opencode/src/cli/cmd/platform-campaign.ts @@ -0,0 +1,387 @@ +import { cmd } from "./cmd" +import * as prompts from "@clack/prompts" +import { UI } from "../ui" +import { irisFetch, requireAuth, requireUserId, handleApiError, printDivider, printKV, dim, bold, success } from "./iris-api" +import { existsSync, readFileSync, writeFileSync } from "fs" +import { join } from "path" +import { homedir } from "os" +import { execSync } from "child_process" + +// ============================================================================ +// iris campaign create — interactive wizard for new outreach campaigns +// ============================================================================ + +const CampaignCreateCommand = cmd({ + command: "create", + describe: "create a new outreach campaign (interactive wizard)", + builder: (yargs) => + yargs + .option("vertical", { describe: "campaign vertical", type: "string" }) + .option("ig-account", { describe: "Instagram account handle", type: "string" }) + .option("location", { describe: "target location (e.g., Austin, TX)", type: "string" }) + .option("keywords", { describe: "target keywords (comma-separated)", type: "string" }) + .option("frequency", { describe: "run frequency", type: "string" }) + .option("user-id", { describe: "user ID", type: "number" }), + async handler(args) { + UI.empty() + prompts.intro("◈ Create Campaign") + + const token = await requireAuth() + if (!token) { prompts.outro("Done"); return } + + const userId = await requireUserId(args["user-id"]) + if (!userId) { prompts.outro("Done"); return } + + // Step 1: Vertical + let vertical = args.vertical + if (!vertical) { + vertical = (await prompts.select({ + message: "What vertical is this campaign for?", + options: [ + { value: "restaurants", label: "Restaurants & Food" }, + { value: "venues", label: "Venues & Event Spaces" }, + { value: "dentists", label: "Dentists & Medical" }, + { value: "creators", label: "Content Creators" }, + { value: "realtors", label: "Real Estate Agents" }, + { value: "fitness", label: "Fitness & Wellness" }, + { value: "beauty", label: "Beauty & Salons" }, + { value: "lawyers", label: "Law Firms" }, + { value: "custom", label: "Custom (enter your own)" }, + ], + })) as string + if (prompts.isCancel(vertical)) { prompts.outro("Cancelled"); return } + } + + if (vertical === "custom") { + vertical = (await prompts.text({ + message: "Enter your vertical name", + placeholder: "e.g., pet grooming", + validate: (x) => (x && x.length > 0 ? undefined : "Required"), + })) as string + if (prompts.isCancel(vertical)) { prompts.outro("Cancelled"); return } + } + + // Step 2: IG Account + let igAccount = args["ig-account"] + if (!igAccount) { + igAccount = (await prompts.text({ + message: "Your Instagram handle (for sending DMs)", + placeholder: "e.g., myrestaurant", + validate: (x) => (x && x.length > 0 ? undefined : "Required"), + })) as string + if (prompts.isCancel(igAccount)) { prompts.outro("Cancelled"); return } + } + igAccount = igAccount.replace("@", "") + + // Step 3: Location + let location = args.location + if (!location) { + location = (await prompts.text({ + message: "Target location", + placeholder: "e.g., Austin, TX", + })) as string + if (prompts.isCancel(location)) { prompts.outro("Cancelled"); return } + } + + // Step 4: Keywords + let keywords = args.keywords + if (!keywords) { + keywords = (await prompts.text({ + message: "Target keywords (comma-separated)", + placeholder: `e.g., ${vertical}, local businesses`, + initialValue: vertical, + })) as string + if (prompts.isCancel(keywords)) { prompts.outro("Cancelled"); return } + } + + // Step 5: Channel + const channel = (await prompts.select({ + message: "Outreach channel", + options: [ + { value: "instagram", label: "Instagram DM (Recommended)" }, + { value: "email", label: "Email" }, + { value: "both", label: "Instagram DM + Email" }, + { value: "linkedin", label: "LinkedIn" }, + ], + })) as string + if (prompts.isCancel(channel)) { prompts.outro("Cancelled"); return } + + // Step 6: Frequency + let frequency = args.frequency + if (!frequency) { + frequency = (await prompts.select({ + message: "How often should outreach run?", + options: [ + { value: "hourly", label: "Every hour" }, + { value: "every_4_hours", label: "Every 4 hours (Recommended)" }, + { value: "daily", label: "Daily" }, + { value: "weekly", label: "Weekly" }, + ], + })) as string + if (prompts.isCancel(frequency)) { prompts.outro("Cancelled"); return } + } + + // Step 7: Message style + const messageStyle = (await prompts.select({ + message: "Message style", + options: [ + { value: "friendly", label: "Friendly & casual" }, + { value: "professional", label: "Professional & direct" }, + { value: "custom", label: "Custom (write your own)" }, + ], + })) as string + if (prompts.isCancel(messageStyle)) { prompts.outro("Cancelled"); return } + + let messageTemplate = "" + if (messageStyle === "custom") { + messageTemplate = (await prompts.text({ + message: "Enter your DM template (use {name}, {company} for personalization)", + placeholder: "Hey {name}! I saw your page and loved...", + })) as string + if (prompts.isCancel(messageTemplate)) { prompts.outro("Cancelled"); return } + } else { + const templates: Record = { + friendly: `Hey {name}! Love what you're doing with {company}. We're building something that could help you reach more people in ${location || "your area"} — would love to show you. What are you working on?`, + professional: `Hi {name}, I came across {company} and think there's a strong fit with what we're building. We help businesses like yours automate outreach and grow their audience. Would you be open to a quick chat?`, + } + messageTemplate = templates[messageStyle] || templates.friendly + } + + // ──── BUILD CAMPAIGN ──────────────────────────────── + + const spinner = prompts.spinner() + const slug = vertical.toLowerCase().replace(/[^a-z0-9]+/g, "-") + const campaignName = `${vertical.charAt(0).toUpperCase() + vertical.slice(1)} Outreach${location ? ` - ${location}` : ""}` + + // 1. Create Board (Bloq) + spinner.start("Creating board...") + try { + const bloqRes = await irisFetch(`/api/v1/users/${userId}/bloqs`, { + method: "POST", + body: JSON.stringify({ + name: campaignName, + type: "user", + }), + }) + if (!(await handleApiError(bloqRes, "Create board"))) { spinner.stop("Failed", 1); prompts.outro("Done"); return } + const bloq = ((await bloqRes.json()) as any)?.data ?? (await bloqRes.json()) + const boardId = bloq.id + spinner.stop(success(`Board #${boardId}: ${campaignName}`)) + + // 2. Create Outreach Strategy + spinner.start("Creating outreach strategy...") + const strategyName = `${vertical.charAt(0).toUpperCase() + vertical.slice(1)} Outreach | V1` + const steps = [] + + if (channel === "instagram" || channel === "both") { + steps.push({ + title: "Instagram DM", + type: "instagram", + instructions: messageTemplate, + delay_hours: 0, + }) + steps.push({ + title: "Follow-up DM", + type: "instagram", + instructions: `Hey {name}, just following up on my last message! Would love to connect about {company}. Let me know if you're interested.`, + delay_hours: 48, + }) + } + + if (channel === "email" || channel === "both") { + steps.push({ + title: "Email Outreach", + type: "email", + instructions: `Subject: Quick question about {company}\n\n${messageTemplate}`, + delay_hours: channel === "both" ? 72 : 0, + }) + } + + if (channel === "linkedin") { + steps.push({ + title: "LinkedIn Connection", + type: "linkedin", + instructions: messageTemplate, + delay_hours: 0, + }) + } + + const strategyRes = await irisFetch(`/api/v1/users/${userId}/bloqs/${boardId}/outreach-strategies`, { + method: "POST", + body: JSON.stringify({ + name: strategyName, + category: "cold_outreach", + icon: "fab fa-instagram", + steps, + }), + }) + const strategyOk = await handleApiError(strategyRes, "Create strategy") + if (strategyOk) { + const strategy = ((await strategyRes.json()) as any)?.data + spinner.stop(success(`Strategy: ${strategyName} (${steps.length} steps)`)) + } else { + spinner.stop(dim("Strategy creation skipped (API may not support it)")) + } + + // 3. Create Schedule + spinner.start("Creating schedule...") + const cronMap: Record = { + hourly: "0 * * * *", + every_4_hours: "0 */4 * * *", + daily: "0 9 * * *", + weekly: "0 9 * * 1", + } + + // Create cloud schedule + const scheduleRes = await irisFetch(`/api/v1/users/${userId}/bloqs/scheduled-jobs`, { + method: "POST", + body: JSON.stringify({ + agent_id: 11, + task_name: campaignName, + prompt: `${slug} limit=15 warmup=1`, + time: "09:00", + frequency, + timezone: "America/Chicago", + data: { + type: "hive_task_dispatch", + campaign_id: slug, + params: { + boardId: String(boardId), + strategy: strategyName, + igAccount, + location, + keywords, + channel, + }, + }, + }), + }) + if (await handleApiError(scheduleRes, "Create schedule")) { + const sched = ((await scheduleRes.json()) as any)?.data + spinner.stop(success(`Schedule: ${frequency} (${cronMap[frequency] || frequency})`)) + } else { + spinner.stop(dim("Schedule creation skipped")) + } + + // 4. Summary + printDivider() + printKV("Campaign", campaignName) + printKV("Board ID", boardId) + printKV("Vertical", vertical) + printKV("IG Account", `@${igAccount}`) + printKV("Location", location || "—") + printKV("Keywords", keywords || "—") + printKV("Channel", channel) + printKV("Frequency", frequency) + printKV("Strategy", strategyName) + printKV("Message", messageTemplate.slice(0, 80) + "...") + printDivider() + + // 5. Offer to save IG session + if (channel === "instagram" || channel === "both") { + const saveSession = await prompts.confirm({ + message: "Save Instagram session now? (opens browser for login)", + }) + if (saveSession) { + prompts.log.info("Opening browser for Instagram login...") + try { + execSync( + `IG_ACCOUNT=${igAccount} IG_USERNAME=${igAccount} npx playwright test tests/e2e/save-instagram-session.spec.ts --headed --timeout 300000`, + { cwd: join(homedir(), "Sites", "freelabel"), stdio: "inherit" } + ) + prompts.log.info(success("IG session saved")) + } catch { + prompts.log.warn("Session save failed or was cancelled. Run later:") + prompts.log.info(dim(`IG_ACCOUNT=${igAccount} npm run som:save-session`)) + } + } else { + prompts.log.info(dim(`Save later: IG_ACCOUNT=${igAccount} npm run som:save-session`)) + } + } + + // 6. Offer to add to som-config.js + prompts.log.info("") + prompts.log.info(dim(`To add to campaign registry:`)) + prompts.log.info(dim(` Edit tests/e2e/som-config.js and add:`)) + prompts.log.info(dim(` ${slug}: { boardId: '${boardId}', strategy: '${strategyName}', igAccount: '${igAccount}', active: true, label: '${campaignName}', color: '\\x1b[36m' }`)) + + prompts.log.info("") + prompts.log.info(dim("Monitor: iris bridge runs")) + prompts.log.info(dim(`Leads: iris leads list --bloq-id ${boardId}`)) + prompts.outro(success("Campaign created")) + + } catch (err) { + spinner.stop("Error", 1) + prompts.log.error(err instanceof Error ? err.message : String(err)) + prompts.outro("Done") + } + }, +}) + +// ============================================================================ +// iris campaign list — show all campaigns from som-config.js +// ============================================================================ + +const CampaignListCommand = cmd({ + command: "list", + aliases: ["ls"], + describe: "list all outreach campaigns", + async handler() { + UI.empty() + prompts.intro("◈ Campaigns") + + // Try to read som-config.js + const configPaths = [ + join(homedir(), "Sites", "freelabel", "tests", "e2e", "som-config.js"), + join(process.cwd(), "tests", "e2e", "som-config.js"), + ] + + let config: any = null + for (const p of configPaths) { + if (existsSync(p)) { + try { + delete require.cache[require.resolve(p)] + config = require(p) + break + } catch {} + } + } + + if (!config?.campaigns) { + prompts.log.warn("som-config.js not found. Run from the freelabel project root.") + prompts.outro("Done") + return + } + + printDivider() + for (const [id, c] of Object.entries(config.campaigns) as [string, any][]) { + const status = c.active + ? `${UI.Style.TEXT_SUCCESS}● active${UI.Style.TEXT_NORMAL}` + : dim("○ inactive") + const ig = c.igAccount ? `@${c.igAccount}` : "—" + console.log(` ${bold(c.label || id)} ${status} ${dim(ig)} ${dim(`board:${c.boardId}`)}`) + console.log(` ${dim(`Strategy: ${c.strategy}`)}`) + console.log() + } + printDivider() + + prompts.log.info(dim("iris campaign create — create a new campaign")) + prompts.outro("Done") + }, +}) + +// ============================================================================ +// Root command +// ============================================================================ + +export const PlatformCampaignCommand = cmd({ + command: "campaign", + aliases: ["campaigns"], + describe: "manage outreach campaigns — create, list, monitor", + builder: (yargs) => + yargs + .command(CampaignCreateCommand) + .command(CampaignListCommand) + .demandCommand(), + async handler() {}, +}) diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 8ef14b570f0b..51d3a024d080 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -67,6 +67,7 @@ import { PlatformBugCommand } from "./cli/cmd/platform-bug" import { PlatformAtlasMeetingsCommand } from "./cli/cmd/platform-atlas-meetings" import { PlatformAtlasBrandKitCommand } from "./cli/cmd/platform-atlas-brand-kit" import { PlatformLeadsMeetingCommand } from "./cli/cmd/platform-leads-meeting" +import { PlatformCampaignCommand } from "./cli/cmd/platform-campaign" import { PlatformDaemonCommand } from "./cli/cmd/platform-daemon" import { PlatformOnboardCommand } from "./cli/cmd/platform-onboard" import { PlatformProposalsCommand } from "./cli/cmd/platform-proposals" @@ -93,6 +94,7 @@ import { PlatformVoiceCommand } from "./cli/cmd/platform-voice" // TODO: platform-mail and platform-imessage commands (not yet implemented) // import { PlatformMailCommand } from "./cli/cmd/platform-mail" // import { PlatformImessageCommand } from "./cli/cmd/platform-imessage" +import { PlatformCalendarCommand } from "./cli/cmd/platform-calendar" import { PlatformWalletCommand } from "./cli/cmd/platform-wallet" import { PlatformConfigCommand } from "./cli/cmd/platform-config" import { PlatformAppCommand } from "./cli/cmd/platform-app" @@ -229,6 +231,7 @@ const cli = yargs(rawArgs) .command(reg(PlatformAtlasMeetingsCommand)) .command(reg(PlatformAtlasBrandKitCommand)) .command(reg(PlatformLeadsMeetingCommand)) + .command(reg(PlatformCampaignCommand)) .command(reg(PlatformDaemonCommand)) .command(reg(PlatformOnboardCommand)) .command(reg(PlatformProposalsCommand)) @@ -254,6 +257,7 @@ const cli = yargs(rawArgs) .command(reg(PlatformVoiceCommand)) // .command(reg(PlatformMailCommand)) // TODO: not yet implemented // .command(reg(PlatformImessageCommand)) // TODO: not yet implemented + .command(reg(PlatformCalendarCommand)) .command(reg(PlatformWalletCommand)) // PHP-port commands — config, app, automation, automation:test // (integrations is owned by PlatformRunCommand in platform-run.ts) From 47707cbe7e9952ba6f0d561438fd59073b69acf3 Mon Sep 17 00:00:00 2001 From: Alexander Mayo Date: Tue, 14 Apr 2026 19:37:03 -0500 Subject: [PATCH 2/2] fix: auth login now targets Railway DB (raichu.heyiris.io) instead of sunsetted DO iris auth login was creating API tokens on apiv2.heyiris.io (old DigitalOcean) while all subsequent API calls went to raichu.heyiris.io (Railway). Tokens created on the wrong DB caused immediate 401 on copycat and other commands. Fixes bug #53000 and #53001. Co-Authored-By: Claude Opus 4.6 (1M context) --- .opencode/skill/iris-platform/SKILL.md | 2 +- install | 14 +++++++------- install.ps1 | 2 +- packages/opencode/src/cli/cmd/auth.ts | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.opencode/skill/iris-platform/SKILL.md b/.opencode/skill/iris-platform/SKILL.md index bde93ea2a5cf..5b4325184585 100644 --- a/.opencode/skill/iris-platform/SKILL.md +++ b/.opencode/skill/iris-platform/SKILL.md @@ -139,7 +139,7 @@ iris schedules list --agent-id |----------|---------| | `IRIS_API_KEY` | Authentication token (or use `iris auth login`) | | `IRIS_USER_ID` | Your numeric user ID (needed for agents, bloqs, workflows) | -| `IRIS_FL_API_URL` | Override API base (default: `https://apiv2.heyiris.io`) | +| `IRIS_FL_API_URL` | Override API base (default: `https://raichu.heyiris.io`) | | `IRIS_API_URL` | Override IRIS API URL (default: `https://iris-api.heyiris.io`) | --- diff --git a/install b/install index 11f4177a6d20..ab7707e3e555 100755 --- a/install +++ b/install @@ -1049,7 +1049,7 @@ case "${1:-status}" in -H "Authorization: Bearer $_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"name\": \"$_HLABEL\", \"capabilities\": {\"docker\": $_HDOCK}}" \ - "https://apiv2.heyiris.io/api/v1/hive/register-node" 2>/dev/null || echo '{}') + "https://raichu.heyiris.io/api/v1/hive/register-node" 2>/dev/null || echo '{}') _NKEY=$(echo "$_RESP" | jq -r '.node_key // empty' 2>/dev/null || \ echo "$_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('node_key',''))" 2>/dev/null || \ echo "$_RESP" | sed -n 's/.*"node_key"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p') @@ -1126,7 +1126,7 @@ case "${1:-status}" in -H "Authorization: Bearer $SDK_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"name\": \"$HOSTNAME_LABEL\", \"capabilities\": {\"docker\": $HAS_DOCKER}}" \ - "https://apiv2.heyiris.io/api/v1/hive/register-node" 2>/dev/null || echo "000") + "https://raichu.heyiris.io/api/v1/hive/register-node" 2>/dev/null || echo "000") RESP=$(cat "$REG_TMP" 2>/dev/null || echo '{}'); rm -f "$REG_TMP" NODE_KEY=$(echo "$RESP" | jq -r '.node_key // empty' 2>/dev/null || \ echo "$RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('node_key',''))" 2>/dev/null || \ @@ -1211,7 +1211,7 @@ CYAN='\033[0;36m'; GREEN='\033[0;32m'; RED='\033[0;31m'; MUTED='\033[0;2m'; NC=' IRIS_DIR="$HOME/.iris" SDK_ENV="$IRIS_DIR/sdk/.env" CONFIG_JSON="$IRIS_DIR/config.json" -API_BASE="https://apiv2.heyiris.io" +API_BASE="https://raichu.heyiris.io" # Parse flags PRE_TOKEN="" ; PRE_USER_ID="" @@ -2071,7 +2071,7 @@ ENVFILE -H "Authorization: Bearer $SDK_TOKEN" \ -H "Content-Type: application/json" \ --data-raw "{\"name\": \"$(echo "$HOSTNAME_LABEL" | sed 's/[\"\\]/\\&/g')\", \"capabilities\": {\"docker\": $HAS_DOCKER}}" \ - "https://apiv2.heyiris.io/api/v1/hive/register-node" 2>/dev/null || echo '{}') + "https://raichu.heyiris.io/api/v1/hive/register-node" 2>/dev/null || echo '{}') NODE_KEY=$(json_field "$NODE_RESPONSE" "node_key") if [ -n "$NODE_KEY" ] && [ "$NODE_KEY" != "null" ]; then mkdir -p "$HOME/.iris" @@ -2106,7 +2106,7 @@ if [ "$tty_available" = "true" ] && [ -z "$pre_token" ]; then echo -e "" echo -e "${MUTED} Sending verification code to ${NC}${USER_EMAIL}${MUTED}...${NC}" - SEND_RESPONSE=$(curl -s --max-time 15 -X POST https://apiv2.heyiris.io/api/v1/auth/send-login-code \ + SEND_RESPONSE=$(curl -s --max-time 15 -X POST https://raichu.heyiris.io/api/v1/auth/send-login-code \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ --data-raw "{\"email\": \"$(echo "$USER_EMAIL" | sed 's/[\"\\]/\\&/g')\", \"method\": \"with_login_code\", \"expiration_minutes\": 30, \"auto_create\": true}" 2>/dev/null || echo '{"success":false,"error":"Network error"}') @@ -2134,7 +2134,7 @@ if [ "$tty_available" = "true" ] && [ -z "$pre_token" ]; then echo -e "" echo -e "${MUTED} Verifying...${NC}" - LOGIN_RESPONSE=$(curl -s --max-time 15 -X POST https://apiv2.heyiris.io/api/v1/auth/login-with-code \ + LOGIN_RESPONSE=$(curl -s --max-time 15 -X POST https://raichu.heyiris.io/api/v1/auth/login-with-code \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ --data-raw "{ @@ -2180,7 +2180,7 @@ ENVFILE -H "Authorization: Bearer $SDK_TOKEN" \ -H "Content-Type: application/json" \ --data-raw "{\"name\": \"$(echo "$HOSTNAME_LABEL" | sed 's/[\"\\]/\\&/g')\", \"capabilities\": {\"docker\": $HAS_DOCKER}}" \ - "https://apiv2.heyiris.io/api/v1/hive/register-node" 2>/dev/null || echo '{}') + "https://raichu.heyiris.io/api/v1/hive/register-node" 2>/dev/null || echo '{}') NODE_KEY=$(json_field "$NODE_RESPONSE" "node_key") if [ -n "$NODE_KEY" ] && [ "$NODE_KEY" != "null" ]; then mkdir -p "$HOME/.iris" diff --git a/install.ps1 b/install.ps1 index e19c064bc1cd..24201ec88517 100644 --- a/install.ps1 +++ b/install.ps1 @@ -290,7 +290,7 @@ $ErrorActionPreference = "Stop" $IRIS_DIR = "$env:USERPROFILE\.iris" $SDK_ENV = "$IRIS_DIR\sdk\.env" $CONFIG_JSON = "$IRIS_DIR\config.json" -$API_BASE = "https://apiv2.heyiris.io" +$API_BASE = "https://raichu.heyiris.io" function Get-JsonField { param([string]$Json, [string]$Field) diff --git a/packages/opencode/src/cli/cmd/auth.ts b/packages/opencode/src/cli/cmd/auth.ts index 89c65a42b280..0b8e2967e2af 100644 --- a/packages/opencode/src/cli/cmd/auth.ts +++ b/packages/opencode/src/cli/cmd/auth.ts @@ -222,7 +222,7 @@ export const AuthListCommand = cmd({ const IRIS_DIR = path.join(os.homedir(), ".iris") const SDK_ENV_PATH = path.join(IRIS_DIR, "sdk", ".env") -const IRIS_AUTH_API = process.env.IRIS_AUTH_API_URL ?? "https://apiv2.heyiris.io" +const IRIS_AUTH_API = process.env.IRIS_AUTH_API_URL ?? "https://raichu.heyiris.io" async function irisLoginStatus(): Promise<{ authenticated: boolean; token?: string; userId?: string }> { try {