Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,10 @@ claudecm add work --preset moonshot --api-key sk-ant-xxxxxxxx
# use the network; --dry-run previews the redacted draft without writing.
claudecm add work --from-text 'ANTHROPIC_BASE_URL=https://api.anthropic.com ANTHROPIC_AUTH_TOKEN=sk-ant-xxxxxxxx' --dry-run

# Or sweep local sources at once and skip credentials already recorded
# by the same base_url + api_key.
claudecm add work --auto --dry-run
# Or sweep local sources at once. Each new credential becomes one
# auto-named profile; already-recorded credentials are skipped.
claudecm add --auto --dry-run
claudecm add --auto --yes

# Optional AI parse is opt-in per invocation and requires an interactive TTY.
# claudecm strips secret-shaped tokens locally, shows the desensitized payload
Expand Down Expand Up @@ -151,9 +152,9 @@ Presets are convenience templates, not official provider support, certification,
- `--from-env` reads the Claude Code / Codex environment-variable allowlist.
- `--from-file <path>` parses dotenv, shell, JSON, YAML, or TOML config files.
- `--from-text <text>` or `--from-text -` parses pasted text with local heuristics.
- `--auto` / `-a` sweeps clipboard, environment, `~/.claude/settings.json`, and `~/.codex/{auth.json,config.toml}` in order, drops candidates without an API key, and marks credentials whose `(base_url, api_key)` are already recorded.
- `--auto` / `-a` takes no profile name. It sweeps clipboard, environment, `~/.claude/settings.json`, and `~/.codex/{auth.json,config.toml}` in order, drops candidates without an API key, collapses duplicate credentials, skips credentials already recorded, and creates one profile per remaining credential.

These paths are local-first. `--auto` is zero-network and best-effort: a missing clipboard tool or absent config file is reported and does not stop the other sources. If it finds one new credential, it enters the normal redacted preview/save path; if it finds several, non-interactive runs refuse with a redacted list and interactive runs ask which one to use. Without `--ai`, pasted text never leaves the machine. `--ai` is an explicit escalation for `--from-text`: claudecm requires an interactive terminal, runs the local redaction pass first, shows the full desensitized payload for confirmation, keeps captured secrets in-process, sends only the confirmed desensitized text to an Anthropic-compatible Messages endpoint using the active profile's credentials (or `--ai-profile <name>`), then re-injects the secret locally before the normal redacted preview/save path. Non-interactive or piped `--ai` runs refuse before any parse request is sent.
These paths are local-first. `--auto` is zero-network and best-effort: a missing clipboard tool, absent config file, or malformed optional source is reported and does not stop the other sources. It reads Codex credentials leniently, so an unreadable `config.toml` never suppresses an `auth.json` API key. Use `--dry-run` to preview every profile that would be created. Interactive terminals prompt for each new credential name with a derived default and accept Enter to keep it, then ask for confirmation before writing; `--yes` and non-interactive runs use derived names without prompting, and non-interactive runs require `--yes`. Without `--ai`, pasted text never leaves the machine. `--ai` is an explicit escalation for `--from-text`: claudecm requires an interactive terminal, runs the local redaction pass first, shows the full desensitized payload for confirmation, keeps captured secrets in-process, sends only the confirmed desensitized text to an Anthropic-compatible Messages endpoint using the active profile's credentials (or `--ai-profile <name>`), then re-injects the secret locally before the normal redacted preview/save path. Non-interactive or piped `--ai` runs refuse before any parse request is sent.

## Deeper reading

Expand Down
53 changes: 31 additions & 22 deletions cmd/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ var (
addAIProfileFlag string
addListPresetsFlag bool
addDryRunFlag bool
addYesFlag bool
addOverwriteFlag bool
addOutputFlag string
)
Expand Down Expand Up @@ -189,8 +190,11 @@ EXAMPLES
claudecm add work --from-text 'ANTHROPIC_BASE_URL=https://api.anthropic.com ANTHROPIC_AUTH_TOKEN=sk-...' --dry-run
cat provider.txt | claudecm add work --from-text -

# Sweep local sources and skip credentials already recorded.
claudecm add work --auto --dry-run
# Sweep local sources and auto-name one profile per new credential.
# On an interactive terminal, each new credential prompts:
# Save profile for <source, redacted key> as [derived-name]:
claudecm add --auto --dry-run
claudecm add --auto --yes

# Opt in to one secret-free LLM parse from an interactive terminal.
claudecm add work --from-text 'messy provider note with sk-...' --ai --dry-run
Expand All @@ -210,6 +214,12 @@ to make it the active profile.`,
if addListPresetsFlag {
return cobra.NoArgs(cmd, args)
}
if addAutoFlag {
if len(args) > 0 {
return fmt.Errorf("--auto does not take a profile name; names are derived from discovered sources")
}
return cobra.NoArgs(cmd, args)
}
return cobra.ExactArgs(1)(cmd, args)
},
RunE: runAdd,
Expand All @@ -234,6 +244,7 @@ func init() {
"Sparse overlay entry (repeatable). Format: tools.<tool>.<sub>=<value>. "+
"Supported: tools.claude_code.env.<VAR>=<value>, tools.codex.raw.<key>=<value>")
addCmd.Flags().BoolVar(&addDryRunFlag, "dry-run", false, "Print the would-be profile and exit without writing")
addCmd.Flags().BoolVar(&addYesFlag, "yes", false, "Skip interactive naming and confirmation for --auto")
addCmd.Flags().BoolVar(&addOverwriteFlag, "overwrite", false, "Allow replacing an existing profile with the same name")
addCmd.Flags().StringVarP(&addOutputFlag, "output", "o", "text", "Output format (text|json)")

Expand All @@ -246,16 +257,22 @@ func init() {
// bytes.Buffers.
func runAdd(cmd *cobra.Command, args []string) error {
if addListPresetsFlag {
if len(args) != 0 {
return fmt.Errorf("--list-presets does not take a profile name")
}
format, err := parseAddOutput(addOutputFlag)
if err != nil {
return err
}
return renderPresetList(cmd.OutOrStdout(), format)
}

name := strings.TrimSpace(args[0])
if err := storage.ValidateProfileName(name); err != nil {
return err
if addAutoFlag {
if len(args) != 0 {
return fmt.Errorf("--auto does not take a profile name; names are derived from discovered sources")
}
} else if len(args) != 1 {
return fmt.Errorf("add requires exactly one profile name unless --auto is used")
}

format, err := parseAddOutput(addOutputFlag)
Expand Down Expand Up @@ -298,6 +315,15 @@ func runAdd(cmd *cobra.Command, args []string) error {
}
store := storage.NewFileStorage(resv)

if addAutoFlag {
return runAddAuto(cmd, resv, store, format)
}

name := strings.TrimSpace(args[0])
if err := storage.ValidateProfileName(name); err != nil {
return err
}

provider := addProviderFlag
baseURL := addBaseURLFlag
apiKey := addAPIKeyFlag
Expand All @@ -310,23 +336,6 @@ func runAdd(cmd *cobra.Command, args []string) error {
model = preset.Model
tools = cloneToolMap(preset.Tools)
}
if addAutoFlag {
core, autoTools, done, err := profileDraftFromAuto(cmd, resv, store, format)
if err != nil {
return err
}
if done {
return nil
}
if core.Provider != "" {
provider = core.Provider
}
baseURL = core.BaseURL
apiKey = core.APIKey
model = core.Model
smallFastModel = core.SmallFastModel
tools = mergeToolMaps(tools, autoTools)
}
if addFromEnvFlag {
core, envTools, err := profileDraftFromEnv()
if err != nil {
Expand Down
Loading
Loading