Skip to content

aloewright/harborline

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

41 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Fly

Fly is an AI chat product with two halves:

  1. A Cloudflare Worker that serves a thin web UI, authenticates users, persists conversations, and brokers model calls through Cloudflare AI Gateway.
  2. Native iOS and macOS apps (SwiftUI) that talk to the Worker over a small REST surface and additionally run a local on-device "operator" path against Apple Foundation Models.

The product is Fly. The Cloudflare worker name and a handful of physical resources still carry the legacy cloudchat identifier; see CTO review notes for the migration list.

Architecture at a glance

┌─────────────────────────────┐         ┌─────────────────────────────┐
│  iOS / macOS (SwiftUI)      │         │  Web (React + Kumo)         │
│  CloudChatApp/  Views/      │         │  src/app.tsx                │
│  Packages/HarborlineOperator│         │  src/client.tsx             │
└──────────────┬──────────────┘         └──────────────┬──────────────┘
               │                                       │
               │  REST + WebSocket                     │  WebSocket via agents/react
               ▼                                       ▼
        ┌─────────────────────────────────────────────────────┐
        │  Cloudflare Worker  (src/server.ts)                  │
        │   - ChatAgent Durable Object (AIChatAgent)           │
        │   - /api/native/chat, /api/models, /api/tts/*        │
        │   - /api/auth/*  (better-auth: magic link + Apple)   │
        └──────┬───────────────┬──────────────┬───────────┬────┘
               │               │              │           │
               ▼               ▼              ▼           ▼
            D1 (chats,       R2          Vectorize     AI binding
            users,         (uploads)    (memory)     → AI Gateway
            sessions)                                  dynamic routes

All model traffic goes through Cloudflare AI Gateway dynamic routes (text, image, audio, video, embedding) — no direct provider SDK calls. See wrangler.jsonc for the route bindings.

Quick start

npm install
npm run dev               # vite + wrangler dev → http://localhost:5173
npm run db:migrate:local  # apply migrations to local D1

For the native apps:

xcodegen generate         # regenerate CloudChat.xcodeproj from project.yml
open CloudChat.xcodeproj  # build "CloudChat iOS" or "CloudChat macOS"

The iOS scheme is CloudChat iOS; the macOS scheme is CloudChat macOS. The user-visible product name on both is Fly (PRODUCT_NAME and CFBundleDisplayName in project.yml).

Project layout

src/
  server.ts          ChatAgent Durable Object + HTTP routes (large; see review notes)
  app.tsx            React chat UI (Kumo + Streamdown)
  client.tsx         React entry
  styles.css         Tailwind + Kumo styles
migrations/
  0001_*.sql         D1 schema (chats, uploads, settings, generation_requests, auth tables)
public/              static assets served by Worker
wrangler.jsonc       Worker config: bindings, vars, DO migrations
project.yml          xcodegen spec for the Apple targets
CloudChatApp/        SwiftUI app sources (entry + model + theme + views)
CloudChatShared/     CloudChatClient (REST actor) + CloudChatModels (Codable types)
CloudChatIntents/    AppIntents shortcuts (Open / New chat / Voice / Run prompt / Save note)
Packages/            HarborlineOperator local Swift package (SharedModels, AIChatCore,
                     AIOrchestrator, AIMemory, AITools, AppIntentBridge, AutomationBridgeMac)
docs/                privacy, QA smoke tests, deploy, animation, UI references
fastlane/            release automation (Cloudflare bootstrap, build, screenshots, TestFlight)

What the iOS/macOS app does

  • Chat with cloud (Worker → AI Gateway) or local (OperatorRuntimeAdapter → Foundation Models) routing, toggled by localOperatorEnabled in settings.
  • Projects + threads with archive/restore, starring, forking from any assistant message.
  • Attachments: camera, photo library, files, audio, plus a parallel "system prompt context" upload set that travels with the system prompt.
  • Voice in two layers: live dictation (SpeechRecognizer + AVAudioEngine) and TTS playback (/api/tts/gateway with local AVSpeechSynthesizer fallback).
  • Auth via magic link or Sign in with Apple, brokered by the Worker's better-auth mount.
  • Operator runtime (orchestrator → tool registry → approval manager → audit log) surfaced through ExecutionRailView on wide screens.
  • App Intents / Shortcuts: Open Fly, New chat, Voice prompt, Run prompt in workspace, Save memory note. Bridged into the running app through AppIntentBridgeStore.
  • 16 themes (AppThemeChoice) including Apple system colors and a TweakCN set. Default is fly.

Deploy

npm run deploy            # vite build + wrangler deploy
fastlane bootstrap_cloudflare
fastlane build_ios
fastlane build_macos
fastlane upload_testflight

bootstrap_cloudflare checks Wrangler auth, refreshes Worker types, and uploads any secrets listed in CLOUDFLARE_SECRET_NAMES from Doppler or .env.local. The App Store Connect API key is auto-discovered from APP_STORE_CONNECT_API_KEY_PATH, ASC_KEY_PATH, or a local AuthKey*.p8.

See docs/manual-cloudflare-deploy.md and docs/release-checklist.md for step-by-step.

QA

./scripts/qa/smoke-cloudchat.sh app    # local app smoke
SKIP_WRANGLER=1 ./scripts/qa/smoke-cloudchat.sh app    # skip wrangler dry-run

docs/qa-smoke-tests.md lists the full backend, app, and release smoke checks. UI references for the visual smoke pass live in docs/ui-reference-requirements.md.

CTO review notes

The product brand is Fly, but several deployment-level identifiers still read cloudchat. They are intentionally not renamed in this pass because each one carries a real migration cost:

Identifier Where Rename cost
CloudChat.xcodeproj and target names CloudChat iOS / CloudChat macOS project.yml, Fastlane Renaming the project breaks Xcode history, derived data, and any external CI references. The schemes are user-visible only to developers.
Bundle IDs com.wplus.cloudchat.ios / com.wplus.cloudchat.macos project.yml Once the apps ship to TestFlight under these IDs they cannot be changed without re-listing. Coordinate with App Store Connect before flipping.
Swift symbols CloudChatApp, CloudChatAppModel, CloudChatTheme, CloudChatClient, CloudChatModels, CloudChatRootView etc. CloudChatApp/, CloudChatShared/ Internal symbols, not user-visible. Renaming touches every source file; do it as a dedicated PR.
Worker name cloudchat and deployed URL cloudchat.lazee.workers.dev wrangler.jsonc, CloudChatConfiguration.defaultWorkerBaseURL Renaming the worker creates a new URL and requires updating shipped clients or putting a custom domain in front. Recommend: assign fly.lazee.workers.dev (or a custom domain) and migrate clients before deprecating the old worker.
D1 database cloudchat (d5f6e560-…) wrangler.jsonc The physical database is referenced by ID, not name; the name is cosmetic but a true rename means a new D1 + data migration.
R2 bucket cloudchat-uploads wrangler.jsonc Bucket renames in R2 require migrating objects. Defer.
Vectorize index cloudchat-memory wrangler.jsonc Indexes are immutable on rename; would require rebuilding embeddings. Defer.
UserDefaults keys cloudchat.appearance / cloudchat.theme / scene key cloudchat.executionRailVisible CloudChatAppModel, CloudChatTheme, CloudChatRootView Renaming would lose existing preferences across an update. Migrate with a one-time UserDefaults copy if we change keys.

Suggested cleanup follow-ups

  1. CloudChatAppModel.swift is 1,094 lines and currently owns appearance, themes, projects, threads, attachments, model selection, search options, voice, dictation, TTS, sending, errors, sign-in, magic link, Apple sign-in, operator runtime, approvals, audit logs, transient status, App Intents bridge, and system-prompt context. Split along feature boundaries (e.g. ProjectsModel, ChatModel, AuthModel, VoiceModel).
  2. src/server.ts is 2,255 lines. Split ChatAgent, route handlers, and better-auth plumbing into separate modules under src/.
  3. src/app.tsx is 947 lines. The MCP panel, message rendering, and composer are all colocated; they're each independently testable.
  4. SidebarView hardcodes the user as "Alex Smith". Wire it up to the authenticated user (or a viewer value loaded from /api/auth/me).
  5. OperatorRuntimeAdapter is a struct with stateful in-memory stores (InMemoryStore, InMemoryConversationStore, InMemoryApprovalManager, InMemoryExecutionAuditLog). Today every runPrompt call instantiates a fresh orchestrator but reuses the same stores via the struct's properties — this works because InMemory* are reference types, but the struct semantics are misleading. Make it an actor or a class to make the lifetime explicit.
  6. CloudChatClient.send returns the entire response as one ChatMessage instead of streaming. Worker side already supports streaming via AIChatAgent over WebSocket; bring streaming to the native path.
  7. ChatLayout constants and animation tokens are duplicated between Theme.swift (CloudChatMotion) and the views. Centralize.

Use a different AI Gateway route

The app uses Cloudflare AI Gateway dynamic routes — never a raw provider model id. Bindings live in wrangler.jsonc under vars.DEFAULT_*_ROUTE. Inside the Worker:

env.AI.run("dynamic/text_gen", { messages }, { gateway: { id: "x" } });

Adding a new route is a wrangler.jsonc edit plus a new env.d.ts regen (npm run types).

Learn more

License

MIT

About

Local-first Apple AI operator for iPhone, iPad, and Mac

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors