Skip to content

Latest commit

 

History

History
146 lines (119 loc) · 9.09 KB

appendix-d-files-on-disk.md

File metadata and controls

146 lines (119 loc) · 9.09 KB

Appendix D — Files on disk

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.

The map

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.

The app's own state

~/Library/Application Support/Spettro/sessions.json

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(_:): JSONEncoder with .iso8601 dates, data.write(to:options: .atomic).
  • Read — SessionStore.load(): JSONDecoder with .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 for session/resume), projectPath, title, createdAt, isPinned, isArchived, the transcript as [StoredItem] (messages with role user | assistant | reasoning | notice, optional base image attachments inlined as data, and tool calls with diffs, locations, argsJSON), and the optional last-known configOptions. Live-only fields (isBusy, streaming flags, slash commands) are deliberately omitted. See Session persistence for the full schema.

UserDefaults — ~/Library/Preferences/to.eyed.Spettro.plist

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

The CLI's ~/.spettro directory

Owned by the CLI. The app treats these files as the CLI's contract and only reads them (plus direct markdown edits for memory).

~/.spettro/config.json

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.

~/.spettro/memory.md (user scope)

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.

Project-scope .spettro/ folders

<project>/.spettro/memory.md (project scope)

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.

The spettro binary

Search order in CLILocator.locate(explicitPath:) — first executable file wins (fm.isExecutableFile):

  1. Explicit override — spettro.explicitCLIPath from UserDefaults (set in Settings or onboarding), if non-empty and executable.
  2. Dev checkout — devCheckoutBinary(): ~/Desktop/spettro/bin/spettro plus a walk of up to six ancestors of Bundle.main.bundleURL looking for spettro/bin/spettro. Inserted at index 0 of the candidate list so freshly built dev binaries are picked up without touching the installed CLI.
  3. Installed locations, in order: ~/.local/bin/spettro (the install script's default), /opt/homebrew/bin/spettro, /usr/local/bin/spettro, ~/go/bin/spettro.
  4. Everything on PATH — spettro appended to each entry of ProcessInfo.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.

What is intentionally not on disk

  • Image attachments have no cache directory: ImageAttachment.from(fileURL:) reads the picked file inside a startAccessingSecurityScopedResource() scope, downsamples to JPEG in memory, and the bytes live only inside sessions.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.