Skip to content

feat(profiles): 3rd-party API endpoints via per-profile env#5

Merged
Shahinyanm merged 1 commit into
Digital-Threads:masterfrom
tranquocthong:feat/api-profile-ux
Jun 1, 2026
Merged

feat(profiles): 3rd-party API endpoints via per-profile env#5
Shahinyanm merged 1 commit into
Digital-Threads:masterfrom
tranquocthong:feat/api-profile-ux

Conversation

@tranquocthong
Copy link
Copy Markdown

@tranquocthong tranquocthong commented May 29, 2026

What

Lets a profile point at a self-hosted / 3rd-party Claude-compatible API endpoint instead of an Anthropic subscription.

aimux profile add myapi --api      # interactive: Base URL, hidden token, per-tier models
aimux run myapi                    # injects env, no OAuth

How

Core (env injection)

  • parseDotenv + loadProfileEnv in run.ts: merge a profile's <path>/.env with an optional config.yaml env: block (YAML wins on conflict); injected into both aimux run and aimux auth login.
  • ProfileConfig.env type + validation; .env added to DEFAULT_PRIVATE_ELEMENTS (never symlinked to the shared source).

UX (apiProfile.ts, self-contained)

  • aimux profile add --api: interactive prompt; credentials collected before any disk write (Ctrl+C leaves no orphan); .env written chmod 600; seeds a minimal .claude.json so Claude Code skips onboarding/OAuth.
  • aimux profile update -e KEY=VALUE / --unset-env KEY edits .env in place.
  • aimux run warns when a profile .env is group/other-readable.
  • StatusView distinguishes oauth vs api (N vars).
  • Custom single-consumer StdinLineReader for the prompt flow — avoids readline's lost-input / unsettled-promise quirks and CRLF double-line on Node 22+; masks the secret in TTY, handles piped input for CI.

Relationship to #3

This overlaps with #3 (per-profile env via .env) and adopts the same core API surface (parseDotenv / loadProfileEnv / env: block) to keep the merge trivial. It additionally:

  • addresses all three review notes on feat: per-profile env vars via .env file and YAML env block #3 — strips inline comments after quoted values (the FOO="bar" # x case), documents no ${VAR}/multi-line, and adds the chmod 600 + loose-permission runtime warning;
  • layers the interactive --api UX, .claude.json seeding, and the oauth/api status distinction on top.

If #3 lands first, the core helpers dedupe trivially and this becomes the UX layer; if not, it stands alone.

Tests

130 passing. New: parseDotenv (incl. quoted-comment + CRLF round-trip), loadProfileEnv override order, writeProfileDotEnv 0600 round-trip, mergeProfileDotEnv, permission check.

Closes #2

Copy link
Copy Markdown
Member

@Shahinyanm Shahinyanm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really nice work — this is basically a superset of #3 and you already folded in the three notes I left there. The interactive flow, the chmod 600 default and the oauth/api split in status all read clean, and the parseDotenv tests cover the cases I'd have worried about.

There's one thing I'd like fixed before merging, plus a few small notes you can take or leave.

Blocking — chmod 600 isn't actually enforced on rewrite.
writeProfileDotEnv passes { mode: 0o600 } to writeFileSync, but Node only applies that mode when it creates the file. If the .env already exists, the mode argument is ignored and the old permissions stick. So the path that bites: someone ends up with a 0644 .env (created by hand, or the #3 way), then runs aimux profile update -e KEY=VALmergeProfileDotEnv rewrites it → it stays 0644. The docstring says "Re-writes with chmod 600", which is the behaviour I'd want, just not what happens today. Your perm test passes because it writes into a fresh tmp dir where the file doesn't exist yet.

One extra line fixes it — chmodSync(target, 0o600) right after the write. A merge-over-an-existing-0644-file test would lock it in.

Smaller stuff, non-blocking:

  • checkDotenvPermissions only runs on aimux run. Since env is injected into auth login too, the same warning there would be consistent.
  • Name collision in profile add --api: addProfile throws after collectApiCredentials, so if the name's already taken the user types the token blind and then hits the error. Checking the name before prompting would be kinder. Minor.
  • seedApiClaudeJson writes just { hasCompletedOnboarding: true } — worth a quick sanity check against a current Claude Code build that it's enough to actually skip onboarding (newer versions sometimes want a couple more fields). If it works for you, it's fine.

Fix the chmod one and I'm happy to merge. Thanks for the thorough tests.

Comment thread src/core/apiProfile.ts Outdated
for (const [key, value] of Object.entries(vars)) {
lines.push(`${key}=${serializeDotenvValue(value)}`);
}
writeFileSync(join(profilePath, '.env'), lines.join('\n') + '\n', { encoding: 'utf-8', mode: 0o600 });
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This mode: 0o600 only takes effect when the file is created. On an overwrite (e.g. via mergeProfileDotEnv) Node ignores it and keeps the existing permissions. A chmodSync(join(profilePath, '.env'), 0o600) right after the write makes the 600 guarantee hold every time.

Adds a way to point a profile at a self-hosted or 3rd-party
Claude-compatible API endpoint instead of an Anthropic subscription.

Core (env injection):
- parseDotenv + loadProfileEnv in run.ts: merge a profile's <path>/.env
  dotenv file with an optional config.yaml `env:` block (YAML wins on
  conflict); injected into both `aimux run` and `aimux auth login`
- ProfileConfig.env type + config validation; `.env` added to
  DEFAULT_PRIVATE_ELEMENTS so it is never symlinked to the shared source
- dotenv parser strips inline comments AFTER quoted values too
  (fixes the `FOO="bar" # x` case); no ${VAR}/multi-line by design

UX (apiProfile.ts — additive, self-contained):
- `aimux profile add <name> --api`: interactive prompt (Base URL, hidden
  auth token, per-tier models). Credentials collected before any disk
  write so Ctrl+C leaves no orphan; .env written chmod 600
- Single-consumer StdinLineReader for the prompt flow — avoids the
  lost-input / unsettled-promise quirks of repeated readline interfaces;
  masks the secret in TTY, handles piped input for CI
- `aimux profile update -e KEY=VALUE / --unset-env KEY` edits .env in place
- `aimux run` warns when a profile .env is group/other-readable
- StatusView distinguishes oauth vs api (N vars)

Tests: parseDotenv (incl. quoted-comment bugfix), loadProfileEnv override
order, writeProfileDotEnv 0600 round-trip, mergeProfileDotEnv, perms check.
README + design doc updated.
@tranquocthong tranquocthong force-pushed the feat/api-profile-ux branch from 40d9dce to fee2335 Compare June 1, 2026 05:27
@tranquocthong
Copy link
Copy Markdown
Author

Thanks for the careful review — all addressed in fee2335.

Blocking — chmod 600 on rewrite: fixed. writeProfileDotEnv now calls chmodSync(target, 0o600) after the write, so an overwrite of an existing looser-permission .env (hand-written or #3-style 0644) is tightened too — not just freshly-created files. Added a regression test that pre-creates a 0644 .env, runs mergeProfileDotEnv, and asserts the result is 0600.

Non-blocking, also done:

  • checkDotenvPermissions now runs on aimux auth login as well, so the loose-permission warning is consistent with aimux run (env is injected into both).
  • profile add --api checks the name collision up front and throws before any prompt, so a duplicate name no longer makes the user type the token blind.

seedApiClaudeJson: verified against the current Claude Code build (v2.1.156) — { "hasCompletedOnboarding": true } is enough to skip onboarding and go straight to the API endpoint in my testing. Happy to add more fields if you hit a version that needs them.

131 tests green. Ready for another look 🙏

@Shahinyanm Shahinyanm merged commit b1e84db into Digital-Threads:master Jun 1, 2026
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.

Feature request: Allow setting up profiles with API keys (Microsoft Foundry for example)

3 participants