Everything the app (and the CLI it spawns) touches outside the repo. The
app itself writes one file (sessions.json); the rest of the on-disk
state belongs to the CLI and is read or edited by the app. Nothing here is
shared across machines.
| Path | Written by | Read by | Format |
|---|---|---|---|
~/Library/Application Support/Spettro/sessions.json |
SessionStore.save (atomic) |
SessionStore.load at launch |
JSON array of StoredSession, ISO-8601 dates |
~/Library/Preferences/to.eyed.Spettro.plist |
UserDefaults.standard (app) |
UserDefaults.standard (app) |
Standard preferences plist (see keys below) |
~/.spettro/config.json |
CLI (atomic write + rename) | SubscriptionStore (watched); CLI |
JSON; app reads spettro_email, spettro_plan, spettro_plan_status |
~/.spettro/memory.md |
CLI + MemoryStore.save/.clear |
CLI at session start + MemoryStore.load |
Plain markdown, one fact per line |
<project>/.spettro/memory.md |
CLI + MemoryStore.save/.clear |
CLI at session start + MemoryStore.load |
Plain markdown, one fact per line |
~/.local/bin/spettro |
Official install script (CLIInstaller runs curl -sSfL https://spettro.app/install | sh) |
CLILocator.candidateURLs() (first candidate) |
Executable binary |
/opt/homebrew/bin/spettro, /usr/local/bin/spettro, ~/go/bin/spettro |
Homebrew / go install (outside the app) |
CLILocator.candidateURLs() |
Executable binary |
~/Desktop/spettro/bin/spettro |
Dev builds of the CLI repo (outside the app) | CLILocator.devCheckoutBinary() — searched before every other candidate |
Executable binary (dev checkout) |
<any PATH dir>/spettro |
Outside the app | CLILocator.candidateURLs() (appended last) |
Executable binary |
The app never modifies ~/.local/bin/spettro — the installed CLI is only
ever created or replaced by the install script. The file pickers in
OnboardingView and SettingsView simply open at ~/.local/bin as a
hint when the user chooses a binary manually.
Built in SessionStore.init() as
FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first
(falling back to the home directory) + Spettro/, created with
intermediate directories, then sessions.json inside it. It is the only
file the app owns outright.
- Write —
SessionStore.save(_:):JSONEncoderwith.iso8601dates,data.write(to:options: .atomic). - Read —
SessionStore.load():JSONDecoderwith.iso8601; any failure (missing or corrupt file) decodes to an empty list, not a crash. - Contents — an array of
StoredSession:id,acpSessionId(the CLI's session id, needed forsession/resume),projectPath,title,createdAt,isPinned,isArchived, the transcript as[StoredItem](messages with roleuser | assistant | reasoning | notice, optional base image attachments inlined asdata, and tool calls with diffs, locations,argsJSON), and the optional last-knownconfigOptions. Live-only fields (isBusy, streaming flags, slash commands) are deliberately omitted. See Session persistence for the full schema.
Managed by the system via UserDefaults.standard under the app's bundle
id. Five keys:
| Key | Type | Written / read by | Purpose |
|---|---|---|---|
spettro.explicitCLIPath |
String | AppModel (Settings, onboarding) |
User-picked CLI override; checked by CLILocator.locate(explicitPath:) before all candidates |
spettro.lastProjectPath |
String | AppModel.chooseProject |
Restored at launch as the default project folder (validated with fileExists(isDirectory:), falls back to home) |
spettro.lastConfigOptions |
Data (JSON [ACPConfigOption]) |
AppModel |
Last agent-advertised config options, so the ConfigBar isn't empty on relaunch |
spettro.cachedCommands |
Data (JSON [ACPCommand]) |
ACPCommand.cached / .cache(_:) in ACPProtocol.swift |
Last advertised slash-command list, so the palette works before a connection |
spettro.terminalDrawerVisible |
Bool | TerminalPanelStore in TerminalPanel.swift |
Reopens the terminal drawer in its last state |
Owned by the CLI. The app treats these files as the CLI's contract and only reads them (plus direct markdown edits for memory).
The CLI caches the last-known subscription here under the keys
spettro_email, spettro_plan, and spettro_plan_status, clearing them
on logout — so a present email doubles as the "connected" signal
(SubscriptionStore.reload()). A connected account with no plan field
means the free tier. The store watches the directory ~/.spettro
rather than the file itself (DispatchSourceFileSystemObject, O_EVTONLY
on the dir) because the CLI rewrites the file atomically (write + rename),
which would orphan a file-level descriptor after the first change. The app
never writes this file; the child process is given a login-shell-like
environment with ~/.local/bin prepended to PATH (ACPConnection.start())
specifically so the CLI can find ~/.spettro and its tools.
Plain markdown, one fact per line. The same file the CLI's /memory
command and save-memory tool use. MemoryStore.save creates the parent
directory if needed and writes atomically; MemoryStore.clear deletes the
file. The CLI reads it at session start, so edits take effect on the next
session, matching the CLI's own semantics. See
Memory files.
The project-scope counterpart, edited through the same MemoryStore API.
MemoryStore.path(for: .project, …) walks up from the project folder
and uses the first ancestor directory that already contains a .spettro
folder; if none exists, the project root itself gets one
(<project>/.spettro/memory.md, created on save). This repo's own
Spettro-big/Spettro/.spettro/memory.md is exactly such a file — it is
committed deliberately (see
Project layout).
The CLI subprocess is also launched with the project folder as its
working directory (spettro --acp --cwd <project>,
process.currentDirectoryURL = workingDirectory), which is what lets the
CLI discover project-scope files itself.
Search order in CLILocator.locate(explicitPath:) — first executable file
wins (fm.isExecutableFile):
- Explicit override —
spettro.explicitCLIPathfrom UserDefaults (set in Settings or onboarding), if non-empty and executable. - Dev checkout —
devCheckoutBinary():~/Desktop/spettro/bin/spettroplus a walk of up to six ancestors ofBundle.main.bundleURLlooking forspettro/bin/spettro. Inserted at index 0 of the candidate list so freshly built dev binaries are picked up without touching the installed CLI. - Installed locations, in order:
~/.local/bin/spettro(the install script's default),/opt/homebrew/bin/spettro,/usr/local/bin/spettro,~/go/bin/spettro. - Everything on
PATH—spettroappended to each entry ofProcessInfo.processInfo.environment["PATH"].
If nothing is found, AppModel enters .needsSetup and onboarding offers
CLIInstaller.install(...), which runs
curl -sSfL https://spettro.app/install | sh via /bin/zsh -lc — that
script (not the app) writes the binary to ~/.local/bin. Version strings,
when shown, come from synchronously running spettro --version /
spettro version with a 6-second timeout (CLILocator.readVersion), since
the ACP handshake is the real version source.
- Image attachments have no cache directory:
ImageAttachment.from(fileURL:)reads the picked file inside astartAccessingSecurityScopedResource()scope, downsamples to JPEG in memory, and the bytes live only insidesessions.json(StoredAttachment.data). - No sandbox bookmarks are persisted — the project folder is re-validated by path on each launch, not re-resolved from a bookmark.
- Transcripts are not written anywhere else — there is no log file,
no Caches-directory state, and stderr from the CLI is surfaced in-app
(
onLog) rather than teed to a file.