From b3038ca702837af05e49d6aba3a03ad8ce458246 Mon Sep 17 00:00:00 2001 From: "Evan D. Schoenberg, M.D." Date: Fri, 16 Oct 2026 15:17:26 -0400 Subject: [PATCH] A map of the repository, written where a session will read it The repository has had no CLAUDE.md, AGENTS.md or CONTRIBUTING, and never has. Its conventions are consistent and nowhere written down: a commit title that is a sentence, a body that says what was checked, a header on every script, a library that arrives with its script and its table row. A session that starts without them guesses, and guesses generic. Five files, and the split is deliberate. The one at the root is what every session needs before it knows where to go: what the application is, where each layer lives, how it is built and checked, and what a change has to satisfy. The other four sit in Source/, Frameworks/Adium, Plugins/Purple Service and Dependencies/ and say how that part is put together: the launch order, the model classes and what a feature implements, the two ways a service is registered and every key a descriptor can carry, the phases of the dependency build and which of them are dead. Claude Code loads a CLAUDE.md in a subdirectory only once a file there is opened, so a session pays for the 91 lines at the root on arrival and for the other 260 only as it enters an area. Everything stated was read from the tree rather than remembered: the controller list from AIAdiumProtocol.h, the launch order from AIAdium.m, the component list from AICoreComponentLoader.m, the service registrations from CBPurpleServicePlugin.m, the descriptor keys from the plist, the phase order from build.sh. Every class, file and script the five files name was checked to exist. What is true today and may not be next month, such as which test fails, was left out on purpose. Co-Authored-By: Claude Fable 5.1 --- CLAUDE.md | 91 ++++++++++++++++++++++++++++++++ Dependencies/CLAUDE.md | 49 +++++++++++++++++ Frameworks/Adium/CLAUDE.md | 64 ++++++++++++++++++++++ Plugins/Purple Service/CLAUDE.md | 77 +++++++++++++++++++++++++++ Source/CLAUDE.md | 70 ++++++++++++++++++++++++ 5 files changed, 351 insertions(+) create mode 100644 CLAUDE.md create mode 100644 Dependencies/CLAUDE.md create mode 100644 Frameworks/Adium/CLAUDE.md create mode 100644 Plugins/Purple Service/CLAUDE.md create mode 100644 Source/CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..1e77b2f21 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,91 @@ +# Adium, revival fork + +Adium is a multi-protocol chat client for macOS, written in Objective-C on top +of libpurple. This fork brings it to macOS 13 and arm64 and adds the services +people use now. The README says what it does and how to build it. This file +says where things are and what a change has to satisfy. Four folders carry a +CLAUDE.md of their own that says how that part is put together; it loads when a +file there is opened, so read it before changing anything in that folder. + +## Where things are + +Source/ is the application: the `adium` singleton with one controller per +domain, the contact list window, the preferences window, OTR, voice notes, +logging, and most of the older features, which are small classes called +*Plugin compiled into the application. See Source/CLAUDE.md. + +Frameworks/Adium is Adium.framework: the model (AIAccount, AIListContact, +AIChat, AIContentMessage and their relatives), the protocol each controller +implements, the view the contact list is drawn with, and the loader for +.AdiumPlugin bundles. See Frameworks/Adium/CLAUDE.md. Frameworks/AIUtilities +is Cocoa categories and generic views that know nothing about chat; its +xcconfigs/ folder holds the build settings for every target. + +Plugins/ is the rest of the features, one folder each, compiled into the +application and named in Source/AICoreComponentLoader.m: the message windows +and their tabs (Dual Window Interface), the conversation rendered in a +WKWebView (WebKit Message View), the padlock and encryption menu (Secure +Messaging), JavaScript xtras, Typst formulas, emoticons, the status menu +item and the contact alerts. Plugins/Purple Service is different: it builds +the AdiumLibpurple framework, the bridge to libpurple, and every protocol +lives in it or behind it. See Plugins/Purple Service/CLAUDE.md. PurplePlugins/ +holds the prebuilt third-party protocol plug-ins that bridge loads. + +Frameworks/ also carries the prebuilt arm64 libraries the application links, +and Dependencies/ is what fetches and builds them. See Dependencies/CLAUDE.md. + +Resources/ is nibs, images, default plists, Adium.sdef, the .lproj folders +and the bundled xtras (Message Styles, Emoticons, Sounds, Dock Icons, Status +Icons, Service Icons, Menu Bar Icons, Contact List, Scripts). What the user +installs, with accounts and logs, is under ~/Library/Application Support/Adium +2.0. UnitTests/ is the XCTest target. Testing/ is harnesses run by hand and +Utilities/ is developer tools; both READMEs are generated from the scripts' +headers. Other/XtrasCreator is the companion application for making xtras, +and AdiumHelp/ is the help book. + +## Building and checking + +Build with ./install.sh --build-only, which is what continuous integration +runs: it fetches what the repository does not carry, builds, checks that every +protocol plug-in registers with the bundled libpurple, and verifies the +application's signature. Xcode alone skips the fetch and fails at a header it +cannot find. The Makefile and Release/ are leftovers of the upstream project. + +No scheme runs the unit tests. They build and run by hand: + + xcodebuild -project Adium.xcodeproj -target "Unit tests" -configuration Release \ + -destination "platform=macOS,arch=arm64" SYMROOT="$PWD/build" \ + OBJROOT="$PWD/build/Intermediates" CODE_SIGN_IDENTITY=- \ + CODE_SIGN_STYLE=Manual DEVELOPMENT_TEAM= build + DYLD_FRAMEWORK_PATH="$PWD/build/Release" xcrun xctest "build/Release/Unit tests.xctest" + +Continuous integration builds, verifies the plug-ins and the licences, and +runs nothing else. Files join the project by editing +Adium.xcodeproj/project.pbxproj; there is no workspace, package or Podfile. +/Applications/Adium.app is often a symbolic link into build/Debug, so a +script that builds for its own purposes builds somewhere else. + +## What a change has to satisfy + +The code is Objective-C under ARC. No Swift, CocoaPods or Swift Package +Manager; the deployment target and architecture are set once in +Base.xcconfig; nothing the application needs at runtime comes from Homebrew. + +A new library follows the README's "Adding a library" and Dependencies/CLAUDE.md: +a script that produces it from a pinned source, a row in the README's table +and its licence in the bundle, all in the same change. + +Every script opens with a header saying what it is for and how it is run; +Utilities/script-index.py rewrites the two generated READMEs from those +headers, so a new or renamed script means running it again. + +User-visible strings go through the string tables. Utilities/git-hooks/ +pre-commit checks them; enable it once per clone with +`git config core.hooksPath Utilities/git-hooks`. + +docs/ is ignored by the owner's decision: it holds internal working notes, +and commit c034def91 says why. Do not un-ignore it or commit anything in it. + +A commit's title is a full sentence saying what changed; its body says why, +and what was checked and how. Comments say why, not what. Commits written +with Claude end with a Co-Authored-By trailer naming the model. diff --git a/Dependencies/CLAUDE.md b/Dependencies/CLAUDE.md new file mode 100644 index 000000000..59d940278 --- /dev/null +++ b/Dependencies/CLAUDE.md @@ -0,0 +1,49 @@ +# Dependencies/ + +What Adium links against beyond macOS, and how it got there. Three kinds of +thing, and the distinction is the whole point of this folder. + +Checked in: the prebuilt arm64 frameworks and dylibs under Frameworks/ and +the protocol plug-ins under PurplePlugins/. A fresh clone builds without ever +running anything here. + +Fetched, by fetch.sh, which install.sh runs before every build: the +MMTabBarView submodule; webrtc/fetch-webrtc.sh, a prebuilt WebRTC.xcframework +checked against a pinned SHA-256 and kept out of git for its size; and +picomemo/fetch-picomemo.sh, which fetches and builds the cryptographic half +of OMEMO from a pinned commit. All three are idempotent and quiet when there +is nothing to do. + +Built, by build.sh, from Dependencies/ and only when a library has to move: +it sources phases/ in order (utility.sh for fetching and checksums, +build_dependencies.sh for the base libraries, then libffi, the image codecs, +OpenSSL, the OTR trio of gpg-error, gcrypt and libotr, libpurple, and +make_frameworks.sh, which turns the dylibs into frameworks through +framework_maker/frameworkize.py with the *-Info.plist files here). +build_other.sh and build_vv_dependencies.sh are not sourced; they are +upstream leftovers. Every source is fetched from a pinned address and checked +against a recorded SHA-256 before it unpacks, and the version each library is +stamped with must match Base.xcconfig. copy_frameworks.sh puts the output +where the project looks for it. opus/build-opus.sh builds the voice note +codec separately; rebuild-whatsmeow.sh rebuilds the WhatsApp plug-in; +refresh-libpurple-framework.sh swaps a freshly built libpurple into the +framework the application loads. patches/ holds every local change on top of +a library's release, one folder per library, with a README where the change +needs explaining. The build needs autoconf, automake, libtool, pkg-config, +meson, cmake and ninja from Homebrew and takes the better part of an hour. + +build/, source/ and Frameworks/ inside this folder are build material and +ignored; source/ runs to gigabytes and some scripts here expect it, so do not +clear it casually. + +## Adding or moving a library + +The rule is in the README under "Adding a library", and the test is one line: +every path under Frameworks/ or Dependencies/build named in an xcconfig must +be produced by a script here, and listed in the README's table of bundled +binaries or among the three fetched things. The script, the binary and the +table row land in the same change. Every shipped binary also carries its +licence in the bundle: Utilities/collect-licenses.sh gathers them from the +source trees or the pinned release URL, and Utilities/verify-licenses.sh +checks the built application, which continuous integration runs. A library +added without its licence fails there. diff --git a/Frameworks/Adium/CLAUDE.md b/Frameworks/Adium/CLAUDE.md new file mode 100644 index 000000000..90448db7d --- /dev/null +++ b/Frameworks/Adium/CLAUDE.md @@ -0,0 +1,64 @@ +# Adium.framework + +The model, the protocols the application's controllers implement, and the +views the contact list is drawn with. Everything else links it: Source/, +every folder under Plugins/, and the AdiumLibpurple framework. Nothing here +talks to a network, and nothing here is a controller; a protocol lives in +Plugins/Purple Service and a controller in Source/. + +## The model + +AIService is a protocol and AIAccount an account on it. AIAbstractAccount +holds the mechanics of connecting, reconnecting and storing settings; +AIAccount is the part a protocol's subclass overrides. AIAccountPlan describes +an account's settings form as a list of fields, each saying once where its +value lives, and AIAccountPlanFormBuilder lays that out on an AISettingsFormView. + +AIListObject is anything that can appear in the contact list. AIListContact +is one person on one account; AIMetaContact is several of those seen as one +person; AIListGroup is a group; AIListBookmark is a saved group chat; +AIContactList is the root. AIProxyListObject stands for one object in one +place in the outline view, which is how a contact appears in two groups. +AIContactObserverManager is how a change to a list object reaches everything +that displays it. + +AIChat is a conversation, with one contact or a room. What passes through it +is an AIContentObject: AIContentMessage for a message, AIContentContext for +one restored from history, and AIContentEvent, AIContentNotification, +AIContentStatus, AIContentTopic and AIContentTyping for the rest. + +AIStatus is a presence state, AIStatusGroup a folder of them, AIStatusItem +their common parent; AIStatusDefines has the keys. + +AIXtraInfo is an installed xtra of any kind; AIEmoticonPack and AISoundSet are +two kinds read into objects. AIServiceIcons, AIStatusIcons and AIUserIcons +hand out images, the last from the *UserIconSource classes. + +## What a feature implements + +AIPlugin is the base class of every component; installPlugin and +uninstallPlugin are the whole contract. A preference pane is an +AIPreferencePane, laid out on an AISettingsFormView, the fork's System +Settings-style form, with AISettingsNavigationController behind the sidebar. +An action a contact alert can take is an AIActionDetailsPane. + +Each controller in Source/ implements one of the AI*ControllerProtocol.h +files here, and AIAdiumProtocol declares `adium`, the object that owns them +all (AISharedAdium.h makes it a global). A component reaches a controller only +through those protocols. + +AICorePluginLoader loads .AdiumPlugin bundles from the application's +Contents/PlugIns and the user's PlugIns folder; the fork's JavaScript xtras +are that kind. The loader for compiled-in components is in Source/. + +## Views and helpers + +The contact list view is AIAbstractListController with AIListOutlineView +(drawing in AIListOutlineView+Drawing) and the AIListCell, AIListContactCell, +AIListGroupCell and AIListRowView classes; the window controllers that hold +one are in Source/. AIAccountMenu, AIContactMenu and AIStatusMenu build the +menus the application shows in several places. AIHTMLDecoder converts between +HTML and attributed strings and is on the path of every message in and out. +AIAddressBookController and AIAddressBookPerson are the Contacts framework +integration. AILocalizationAssistance.h has the AILocalizedString macros, and +the AILocalization* controls size themselves to a translated title. diff --git a/Plugins/Purple Service/CLAUDE.md b/Plugins/Purple Service/CLAUDE.md new file mode 100644 index 000000000..82f130f3d --- /dev/null +++ b/Plugins/Purple Service/CLAUDE.md @@ -0,0 +1,77 @@ +# Plugins/Purple Service, the bridge to libpurple + +This folder builds the AdiumLibpurple framework (target AdiumLibpurple, +Info.plist in Plists/AdiumLibpurple). Everything that speaks a protocol goes +through here: libpurple's own protocols, the prebuilt third-party plug-ins in +PurplePlugins/, and the fork's XMPP work written against the XEPs. + +## The bridge + +SLPurpleCocoaAdapter owns libpurple's core: it brings the core up when the +plug-in loads (it used to wait for the first connection), runs libpurple's +event loop on the main run loop (adiumPurpleEventloop.m) and dispatches into +it. The adiumPurple*.m files are libpurple's ui-ops, one file per subsystem, +mapping C callbacks onto the controllers: accounts, blist (the buddy list), +connection, conversation, core, dns, ft (file transfer), notify, privacy, +request (libpurple's dialogs, made native), roomlist, signals, and +certificate trust, which verifies through SecTrust and asks the user. The +fork added files beside them for what libpurple's jabber plug-in does not do: +bookmarks (XEP-0402), carbons (XEP-0280), CSI (XEP-0352), MUC self-ping +(XEP-0410), fallback indication (XEP-0428), Jingle, OMEMO and WhatsApp. +libpurple_extensions/ssl-cdsa.c is the SSL plug-in on Apple's Security +framework that every connection uses. + +CBPurpleAccount is the account base: an AIAccount whose connection is a +PurpleAccount. Its ForSubclasses category is what a protocol overrides. + +## Services + +A service is registered one of two ways, both in CBPurpleServicePlugin's +installPlugin. First by class: Jabber, Gadu-Gadu, WhatsApp, Novell and SIMPLE, each a PurpleService or AIService +subclass with a CBPurpleAccount subclass beside it (ESJabberService with +ESPurpleJabberAccount, AIWhatsAppService with AIPurpleWhatsAppAccount, and so +on). Then by descriptor: AIPurpleGenericService reads +AIPurpleServiceDescriptors.plist and registers one service for every protocol +libpurple loaded that the class list does not already answer for, each driven +by AIPurpleGenericAccount with no code of its own. That covers Bonjour, +Telegram, IRCv3, Signal, Teams (work and personal) and Matrix. The ESIRCService class remains in the folder but is not registered: IRC is served by the ircv3 plug-in through its descriptor, and the comment at that spot says how to bring the class back. + +A descriptor row carries ServiceID, ServiceClass, ServiceCodeUniqueID, the +names and descriptions, IconBaseName and ServiceImportance, and optionally +UserNameLabel, CaseSensitive, AllowedLength, AllowedCharacters, +ForbiddenCharacters, AccountNameFromUID, AccountNameIncludesHost, +UserNamesAreEmailAddresses, UserNamesArePhoneNumbers, NetworkReachability, +EchoesOutgoingMessages and Experimental. Experimental keeps the service out +of every menu until the user runs + + defaults write com.adiumX.adiumX AIShowExperimentalServices -bool YES + +and hides the offer, not accounts already made (see -isHidden in +AIPurpleGenericService). So a protocol that arrives as a libpurple plug-in is +a .so in PurplePlugins/ plus a row, and nothing else, unless it needs +behaviour of its own, as WhatsApp did. + +## XMPP + +ESPurpleJabberAccount is where the fork's XMPP work hangs, and +ESPurpleJabberAccountPlan is its settings form. The AMPurpleJabber* classes +ride on libpurple's jabber plug-in: ad hoc commands, external service +discovery (XEP-0215, where STUN and TURN come from), HTTP upload (XEP-0363, +used for pictures and voice notes), MAM (XEP-0313), service discovery +browsing and data forms among them. + +OMEMO (XEP-0384): AIOMEMOController per account; AIOMEMOStore for identity, +sessions and skipped keys on disk; AIOMEMOMessage and AIOMEMOStanza for the +wire form; AIOMEMOMedia for encrypted files (XEP-0454); and +adiumPurpleOMEMO.m where libpurple hands messages over. The cryptography is +picomemo, which Dependencies/fetch.sh builds. Testing/omemo checks each piece +against published vectors. + +Calls: AIJingleEngine does SDP and the Jingle transport; AIJingleSessionMachine +is the session state; AIJingleCallManager holds the calls and the ringing +(XEP-0353); AIJingleCallController runs one call on WebRTC; AIJingleCallUI, +AIJingleCallWindowController and AIJingleVideoView are the window; +AIJingleCallDiagnostics is "Check Call Readiness"; adiumPurpleJingle.m is the +ringing messages. Testing/webrtc has the harnesses. + +Resources/ here is the plug-in's own strings, one .lproj per language. diff --git a/Source/CLAUDE.md b/Source/CLAUDE.md new file mode 100644 index 000000000..cd6b2a722 --- /dev/null +++ b/Source/CLAUDE.md @@ -0,0 +1,70 @@ +# Source/, the application + +The Adium target. The `adium` singleton, its controllers, the contact list +and preferences windows, and some seventy small features compiled in. + +## Launch + +main.m starts AIAdium, the application delegate. applicationDidFinishLaunching +creates the AILoginController, which picks the user profile; completeLogin +then allocates the controllers in this order: preference, toolbar, debug, +contact alerts, sound, emoticon, account, contact, chat, content, dock, file +transfer, applescriptability, status (menu and interface come from the nib). +The preference and debug controllers finish loading first; then +AICoreComponentLoader instantiates every component in its list and calls +installPlugin on each; then AICorePluginLoader loads bundles; then the rest +of the controllers get controllerDidLoad, account before contact before +interface, because accounts must exist before contacts and contacts before +the list that shows them. A component may therefore use preferences and +debug logging in installPlugin and nothing else until controllerDidLoad. + +## Controllers + +Each owns one domain and implements the matching protocol in the framework. +AIAccountController: accounts, services and passwords (its categories are the +Adium*.m files: AdiumAccounts, AdiumServices, AdiumPasswords, +AdiumPreferredAccounts). AIContactController: every list object, grouping, +metacontacts and sorting (AIAlphabeticalSort, AIManualSort, ESStatusSort). +AIChatController: open chats. AIContentController: messages in and out, with +filtering, formatting, typing and events in AdiumContentFiltering, +AdiumFormatting, AdiumTyping, AdiumMessageEvents and AdiumChatEvents. +AIInterfaceController: windows, the message view and the contact list. +AIStatusController: presence, with AIAutomaticStatus for idle and away. +AIPreferenceController: defaults and panes, with AIPreferenceContainer for +per-object settings. AIMenuController, AIToolbarController, AISoundController +(AdiumSound, AdiumSoundSets, AdiumSpeech), AIDockController, +AIEmoticonController, ESContactAlertsController, ESFileTransferController, +ESDebugController and AILoginController are what their names say. + +## Where the bigger things are + +The contact list window: AIListController and AIListWindowController, with +AIStandardListWindowController and AIBorderlessListWindowController for the +two window styles; AISCLViewPlugin is the component that owns it. + +The preferences window: AIModernPreferencesWindowController, the fork's, +with AIPreferenceWindowController kept as a thin front to it. Panes are +AIPreferencePane subclasses spread across Source/ and Plugins/. Installed +xtras are managed there too: AIXtrasPreferences, AIXtraDetailsPage, +AIXtrasManager, and XtrasInstaller for the files. + +OTR: AdiumOTREncryption on libotr, with the ESOTR* windows and preferences +and OTRCommon.h. OMEMO is not here; it is in Plugins/Purple Service. + +Voice notes: AIVoiceNotePlugin, AIVoiceNoteShelfView, AIVoiceRecorder and +AIOpusEncoder. + +Logging: AILoggerPlugin writes the XML chat logs; AILogViewerWindowController, +AIChatLog, AILogFromGroup and AILogToGroup read them back; AIXMLAppender and +AIXMLChatlogConverter are the file format. + +Inline pictures in a conversation: AIInlineMediaLinkPlugin. Now-playing +status: ESiTunesPlugin. Global hotkeys: the SG* classes. First run: +AdiumSetupWizard. + +## The components + +Most *Plugin classes here are one feature each, and the fastest way to find +a feature is the list in AICoreComponentLoader.m, then the class it names. +A new feature is a new AIPlugin subclass added to that list. The other +components live in the folders under Plugins/.