SLOPSHOPPER

compact-helper

Watch context fill; warn early, and make every compaction keep the darkroom campaign's HQ packet (state.json, fronts, agents, worktrees, schedules, user…

newcommandtoaststatus
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · compact-helper
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /hq-compact ⎿ compact-helper: 已壓縮(保留戰役 HQ 封包)。 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts ⚠ compact-helper: ctx 49%
README

<img src="docs/assets/banner.svg" alt="darkroom — Your Lightroom presets, on your own machine" width="100%">

<b>在自己電腦上,用你買的 Lightroom preset 修照片。</b><br> 挑 preset → 調強度 → 微調滑桿 → 即時預覽 → 匯出。不用訂閱,不上傳雲端。


這是什麼

退了 Lightroom 訂閱之後,手上買過的幾百個 XMP preset 就沒地方用了。darkroom 直接讀這些 .xmp,把裡面的 Lightroom 設定(曝光、對比、亮部/陰影、HSL、色彩分級、曲線、漸層遮罩……)用 GPU 在本機重算出來,拖滑桿就能即時看到結果。

讓人省心是這個工具唯一的目標:你只需要專注在擅長的照片編輯,其他的——檔案、路徑、格式、設定——交給 darkroom。 原則很簡單:preset 裡寫的設定一律由程式照算;AI 只做程式做不到的事(看懂畫面、找出位置、重畫內容)。照片原檔和買來的 preset 原檔,darkroom 永遠只讀不寫。

<img src="docs/screenshots/editor-v2.png" alt="darkroom 編輯畫面:左邊 preset 庫、中間預覽與強度、右邊滑桿" width="90%">

<img src="docs/assets/flow.svg" alt="Preset → Strength → Adjust → Preview → Export" width="90%">

功能

功能說明
套 preset解析 Lightroom PV2012 系列 XMP(ProcessVersion 6.7/10.0/11.0/15.4),強度 0~200%;Adobe 沒公開的部分(清晰度、紋理、去朦朧、亮部陰影)用公開演算法近似
即時預覽GPU 渲染,1.5MP 全套運算約 15~20 ms;拖滑桿時只算最新一次
微調滑桿在 preset 之上逐項加減;復原/重做;按住看原圖
讀檔JPEG、PNG、TIFF(含 16-bit)、HEIC/HEIF(iPhone,10-bit、Display P3 轉 sRGB);依 EXIF 自動轉正
A/B 對照編輯前/後拖分隔線對照,隨時「還原成原圖」,也能取回上一份編輯
裁切與旋轉拖框、鎖比例(原始、1:1、4:5、2:3、16:9、自由…)、拉直、轉 90°、鏡像;縮圖、預覽、匯出都套同一個範圍
匯出JPEG(品質或檔案大小上限)、PNG、TIFF(8/16-bit)、WebP;只縮不放的尺寸;中繼資料全留/只留版權/全移除,可單獨拿掉 GPS;輸出銳利化;匯出預設;嵌入 sRGB,永不覆蓋;24MP 約 0.4 秒/張
preset 庫群組樹、搜尋(含 AI 寫的中英文風格標籤)、最愛、改名、搬移、匯入 .xmp、把目前的修改存成自己的 preset、匯出成 Lightroom 讀得到的 .xmp——整理只改索引,原檔不動
照片庫資料夾縮圖格、多選;每張照片的修改自動保存(以照片內容對應,搬移改名都不會掉);複製/貼上修改;匯出所選
設定設定頁(語言 zh-TW/en-US、資料夾、AI、ComfyUI)、改了立即生效;設定可匯出/匯入
給 AI 代理用CLI(--json 固定格式)與 MCP server(38 個 darkroom_* 工具,例如取回上一份編輯 darkroom_edit_restore/CLI edit restore),和 App 共用同一套操作與錯誤訊息

偵測不到的功能(CUDA、HEIC、WebP、語意搜尋、ComfyUI…)會自動關掉並說明原因。還沒做的:RAW、AI 修圖建議、AI 遮罩、去雜物/美顏,見 開發文件的路線圖。

下載安裝

安裝檔(建議):到 GitHub Releases 下載 Windows 的 .msi/-setup.exe,或 Linux 的 .deb/.AppImage。第一次開啟時會說明要下載 Python 與 PyTorch(有 NVIDIA 顯示卡約 3 GB),你按同意才下載,之後就直接進編輯畫面。同一頁還有命令列執行檔 darkroom(給 AI 代理或腳本用)。

從原始碼:需要 Python 3.13、Node.js 22+、NVIDIA GPU(CUDA)。或者把 docs/agent-install.md 交給你的 Claude Code/Codex/Cursor,它會一步一步裝好、遇到要你決定的地方先問你。

兩種方式的完整步驟、GPU/CUDA 說明與疑難排解都在 安裝說明。

快速開始

5 分鐘上手:第一次啟動 → 指定 preset 資料夾 → 修一張 → 匯出,以及 CLI 與 MCP 各一個例子。

其他文件:設定、架構、開發與發版、給 AI 代理的使用手冊、用詞定義。

安全保證

darkroom 會被你自己和 AI 代理操作,所以這幾條是寫成測試守住的,不是口頭承諾:

  • 照片原檔、買來的 preset 原檔永遠只讀。 程式只有一個寫檔出口(safe_write):只建新檔、限定在指定資料夾、拒絕寫進 preset 資料夾、拒絕替換或刪除照片與 .xmp。
  • 匯出永不覆蓋,同名一律加序號;匯出到照片所在資料夾時,原檔前後的 SHA-256 不變。
  • 只綁 127.0.0.1,而且擋掉「你瀏覽器裡的其他網頁」:檢查 Host、Origin、Sec-Fetch-Site、Content-Type,讀照片路徑的請求要帶專用標頭,HTTP 也不收任意寫入路徑。
  • 金鑰不落地:AI 功能的金鑰只記 1Password 參照(op://…)或讀環境變數,設定檔裡放金鑰本身會被拒絕。
  • 測試全程掛寫檔守門(Python audit hook):任何測試只要寫到宣告範圍以外就直接失敗,受保護的資料夾前後比對雜湊。

授權

MIT。darkroom 不附任何 preset;你買來的 preset 依它們原本的授權使用。

Source 1 files
hooks/register.ts 66 lines
1import type { Register } from 'claude-code'
2
3// Temporary mod for the darkroom campaign (2026-10-10). The main session runs a long
4// /common:strategic-advance campaign with many background agents; a compaction that drops the
5// HQ packet (which fronts are active, which agents and schedules are live, what the user decided)
6// costs a full reconstruction. This mod watches the context fill and steers every compaction.
7const WARN_PERCENT = 70
8const URGENT_PERCENT = 85
9
10const KEEP = [
11  'This is a long darkroom campaign run with /common:strategic-advance. The summary MUST keep, verbatim where possible:',
12  '1. Where truth lives: D:/Code/darkroom/.strategic-advance/darkroom-alpha/state.json (rebuild the HQ packet from it after compaction, never from chat memory), the ledger run-ledger.jsonl next to it, the map .claude/wayfinder/darkroom/map.md, the contracts in .claude/contract/, CONTEXT.md, AGENTS.md, and the memory folder C:/Users/powde/.claude/projects/D--Code-darkroom/memory/.',
13  '2. Every background agent still running: its id, what it was asked to do, its model, and whether it works on main or in a worktree (with the worktree path and branch).',
14  '3. Every scheduled job (cron id, what it fires, when it ends) and every one-shot schedule still pending.',
15  '4. The active fronts and their state (sealed / in progress / deferred / needs the user), and the next move.',
16  '5. Every decision the user made in this session, in their words, with the date: scope additions, product core (讓人省心), model policy, privacy rules, what must never be written or published.',
17  '6. Open findings not yet fixed, with file:line, and anything the user is waiting on from the agent or the agent from the user.',
18  'Drop raw tool output, file dumps, and resolved back-and-forth.',
19].join('\n')
20
21let percent: number | undefined
22let warnedAt = 0
23
24const statusText = () => (percent === undefined ? undefined : `ctx ${percent}%`)
25
26export const register: Register = on => {
27  on('session.start', async ($, e, next) => {
28    const r = await next(e)
29    await $.command.register({
30      name: 'hq-compact',
31      description: 'Compact now, keeping the darkroom campaign HQ packet (fronts, agents, schedules, user decisions)',
32    })
33    try { percent = (await $.session.usage()).context.percent } catch { /* no reading yet */ }
34    $.ui.status(statusText())
35    return r
36  })
37
38  on('session.measure', ($, e, next) => {
39    percent = e.context.percent
40    $.ui.status(statusText())
41    if (percent !== undefined) {
42      const level = percent >= URGENT_PERCENT ? URGENT_PERCENT : percent >= WARN_PERCENT ? WARN_PERCENT : 0
43      if (level > warnedAt) {
44        warnedAt = level
45        $.ui.toast(level === URGENT_PERCENT
46          ? `compact-helper:context ${percent}%,建議現在就 /hq-compact`
47          : `compact-helper:context ${percent}%,找個空檔 /hq-compact`)
48      }
49      if (percent < WARN_PERCENT) warnedAt = 0
50    }
51    return next(e)
52  })
53
54  on('command.run', { command: 'hq-compact' }, async $ => {
55    await $.session.compact({ instructions: KEEP })
56    return { text: '已壓縮(保留戰役 HQ 封包)。' }
57  })
58
59  // Every compaction of the main conversation (manual, threshold, or ahead of time) keeps the packet.
60  on('session.compact', ($, e, next) => {
61    if (e.agentId) return next(e)
62    const instructions = e.instructions ? `${e.instructions}\n\n${KEEP}` : KEEP
63    return next({ ...e, instructions })
64  }).catch(($, e, next) => next(e))
65}
66