ai-switch-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ai_switch.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Switch Codex and Claude Code configuration profiles without a GUI."""
|
|
3
|
+
import argparse, getpass, json, os, re, shutil, sys, tempfile, time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
HOME = Path.home()
|
|
7
|
+
CODEX = HOME / ".codex" / "config.toml"
|
|
8
|
+
CLAUDE = HOME / ".claude" / "settings.json"
|
|
9
|
+
CODEX_AUTH = HOME / ".codex" / "auth.json"
|
|
10
|
+
CODEX_MODELS = HOME / ".codex" / "models.json"
|
|
11
|
+
ROOT = Path(os.environ.get("AI_SWITCH_HOME", HOME / ".config" / "ai-switch"))
|
|
12
|
+
PROFILES = ROOT / "profiles"
|
|
13
|
+
STATE = ROOT / "current"
|
|
14
|
+
|
|
15
|
+
def secure(p):
|
|
16
|
+
try: p.chmod(0o700 if p.is_dir() else 0o600)
|
|
17
|
+
except OSError: pass
|
|
18
|
+
|
|
19
|
+
def write_atomic(path, data):
|
|
20
|
+
path.parent.mkdir(parents=True, exist_ok=True); secure(path.parent)
|
|
21
|
+
fd, tmp = tempfile.mkstemp(prefix=".tmp-", dir=str(path.parent))
|
|
22
|
+
try:
|
|
23
|
+
with os.fdopen(fd, "w") as f: f.write(data)
|
|
24
|
+
os.chmod(tmp, 0o600); os.replace(tmp, path)
|
|
25
|
+
finally:
|
|
26
|
+
if os.path.exists(tmp): os.unlink(tmp)
|
|
27
|
+
|
|
28
|
+
def profile(name):
|
|
29
|
+
if not name or Path(name).name != name or name in (".", ".."): raise ValueError("invalid profile name")
|
|
30
|
+
return PROFILES / name
|
|
31
|
+
|
|
32
|
+
def init(name):
|
|
33
|
+
d = profile(name); d.mkdir(parents=True, exist_ok=False); secure(d)
|
|
34
|
+
if CODEX.exists(): shutil.copy2(CODEX, d / "codex-config.toml")
|
|
35
|
+
if CLAUDE.exists(): shutil.copy2(CLAUDE, d / "claude-settings.json")
|
|
36
|
+
if CODEX_AUTH.exists(): shutil.copy2(CODEX_AUTH, d / "codex-auth.json")
|
|
37
|
+
if CODEX_MODELS.exists(): shutil.copy2(CODEX_MODELS, d / "codex-models.json")
|
|
38
|
+
for p in (d / "codex-config.toml", d / "claude-settings.json", d / "codex-auth.json", d / "codex-models.json"): secure(p)
|
|
39
|
+
write_atomic(d / "profile.json", json.dumps({"description": getattr(init, "description", ""), "created": time.strftime("%Y-%m-%d %H:%M:%S")}, ensure_ascii=True, indent=2) + "\n")
|
|
40
|
+
print(f"Created profile: {name}")
|
|
41
|
+
|
|
42
|
+
def summary(d):
|
|
43
|
+
meta = {}
|
|
44
|
+
try: meta = json.loads((d / "profile.json").read_text())
|
|
45
|
+
except (OSError, ValueError): pass
|
|
46
|
+
clients=[]; details=[]
|
|
47
|
+
c=d/"codex-config.toml"
|
|
48
|
+
if c.exists():
|
|
49
|
+
clients.append("Codex")
|
|
50
|
+
s=c.read_text(errors="replace")
|
|
51
|
+
model=re.search(r'^model\s*=\s*["\']([^"\']+)',s,re.M)
|
|
52
|
+
base=re.search(r'^base_url\s*=\s*["\']([^"\']+)',s,re.M)
|
|
53
|
+
if model: details.append("Codex model="+model.group(1))
|
|
54
|
+
if base: details.append("Codex endpoint="+base.group(1).split('/')[2] if '://' in base.group(1) else "Codex endpoint configured")
|
|
55
|
+
c=d/"claude-settings.json"
|
|
56
|
+
if c.exists():
|
|
57
|
+
clients.append("Claude")
|
|
58
|
+
try:
|
|
59
|
+
x=json.loads(c.read_text()); env=x.get("env",{}); model=x.get("model")
|
|
60
|
+
if model: details.append("Claude model="+str(model))
|
|
61
|
+
if env.get("ANTHROPIC_BASE_URL"): details.append("Claude endpoint="+str(env["ANTHROPIC_BASE_URL"]).split('/')[2] if '://' in str(env["ANTHROPIC_BASE_URL"]) else "Claude endpoint configured")
|
|
62
|
+
except ValueError: details.append("Claude settings invalid JSON")
|
|
63
|
+
return meta.get("description") or "No description", ",".join(clients) or "No config", "; ".join(details) or "No automatic summary"
|
|
64
|
+
|
|
65
|
+
def list_profiles(_):
|
|
66
|
+
current = STATE.read_text().strip() if STATE.exists() else ""
|
|
67
|
+
for d in sorted(PROFILES.iterdir() if PROFILES.exists() else []):
|
|
68
|
+
if d.is_dir():
|
|
69
|
+
desc, clients, details = summary(d)
|
|
70
|
+
print(f"{'*' if d.name == current else ' '} {d.name:<16} {clients:<12} {desc} [{details}]")
|
|
71
|
+
|
|
72
|
+
def use(name):
|
|
73
|
+
d = profile(name)
|
|
74
|
+
if not d.is_dir(): raise FileNotFoundError(f"profile not found: {name}")
|
|
75
|
+
backup = ROOT / "backups" / time.strftime("%Y%m%d-%H%M%S")
|
|
76
|
+
backup.mkdir(parents=True, exist_ok=True); secure(backup)
|
|
77
|
+
for target, source, label in ((CODEX,d/"codex-config.toml","Codex"),(CLAUDE,d/"claude-settings.json","Claude"),(CODEX_AUTH,d/"codex-auth.json","Codex auth"),(CODEX_MODELS,d/"codex-models.json","Codex models")):
|
|
78
|
+
if source.exists():
|
|
79
|
+
if target.exists(): shutil.copy2(target, backup / target.name); secure(backup / target.name)
|
|
80
|
+
write_atomic(target, source.read_text())
|
|
81
|
+
print(f"Switched {label}")
|
|
82
|
+
if (d/"codex-config.toml").exists() and not (d/"codex-auth.json").exists():
|
|
83
|
+
print("Warning: profile has no Codex auth.json; run 'ai-switch init' again after configuring its API key.", file=sys.stderr)
|
|
84
|
+
write_atomic(STATE, name + "\n")
|
|
85
|
+
print(f"Active profile: {name}\nBackup: {backup}")
|
|
86
|
+
|
|
87
|
+
def current(_): print(STATE.read_text().strip() if STATE.exists() else "(none)")
|
|
88
|
+
|
|
89
|
+
def describe(name, text):
|
|
90
|
+
d=profile(name)
|
|
91
|
+
if not d.is_dir(): raise FileNotFoundError(f"profile not found: {name}")
|
|
92
|
+
write_atomic(d/"profile.json", json.dumps({"description": text, "updated": time.strftime("%Y-%m-%d %H:%M:%S")}, ensure_ascii=True, indent=2)+"\n")
|
|
93
|
+
print(f"Updated description: {name}")
|
|
94
|
+
|
|
95
|
+
def add_interactive(_):
|
|
96
|
+
print("Create a provider profile (input is not echoed for API keys).")
|
|
97
|
+
print("At any prompt, type 'cancel' or press Ctrl-C to exit without saving.\n")
|
|
98
|
+
provider=input("Provider [glm/deepseek/custom] (default: glm): ").strip().lower() or "glm"
|
|
99
|
+
if provider not in ("glm","deepseek","custom"): raise ValueError("provider must be glm, deepseek, or custom")
|
|
100
|
+
if provider == "glm":
|
|
101
|
+
endpoint="https://open.bigmodel.cn/api/v1"; model="glm-5.3"; claude_endpoint="https://open.bigmodel.cn/api/anthropic"
|
|
102
|
+
print("Using GLM preset: Codex Responses API and Claude model mappings will be configured automatically.")
|
|
103
|
+
elif provider == "deepseek":
|
|
104
|
+
endpoint="https://api.deepseek.com/"; model="deepseek-v4-flash"; claude_endpoint="https://api.deepseek.com/anthropic"
|
|
105
|
+
print("Using DeepSeek preset: Codex Responses API, model catalog, and Claude model mappings will be configured automatically.")
|
|
106
|
+
else:
|
|
107
|
+
endpoint=input("API endpoint URL (e.g. https://api.example.com/v1): ").strip()
|
|
108
|
+
model=input("Model name: ").strip(); claude_endpoint=endpoint[:-3].rstrip("/") if endpoint.endswith("/v1") else endpoint.rstrip("/")
|
|
109
|
+
name=input("Profile name: ").strip(); desc=input("Description: ").strip()
|
|
110
|
+
key=getpass.getpass("API key: ")
|
|
111
|
+
clients=input("Configure clients [both/codex/claude] (default: both): ").strip().lower() or "both"
|
|
112
|
+
if clients not in ("both","codex","claude"): raise ValueError("clients must be both, codex, or claude")
|
|
113
|
+
d=profile(name); d.mkdir(parents=True, exist_ok=False); secure(d)
|
|
114
|
+
if clients in ("both","codex"):
|
|
115
|
+
if provider == "glm":
|
|
116
|
+
base='model_provider = "ZAI"\nmodel = "glm-5.3"\nmodel_reasoning_effort = "max"\nmodel_catalog_json = "~/.codex/models.json"\n\n[model_providers.ZAI]\nname = "ZAI"\nbase_url = "https://open.bigmodel.cn/api/v1"\nexperimental_bearer_token = "'+key+'"\nwire_api = "responses"\n'
|
|
117
|
+
elif provider == "deepseek":
|
|
118
|
+
base='model = "deepseek-v4-flash"\nmodel_provider = "deepseek"\npreferred_auth_method = "apikey"\nforced_login_method = "api"\nmodel_reasoning_effort = "high"\nmodel_catalog_json = "~/.codex/models.json"\n\n[model_providers.deepseek]\nname = "deepseek"\nbase_url = "https://api.deepseek.com/"\nwire_api = "responses"\nexperimental_bearer_token = "'+key+'"\n'
|
|
119
|
+
else: base=CODEX.read_text() if CODEX.exists() else 'model_provider = "custom"\nmodel = "MODEL"\n\n[model_providers.custom]\nname = "custom"\nbase_url = "ENDPOINT"\nwire_api = "responses"\nrequires_openai_auth = true\n'
|
|
120
|
+
base=re.sub(r'(?m)^model\s*=\s*["\'][^"\']*["\']', f'model = "{model}"', base, count=1)
|
|
121
|
+
base=re.sub(r'(?m)^base_url\s*=\s*["\'][^"\']*["\']', f'base_url = "{endpoint}"', base, count=1)
|
|
122
|
+
write_atomic(d/"codex-config.toml", base)
|
|
123
|
+
write_atomic(d/"codex-auth.json", json.dumps({"OPENAI_API_KEY":key}, indent=2)+"\n")
|
|
124
|
+
if provider in ("glm", "deepseek"):
|
|
125
|
+
if provider == "deepseek":
|
|
126
|
+
models=[]
|
|
127
|
+
for slug, desc, modalities, priority, context in (("deepseek-v4-flash","Fast general-purpose DeepSeek model",["text"],0,1048576),("deepseek-v4-pro","Deep reasoning DeepSeek model",["text"],1,1048576),("deepseek-v4-flash-vision-exp","DeepSeek vision model",["text","image"],2,1048576)):
|
|
128
|
+
models.append({"slug":slug,"display_name":slug,"description":desc,"default_reasoning_level":"high","supported_reasoning_levels":[{"effort":"low","description":"Light reasoning"},{"effort":"high","description":"Enhanced reasoning"},{"effort":"max","description":"Deep reasoning"}],"shell_type":"shell_command","visibility":"list","supported_in_api":True,"priority":priority,"base_instructions":"","supports_reasoning_summaries":True,"default_reasoning_summary":"none","support_verbosity":False,"apply_patch_tool_type":"freeform","truncation_policy":{"mode":"bytes","limit":10000},"context_window":context,"max_context_window":context,"effective_context_window_percent":95,"supports_parallel_tool_calls":True,"experimental_supported_tools":[],"input_modalities":modalities})
|
|
129
|
+
write_atomic(d/"codex-models.json", json.dumps({"models":models}, indent=2)+"\n")
|
|
130
|
+
else:
|
|
131
|
+
catalog={"models":[{"slug":"glm-5.3","display_name":"glm-5.3","description":"Z.ai flagship model","default_reasoning_level":"max","supported_reasoning_levels":[{"effort":"low","description":"Light reasoning"},{"effort":"high","description":"Enhanced reasoning"},{"effort":"max","description":"Deep reasoning"}],"shell_type":"shell_command","visibility":"list","supported_in_api":True,"priority":0,"base_instructions":"","supports_reasoning_summaries":True,"default_reasoning_summary":"none","support_verbosity":False,"apply_patch_tool_type":"freeform","truncation_policy":{"mode":"bytes","limit":10000},"context_window":1048576,"max_context_window":1048576,"effective_context_window_percent":95,"supports_parallel_tool_calls":True,"experimental_supported_tools":[],"input_modalities":["text"]}]}
|
|
132
|
+
write_atomic(d/"codex-models.json", json.dumps(catalog, indent=2)+"\n")
|
|
133
|
+
if clients in ("both","claude"):
|
|
134
|
+
obj=json.loads(CLAUDE.read_text()) if CLAUDE.exists() else {}
|
|
135
|
+
obj.setdefault("env",{}).update({"ANTHROPIC_BASE_URL":claude_endpoint,"ANTHROPIC_AUTH_TOKEN":key})
|
|
136
|
+
if provider == "glm": obj["env"].update({"ANTHROPIC_DEFAULT_HAIKU_MODEL":"glm-5.3-flash[1m]","ANTHROPIC_DEFAULT_SONNET_MODEL":"glm-5.3[1m]","ANTHROPIC_DEFAULT_OPUS_MODEL":"glm-5.3[1m]","CLAUDE_CODE_AUTO_COMPACT_WINDOW":"1000000","CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC":1,"API_TIMEOUT_MS":"3000000"})
|
|
137
|
+
elif provider == "deepseek": obj["env"].update({"ANTHROPIC_MODEL":"deepseek-v4-pro[1m]","ANTHROPIC_DEFAULT_OPUS_MODEL":"deepseek-v4-pro[1m]","ANTHROPIC_DEFAULT_SONNET_MODEL":"deepseek-v4-pro[1m]","ANTHROPIC_DEFAULT_HAIKU_MODEL":"deepseek-v4-flash","CLAUDE_CODE_SUBAGENT_MODEL":"deepseek-v4-flash","CLAUDE_CODE_EFFORT_LEVEL":"max","CLAUDE_CODE_AUTO_COMPACT_WINDOW":"786432"})
|
|
138
|
+
obj["model"]=model; write_atomic(d/"claude-settings.json", json.dumps(obj, indent=2)+"\n")
|
|
139
|
+
write_atomic(d/"profile.json", json.dumps({"description":desc,"created":time.strftime("%Y-%m-%d %H:%M:%S")}, indent=2)+"\n")
|
|
140
|
+
print(f"Created profile: {name}. Activate it with: ai-switch use {name}")
|
|
141
|
+
|
|
142
|
+
def main():
|
|
143
|
+
description = "Switch Codex and Claude Code API profiles without a GUI."
|
|
144
|
+
epilog = """Examples:
|
|
145
|
+
ai-switch init openai Save the current configuration as 'openai'
|
|
146
|
+
ai-switch list List profiles (* marks the active one)
|
|
147
|
+
ai-switch use glm Activate the 'glm' profile
|
|
148
|
+
ai-switch current Show the active profile
|
|
149
|
+
|
|
150
|
+
Profiles: ~/.config/ai-switch/profiles/
|
|
151
|
+
Backups: ~/.config/ai-switch/backups/
|
|
152
|
+
Set AI_SWITCH_HOME to override the storage directory.
|
|
153
|
+
After switching, restart claude/codex so they reload their configuration."""
|
|
154
|
+
ap=argparse.ArgumentParser(prog="ai-switch", description=description,
|
|
155
|
+
epilog=epilog, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
156
|
+
sp=ap.add_subparsers(dest="cmd")
|
|
157
|
+
p=sp.add_parser("init", help="save current Codex/Claude config as a new profile", description="Save the current configuration files as a new profile.")
|
|
158
|
+
p.add_argument("name", help="profile name (letters, numbers, . _ -; no path separators)")
|
|
159
|
+
p.add_argument("-d", "--description", default="", help="human-readable purpose, e.g. 'GLM Coding Plan'")
|
|
160
|
+
p.set_defaults(fn=init)
|
|
161
|
+
p=sp.add_parser("list", help="list all profiles", description="List profiles; '*' marks the active profile."); p.set_defaults(fn=list_profiles)
|
|
162
|
+
p=sp.add_parser("use", help="activate a profile", description="Back up current files and atomically activate the selected profile.")
|
|
163
|
+
p.add_argument("name", help="profile name"); p.set_defaults(fn=use)
|
|
164
|
+
p=sp.add_parser("current", help="show active profile", description="Print the active profile name, or '(none)'."); p.set_defaults(fn=current)
|
|
165
|
+
p=sp.add_parser("describe", help="set a profile description", description="Update the human-readable description of an existing profile.")
|
|
166
|
+
p.add_argument("name", help="profile name"); p.add_argument("text", help="description"); p.set_defaults(fn=None)
|
|
167
|
+
p=sp.add_parser("add", help="create a profile interactively", description="Interactively create a profile without editing configuration files."); p.set_defaults(fn=add_interactive)
|
|
168
|
+
a=ap.parse_args()
|
|
169
|
+
if not a.cmd:
|
|
170
|
+
ap.print_help()
|
|
171
|
+
print("\nAvailable profiles:")
|
|
172
|
+
list_profiles(a)
|
|
173
|
+
return 0
|
|
174
|
+
if a.cmd == "init": init.description = a.description
|
|
175
|
+
if a.cmd == "describe": a.fn = lambda n: describe(n, a.text)
|
|
176
|
+
try: a.fn(getattr(a,"name",a))
|
|
177
|
+
except KeyboardInterrupt:
|
|
178
|
+
print("\nCancelled. No profile was created.", file=sys.stderr)
|
|
179
|
+
return 130
|
|
180
|
+
except (OSError, ValueError) as e: print(f"Error: {e}", file=sys.stderr); return 1
|
|
181
|
+
return 0
|
|
182
|
+
if __name__ == "__main__": raise SystemExit(main())
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ai-switch-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A secure CLI profile switcher for Codex and Claude Code
|
|
5
|
+
Author: ZidongS
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: codex,claude-code,api,profile,cli
|
|
8
|
+
Classifier: Environment :: Console
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: POSIX
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# ai-switch
|
|
16
|
+
|
|
17
|
+
A small, secure command-line profile switcher for Codex and Claude Code. It works on headless servers and does not require administrator privileges.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
Install the latest development version directly from GitHub:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
python3 -m pip install --user "git+https://github.com/ZidongS/ai-switch.git"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Or install a local checkout:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
python3 -m pip install --user .
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
If `ai-switch` is not found afterwards, add the user script directory to `PATH`:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
export PATH="$HOME/.local/bin:$PATH"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick start
|
|
40
|
+
|
|
41
|
+
Save the configuration currently in use:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
ai-switch init bairuo --description "Bairuo relay for daily work"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Edit `~/.codex/config.toml` and `~/.claude/settings.json` for another provider, then save that configuration as a second profile:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
ai-switch init glm --description "GLM Coding Plan"
|
|
51
|
+
ai-switch list
|
|
52
|
+
ai-switch use glm
|
|
53
|
+
ai-switch current
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Create a profile through an interactive prompt (no editor required; API keys are hidden while typing). Choose the built-in `glm` preset to generate the complete ZAI Codex Responses configuration, Codex model catalog, and Claude Code model/environment mappings automatically. The `deepseek` preset creates the three DeepSeek model entries, including image input metadata for `deepseek-v4-flash-vision-exp`, plus the recommended Claude Code mappings:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
ai-switch add
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Update an existing profile description:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
ai-switch describe bairuo "Bairuo relay for daily work"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`list` shows the active marker, profile name, configured clients, description, detected models, and endpoint hostnames. API keys are never printed.
|
|
69
|
+
|
|
70
|
+
## Safety and storage
|
|
71
|
+
|
|
72
|
+
Before activation, the current Codex and Claude files are backed up under `~/.config/ai-switch/backups/`. Profiles are stored under `~/.config/ai-switch/profiles/`; directories use mode 700 and files use mode 600. Restart `claude` or `codex` after switching so the process reloads its configuration.
|
|
73
|
+
|
|
74
|
+
Set `AI_SWITCH_HOME` to use a different profile directory.
|
|
75
|
+
|
|
76
|
+
## Commands
|
|
77
|
+
|
|
78
|
+
```text
|
|
79
|
+
ai-switch init NAME [-d DESCRIPTION] Save current files as a new profile
|
|
80
|
+
ai-switch list List profiles and configuration summaries
|
|
81
|
+
ai-switch use NAME Back up and activate a profile
|
|
82
|
+
ai-switch current Print the active profile
|
|
83
|
+
ai-switch describe NAME TEXT Set a profile description
|
|
84
|
+
ai-switch add Create a profile interactively (GLM, DeepSeek, or custom)
|
|
85
|
+
ai-switch --help Show full usage and examples
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Development
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
python3 -m unittest -v
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The project uses only the Python standard library.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
ai_switch.py,sha256=TkkVaPTmCObzmRXDZy93pVTtKlf6S4S46bOfjgp1yTA,13878
|
|
2
|
+
ai_switch_cli-0.1.0.dist-info/METADATA,sha256=gRK2717Xo1FvS8hUs56BUddqFeTOQK70ZDIknB2PEDk,3112
|
|
3
|
+
ai_switch_cli-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
4
|
+
ai_switch_cli-0.1.0.dist-info/entry_points.txt,sha256=dygvfzbH-sh8Ll2K6R0hZT1G93wz5CGs3l9T84VdysE,45
|
|
5
|
+
ai_switch_cli-0.1.0.dist-info/top_level.txt,sha256=1KbjpO07l32H-mrv-bF-nbpTj2wuBXmldINYlCVwwVQ,10
|
|
6
|
+
ai_switch_cli-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ai_switch
|