alice-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.
- alice_cli/.kiro/settings/lsp.json +198 -0
- alice_cli/__init__.py +1 -0
- alice_cli/auth.py +64 -0
- alice_cli/cli.py +98 -0
- alice_cli/commands/__init__.py +1 -0
- alice_cli/commands/appraise.py +213 -0
- alice_cli/commands/auth.py +197 -0
- alice_cli/commands/batch.py +364 -0
- alice_cli/commands/chat.py +229 -0
- alice_cli/commands/compare.py +310 -0
- alice_cli/commands/compose.py +266 -0
- alice_cli/commands/config_cmd.py +60 -0
- alice_cli/commands/diagnose.py +325 -0
- alice_cli/commands/dialog.py +240 -0
- alice_cli/commands/get_key.py +51 -0
- alice_cli/commands/get_secret.py +87 -0
- alice_cli/commands/invoke.py +124 -0
- alice_cli/commands/list_aliases.py +24 -0
- alice_cli/commands/list_models.py +49 -0
- alice_cli/commands/list_secrets.py +77 -0
- alice_cli/commands/recall.py +229 -0
- alice_cli/commands/run.py +44 -0
- alice_cli/commands/status.py +46 -0
- alice_cli/commands/summarize.py +173 -0
- alice_cli/compose_engine.py +125 -0
- alice_cli/config.py +90 -0
- alice_cli/console.py +71 -0
- alice_cli/errors.py +40 -0
- alice_cli/formatting.py +56 -0
- alice_cli/locker.py +338 -0
- alice_cli/logo.py +29 -0
- alice_cli/models.py +252 -0
- alice_cli/personality.py +174 -0
- alice_cli/pricing.py +33 -0
- alice_cli/py.typed +1 -0
- alice_cli/save.py +173 -0
- alice_cli/secrets.py +89 -0
- alice_cli/session_record.py +84 -0
- alice_cli/store.py +53 -0
- alice_cli/tui/__init__.py +1 -0
- alice_cli/tui/app.py +102 -0
- alice_cli/tui/screens/__init__.py +1 -0
- alice_cli/tui/screens/compose.py +365 -0
- alice_cli/tui/screens/get_key.py +88 -0
- alice_cli/tui/screens/get_secret.py +111 -0
- alice_cli/tui/screens/home.py +92 -0
- alice_cli/tui/screens/invoke.py +99 -0
- alice_cli/tui/screens/quit.py +112 -0
- alice_cli/tui/screens/status.py +119 -0
- alice_cli/tui/theme.py +12 -0
- alice_cli/tui/theme.tcss +219 -0
- alice_cli/tui/widgets/__init__.py +1 -0
- alice_cli/tui/widgets/banner.py +76 -0
- alice_cli/tui/widgets/clock.py +41 -0
- alice_cli/tui/widgets/header_bar.py +26 -0
- alice_cli/tui/widgets/output.py +11 -0
- alice_cli/tui/widgets/typewriter.py +43 -0
- alice_cli/validators.py +36 -0
- alice_cli-0.1.0.dist-info/METADATA +309 -0
- alice_cli-0.1.0.dist-info/RECORD +62 -0
- alice_cli-0.1.0.dist-info/WHEEL +4 -0
- alice_cli-0.1.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
{
|
|
2
|
+
"languages": {
|
|
3
|
+
"rust": {
|
|
4
|
+
"name": "rust-analyzer",
|
|
5
|
+
"command": "rust-analyzer",
|
|
6
|
+
"args": [],
|
|
7
|
+
"file_extensions": [
|
|
8
|
+
"rs"
|
|
9
|
+
],
|
|
10
|
+
"project_patterns": [
|
|
11
|
+
"Cargo.toml"
|
|
12
|
+
],
|
|
13
|
+
"exclude_patterns": [
|
|
14
|
+
"**/target/**"
|
|
15
|
+
],
|
|
16
|
+
"multi_workspace": false,
|
|
17
|
+
"initialization_options": {
|
|
18
|
+
"cargo": {
|
|
19
|
+
"buildScripts": {
|
|
20
|
+
"enable": true
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"diagnostics": {
|
|
24
|
+
"enable": true,
|
|
25
|
+
"enableExperimental": true
|
|
26
|
+
},
|
|
27
|
+
"workspace": {
|
|
28
|
+
"symbol": {
|
|
29
|
+
"search": {
|
|
30
|
+
"scope": "workspace"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"request_timeout_secs": 60
|
|
36
|
+
},
|
|
37
|
+
"ruby": {
|
|
38
|
+
"name": "solargraph",
|
|
39
|
+
"command": "solargraph",
|
|
40
|
+
"args": [
|
|
41
|
+
"stdio"
|
|
42
|
+
],
|
|
43
|
+
"file_extensions": [
|
|
44
|
+
"rb"
|
|
45
|
+
],
|
|
46
|
+
"project_patterns": [
|
|
47
|
+
"Gemfile",
|
|
48
|
+
"Rakefile"
|
|
49
|
+
],
|
|
50
|
+
"exclude_patterns": [
|
|
51
|
+
"**/vendor/**",
|
|
52
|
+
"**/tmp/**"
|
|
53
|
+
],
|
|
54
|
+
"multi_workspace": false,
|
|
55
|
+
"initialization_options": {},
|
|
56
|
+
"request_timeout_secs": 60
|
|
57
|
+
},
|
|
58
|
+
"cpp": {
|
|
59
|
+
"name": "clangd",
|
|
60
|
+
"command": "clangd",
|
|
61
|
+
"args": [
|
|
62
|
+
"--background-index"
|
|
63
|
+
],
|
|
64
|
+
"file_extensions": [
|
|
65
|
+
"cpp",
|
|
66
|
+
"cc",
|
|
67
|
+
"cxx",
|
|
68
|
+
"c",
|
|
69
|
+
"h",
|
|
70
|
+
"hpp",
|
|
71
|
+
"hxx"
|
|
72
|
+
],
|
|
73
|
+
"project_patterns": [
|
|
74
|
+
"CMakeLists.txt",
|
|
75
|
+
"compile_commands.json",
|
|
76
|
+
"Makefile"
|
|
77
|
+
],
|
|
78
|
+
"exclude_patterns": [
|
|
79
|
+
"**/build/**",
|
|
80
|
+
"**/cmake-build-**/**"
|
|
81
|
+
],
|
|
82
|
+
"multi_workspace": false,
|
|
83
|
+
"initialization_options": {},
|
|
84
|
+
"request_timeout_secs": 60
|
|
85
|
+
},
|
|
86
|
+
"python": {
|
|
87
|
+
"name": "pyright",
|
|
88
|
+
"command": "pyright-langserver",
|
|
89
|
+
"args": [
|
|
90
|
+
"--stdio"
|
|
91
|
+
],
|
|
92
|
+
"file_extensions": [
|
|
93
|
+
"py"
|
|
94
|
+
],
|
|
95
|
+
"project_patterns": [
|
|
96
|
+
"pyproject.toml",
|
|
97
|
+
"setup.py",
|
|
98
|
+
"requirements.txt",
|
|
99
|
+
"pyrightconfig.json"
|
|
100
|
+
],
|
|
101
|
+
"exclude_patterns": [
|
|
102
|
+
"**/__pycache__/**",
|
|
103
|
+
"**/venv/**",
|
|
104
|
+
"**/.venv/**",
|
|
105
|
+
"**/.pytest_cache/**"
|
|
106
|
+
],
|
|
107
|
+
"multi_workspace": false,
|
|
108
|
+
"initialization_options": {},
|
|
109
|
+
"request_timeout_secs": 60
|
|
110
|
+
},
|
|
111
|
+
"go": {
|
|
112
|
+
"name": "gopls",
|
|
113
|
+
"command": "gopls",
|
|
114
|
+
"args": [],
|
|
115
|
+
"file_extensions": [
|
|
116
|
+
"go"
|
|
117
|
+
],
|
|
118
|
+
"project_patterns": [
|
|
119
|
+
"go.mod",
|
|
120
|
+
"go.sum"
|
|
121
|
+
],
|
|
122
|
+
"exclude_patterns": [
|
|
123
|
+
"**/vendor/**"
|
|
124
|
+
],
|
|
125
|
+
"multi_workspace": false,
|
|
126
|
+
"initialization_options": {
|
|
127
|
+
"usePlaceholders": true,
|
|
128
|
+
"completeUnimported": true
|
|
129
|
+
},
|
|
130
|
+
"request_timeout_secs": 60
|
|
131
|
+
},
|
|
132
|
+
"java": {
|
|
133
|
+
"name": "jdtls",
|
|
134
|
+
"command": "jdtls",
|
|
135
|
+
"args": [],
|
|
136
|
+
"file_extensions": [
|
|
137
|
+
"java"
|
|
138
|
+
],
|
|
139
|
+
"project_patterns": [
|
|
140
|
+
"pom.xml",
|
|
141
|
+
"build.gradle",
|
|
142
|
+
"build.gradle.kts",
|
|
143
|
+
".project"
|
|
144
|
+
],
|
|
145
|
+
"exclude_patterns": [
|
|
146
|
+
"**/target/**",
|
|
147
|
+
"**/build/**",
|
|
148
|
+
"**/.gradle/**"
|
|
149
|
+
],
|
|
150
|
+
"multi_workspace": false,
|
|
151
|
+
"initialization_options": {
|
|
152
|
+
"settings": {
|
|
153
|
+
"java": {
|
|
154
|
+
"compile": {
|
|
155
|
+
"nullAnalysis": {
|
|
156
|
+
"mode": "automatic"
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
"configuration": {
|
|
160
|
+
"annotationProcessing": {
|
|
161
|
+
"enabled": true
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
"request_timeout_secs": 60
|
|
168
|
+
},
|
|
169
|
+
"typescript": {
|
|
170
|
+
"name": "typescript-language-server",
|
|
171
|
+
"command": "typescript-language-server",
|
|
172
|
+
"args": [
|
|
173
|
+
"--stdio"
|
|
174
|
+
],
|
|
175
|
+
"file_extensions": [
|
|
176
|
+
"ts",
|
|
177
|
+
"js",
|
|
178
|
+
"tsx",
|
|
179
|
+
"jsx"
|
|
180
|
+
],
|
|
181
|
+
"project_patterns": [
|
|
182
|
+
"package.json",
|
|
183
|
+
"tsconfig.json"
|
|
184
|
+
],
|
|
185
|
+
"exclude_patterns": [
|
|
186
|
+
"**/node_modules/**",
|
|
187
|
+
"**/dist/**"
|
|
188
|
+
],
|
|
189
|
+
"multi_workspace": false,
|
|
190
|
+
"initialization_options": {
|
|
191
|
+
"preferences": {
|
|
192
|
+
"disableSuggestions": false
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
"request_timeout_secs": 60
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
alice_cli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
alice_cli/auth.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""JHED detection from STS/SSO caller identity."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import boto3
|
|
6
|
+
from botocore.exceptions import BotoCoreError, ClientError
|
|
7
|
+
|
|
8
|
+
from alice_cli import personality
|
|
9
|
+
from alice_cli.errors import AuthError
|
|
10
|
+
from alice_cli.models import JHEDResult
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def detect_identity_from_key(access_key: str | None) -> str:
|
|
14
|
+
"""Extract a display name from the Bedrock access key alias.
|
|
15
|
+
|
|
16
|
+
The ``BEDROCK_ACCESS_KEY`` (``service_credential_alias``) follows the
|
|
17
|
+
pattern ``BedrockAPIKey-<JHED>-at-<ACCOUNT>``. We extract the JHED
|
|
18
|
+
portion when the pattern matches, otherwise return the raw value.
|
|
19
|
+
|
|
20
|
+
Returns ``"unknown"`` when the access key is empty / absent.
|
|
21
|
+
"""
|
|
22
|
+
if not access_key:
|
|
23
|
+
return "unknown"
|
|
24
|
+
|
|
25
|
+
# Pattern: BedrockAPIKey-<jhed>-at-<account_id>
|
|
26
|
+
if access_key.startswith("BedrockAPIKey-") and "-at-" in access_key:
|
|
27
|
+
return access_key.removeprefix("BedrockAPIKey-").split("-at-", maxsplit=1)[0]
|
|
28
|
+
|
|
29
|
+
return access_key
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def detect_jhed(profile: str | None, region: str) -> JHEDResult:
|
|
33
|
+
"""Call STS get-caller-identity and extract the JHED from the ARN session name.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
profile: AWS CLI profile name, or None if not provided.
|
|
37
|
+
region: AWS region to use for the STS call.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
JHEDResult with jhed, session_name, and full ARN.
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
AuthError: If no profile is provided, STS call fails, or JHED cannot be extracted.
|
|
44
|
+
"""
|
|
45
|
+
if profile is None:
|
|
46
|
+
raise AuthError(personality.error_no_profile())
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
session = boto3.Session(profile_name=profile, region_name=region)
|
|
50
|
+
sts = session.client("sts")
|
|
51
|
+
identity = sts.get_caller_identity()
|
|
52
|
+
except (ClientError, BotoCoreError):
|
|
53
|
+
raise AuthError(personality.error_sso_expired(profile))
|
|
54
|
+
|
|
55
|
+
arn: str = identity["Arn"]
|
|
56
|
+
# ARN format: arn:aws:sts::ACCOUNT:assumed-role/ROLE/session_name
|
|
57
|
+
session_name = arn.rsplit("/", maxsplit=1)[-1]
|
|
58
|
+
|
|
59
|
+
if "@" not in session_name:
|
|
60
|
+
raise AuthError(personality.error_jhed_extraction(session_name))
|
|
61
|
+
|
|
62
|
+
jhed = session_name.split("@", maxsplit=1)[0]
|
|
63
|
+
|
|
64
|
+
return JHEDResult(jhed=jhed, session_name=session_name, arn=arn)
|
alice_cli/cli.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Top-level Click group and global options for the alice CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from alice_cli import __version__
|
|
8
|
+
from alice_cli.config import resolve_config
|
|
9
|
+
from alice_cli.errors import AliceCLIError
|
|
10
|
+
from alice_cli.models import CLIConfig
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def launch_tui(config: CLIConfig) -> None:
|
|
14
|
+
"""Launch the interactive TUI mode.
|
|
15
|
+
|
|
16
|
+
Lazily imports ``textual`` so CLI-only users never pay for the dependency.
|
|
17
|
+
Raises :class:`AliceCLIError` if the ``textual`` extra is not installed.
|
|
18
|
+
"""
|
|
19
|
+
try:
|
|
20
|
+
from alice_cli.tui.app import AliceTUIApp
|
|
21
|
+
except ImportError:
|
|
22
|
+
raise AliceCLIError(
|
|
23
|
+
"TUI mode needs the 'textual' package.\n"
|
|
24
|
+
" Install it with: poetry install --extras tui"
|
|
25
|
+
)
|
|
26
|
+
app = AliceTUIApp(config)
|
|
27
|
+
app.run()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@click.group(invoke_without_command=True)
|
|
31
|
+
@click.option("--profile", envvar="AWS_PROFILE", default=None, help="AWS CLI profile")
|
|
32
|
+
@click.option("--region", default=None, help="AWS region (default: us-east-1)")
|
|
33
|
+
@click.option("--namespace", default=None, help="Resource namespace (default: drcc)")
|
|
34
|
+
@click.option("--environment", default=None, help="Deployment environment (default: ai)")
|
|
35
|
+
@click.option("--api-key", envvar="BEDROCK_SECRET_KEY", default=None, help="Bedrock API key (skips SSO/Secrets Manager; or set BEDROCK_SECRET_KEY)")
|
|
36
|
+
@click.option("--quiet", is_flag=True, help="Suppress banner and status messages")
|
|
37
|
+
@click.option("--tui", is_flag=True, help="Launch interactive TUI mode")
|
|
38
|
+
@click.version_option(version=__version__)
|
|
39
|
+
@click.pass_context
|
|
40
|
+
def cli(
|
|
41
|
+
ctx: click.Context,
|
|
42
|
+
profile: str | None,
|
|
43
|
+
region: str | None,
|
|
44
|
+
namespace: str | None,
|
|
45
|
+
environment: str | None,
|
|
46
|
+
api_key: str | None,
|
|
47
|
+
quiet: bool,
|
|
48
|
+
tui: bool,
|
|
49
|
+
) -> None:
|
|
50
|
+
"""ALiCE — Your AI research companion at Johns Hopkins."""
|
|
51
|
+
ctx.ensure_object(dict)
|
|
52
|
+
ctx.obj["config"] = resolve_config(
|
|
53
|
+
profile, region, namespace, environment, quiet, api_key
|
|
54
|
+
)
|
|
55
|
+
if tui:
|
|
56
|
+
launch_tui(ctx.obj["config"])
|
|
57
|
+
ctx.exit()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
from alice_cli.commands.appraise import appraise_cmd # noqa: E402
|
|
61
|
+
from alice_cli.commands.auth import auth # noqa: E402
|
|
62
|
+
from alice_cli.commands.batch import batch_cmd # noqa: E402
|
|
63
|
+
from alice_cli.commands.chat import chat_cmd # noqa: E402
|
|
64
|
+
from alice_cli.commands.compare import compare_cmd # noqa: E402
|
|
65
|
+
from alice_cli.commands.compose import compose # noqa: E402
|
|
66
|
+
from alice_cli.commands.config_cmd import config_cmd # noqa: E402
|
|
67
|
+
from alice_cli.commands.diagnose import diagnose_cmd # noqa: E402
|
|
68
|
+
from alice_cli.commands.dialog import dialog_cmd # noqa: E402
|
|
69
|
+
from alice_cli.commands.get_key import get_key # noqa: E402
|
|
70
|
+
from alice_cli.commands.get_secret import get_secret # noqa: E402
|
|
71
|
+
from alice_cli.commands.invoke import invoke_cmd # noqa: E402
|
|
72
|
+
from alice_cli.commands.list_aliases import list_aliases # noqa: E402
|
|
73
|
+
from alice_cli.commands.list_models import list_models # noqa: E402
|
|
74
|
+
from alice_cli.commands.list_secrets import list_secrets # noqa: E402
|
|
75
|
+
from alice_cli.commands.recall import recall_cmd # noqa: E402
|
|
76
|
+
from alice_cli.commands.run import run_cmd # noqa: E402
|
|
77
|
+
from alice_cli.commands.status import status # noqa: E402
|
|
78
|
+
from alice_cli.commands.summarize import summarize_cmd # noqa: E402
|
|
79
|
+
|
|
80
|
+
cli.add_command(appraise_cmd)
|
|
81
|
+
cli.add_command(auth)
|
|
82
|
+
cli.add_command(batch_cmd)
|
|
83
|
+
cli.add_command(chat_cmd)
|
|
84
|
+
cli.add_command(compare_cmd)
|
|
85
|
+
cli.add_command(compose)
|
|
86
|
+
cli.add_command(config_cmd)
|
|
87
|
+
cli.add_command(diagnose_cmd)
|
|
88
|
+
cli.add_command(dialog_cmd)
|
|
89
|
+
cli.add_command(get_key)
|
|
90
|
+
cli.add_command(get_secret)
|
|
91
|
+
cli.add_command(invoke_cmd)
|
|
92
|
+
cli.add_command(list_aliases)
|
|
93
|
+
cli.add_command(list_models)
|
|
94
|
+
cli.add_command(list_secrets)
|
|
95
|
+
cli.add_command(recall_cmd)
|
|
96
|
+
cli.add_command(run_cmd)
|
|
97
|
+
cli.add_command(status)
|
|
98
|
+
cli.add_command(summarize_cmd)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""alice appraise — Token usage and cost reporting from the Cloud Locker."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
|
|
10
|
+
from alice_cli import personality
|
|
11
|
+
from alice_cli.auth import detect_identity_from_key, detect_jhed
|
|
12
|
+
from alice_cli.commands.recall import filter_entries
|
|
13
|
+
from alice_cli.console import (
|
|
14
|
+
err_console,
|
|
15
|
+
print_banner,
|
|
16
|
+
print_status,
|
|
17
|
+
print_success,
|
|
18
|
+
spinner,
|
|
19
|
+
)
|
|
20
|
+
from alice_cli.locker import CloudLocker
|
|
21
|
+
from alice_cli.models import IndexEntry, model_help_text
|
|
22
|
+
from alice_cli.pricing import estimate_cost, format_cost
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@click.command("appraise")
|
|
26
|
+
@click.option("--since", default=None, help="Include sessions on or after this date (YYYY-MM-DD)")
|
|
27
|
+
@click.option("--model", default=None, help="Filter by model alias or ID (substring match)")
|
|
28
|
+
@click.option(
|
|
29
|
+
"--detailed",
|
|
30
|
+
is_flag=True,
|
|
31
|
+
default=False,
|
|
32
|
+
help="Show per-session token counts instead of per-model aggregates",
|
|
33
|
+
)
|
|
34
|
+
@click.pass_context
|
|
35
|
+
def appraise_cmd(
|
|
36
|
+
ctx: click.Context,
|
|
37
|
+
since: str | None,
|
|
38
|
+
model: str | None,
|
|
39
|
+
detailed: bool,
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Show token usage and approximate costs for past sessions.
|
|
42
|
+
|
|
43
|
+
\b
|
|
44
|
+
Examples:
|
|
45
|
+
alice appraise
|
|
46
|
+
alice appraise --since 2025-01-01
|
|
47
|
+
alice appraise --model sonnet
|
|
48
|
+
alice appraise --detailed
|
|
49
|
+
alice appraise --since 2025-06-01 --model haiku --detailed
|
|
50
|
+
"""
|
|
51
|
+
config = ctx.obj["config"]
|
|
52
|
+
|
|
53
|
+
print_banner(personality.BANNER, config.quiet)
|
|
54
|
+
|
|
55
|
+
# ── Authenticate ──────────────────────────────────────────────────
|
|
56
|
+
jhed: str
|
|
57
|
+
|
|
58
|
+
if config.bedrock_secret_key and not config.profile:
|
|
59
|
+
jhed = detect_identity_from_key(config.bedrock_access_key)
|
|
60
|
+
print_status(personality.status_using_api_key(jhed), config.quiet)
|
|
61
|
+
else:
|
|
62
|
+
with spinner(personality.status_detecting_identity(), config.quiet):
|
|
63
|
+
result = detect_jhed(config.profile, config.region)
|
|
64
|
+
jhed = result.jhed
|
|
65
|
+
print_status(personality.status_identity_detected(jhed), config.quiet)
|
|
66
|
+
|
|
67
|
+
locker = CloudLocker(config=config, jhed=jhed, bucket="jh-drcc-alice-memory")
|
|
68
|
+
|
|
69
|
+
# ── Load index and filter ─────────────────────────────────────────
|
|
70
|
+
with spinner("Loading session index…", config.quiet):
|
|
71
|
+
index = locker.load_index()
|
|
72
|
+
|
|
73
|
+
# Reuse recall's filter_entries for --since and --model filtering.
|
|
74
|
+
# We pass a very large limit so nothing is truncated.
|
|
75
|
+
filtered = filter_entries(
|
|
76
|
+
index.entries,
|
|
77
|
+
since=since,
|
|
78
|
+
model=model,
|
|
79
|
+
limit=0, # 0 means no limit in filter_entries
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Keep only entries that have token usage data
|
|
83
|
+
entries_with_tokens = [e for e in filtered if e.tokens is not None]
|
|
84
|
+
|
|
85
|
+
# If we matched sessions but none have tokens, try backfilling from
|
|
86
|
+
# the full session records stored in S3 / local cache.
|
|
87
|
+
if filtered and not entries_with_tokens:
|
|
88
|
+
with spinner("Backfilling token data from session records…", config.quiet):
|
|
89
|
+
filtered, filled = locker.backfill_tokens(filtered)
|
|
90
|
+
if filled:
|
|
91
|
+
err_console.print(f" [dim]Backfilled token data for {filled} session(s).[/dim]")
|
|
92
|
+
entries_with_tokens = [e for e in filtered if e.tokens is not None]
|
|
93
|
+
|
|
94
|
+
if not entries_with_tokens:
|
|
95
|
+
if filtered:
|
|
96
|
+
err_console.print(
|
|
97
|
+
f" [dim]Found {len(filtered)} session(s) but none have token usage recorded.[/dim]"
|
|
98
|
+
)
|
|
99
|
+
else:
|
|
100
|
+
err_console.print(" [dim]No matching sessions found.[/dim]")
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
if detailed:
|
|
104
|
+
_display_detailed(entries_with_tokens, config.quiet)
|
|
105
|
+
else:
|
|
106
|
+
_display_aggregated(entries_with_tokens, config.quiet)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _display_aggregated(entries: list[IndexEntry], quiet: bool) -> None:
|
|
110
|
+
"""Display per-model aggregate token usage with a summary row."""
|
|
111
|
+
# Aggregate by model
|
|
112
|
+
model_stats: dict[str, list[int]] = defaultdict(lambda: [0, 0, 0])
|
|
113
|
+
|
|
114
|
+
for entry in entries:
|
|
115
|
+
assert entry.tokens is not None
|
|
116
|
+
stats = model_stats[entry.model]
|
|
117
|
+
stats[0] += entry.tokens.input
|
|
118
|
+
stats[1] += entry.tokens.output
|
|
119
|
+
stats[2] += entry.tokens.total
|
|
120
|
+
|
|
121
|
+
table = Table(title="Token Usage by Model", show_lines=False)
|
|
122
|
+
table.add_column("Model", style="green")
|
|
123
|
+
table.add_column("Input Tokens", style="cyan", justify="right")
|
|
124
|
+
table.add_column("Output Tokens", style="cyan", justify="right")
|
|
125
|
+
table.add_column("Total Tokens", style="cyan", justify="right")
|
|
126
|
+
table.add_column("Est. Cost", style="yellow", justify="right")
|
|
127
|
+
|
|
128
|
+
total_input = 0
|
|
129
|
+
total_output = 0
|
|
130
|
+
total_total = 0
|
|
131
|
+
total_cost = 0.0
|
|
132
|
+
|
|
133
|
+
for model_name, (inp, out, tot) in sorted(model_stats.items()):
|
|
134
|
+
cost = estimate_cost(model_name, inp, out)
|
|
135
|
+
total_input += inp
|
|
136
|
+
total_output += out
|
|
137
|
+
total_total += tot
|
|
138
|
+
total_cost += cost
|
|
139
|
+
table.add_row(
|
|
140
|
+
model_name,
|
|
141
|
+
f"{inp:,}",
|
|
142
|
+
f"{out:,}",
|
|
143
|
+
f"{tot:,}",
|
|
144
|
+
format_cost(cost),
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Summary row
|
|
148
|
+
table.add_section()
|
|
149
|
+
table.add_row(
|
|
150
|
+
"[bold]Total[/bold]",
|
|
151
|
+
f"[bold]{total_input:,}[/bold]",
|
|
152
|
+
f"[bold]{total_output:,}[/bold]",
|
|
153
|
+
f"[bold]{total_total:,}[/bold]",
|
|
154
|
+
f"[bold]{format_cost(total_cost)}[/bold]",
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
err_console.print(table)
|
|
158
|
+
print_success(
|
|
159
|
+
f"{len(entries)} session(s), {total_total:,} tokens, {format_cost(total_cost)}",
|
|
160
|
+
quiet,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _display_detailed(entries: list[IndexEntry], quiet: bool) -> None:
|
|
165
|
+
"""Display per-session token usage with a summary row."""
|
|
166
|
+
table = Table(title="Token Usage by Session", show_lines=False)
|
|
167
|
+
table.add_column("Timestamp", style="cyan", no_wrap=True)
|
|
168
|
+
table.add_column("Type", style="magenta")
|
|
169
|
+
table.add_column("Model", style="green")
|
|
170
|
+
table.add_column("Input Tokens", style="cyan", justify="right")
|
|
171
|
+
table.add_column("Output Tokens", style="cyan", justify="right")
|
|
172
|
+
table.add_column("Total Tokens", style="cyan", justify="right")
|
|
173
|
+
table.add_column("Est. Cost", style="yellow", justify="right")
|
|
174
|
+
|
|
175
|
+
total_input = 0
|
|
176
|
+
total_output = 0
|
|
177
|
+
total_total = 0
|
|
178
|
+
total_cost = 0.0
|
|
179
|
+
|
|
180
|
+
for entry in entries:
|
|
181
|
+
assert entry.tokens is not None
|
|
182
|
+
cost = estimate_cost(entry.model, entry.tokens.input, entry.tokens.output)
|
|
183
|
+
total_input += entry.tokens.input
|
|
184
|
+
total_output += entry.tokens.output
|
|
185
|
+
total_total += entry.tokens.total
|
|
186
|
+
total_cost += cost
|
|
187
|
+
table.add_row(
|
|
188
|
+
entry.timestamp[:19],
|
|
189
|
+
entry.type,
|
|
190
|
+
entry.model,
|
|
191
|
+
f"{entry.tokens.input:,}",
|
|
192
|
+
f"{entry.tokens.output:,}",
|
|
193
|
+
f"{entry.tokens.total:,}",
|
|
194
|
+
format_cost(cost),
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Summary row
|
|
198
|
+
table.add_section()
|
|
199
|
+
table.add_row(
|
|
200
|
+
"[bold]Total[/bold]",
|
|
201
|
+
"",
|
|
202
|
+
"",
|
|
203
|
+
f"[bold]{total_input:,}[/bold]",
|
|
204
|
+
f"[bold]{total_output:,}[/bold]",
|
|
205
|
+
f"[bold]{total_total:,}[/bold]",
|
|
206
|
+
f"[bold]{format_cost(total_cost)}[/bold]",
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
err_console.print(table)
|
|
210
|
+
print_success(
|
|
211
|
+
f"{len(entries)} session(s), {total_total:,} tokens, {format_cost(total_cost)}",
|
|
212
|
+
quiet,
|
|
213
|
+
)
|