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
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +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

# Optional AI parse is opt-in per invocation and requires an interactive TTY.
# claudecm strips secret-shaped tokens locally, shows the desensitized payload
# for confirmation, then sends one Anthropic-compatible messages request.
Expand Down Expand Up @@ -147,8 +151,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.

These paths are local-first. 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 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.

## Deeper reading

Expand Down
36 changes: 32 additions & 4 deletions cmd/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ var (
addFromEnvFlag bool
addFromFileFlag string
addFromTextFlag string
addAutoFlag bool
addAIFlag bool
addAIProfileFlag string
addListPresetsFlag bool
Expand Down Expand Up @@ -188,6 +189,9 @@ 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

# 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 Down Expand Up @@ -222,6 +226,7 @@ func init() {
addCmd.Flags().BoolVar(&addFromEnvFlag, "from-env", false, "Build the profile draft from Claude Code / Codex environment variables")
addCmd.Flags().StringVar(&addFromFileFlag, "from-file", "", "Build the profile draft from a dotenv, shell, JSON, YAML, or TOML file")
addCmd.Flags().StringVar(&addFromTextFlag, "from-text", "", "Build the profile draft from pasted text locally; use '-' to read stdin")
addCmd.Flags().BoolVarP(&addAutoFlag, "auto", "a", false, "Sweep clipboard, environment, and known tool configs; skip already-recorded credentials")
addCmd.Flags().BoolVar(&addAIFlag, "ai", false, "With --from-text, opt in to one interactive, reviewed, secret-free Anthropic-compatible LLM parse")
addCmd.Flags().StringVar(&addAIProfileFlag, "ai-profile", "", "Profile whose Anthropic-compatible credentials are borrowed for --ai parsing")
addCmd.Flags().BoolVar(&addListPresetsFlag, "list-presets", false, "List built-in provider presets and exit")
Expand Down Expand Up @@ -268,10 +273,10 @@ func runAdd(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
if err := validateAddInputSources(hasPreset); err != nil {
if err := validateAddInputSources(hasPreset, baseURLFlagSet, apiKeyFlagSet); err != nil {
return err
}
fromInputSource := addFromEnvFlag || strings.TrimSpace(addFromFileFlag) != "" || strings.TrimSpace(addFromTextFlag) != ""
fromInputSource := addAutoFlag || addFromEnvFlag || strings.TrimSpace(addFromFileFlag) != "" || strings.TrimSpace(addFromTextFlag) != ""
if fromInputSource {
providerFlagSet = flagWasExplicit(cmd, "provider", false)
baseURLFlagSet = flagWasExplicit(cmd, "base-url", false)
Expand Down Expand Up @@ -305,6 +310,23 @@ 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 Expand Up @@ -445,7 +467,7 @@ func resolveAddPreset(raw string) (presets.Preset, bool, error) {
return p, true, nil
}

func validateAddInputSources(hasPreset bool) error {
func validateAddInputSources(hasPreset, baseURLFlagSet, apiKeyFlagSet bool) error {
fromFileSet := strings.TrimSpace(addFromFileFlag) != ""
fromTextSet := strings.TrimSpace(addFromTextFlag) != ""
count := 0
Expand All @@ -461,8 +483,14 @@ func validateAddInputSources(hasPreset bool) error {
if fromTextSet {
count++
}
if addAutoFlag {
count++
}
if count > 1 {
return fmt.Errorf("choose only one add input source: --preset, --from-env, --from-file, or --from-text")
return fmt.Errorf("choose only one add input source: --preset, --from-env, --from-file, --from-text, or --auto")
}
if addAutoFlag && (baseURLFlagSet || apiKeyFlagSet) {
return fmt.Errorf("choose only one add identity source: --auto, --base-url, or --api-key")
}
if addAIFlag && !fromTextSet {
return fmt.Errorf("--ai requires --from-text")
Expand Down
Loading
Loading