Screenshot of the imgupv2 GUI prepared to upload the selected file from Photos

Years ago when I was first learning to write scripts, I’d come up with stuff that made my life much better and would have helped other people on my team, and I’d run into the kinds of distribution problems you’d expect: I was a Linux/Mac guy, everyone around me was a Windows person, etc. I am guessing the 20th anniversary of the first time I said “well, it runs on my laptop” is right around the corner. Even people who could download the OSAX I was using for an AppleScript weren’t necessarily interested in doing it.

I learned Sinatra and HAML because I wanted to make work I was doing accessible to other people, and the best way to solve the distribution problem was to just build a web GUI and plumb all those Ruby scripts in as controllers.

When I wrote the first imgup, I did it in Sinatra and got it going on Heroku well enough that I could document how someone else could grab the source and run it for themselves. When I decided to pivot it into a command line app, the distribution problem came back, but I’ve learned enough over the years that if there were any issues with someone else using the code, I can’t really blame it on distribution woes, just my own disinterest in a test matrix bigger than “the OS running on the machine I am testing on.”

So after deciding to quit futzing with imgup – that it was as done as it can be – I started writing a blog post about the experience of doing the migration from Sinatra/Web to a command line tool using Claude.

The development loop I landed on over a few weeks of intermittent fussing had come down to leveraging the Basic Memory MCP to preserve more project context between sessions, and to chain project context with a special project architecture note in Basic Memory and what I came to call “continuity prompts.”

Basic Memory is just an archive of Markdown/YAML files kept on your disk with light markup to create persistence: You can annotate Basic Memory notes with “Observations” and “Relationships” that it uses to build a graph out of notes.

Observations look like:

- [architecture] Direct API metadata is cleaner than file embedding
- [performance] Removing exiftool dependency significantly improved upload speed
- [simplicity] Upload-then-set pattern is more maintainable than metadata embedding
- [consistency] Aligning Flickr with SmugMug's approach simplified the codebase

… and they work, to the extent that when I would turn on Extended Thinking and watch Claude talking to itself, it would mention them as things it “knew” or was trying to take into account when working on a chunk of code.

My Project Context note provides all the things Claude needed to know to start each session without going through rediscovery each time we worked on a new feature or bugfix. I devised a continuity prompt that reminded Claude to check out the project context:

I'm working on imgupv2, my fast image uploader for photographers. Please read the project context first:

memory://imgupv2-project-context-working-preferences

Key reminders:

- The GUI is Wails (Go + Web), NOT Swift
- External dependencies need full paths (GUI apps don't inherit shell PATH)
- Always ask before making changes
- Work incrementally and wait for confirmation

The Project Context note includes things like:

### 1. CLI Binary (`imgup`)

- **Location**: `/Users/mph/code/imgupv2/cmd/imgup/`
- **Binary locations**:
  - Development: `/Users/mph/code/imgupv2/imgup`
  - Installed: `~/go/bin/imgup` or `/usr/local/bin/imgup` or `/opt/homebrew/bin/imgup`
- **Purpose**: Core upload functionality, authentication, configuration

### 2. GUI Application (`imgupv2-gui.app`)

- **Location**: `/Users/mph/code/imgupv2/gui/`
- **Technology**: Wails (Go + Web frontend, NOT Swift)
  - [architecture] GUI uses Wails (Go + Web frontend), NOT Swift #imgupv2 #wails (correcting common misconception)
- **Frontend**: `/Users/mph/code/imgupv2/gui/frontend/` (HTML/JS/CSS)
- **Features**:
  - Detects selected images from Finder/Photos.app
  - Metadata review before upload
  - Mastodon integration with dynamic form
  - Seamless white window design

and …

## Project Structure

/Users/mph/code/imgupv2/ ├── cmd/imgup/ # CLI entry point ├── pkg/ │ ├── services/ # Service implementations │ │ ├── flickr/ # Flickr client │ │ └── mastodon/ # Mastodon client │ ├── config/ # Configuration management │ └── metadata/ # EXIF/metadata extraction ├── gui/ # Wails GUI application │ └── frontend/ # Web frontend (NOT Swift!) │ └── dist/ # Must copy files here for build ├── homebrew/ # Homebrew cask definition ├── build-macos-release.sh # Release packaging script ├── release.sh # GitHub release automation └── build-and-release.sh # Complete release automation

That is, btw, a Go project. Because in the midst of writing up my notes about what I learned working with Claude, I realized ways in which I’d made a very useful workflow that would have saved me a ton of time doing what should have been a simple migration of some controller logic out to a command line script, and that if could use Claude poorly to get a mostly okay reimplementation in a language I know, maybe if I used it well I could get a good reimplementation – and a more portable, accessible one – with a language I don’t know so well.

So I started with a clean slate, started a new Claude “Project,” and set out to make imgupv2 in Go using everything I learned to work with Claude and the Basic Memory MCP.

I would have been happy to just have a distributable binary I could share, but the approach I took to the whole thing let everything move super fast. To the point imgup can be installed with Homebrew, and ships with a little (optional) popup GUI so you can just select an image in the Finder or Photos, invoke a Shortcut with a hotkey. Its backward compatible with the old Ruby version, so it can be wired into anything you’d care to script, like Raycast Script Actions or Hazel folder actions.

I also added some new features because the core feature set went in fast. Oauth registration with Flickr, SmugMug, and Mastodon works without pasting URLs, etc. because imgup config fires up a little callback server duing onboarding. And after Jack Baty mentioned wanting to customize another little flickr script I had come up with, I added snippet templating:

  "templates": {
    "html": "\u003cimg src=\"%image_url%\" alt=\"%alt|description|title|filename%\"\u003e",
    "json": "{\"photo_id\":\"%photo_id%\",\"url\":\"%url%\",\"image_url\":\"%image_url%\"}",
    "markdown": "![%alt|description|title|filename%](%image_url%)",
    "org": "[[%image_url%][%alt|description|title|filename%]]",
    "url": "%url%"
  }

… so instead of living with my particular idea of how an HTML snippet should look, you can roll your own and override the default.

I also added accessibilty prompts and support (you get a reminder if you don’t specify an --alt flag, but the snippet will degrade from alt to the image description to the image title when making a post.)

… and I successfully used Claude itself as a sort of consultant. I was miserable about the way I’d added flickr support because it was dog slow due to my own limitations, but I was able to use Claude to reimplement flickr uploads in a way that makes them super fast and removes external dependencies on exiftool: I told it the pain point, which involved extracting images, writing them out to disk, and embedding the metadata from Photos then uploading them; and it suggested a pair of API calls: One to upload the image, and one to write the metadata over the API.

And the last “feature” is an automated build and distribution pipeline, complete with Apple notarization. One build script compiles the macOS and Linux binaries, signs the macOS binaries, builds the GUI, tags a release, and updates the Homebrew cask:

brew install --cask pdxmph/tap/imgupv2

… but also just …

go install github.com/pdxmph/imgupv2/cmd/imgup@latest if you don’t care about the GUI or want to build something in Raycast, Apple Shortcuts, Alfred, etc.

But why tho

Just curiosity, I guess. AI is a big deal at work right now, and my job puts me in the middle of a lot of it: Working through security issues, thinking about how to onboard and configure the flood of new AI features rolling out from all our vendors, and lately helping out with a training program. Our product is going to be using AI a lot more in the coming year.

There are a lot of folks around me at work who have a lot of takes, good and bad, realistic and unrealistic. One person scoffed, “I tried to get AI to write a program for my kids and it couldn’t! Smoke and mirrors!” Plot that take on a skepticism -> credulity bell curve, and I can probably think of equal and opposite occupants on the other end that I’ve heard.

Up until this past month, I’d mostly used LLMs as a coding augment. When I was writing the first, Sinatra-based imgup, I struggled with the syntax for building and tearing down OAuth sessions in Ruby, so ChatGPT helped me out there. I did a few proofs of concept for things like “write some Ruby that uses the TriMet API to tell me when the next three trains are due at my Max station” just to understand how it worked and what it could do. And if I’d come across an error, I’d use it to paste in the error, provide a little context, and get something as serviceable as a direct visit to Stack Overflow.

I’d never tried to use one to do a whole project with multiple discrete pieces.

Could I have, as that one person apparently tried, just said “make a thing called imgup. It needs to post pictures to flickr or smugmug from the command line, operate on the selected file in the Finder or Photos, and optionally post to Mastodon,” well, no, that would not have worked.

It didn’t even “work,” in my early first attempt to allow for its shortcomings, to make a thorough project prompt and keep sessions concise: It was incredibly inefficient, forgetful, and self-defeating, constantly introducing regressions as it forgot how one piece connected to the other, even when it had a copy of the repo attached to its project memory.

What finally made it “work,” if our definition of “work” is “make code that does what I want with about as many obvious bugs or issues as if I’d devoted months to this project starting from scratch,” was leveraging Basic Memory to give it some persistent project knowledge, careful marshaling of its limited session lengths, fastidious continuity prompting, and the occasional “go describe this problem to another instance outside the project to see if an evil twin can poke holes in the solutions offered by the other AI ‘developer.’”

So my curiosity is satisfied, for the most part.

Less public than this project, though, is all the writing I’m doing for myself right now not about how to make it work, but what to make of it – “AI” – generally. Because as surely as there’s a wide and varied distribution of takes on whether AI can do this or do that, if it’s “real” or “just smoke and mirrors,” the social issues are at our doorstep. This almost felt like me saying to myself, “well, you know what it can do; so now you can figure out what it means.”