abstract-claude 0.1.0__tar.gz
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.
- abstract_claude-0.1.0/PKG-INFO +71 -0
- abstract_claude-0.1.0/README.md +60 -0
- abstract_claude-0.1.0/pyproject.toml +32 -0
- abstract_claude-0.1.0/setup.cfg +4 -0
- abstract_claude-0.1.0/src/abstract_claude/__init__.py +2 -0
- abstract_claude-0.1.0/src/abstract_claude/__main__.py +6 -0
- abstract_claude-0.1.0/src/abstract_claude/actions.py +135 -0
- abstract_claude-0.1.0/src/abstract_claude/cli.py +136 -0
- abstract_claude-0.1.0/src/abstract_claude/launch.py +47 -0
- abstract_claude-0.1.0/src/abstract_claude/paths.py +29 -0
- abstract_claude-0.1.0/src/abstract_claude/server.py +218 -0
- abstract_claude-0.1.0/src/abstract_claude/templates/dot-claude.json +6 -0
- abstract_claude-0.1.0/src/abstract_claude/templates/settings.json +20 -0
- abstract_claude-0.1.0/src/abstract_claude/util.py +51 -0
- abstract_claude-0.1.0/src/abstract_claude.egg-info/PKG-INFO +71 -0
- abstract_claude-0.1.0/src/abstract_claude.egg-info/SOURCES.txt +17 -0
- abstract_claude-0.1.0/src/abstract_claude.egg-info/dependency_links.txt +1 -0
- abstract_claude-0.1.0/src/abstract_claude.egg-info/entry_points.txt +2 -0
- abstract_claude-0.1.0/src/abstract_claude.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: abstract-claude
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Fresh Claude Code session manager: settings-only template, durable OAuth, quota fallback wrapper, and a web console.
|
|
5
|
+
Author: jrputkey
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://abstractendeavors.com
|
|
8
|
+
Keywords: claude,claude-code,oauth,cli
|
|
9
|
+
Requires-Python: >=3.9
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# abstract-claude
|
|
13
|
+
|
|
14
|
+
Fresh **Claude Code** session manager: a settings-only template, durable OAuth,
|
|
15
|
+
automatic quota fallback, and a web console — packaged as one `pip`-installable CLI.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install abstract-claude # from your index
|
|
21
|
+
# or from the private index:
|
|
22
|
+
pip install -i https://<index>/api/pypi/simple/ abstract-claude
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quickstart
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
abstract-claude init # materialize AC_ROOT (config + template)
|
|
29
|
+
abstract-claude serve # web console (default http://127.0.0.1:9111)
|
|
30
|
+
sudo abstract-claude install-service # install + enable the systemd unit
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Commands
|
|
34
|
+
|
|
35
|
+
| Command | What it does |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `serve [--host --port]` | Run the web console (Model+fallback, OAuth, Template+reset, Session settings). |
|
|
38
|
+
| `init` | Create `AC_ROOT` with `config.json` + `template/` from shipped defaults. |
|
|
39
|
+
| `save-template` | Capture current `~/.claude/settings.json` + minimal flags as the template. |
|
|
40
|
+
| `restore` | Create a fresh `~/.claude` (settings + flags) if it doesn't exist. |
|
|
41
|
+
| `reset [-y]` | Wipe ALL Claude state (transcripts/history/creds) → reseed template → clean session. |
|
|
42
|
+
| `oauth-status` / `oauth-set <token>` | Show / store the durable 1-year `claude-auth` token. |
|
|
43
|
+
| `launch -- <claude args>` | Launch Claude Code, auto-switching to the fallback model when the default is over quota. |
|
|
44
|
+
| `install-service [--user]` | Write + enable `9111_abstract_claude_web.service` (needs sudo). |
|
|
45
|
+
|
|
46
|
+
## How it works
|
|
47
|
+
|
|
48
|
+
- **Template = config only.** `template/settings.json` + a minimal `~/.claude.json`
|
|
49
|
+
(onboarding/trust flags). No credentials, history, or transcripts — a reset gives a
|
|
50
|
+
genuinely clean session, not a restored snapshot.
|
|
51
|
+
- **OAuth is not templated.** Login rides on the durable 1-year `CLAUDE_CODE_OAUTH_TOKEN`
|
|
52
|
+
(managed by `claude-auth`), so a fresh session auto-authenticates — no `/login`.
|
|
53
|
+
- **Quota fallback is the wrapper.** Claude Code's `fallbackModel` only fires on
|
|
54
|
+
*overload*, never on quota; `autoContinueAtUsageLimit` doesn't exist before v2.1.234.
|
|
55
|
+
`abstract-claude launch` handles the real case: default Fable maxed → run on Opus 4.8.
|
|
56
|
+
Wire it up with:
|
|
57
|
+
```bash
|
|
58
|
+
alias claude='abstract-claude launch --dangerously-skip-permissions'
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Config
|
|
62
|
+
|
|
63
|
+
`AC_ROOT` (default `/srv/abstract_claude`) holds `config.json`:
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{ "listen_host": "127.0.0.1", "listen_port": 9111,
|
|
67
|
+
"default_model": "claude-fable-5[1m]", "quota_fallback_model": "claude-opus-4-8",
|
|
68
|
+
"preflight_quota_check": true }
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Override the data root with `AC_ROOT=/path abstract-claude ...`.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# abstract-claude
|
|
2
|
+
|
|
3
|
+
Fresh **Claude Code** session manager: a settings-only template, durable OAuth,
|
|
4
|
+
automatic quota fallback, and a web console — packaged as one `pip`-installable CLI.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install abstract-claude # from your index
|
|
10
|
+
# or from the private index:
|
|
11
|
+
pip install -i https://<index>/api/pypi/simple/ abstract-claude
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Quickstart
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
abstract-claude init # materialize AC_ROOT (config + template)
|
|
18
|
+
abstract-claude serve # web console (default http://127.0.0.1:9111)
|
|
19
|
+
sudo abstract-claude install-service # install + enable the systemd unit
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Commands
|
|
23
|
+
|
|
24
|
+
| Command | What it does |
|
|
25
|
+
|---|---|
|
|
26
|
+
| `serve [--host --port]` | Run the web console (Model+fallback, OAuth, Template+reset, Session settings). |
|
|
27
|
+
| `init` | Create `AC_ROOT` with `config.json` + `template/` from shipped defaults. |
|
|
28
|
+
| `save-template` | Capture current `~/.claude/settings.json` + minimal flags as the template. |
|
|
29
|
+
| `restore` | Create a fresh `~/.claude` (settings + flags) if it doesn't exist. |
|
|
30
|
+
| `reset [-y]` | Wipe ALL Claude state (transcripts/history/creds) → reseed template → clean session. |
|
|
31
|
+
| `oauth-status` / `oauth-set <token>` | Show / store the durable 1-year `claude-auth` token. |
|
|
32
|
+
| `launch -- <claude args>` | Launch Claude Code, auto-switching to the fallback model when the default is over quota. |
|
|
33
|
+
| `install-service [--user]` | Write + enable `9111_abstract_claude_web.service` (needs sudo). |
|
|
34
|
+
|
|
35
|
+
## How it works
|
|
36
|
+
|
|
37
|
+
- **Template = config only.** `template/settings.json` + a minimal `~/.claude.json`
|
|
38
|
+
(onboarding/trust flags). No credentials, history, or transcripts — a reset gives a
|
|
39
|
+
genuinely clean session, not a restored snapshot.
|
|
40
|
+
- **OAuth is not templated.** Login rides on the durable 1-year `CLAUDE_CODE_OAUTH_TOKEN`
|
|
41
|
+
(managed by `claude-auth`), so a fresh session auto-authenticates — no `/login`.
|
|
42
|
+
- **Quota fallback is the wrapper.** Claude Code's `fallbackModel` only fires on
|
|
43
|
+
*overload*, never on quota; `autoContinueAtUsageLimit` doesn't exist before v2.1.234.
|
|
44
|
+
`abstract-claude launch` handles the real case: default Fable maxed → run on Opus 4.8.
|
|
45
|
+
Wire it up with:
|
|
46
|
+
```bash
|
|
47
|
+
alias claude='abstract-claude launch --dangerously-skip-permissions'
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Config
|
|
51
|
+
|
|
52
|
+
`AC_ROOT` (default `/srv/abstract_claude`) holds `config.json`:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{ "listen_host": "127.0.0.1", "listen_port": 9111,
|
|
56
|
+
"default_model": "claude-fable-5[1m]", "quota_fallback_model": "claude-opus-4-8",
|
|
57
|
+
"preflight_quota_check": true }
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Override the data root with `AC_ROOT=/path abstract-claude ...`.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "abstract-claude"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Fresh Claude Code session manager: settings-only template, durable OAuth, quota fallback wrapper, and a web console."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "jrputkey" }]
|
|
13
|
+
keywords = ["claude", "claude-code", "oauth", "cli"]
|
|
14
|
+
dependencies = []
|
|
15
|
+
|
|
16
|
+
[project.scripts]
|
|
17
|
+
abstract-claude = "abstract_claude.cli:main"
|
|
18
|
+
|
|
19
|
+
[project.urls]
|
|
20
|
+
Homepage = "https://abstractendeavors.com"
|
|
21
|
+
|
|
22
|
+
[tool.pypit]
|
|
23
|
+
github_push = false
|
|
24
|
+
|
|
25
|
+
[tool.setuptools]
|
|
26
|
+
package-dir = { "" = "src" }
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.packages.find]
|
|
29
|
+
where = ["src"]
|
|
30
|
+
|
|
31
|
+
[tool.setuptools.package-data]
|
|
32
|
+
abstract_claude = ["templates/*.json"]
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Core actions: template save/restore/reset, oauth, state, and root init."""
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
|
|
5
|
+
from . import paths
|
|
6
|
+
from .paths import (CONFIG_PATH, HOME, LIVE_CLAUDE_DIR, LIVE_DOTCLAUDE,
|
|
7
|
+
LIVE_SETTINGS, ROOT, TEMPLATE_DIR, TPL_DOTCLAUDE, TPL_SETTINGS)
|
|
8
|
+
from .util import load_json, run, save_json, which_claude_auth
|
|
9
|
+
|
|
10
|
+
# Session-settings surfaced by the console (valid keys in current CC schema).
|
|
11
|
+
BOOL_KEYS = [
|
|
12
|
+
"enableArtifact", "workflowKeywordTriggerEnabled", "promptSuggestionEnabled",
|
|
13
|
+
"awaySummaryEnabled", "skipDangerousModePermissionPrompt", "autoCompactEnabled",
|
|
14
|
+
"switchModelsOnFlag", "autoScrollEnabled", "fileCheckpointingEnabled",
|
|
15
|
+
"terminalProgressBarEnabled", "remoteControlAtStartup", "inputNeededNotifEnabled",
|
|
16
|
+
"agentPushNotifEnabled",
|
|
17
|
+
]
|
|
18
|
+
EFFORT_VALUES = ["low", "medium", "high", "xhigh"]
|
|
19
|
+
PERM_MODES = ["acceptEdits", "auto", "bypassPermissions", "default", "plan"]
|
|
20
|
+
|
|
21
|
+
# Minimal flags a fresh ~/.claude.json needs to skip onboarding/trust prompts.
|
|
22
|
+
FLAG_KEYS = ["hasCompletedOnboarding", "bypassPermissionsModeAccepted",
|
|
23
|
+
"hasSeenAutoModeEntryWarning", "hasSeenTasksHint"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _pkg_default(name):
|
|
27
|
+
"""Read a shipped default template file from package data."""
|
|
28
|
+
try:
|
|
29
|
+
from importlib.resources import files
|
|
30
|
+
return (files("abstract_claude.templates") / name).read_text()
|
|
31
|
+
except Exception:
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def init_root():
|
|
36
|
+
"""Materialize AC_ROOT with config.json + template/ from shipped defaults if missing."""
|
|
37
|
+
os.makedirs(TEMPLATE_DIR, exist_ok=True)
|
|
38
|
+
created = []
|
|
39
|
+
if not os.path.exists(CONFIG_PATH):
|
|
40
|
+
save_json(CONFIG_PATH, dict(paths.DEFAULTS))
|
|
41
|
+
created.append(CONFIG_PATH)
|
|
42
|
+
for dst, name in ((TPL_SETTINGS, "settings.json"), (TPL_DOTCLAUDE, "dot-claude.json")):
|
|
43
|
+
if not os.path.exists(dst):
|
|
44
|
+
data = _pkg_default(name)
|
|
45
|
+
if data is not None:
|
|
46
|
+
with open(dst, "w") as f:
|
|
47
|
+
f.write(data)
|
|
48
|
+
created.append(dst)
|
|
49
|
+
return created
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def save_template():
|
|
53
|
+
"""Capture current settings.json + derive minimal flags into the template."""
|
|
54
|
+
os.makedirs(TEMPLATE_DIR, exist_ok=True)
|
|
55
|
+
live = load_json(LIVE_SETTINGS, {})
|
|
56
|
+
save_json(TPL_SETTINGS, live)
|
|
57
|
+
dot = load_json(LIVE_DOTCLAUDE, {})
|
|
58
|
+
flags = {k: bool(dot.get(k, True)) for k in FLAG_KEYS}
|
|
59
|
+
save_json(TPL_DOTCLAUDE, flags)
|
|
60
|
+
return {"ok": True, "out": f"saved settings + flags -> {TEMPLATE_DIR}"}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def restore():
|
|
64
|
+
"""Create a fresh ~/.claude (settings + flags) if it does not exist."""
|
|
65
|
+
if os.path.isdir(LIVE_CLAUDE_DIR):
|
|
66
|
+
return {"ok": True, "out": "~/.claude exists (use reset to force a fresh session)"}
|
|
67
|
+
os.makedirs(LIVE_CLAUDE_DIR, mode=0o700, exist_ok=True)
|
|
68
|
+
tpl = load_json(TPL_SETTINGS, {})
|
|
69
|
+
save_json(LIVE_SETTINGS, tpl)
|
|
70
|
+
save_json(LIVE_DOTCLAUDE, load_json(TPL_DOTCLAUDE, {}))
|
|
71
|
+
return {"ok": True, "out": "fresh ~/.claude created; login via CLAUDE_CODE_OAUTH_TOKEN"}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def reset():
|
|
75
|
+
"""Wipe ALL prior state, then reseed the template -> a clean session."""
|
|
76
|
+
shutil.rmtree(LIVE_CLAUDE_DIR, ignore_errors=True)
|
|
77
|
+
try:
|
|
78
|
+
os.remove(LIVE_DOTCLAUDE)
|
|
79
|
+
except FileNotFoundError:
|
|
80
|
+
pass
|
|
81
|
+
r = restore()
|
|
82
|
+
r["out"] = "wiped prior state; " + r["out"]
|
|
83
|
+
return r
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def oauth_status():
|
|
87
|
+
r = run([which_claude_auth(), "status"], timeout=20)
|
|
88
|
+
return (r["out"] + (("\n" + r["err"]) if r["err"] else "")).strip()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def oauth_set(token):
|
|
92
|
+
token = (token or "").strip()
|
|
93
|
+
if not token.startswith("sk-ant-"):
|
|
94
|
+
return {"ok": False, "err": "token must start with sk-ant-"}
|
|
95
|
+
return run([which_claude_auth(), "set", token])
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def oauth_probe():
|
|
99
|
+
return run([which_claude_auth(), "probe"], timeout=40)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def set_model(default_model=None, quota_fallback_model=None, fallback_model=None):
|
|
103
|
+
live = load_json(LIVE_SETTINGS, {})
|
|
104
|
+
cfg = load_json(CONFIG_PATH, {})
|
|
105
|
+
if default_model:
|
|
106
|
+
live["model"] = default_model
|
|
107
|
+
cfg["default_model"] = default_model
|
|
108
|
+
if fallback_model is not None:
|
|
109
|
+
fm = [x for x in fallback_model if x]
|
|
110
|
+
if fm:
|
|
111
|
+
live["fallbackModel"] = fm
|
|
112
|
+
else:
|
|
113
|
+
live.pop("fallbackModel", None)
|
|
114
|
+
if quota_fallback_model:
|
|
115
|
+
cfg["quota_fallback_model"] = quota_fallback_model
|
|
116
|
+
save_json(LIVE_SETTINGS, live)
|
|
117
|
+
save_json(CONFIG_PATH, cfg)
|
|
118
|
+
return {"ok": True}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def build_state():
|
|
122
|
+
ver = run(["claude", "--version"], timeout=15)
|
|
123
|
+
return {
|
|
124
|
+
"config": paths.load_config(),
|
|
125
|
+
"live_settings": load_json(LIVE_SETTINGS, {}),
|
|
126
|
+
"template_settings": load_json(TPL_SETTINGS, {}),
|
|
127
|
+
"template_dotclaude": load_json(TPL_DOTCLAUDE, {}),
|
|
128
|
+
"oauth": oauth_status(),
|
|
129
|
+
"version": ver["out"] or ver["err"],
|
|
130
|
+
"bool_keys": BOOL_KEYS,
|
|
131
|
+
"effort_values": EFFORT_VALUES,
|
|
132
|
+
"perm_modes": PERM_MODES,
|
|
133
|
+
"live_settings_path": LIVE_SETTINGS,
|
|
134
|
+
"root": ROOT,
|
|
135
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Command-line interface: `abstract-claude <subcommand>`."""
|
|
2
|
+
import argparse
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from . import __version__, actions
|
|
8
|
+
from .paths import ROOT
|
|
9
|
+
|
|
10
|
+
UNIT = "9111_abstract_claude_web.service"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _p(obj):
|
|
14
|
+
if isinstance(obj, dict):
|
|
15
|
+
print(obj.get("out") or obj.get("err") or json.dumps(obj))
|
|
16
|
+
else:
|
|
17
|
+
print(obj)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def cmd_serve(a):
|
|
21
|
+
from .server import serve
|
|
22
|
+
serve(host=a.host, port=a.port)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def cmd_init(a):
|
|
26
|
+
created = actions.init_root()
|
|
27
|
+
print("initialized " + ROOT)
|
|
28
|
+
for c in created:
|
|
29
|
+
print(" + " + c)
|
|
30
|
+
if not created:
|
|
31
|
+
print(" (already initialized)")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def cmd_save_template(a):
|
|
35
|
+
_p(actions.save_template())
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def cmd_restore(a):
|
|
39
|
+
_p(actions.restore())
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def cmd_reset(a):
|
|
43
|
+
if not a.yes:
|
|
44
|
+
ans = input("Wipe ALL Claude state and start fresh? type RESET: ").strip()
|
|
45
|
+
if ans != "RESET":
|
|
46
|
+
print("aborted")
|
|
47
|
+
return 1
|
|
48
|
+
_p(actions.reset())
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def cmd_oauth_status(a):
|
|
52
|
+
print(actions.oauth_status())
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def cmd_oauth_set(a):
|
|
56
|
+
_p(actions.oauth_set(a.token))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def cmd_launch(a):
|
|
60
|
+
from .launch import launch
|
|
61
|
+
return launch(a.args)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def cmd_install_service(a):
|
|
65
|
+
from .paths import load_config
|
|
66
|
+
if os.geteuid() != 0:
|
|
67
|
+
print("run with sudo: sudo abstract-claude install-service", file=sys.stderr)
|
|
68
|
+
return 1
|
|
69
|
+
exe = os.environ.get("AC_EXE") or sys.argv[0]
|
|
70
|
+
if not os.path.isabs(exe):
|
|
71
|
+
import shutil
|
|
72
|
+
exe = shutil.which("abstract-claude") or exe
|
|
73
|
+
port = load_config()["listen_port"]
|
|
74
|
+
unit = f"""[Unit]
|
|
75
|
+
Description=abstract_claude console
|
|
76
|
+
After=network.target
|
|
77
|
+
|
|
78
|
+
[Service]
|
|
79
|
+
Type=simple
|
|
80
|
+
User={a.user}
|
|
81
|
+
Group={a.user}
|
|
82
|
+
Environment=HOME=/home/{a.user}
|
|
83
|
+
Environment=AC_HOST=127.0.0.1
|
|
84
|
+
Environment=AC_PORT={port}
|
|
85
|
+
Environment=AC_ROOT={ROOT}
|
|
86
|
+
ExecStart={exe} serve
|
|
87
|
+
Restart=on-failure
|
|
88
|
+
RestartSec=3
|
|
89
|
+
|
|
90
|
+
[Install]
|
|
91
|
+
WantedBy=multi-user.target
|
|
92
|
+
"""
|
|
93
|
+
dst = f"/etc/systemd/system/{UNIT}"
|
|
94
|
+
with open(dst, "w") as f:
|
|
95
|
+
f.write(unit)
|
|
96
|
+
os.system("systemctl daemon-reload")
|
|
97
|
+
os.system(f"systemctl enable --now {UNIT}")
|
|
98
|
+
print(f"installed {dst} -> http://127.0.0.1:{port}")
|
|
99
|
+
print(f"logs: journalctl -u {UNIT} -f")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def main(argv=None):
|
|
103
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
104
|
+
ap = argparse.ArgumentParser(prog="abstract-claude",
|
|
105
|
+
description="Fresh Claude Code session manager.")
|
|
106
|
+
ap.add_argument("-V", "--version", action="version", version=f"abstract-claude {__version__}")
|
|
107
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
108
|
+
|
|
109
|
+
s = sub.add_parser("serve", help="run the web console")
|
|
110
|
+
s.add_argument("--host"); s.add_argument("--port", type=int)
|
|
111
|
+
s.set_defaults(fn=cmd_serve)
|
|
112
|
+
|
|
113
|
+
sub.add_parser("init", help="materialize AC_ROOT (config + template)").set_defaults(fn=cmd_init)
|
|
114
|
+
sub.add_parser("save-template", help="save current settings as template").set_defaults(fn=cmd_save_template)
|
|
115
|
+
sub.add_parser("restore", help="create fresh ~/.claude if missing").set_defaults(fn=cmd_restore)
|
|
116
|
+
|
|
117
|
+
r = sub.add_parser("reset", help="wipe all state -> fresh session")
|
|
118
|
+
r.add_argument("-y", "--yes", action="store_true"); r.set_defaults(fn=cmd_reset)
|
|
119
|
+
|
|
120
|
+
sub.add_parser("oauth-status", help="show claude-auth status").set_defaults(fn=cmd_oauth_status)
|
|
121
|
+
o = sub.add_parser("oauth-set", help="store a 1-year token")
|
|
122
|
+
o.add_argument("token"); o.set_defaults(fn=cmd_oauth_set)
|
|
123
|
+
|
|
124
|
+
l = sub.add_parser("launch", help="launch Claude with quota fallback")
|
|
125
|
+
l.add_argument("args", nargs=argparse.REMAINDER); l.set_defaults(fn=cmd_launch)
|
|
126
|
+
|
|
127
|
+
i = sub.add_parser("install-service", help="install+enable systemd unit (sudo)")
|
|
128
|
+
i.add_argument("--user", default=os.environ.get("SUDO_USER") or "solcatcher")
|
|
129
|
+
i.set_defaults(fn=cmd_install_service)
|
|
130
|
+
|
|
131
|
+
a = ap.parse_args(argv)
|
|
132
|
+
return a.fn(a) or 0
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
if __name__ == "__main__":
|
|
136
|
+
sys.exit(main())
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""`abstract-claude launch` — start Claude Code with automatic quota fallback."""
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import subprocess
|
|
5
|
+
|
|
6
|
+
from .paths import load_config
|
|
7
|
+
from .util import real_claude
|
|
8
|
+
|
|
9
|
+
LIMIT_RE = re.compile(r"(reached your .*limit|usage limit reached|Fable .*limit)", re.I)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _is_headless(argv):
|
|
13
|
+
return any(a in ("-p", "--print") for a in argv)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def launch(argv):
|
|
17
|
+
cfg = load_config()
|
|
18
|
+
real = real_claude()
|
|
19
|
+
default_model = cfg.get("default_model") or "claude-fable-5"
|
|
20
|
+
fallback_model = cfg.get("quota_fallback_model") or "claude-opus-4-8"
|
|
21
|
+
preflight = cfg.get("preflight_quota_check", True)
|
|
22
|
+
env = dict(os.environ)
|
|
23
|
+
env["HOME"] = os.path.expanduser("~")
|
|
24
|
+
|
|
25
|
+
if _is_headless(argv):
|
|
26
|
+
p = subprocess.run([real, "--model", default_model, *argv],
|
|
27
|
+
capture_output=True, text=True, env=env)
|
|
28
|
+
out = (p.stdout or "") + (p.stderr or "")
|
|
29
|
+
if LIMIT_RE.search(out) and default_model != fallback_model:
|
|
30
|
+
print(f"[claude-launch] {default_model} over quota — retrying on {fallback_model}",
|
|
31
|
+
flush=True)
|
|
32
|
+
os.execvpe(real, [real, "--model", fallback_model, *argv], env)
|
|
33
|
+
print(p.stdout, end="")
|
|
34
|
+
return p.returncode
|
|
35
|
+
|
|
36
|
+
model = default_model
|
|
37
|
+
if preflight and default_model != fallback_model:
|
|
38
|
+
try:
|
|
39
|
+
probe = subprocess.run([real, "-p", "--model", default_model, "ok"],
|
|
40
|
+
capture_output=True, text=True, timeout=30, env=env)
|
|
41
|
+
if LIMIT_RE.search((probe.stdout or "") + (probe.stderr or "")):
|
|
42
|
+
print(f"[claude-launch] {default_model} is maxed — starting on {fallback_model}",
|
|
43
|
+
flush=True)
|
|
44
|
+
model = fallback_model
|
|
45
|
+
except Exception:
|
|
46
|
+
pass
|
|
47
|
+
os.execvpe(real, [real, "--model", model, *argv], env)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Path + config resolution. Runtime data root is AC_ROOT (default /srv/abstract_claude)."""
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
ROOT = os.environ.get("AC_ROOT", "/srv/abstract_claude")
|
|
5
|
+
HOME = os.path.expanduser("~")
|
|
6
|
+
|
|
7
|
+
CONFIG_PATH = os.path.join(ROOT, "config.json")
|
|
8
|
+
TEMPLATE_DIR = os.path.join(ROOT, "template")
|
|
9
|
+
TPL_SETTINGS = os.path.join(TEMPLATE_DIR, "settings.json")
|
|
10
|
+
TPL_DOTCLAUDE = os.path.join(TEMPLATE_DIR, "dot-claude.json")
|
|
11
|
+
|
|
12
|
+
LIVE_CLAUDE_DIR = os.path.join(HOME, ".claude")
|
|
13
|
+
LIVE_SETTINGS = os.path.join(LIVE_CLAUDE_DIR, "settings.json")
|
|
14
|
+
LIVE_DOTCLAUDE = os.path.join(HOME, ".claude.json")
|
|
15
|
+
|
|
16
|
+
DEFAULTS = {
|
|
17
|
+
"listen_host": "127.0.0.1",
|
|
18
|
+
"listen_port": 9111,
|
|
19
|
+
"default_model": "claude-fable-5[1m]",
|
|
20
|
+
"quota_fallback_model": "claude-opus-4-8",
|
|
21
|
+
"preflight_quota_check": True,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def load_config():
|
|
26
|
+
from .util import load_json
|
|
27
|
+
cfg = dict(DEFAULTS)
|
|
28
|
+
cfg.update(load_json(CONFIG_PATH, {}))
|
|
29
|
+
return cfg
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""Web console served by `abstract-claude serve`."""
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
5
|
+
|
|
6
|
+
from . import actions
|
|
7
|
+
from .paths import load_config
|
|
8
|
+
from .util import save_json
|
|
9
|
+
from .paths import LIVE_SETTINGS
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Handler(BaseHTTPRequestHandler):
|
|
13
|
+
def _send(self, code, body, ctype="application/json"):
|
|
14
|
+
data = body.encode() if isinstance(body, str) else body
|
|
15
|
+
self.send_response(code)
|
|
16
|
+
self.send_header("Content-Type", ctype)
|
|
17
|
+
self.send_header("Content-Length", str(len(data)))
|
|
18
|
+
self.end_headers()
|
|
19
|
+
self.wfile.write(data)
|
|
20
|
+
|
|
21
|
+
def _json(self, code, obj):
|
|
22
|
+
self._send(code, json.dumps(obj))
|
|
23
|
+
|
|
24
|
+
def _body(self):
|
|
25
|
+
n = int(self.headers.get("Content-Length", 0) or 0)
|
|
26
|
+
if not n:
|
|
27
|
+
return {}
|
|
28
|
+
try:
|
|
29
|
+
return json.loads(self.rfile.read(n).decode() or "{}")
|
|
30
|
+
except Exception:
|
|
31
|
+
return {}
|
|
32
|
+
|
|
33
|
+
def log_message(self, *a):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
def do_GET(self):
|
|
37
|
+
if self.path == "/" or self.path.startswith("/?"):
|
|
38
|
+
return self._send(200, PAGE, "text/html; charset=utf-8")
|
|
39
|
+
if self.path == "/api/state":
|
|
40
|
+
return self._json(200, actions.build_state())
|
|
41
|
+
return self._json(404, {"error": "not found"})
|
|
42
|
+
|
|
43
|
+
def do_POST(self):
|
|
44
|
+
b = self._body()
|
|
45
|
+
try:
|
|
46
|
+
if self.path == "/api/settings":
|
|
47
|
+
s = b.get("settings")
|
|
48
|
+
if not isinstance(s, dict):
|
|
49
|
+
return self._json(400, {"error": "settings must be an object"})
|
|
50
|
+
save_json(LIVE_SETTINGS, s)
|
|
51
|
+
return self._json(200, {"ok": True, "written": LIVE_SETTINGS})
|
|
52
|
+
if self.path == "/api/model":
|
|
53
|
+
actions.set_model(b.get("default_model"), b.get("quota_fallback_model"),
|
|
54
|
+
b.get("fallback_model"))
|
|
55
|
+
return self._json(200, {"ok": True})
|
|
56
|
+
if self.path == "/api/template/save":
|
|
57
|
+
return self._json(200, actions.save_template())
|
|
58
|
+
if self.path == "/api/template/reset":
|
|
59
|
+
if b.get("confirm") != "RESET":
|
|
60
|
+
return self._json(400, {"error": "type RESET to confirm"})
|
|
61
|
+
return self._json(200, actions.reset())
|
|
62
|
+
if self.path == "/api/oauth/set":
|
|
63
|
+
r = actions.oauth_set(b.get("token"))
|
|
64
|
+
return self._json(200 if r["ok"] else 400, r)
|
|
65
|
+
if self.path == "/api/oauth/probe":
|
|
66
|
+
return self._json(200, actions.oauth_probe())
|
|
67
|
+
except Exception as e:
|
|
68
|
+
return self._json(500, {"error": str(e)})
|
|
69
|
+
return self._json(404, {"error": "not found"})
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def serve(host=None, port=None):
|
|
73
|
+
actions.init_root()
|
|
74
|
+
cfg = load_config()
|
|
75
|
+
host = os.environ.get("AC_HOST", host or cfg["listen_host"])
|
|
76
|
+
port = int(os.environ.get("AC_PORT", port or cfg["listen_port"]))
|
|
77
|
+
srv = ThreadingHTTPServer((host, port), Handler)
|
|
78
|
+
print(f"abstract_claude console on http://{host}:{port}", flush=True)
|
|
79
|
+
try:
|
|
80
|
+
srv.serve_forever()
|
|
81
|
+
except KeyboardInterrupt:
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
PAGE = r"""<!doctype html>
|
|
86
|
+
<html lang="en"><head><meta charset="utf-8">
|
|
87
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
88
|
+
<title>abstract_claude console</title>
|
|
89
|
+
<style>
|
|
90
|
+
:root{--bg:#0d1117;--panel:#161b22;--line:#30363d;--fg:#e6edf3;--mut:#8b949e;
|
|
91
|
+
--acc:#4c8dff;--ok:#3fb950;--warn:#d29922;--bad:#f85149;}
|
|
92
|
+
*{box-sizing:border-box}
|
|
93
|
+
body{margin:0;font:14px/1.5 ui-sans-serif,system-ui,sans-serif;background:var(--bg);color:var(--fg)}
|
|
94
|
+
header{padding:16px 20px;border-bottom:1px solid var(--line);display:flex;align-items:baseline;gap:12px}
|
|
95
|
+
h1{font-size:16px;margin:0;font-weight:600}
|
|
96
|
+
.ver{color:var(--mut);font-size:12px}
|
|
97
|
+
main{max-width:920px;margin:0 auto;padding:20px;display:grid;gap:16px}
|
|
98
|
+
.card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:16px}
|
|
99
|
+
.card h2{margin:0 0 12px;font-size:14px;font-weight:600;letter-spacing:.3px}
|
|
100
|
+
label{display:block;color:var(--mut);font-size:12px;margin:8px 0 3px}
|
|
101
|
+
input,select{width:100%;background:#0d1117;border:1px solid var(--line);color:var(--fg);
|
|
102
|
+
padding:7px 9px;border-radius:7px;font:13px monospace}
|
|
103
|
+
.row{display:grid;grid-template-columns:1fr 1fr;gap:12px}
|
|
104
|
+
.toggles{display:grid;grid-template-columns:1fr 1fr;gap:6px 18px;margin-top:6px}
|
|
105
|
+
.tog{display:flex;align-items:center;gap:8px;color:var(--fg);font-size:13px}
|
|
106
|
+
.tog input{width:auto}
|
|
107
|
+
button{background:var(--acc);color:#fff;border:0;padding:8px 14px;border-radius:7px;
|
|
108
|
+
cursor:pointer;font-size:13px;font-weight:600;margin-top:12px}
|
|
109
|
+
button.ghost{background:#21262d;border:1px solid var(--line)}
|
|
110
|
+
button.danger{background:var(--bad)}
|
|
111
|
+
pre{background:#0d1117;border:1px solid var(--line);border-radius:7px;padding:10px;
|
|
112
|
+
overflow:auto;font-size:12px;color:var(--mut);white-space:pre-wrap;margin:8px 0 0}
|
|
113
|
+
.msg{font-size:12px;margin-top:8px;min-height:16px}
|
|
114
|
+
.msg.ok{color:var(--ok)} .msg.bad{color:var(--bad)}
|
|
115
|
+
.hint{color:var(--mut);font-size:12px;margin:4px 0}
|
|
116
|
+
.flex{display:flex;gap:10px;flex-wrap:wrap;align-items:center}
|
|
117
|
+
</style></head>
|
|
118
|
+
<body>
|
|
119
|
+
<header><h1>abstract_claude console</h1><span class="ver" id="ver"></span></header>
|
|
120
|
+
<main>
|
|
121
|
+
|
|
122
|
+
<div class="card">
|
|
123
|
+
<h2>Model & fallback</h2>
|
|
124
|
+
<div class="row">
|
|
125
|
+
<div><label>Default model (settings.json)</label><input id="model"></div>
|
|
126
|
+
<div><label>Quota fallback → (wrapper: Fable maxed → this)</label><input id="qfb"></div>
|
|
127
|
+
</div>
|
|
128
|
+
<label>fallbackModel chain (overload only, comma-separated)</label>
|
|
129
|
+
<input id="fbchain" placeholder="claude-opus-4-8, claude-sonnet-5">
|
|
130
|
+
<p class="hint">fallbackModel fires on overload/unavailable, never on quota. The quota
|
|
131
|
+
switch is handled by <code>abstract-claude launch</code> using the value above.</p>
|
|
132
|
+
<button onclick="saveModel()">Save model config</button>
|
|
133
|
+
<div class="msg" id="m-model"></div>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<div class="card">
|
|
137
|
+
<h2>OAuth (claude-auth)</h2>
|
|
138
|
+
<pre id="oauth">loading…</pre>
|
|
139
|
+
<div class="flex"><button class="ghost" onclick="probe()">Live probe</button></div>
|
|
140
|
+
<label>Set / replace 1-year token (sk-ant-oat…)</label>
|
|
141
|
+
<input id="token" placeholder="sk-ant-oat01…">
|
|
142
|
+
<button onclick="setToken()">Store token</button>
|
|
143
|
+
<p class="hint">Re-minting needs a browser: run <code>claude-auth mint</code> in a
|
|
144
|
+
terminal, or paste a token minted elsewhere.</p>
|
|
145
|
+
<div class="msg" id="m-oauth"></div>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<div class="card">
|
|
149
|
+
<h2>Template & fresh session</h2>
|
|
150
|
+
<p class="hint">Template = settings.json + flags only. Reset wipes ALL prior state and
|
|
151
|
+
reseeds the template → a clean session. Login rides on the durable env token.</p>
|
|
152
|
+
<div class="row">
|
|
153
|
+
<div><label>template/settings.json</label><pre id="tpl-set"></pre></div>
|
|
154
|
+
<div><label>template/dot-claude.json → ~/.claude.json</label><pre id="tpl-dot"></pre></div>
|
|
155
|
+
</div>
|
|
156
|
+
<div class="flex"><button class="ghost" onclick="saveTpl()">Save current settings as template</button></div>
|
|
157
|
+
<label>Type RESET to wipe & start fresh</label>
|
|
158
|
+
<div class="flex">
|
|
159
|
+
<input id="confirm" placeholder="RESET" style="max-width:160px">
|
|
160
|
+
<button class="danger" onclick="reset()">reset (fresh session)</button>
|
|
161
|
+
</div>
|
|
162
|
+
<div class="msg" id="m-tpl"></div>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
<div class="card">
|
|
166
|
+
<h2>Session settings (settings.json)</h2>
|
|
167
|
+
<div class="row">
|
|
168
|
+
<div><label>Permission default mode</label><select id="perm"></select></div>
|
|
169
|
+
<div><label>Effort level</label><select id="effort"></select></div>
|
|
170
|
+
</div>
|
|
171
|
+
<div class="toggles" id="toggles"></div>
|
|
172
|
+
<button onclick="saveSettings()">Save session settings</button>
|
|
173
|
+
<p class="hint" id="settings-path"></p>
|
|
174
|
+
<div class="msg" id="m-set"></div>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
</main>
|
|
178
|
+
<script>
|
|
179
|
+
let S={};const $=id=>document.getElementById(id);
|
|
180
|
+
function msg(id,t,ok){const e=$(id);e.textContent=t;e.className="msg "+(ok?"ok":"bad");}
|
|
181
|
+
async function api(p,b){const o=b?{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(b)}:{};const r=await fetch(p,o);return{status:r.status,data:await r.json()};}
|
|
182
|
+
async function load(){
|
|
183
|
+
const {data}=await api("/api/state");S=data;
|
|
184
|
+
$("ver").textContent=(data.version||"")+" · "+(data.root||"");
|
|
185
|
+
$("model").value=data.live_settings.model||data.config.default_model||"";
|
|
186
|
+
$("qfb").value=data.config.quota_fallback_model||"";
|
|
187
|
+
$("fbchain").value=(data.live_settings.fallbackModel||[]).join(", ");
|
|
188
|
+
$("oauth").textContent=data.oauth||"(no status)";
|
|
189
|
+
$("tpl-set").textContent=JSON.stringify(data.template_settings,null,2);
|
|
190
|
+
$("tpl-dot").textContent=JSON.stringify(data.template_dotclaude,null,2);
|
|
191
|
+
$("settings-path").textContent="writes: "+data.live_settings_path;
|
|
192
|
+
const perm=$("perm");perm.innerHTML="";data.perm_modes.forEach(v=>perm.add(new Option(v,v)));
|
|
193
|
+
perm.value=((data.live_settings.permissions||{}).defaultMode)||"bypassPermissions";
|
|
194
|
+
const eff=$("effort");eff.innerHTML="";data.effort_values.forEach(v=>eff.add(new Option(v,v)));
|
|
195
|
+
eff.value=data.live_settings.effortLevel||"high";
|
|
196
|
+
const t=$("toggles");t.innerHTML="";
|
|
197
|
+
data.bool_keys.forEach(k=>{const c=!!data.live_settings[k];const w=document.createElement("label");
|
|
198
|
+
w.className="tog";w.innerHTML=`<input type="checkbox" data-k="${k}" ${c?"checked":""}> ${k}`;t.appendChild(w);});
|
|
199
|
+
}
|
|
200
|
+
async function saveModel(){
|
|
201
|
+
const fb=$("fbchain").value.split(",").map(s=>s.trim()).filter(Boolean);
|
|
202
|
+
const{status,data}=await api("/api/model",{default_model:$("model").value.trim(),quota_fallback_model:$("qfb").value.trim(),fallback_model:fb});
|
|
203
|
+
msg("m-model",status==200?"Saved.":"Error: "+(data.error||""),status==200);load();
|
|
204
|
+
}
|
|
205
|
+
async function saveSettings(){
|
|
206
|
+
const s=JSON.parse(JSON.stringify(S.live_settings||{}));
|
|
207
|
+
s.permissions=s.permissions||{};s.permissions.defaultMode=$("perm").value;s.effortLevel=$("effort").value;
|
|
208
|
+
document.querySelectorAll("#toggles input").forEach(c=>{s[c.dataset.k]=c.checked;});
|
|
209
|
+
const{status,data}=await api("/api/settings",{settings:s});
|
|
210
|
+
msg("m-set",status==200?("Saved → "+data.written):"Error: "+(data.error||""),status==200);load();
|
|
211
|
+
}
|
|
212
|
+
async function saveTpl(){const{status,data}=await api("/api/template/save",{});msg("m-tpl",status==200?(data.out||"Saved."):"Error: "+(data.err||data.error||""),status==200);load();}
|
|
213
|
+
async function reset(){const{status,data}=await api("/api/template/reset",{confirm:$("confirm").value.trim()});msg("m-tpl",status==200?(data.out||"Fresh."):"Error: "+(data.error||data.err||""),status==200);$("confirm").value="";load();}
|
|
214
|
+
async function setToken(){const{status,data}=await api("/api/oauth/set",{token:$("token").value});msg("m-oauth",status==200?(data.out||"Stored."):"Error: "+(data.error||data.err||""),status==200);$("token").value="";load();}
|
|
215
|
+
async function probe(){msg("m-oauth","probing…",true);const{data}=await api("/api/oauth/probe",{});msg("m-oauth",(data.out||data.err||"done"),data.ok);}
|
|
216
|
+
load();
|
|
217
|
+
</script>
|
|
218
|
+
</body></html>"""
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"defaultMode": "bypassPermissions"
|
|
4
|
+
},
|
|
5
|
+
"model": "claude-fable-5[1m]",
|
|
6
|
+
"enableArtifact": false,
|
|
7
|
+
"workflowKeywordTriggerEnabled": false,
|
|
8
|
+
"effortLevel": "high",
|
|
9
|
+
"promptSuggestionEnabled": false,
|
|
10
|
+
"awaySummaryEnabled": false,
|
|
11
|
+
"skipDangerousModePermissionPrompt": true,
|
|
12
|
+
"autoCompactEnabled": false,
|
|
13
|
+
"switchModelsOnFlag": false,
|
|
14
|
+
"autoScrollEnabled": true,
|
|
15
|
+
"fileCheckpointingEnabled": false,
|
|
16
|
+
"terminalProgressBarEnabled": false,
|
|
17
|
+
"remoteControlAtStartup": false,
|
|
18
|
+
"inputNeededNotifEnabled": true,
|
|
19
|
+
"agentPushNotifEnabled": true
|
|
20
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Small shared helpers: JSON IO and subprocess running."""
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
|
|
6
|
+
from .paths import HOME, ROOT
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def load_json(path, default=None):
|
|
10
|
+
try:
|
|
11
|
+
with open(path) as f:
|
|
12
|
+
return json.load(f)
|
|
13
|
+
except Exception:
|
|
14
|
+
return {} if default is None else default
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def save_json(path, obj):
|
|
18
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
19
|
+
tmp = path + ".tmp"
|
|
20
|
+
with open(tmp, "w") as f:
|
|
21
|
+
json.dump(obj, f, indent=2)
|
|
22
|
+
f.write("\n")
|
|
23
|
+
os.replace(tmp, path)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def which_claude_auth():
|
|
27
|
+
for p in (os.path.join(HOME, ".local/bin/claude-auth"),
|
|
28
|
+
"/usr/local/bin/claude-auth"):
|
|
29
|
+
if os.path.exists(p):
|
|
30
|
+
return p
|
|
31
|
+
return "claude-auth"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def real_claude():
|
|
35
|
+
import shutil
|
|
36
|
+
return (shutil.which("claude")
|
|
37
|
+
or os.path.join(HOME, ".local/bin/claude"))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def run(cmd, timeout=60, stdin=None):
|
|
41
|
+
"""Run a command; return {ok, code, out, err}."""
|
|
42
|
+
env = dict(os.environ)
|
|
43
|
+
env["HOME"] = HOME
|
|
44
|
+
env["PATH"] = os.path.join(HOME, ".local/bin") + os.pathsep + env.get("PATH", "")
|
|
45
|
+
try:
|
|
46
|
+
p = subprocess.run(cmd, capture_output=True, text=True,
|
|
47
|
+
timeout=timeout, env=env, input=stdin)
|
|
48
|
+
return {"ok": p.returncode == 0, "code": p.returncode,
|
|
49
|
+
"out": p.stdout.strip(), "err": p.stderr.strip()}
|
|
50
|
+
except Exception as e:
|
|
51
|
+
return {"ok": False, "code": -1, "out": "", "err": str(e)}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: abstract-claude
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Fresh Claude Code session manager: settings-only template, durable OAuth, quota fallback wrapper, and a web console.
|
|
5
|
+
Author: jrputkey
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://abstractendeavors.com
|
|
8
|
+
Keywords: claude,claude-code,oauth,cli
|
|
9
|
+
Requires-Python: >=3.9
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# abstract-claude
|
|
13
|
+
|
|
14
|
+
Fresh **Claude Code** session manager: a settings-only template, durable OAuth,
|
|
15
|
+
automatic quota fallback, and a web console — packaged as one `pip`-installable CLI.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install abstract-claude # from your index
|
|
21
|
+
# or from the private index:
|
|
22
|
+
pip install -i https://<index>/api/pypi/simple/ abstract-claude
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quickstart
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
abstract-claude init # materialize AC_ROOT (config + template)
|
|
29
|
+
abstract-claude serve # web console (default http://127.0.0.1:9111)
|
|
30
|
+
sudo abstract-claude install-service # install + enable the systemd unit
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Commands
|
|
34
|
+
|
|
35
|
+
| Command | What it does |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `serve [--host --port]` | Run the web console (Model+fallback, OAuth, Template+reset, Session settings). |
|
|
38
|
+
| `init` | Create `AC_ROOT` with `config.json` + `template/` from shipped defaults. |
|
|
39
|
+
| `save-template` | Capture current `~/.claude/settings.json` + minimal flags as the template. |
|
|
40
|
+
| `restore` | Create a fresh `~/.claude` (settings + flags) if it doesn't exist. |
|
|
41
|
+
| `reset [-y]` | Wipe ALL Claude state (transcripts/history/creds) → reseed template → clean session. |
|
|
42
|
+
| `oauth-status` / `oauth-set <token>` | Show / store the durable 1-year `claude-auth` token. |
|
|
43
|
+
| `launch -- <claude args>` | Launch Claude Code, auto-switching to the fallback model when the default is over quota. |
|
|
44
|
+
| `install-service [--user]` | Write + enable `9111_abstract_claude_web.service` (needs sudo). |
|
|
45
|
+
|
|
46
|
+
## How it works
|
|
47
|
+
|
|
48
|
+
- **Template = config only.** `template/settings.json` + a minimal `~/.claude.json`
|
|
49
|
+
(onboarding/trust flags). No credentials, history, or transcripts — a reset gives a
|
|
50
|
+
genuinely clean session, not a restored snapshot.
|
|
51
|
+
- **OAuth is not templated.** Login rides on the durable 1-year `CLAUDE_CODE_OAUTH_TOKEN`
|
|
52
|
+
(managed by `claude-auth`), so a fresh session auto-authenticates — no `/login`.
|
|
53
|
+
- **Quota fallback is the wrapper.** Claude Code's `fallbackModel` only fires on
|
|
54
|
+
*overload*, never on quota; `autoContinueAtUsageLimit` doesn't exist before v2.1.234.
|
|
55
|
+
`abstract-claude launch` handles the real case: default Fable maxed → run on Opus 4.8.
|
|
56
|
+
Wire it up with:
|
|
57
|
+
```bash
|
|
58
|
+
alias claude='abstract-claude launch --dangerously-skip-permissions'
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Config
|
|
62
|
+
|
|
63
|
+
`AC_ROOT` (default `/srv/abstract_claude`) holds `config.json`:
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{ "listen_host": "127.0.0.1", "listen_port": 9111,
|
|
67
|
+
"default_model": "claude-fable-5[1m]", "quota_fallback_model": "claude-opus-4-8",
|
|
68
|
+
"preflight_quota_check": true }
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Override the data root with `AC_ROOT=/path abstract-claude ...`.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/abstract_claude/__init__.py
|
|
4
|
+
src/abstract_claude/__main__.py
|
|
5
|
+
src/abstract_claude/actions.py
|
|
6
|
+
src/abstract_claude/cli.py
|
|
7
|
+
src/abstract_claude/launch.py
|
|
8
|
+
src/abstract_claude/paths.py
|
|
9
|
+
src/abstract_claude/server.py
|
|
10
|
+
src/abstract_claude/util.py
|
|
11
|
+
src/abstract_claude.egg-info/PKG-INFO
|
|
12
|
+
src/abstract_claude.egg-info/SOURCES.txt
|
|
13
|
+
src/abstract_claude.egg-info/dependency_links.txt
|
|
14
|
+
src/abstract_claude.egg-info/entry_points.txt
|
|
15
|
+
src/abstract_claude.egg-info/top_level.txt
|
|
16
|
+
src/abstract_claude/templates/dot-claude.json
|
|
17
|
+
src/abstract_claude/templates/settings.json
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
abstract_claude
|