Skip to content

feat(profiles): choose which profile a new profile clones from - #2309

Open
twuijri wants to merge 1 commit into
EKKOLearnAI:mainfrom
twuijri:clone-from-any-profile
Open

feat(profiles): choose which profile a new profile clones from#2309
twuijri wants to merge 1 commit into
EKKOLearnAI:mainfrom
twuijri:clone-from-any-profile

Conversation

@twuijri

@twuijri twuijri commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Creating a profile can clone one — but only ever the profile you happen to be standing in, because hermes profile create --clone takes its source from active_profile and has no flag for anything else. Cloning research while you are in work means switching to research, creating, and switching back. This adds a source picker instead.

Source profile, defaulted to the active one The list of profiles to clone from
The switch now reads Clone from an existing profile, and the source defaults to the active one — flipping the switch and pressing Create does exactly what it did before. Every profile is offered, with the active one marked. The cleanup notice stays where it was.

Server

POST /api/hermes/profiles accepts cloneFrom. clone: true with no cloneFrom still means "the active profile", so existing callers are unaffected.

  • Source is the active profile — unchanged: the CLI's --clone does the work, exactly as today.
  • Source is another profile — the CLI creates an empty profile, and the server copies the same set --clone copies: config.yaml, .env, SOUL.md, skills/. That list lives in one named constant next to the comment documenting the CLI's behaviour, so the two stay together if the CLI ever changes.

Either way the existing post-processing runs afterwards, with the source now explicit rather than assumed:

  • copyModelProviderAuthForClone(name, source) — takes the source profile instead of reading active_profile. Same policy as before: only the auth keys the cloned model.provider needs.
  • smartCloneCleanup(name) — unchanged, so a clone never carries a second copy of a Telegram or Slack token into a profile that would then fight the source for the platform lock.

The source is validated before anything is created: name shape, existence, reserved names, and the requesting user's profile access — a non-admin cannot clone a profile they cannot see.

Client

ProfileCreateModal.vue gains an NSelect under the switch. createProfile(name, cloneFrom) replaces createProfile(name, clone) through the store and API layer. Four new i18n keys replace profiles.cloneFromCurrent, translated in all eleven locales.

Tests

tests/server/profiles-routes.test.ts (5 new) and tests/client/profile-create-modal.test.ts (4 new):

  • cloning a non-active profile copies config, env, SOUL and skills, and pulls OAuth auth from the chosen source rather than the active one
  • a custom provider defined on the source arrives with its key — the point of the feature is not re-entering keys and models
  • the exclusive platform token in the source .env is still stripped
  • an active source still delegates to --clone; clone: true alone still means the active profile
  • a missing source, or one containing path separators, is rejected before the CLI is called
  • the dialog lists every profile, marks the active one, defaults to it, and sends null when cloning is off

I reverted the controller and confirmed all five server tests fail without the change. docs/openapi.json is regenerated output, not a hand edit.

One thing worth deciding, not decided here

OAuth logins in auth.json still only follow the provider named in model.provider. If a profile has several OAuth providers signed in, the clone gets one. That is your existing policy and I left it alone — happy to widen it in a follow-up if you would rather a clone arrive fully signed in.

`hermes profile create --clone` reads `active_profile`, so the Web UI could
only ever clone the profile you happened to be standing in. Switching profiles
first, just to clone a different one, is a detour the UI should not require.

The create dialog now asks for a source profile, defaulting to the active one,
so the previous one-switch flow is unchanged.

Server:
  - `POST /api/hermes/profiles` accepts `cloneFrom`; `clone: true` without it
    keeps meaning "the active profile"
  - source === active still goes through the CLI's `--clone`, untouched
  - other sources create an empty profile and copy the same set `--clone`
    copies (config.yaml / .env / SOUL.md / skills), then run the existing
    post-processing with the source made explicit
  - `copyModelProviderAuthForClone` takes the source profile instead of
    assuming the active one
  - the source is validated (name shape, existence, reserved names, user
    access) before anything is created

Model providers and their keys come across as before; smart clone cleanup
still strips exclusive platform credentials so two profiles never share one
bot token.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@twuijri
twuijri force-pushed the clone-from-any-profile branch from f86e100 to c14da86 Compare August 2, 2026 14:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant