diff --git a/README.md b/README.md index 0cdb3b6a..a9a4e3c2 100644 --- a/README.md +++ b/README.md @@ -479,7 +479,7 @@ PTY Output → 16ms Server Batch → DEC 2026 Wrap → SSE → Client rAF → xt - **OSC 52 clipboard**: a program in a session that sets the clipboard with OSC 52 (Claude Code's `/copy`, a tmux yank, vim) puts the text on your browser's clipboard, in the main terminal and in every tile. Clipboard read requests are refused, and replaying a buffer never copies - **MCP server sync** _(opt-in)_: copies the user-level MCP servers each installed CLI has (Claude, Codex, Gemini, OpenCode, Antigravity, and GitHub Copilot CLI) into the others' own config files. Additive only, a server you switched off is never copied, **Preview** shows the changes first, and the previous file is kept as `.codeman-bak`. App Settings → Agents & CLIs; admin only in multi-user mode - **CJK / IME input** — full composition support for Chinese / Japanese / Korean, with Ctrl- and Alt-modified navigation keys passed through to the CLI -- **Config backups** — timestamped copies of the files that hold your customization (settings, CLI overrides, custom endpoints, webhook, linked cases, remote and Docker hosts, saved dashboards and, in multi-user mode, accounts), made at startup and whenever one changes. On by default into `~/.codeman/backups/config`; the folder, how many to keep (1–500) and how long (days, 0 = no limit) are settings, with **Back up now** and a verified **Restore** in Settings → System. Retention only ever deletes folders Codeman made and never the newest. See the [wiki page](docs/wiki/Config-Backups.md) +- **Config backups** — timestamped copies of the files that hold your customization (settings, CLI overrides, custom endpoints, webhook, linked cases, remote and Docker hosts, saved dashboards and, in multi-user mode, accounts), made at startup and whenever one changes. On by default into `~/.codeman/backups/config`; the folder, how many to keep (2–500) and how long (days, 0 = no limit) are settings, with **Back up now** and a verified **Restore** in Settings → System. Retention only ever deletes folders Codeman made and never the newest. See the [wiki page](docs/wiki/Config-Backups.md) - **Plan usage in the header** — live Claude subscription usage (the 5-hour and weekly windows) from a statusline exporter Codeman hands to `claude` at spawn and never writes into your settings files, plus Codex limits from its own app-server; per device, on for desktops and off for phones - **Prompt-cache readout**: each Claude session shows whether its prompt cache is warm (`cache:until HH:MM`) or cold, in the footer (when you have no statusline of your own) and in Session Options → Respawn, with the cost of a re-cache and a note after a compaction. Display only; it survives a restart - **Session list, your way** — the header strip, a left sidebar with a filter box, or a vertical rail whose detailed rows carry created and state stamps and sort by activity; the phone home screen and the desktop home rail use the same order. On the rail, **Search sessions** narrows the list to the tabs that match, across every group, and a tab with an alert stays visible even when its name does not. Named **tab groups** collapse, reorder and take drags; a group's menu has **New session**, and a worker an agent spawns lands right after its parent (**Spawned tabs follow their parent**, off by default, keeps it moving with the parent). **Add to Focus** pins a shortcut to any tab in a section at the top of the rail, and **CLI Logos on Tabs** can be switched off per device diff --git a/docs/wiki/Config-Backups.md b/docs/wiki/Config-Backups.md index 041f7110..a11be141 100644 --- a/docs/wiki/Config-Backups.md +++ b/docs/wiki/Config-Backups.md @@ -37,10 +37,15 @@ cron jobs. - **Whenever the content changes.** The files are read and hashed every five minutes; a new backup is written only if the set differs from the latest backup. An idle server writes nothing, and the history is one entry per real change rather than one per timer tick. - A field a file refreshes by itself is not a change: `custom-model-hosts.json` carries a - `lastDiscoveredAt` per host that the model-discovery poll rewrites every five minutes, and - the comparison looks through it (the snapshot still stores the file exactly as written). - Without that, every tick looked like an edit and 20 copies covered only about 100 minutes. + A field a file rewrites on its own is not a change, and the comparison looks through it (the + snapshot still stores the file exactly as written, and a restore puts those fields back as they + were): `custom-model-hosts.json` `lastDiscoveredAt` (the model-discovery poll, every five + minutes), `users.json` `lastLoginAt`, `settings.json` `lastUsedCase`, `intents.json` + `recentPrompts` and `updatedAt` (every captured prompt) and `docker-cases.json` + `lastClaudeSessionId` (a Docker case's conversation id after a `/clear`). Without that, every + tick looked like an edit and 20 copies covered only about 100 minutes. So Compare shows + `intents.json` as unchanged when only prompts differ, yet a restore drops the prompts captured + since. - **On demand**, with **Back up now** (always writes one, even if nothing changed). - **Before a restore**, as a `pre-restore` safety copy of the files about to be replaced. @@ -155,8 +160,8 @@ Before you restore, or to find out what changed and when, use **Compare** in the Each file that differs is listed with its changed keys (`theme.mode`, `tags[1]`, a variable name in `.env`) as before → after, plus files that appeared or vanished between the two states. Files that are the same are listed on one line. A file that is not JSON shows only that it differs, and -a file with more than 200 changes is cut short, so use the files themselves for a full diff. A -self-refreshed discovery timestamp in `custom-model-hosts.json` is not counted as a change. +a file with more than 200 changes is cut short, so use the files themselves for a full diff. The +self-rewritten fields listed under "When a backup is made" are not counted as changes. **Credential values are never shown.** `.env`, `users.json`, `push-keys.json`, `webhook.json` (the URL is a bearer secret) and `intents.json` (captured prompts) list only the names that changed, and in every other file a key that looks like a token, secret, password, diff --git a/docs/wiki/Security.md b/docs/wiki/Security.md index d7e169f1..5d72ac5e 100644 --- a/docs/wiki/Security.md +++ b/docs/wiki/Security.md @@ -108,7 +108,7 @@ Two features send data outward, both off by default and both stated where they a dictation through your Claude login, and the Read My Mind prediction call. [Config Backups](Config-Backups) copies some credential-bearing files (`.env`, `users.json`, -`custom-model-hosts.json`, `push-keys.json`) into a backup folder. The default folder is inside +`custom-model-hosts.json`, `push-keys.json`, `webhook.json`) into a backup folder. The default folder is inside the data directory, and every backup is mode `0700`/`0600`, so nothing new is exposed. Only a folder you choose yourself can move them somewhere less private; admin only in multi-user mode. diff --git a/docs/wiki/Settings-Reference.md b/docs/wiki/Settings-Reference.md index bf0a7800..b61ab68e 100644 --- a/docs/wiki/Settings-Reference.md +++ b/docs/wiki/Settings-Reference.md @@ -192,7 +192,7 @@ mode, the **Users** administration entry is injected here. The **Config backups** group controls timestamped copies of your configuration files: an on/off switch (on by default), the backup folder (default `~/.codeman/backups/config`), how -many backups to keep (1 to 500, default 20) and how many days to keep them (0 to 3650, default +many backups to keep (2 to 500, default 20) and how many days to keep them (0 to 3650, default 30; 0 means no age limit), plus **Back up now** and a list with **Restore**. These are server-side settings, read fresh. Details, naming and restore behaviour are on [Config Backups](Config-Backups). diff --git a/docs/wiki/Troubleshooting.md b/docs/wiki/Troubleshooting.md index aa3a83f4..b101e45c 100644 --- a/docs/wiki/Troubleshooting.md +++ b/docs/wiki/Troubleshooting.md @@ -139,8 +139,8 @@ which is the default, you have copies: 1. Open **Settings → System → Config backups** and pick the newest backup from before the change. The list shows when each was made and what it holds. -2. **Restore** it, reload the page, and restart Codeman if the response says `clis.json`, - `users.json`, `.env` or `push-keys.json` were restored. +2. **Restore** it, reload the page, and restart Codeman if the response's `restartRequired` + list is not empty. Without the UI: the backups are plain folders in `~/.codeman/backups/config/` (`cfg-YYYYMMDD-HHMMSS`); copy the file you need back over the one in `~/.codeman/`. diff --git a/src/config-backup.ts b/src/config-backup.ts index a583dcf8..4c6511e4 100644 --- a/src/config-backup.ts +++ b/src/config-backup.ts @@ -205,10 +205,12 @@ const sha256 = (data: Buffer | string): string => createHash('sha256').update(da const VOLATILE_KEYS: Readonly> = { 'custom-model-hosts.json': ['lastDiscoveredAt'], // Rewritten by activity, not by anything the user set: a Basic-auth login (at most once a minute - // per user), a session started in another case, and every captured prompt. + // per user), a session started in another case, every captured prompt, and a Docker case's + // conversation id after a /clear. 'users.json': ['lastLoginAt'], 'settings.json': ['lastUsedCase'], 'intents.json': ['recentPrompts', 'updatedAt'], + 'docker-cases.json': ['lastClaudeSessionId'], }; function withoutKeys(value: unknown, drop: ReadonlySet): unknown { diff --git a/src/web/public/index.html b/src/web/public/index.html index a671bb9c..55150e12 100644 --- a/src/web/public/index.html +++ b/src/web/public/index.html @@ -3269,7 +3269,7 @@
Delete backups older than (days) - 0 keeps backups until the count above pushes them out. + A backup is removed once a newer one has existed this many days. 0 keeps backups until the count above pushes them out.
diff --git a/test/config-backup.test.ts b/test/config-backup.test.ts index bcd17295..5089e8c6 100644 --- a/test/config-backup.test.ts +++ b/test/config-backup.test.ts @@ -148,6 +148,11 @@ describe('createConfigBackup', () => { }); it.each([ + [ + 'docker-cases.json', + (n: number) => JSON.stringify([{ name: 'app', hostId: 'h', lastClaudeSessionId: `c${n}` }]), + 'hostId', + ], ['users.json', (n: number) => JSON.stringify({ users: [{ name: 'a', role: 'admin', lastLoginAt: n }] }), 'role'], ['settings.json', (n: number) => JSON.stringify({ displayName: 'x', lastUsedCase: `case-${n}` }), 'displayName'], [