froid-loop 0.11.1__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.
- froid_loop/__init__.py +11 -0
- froid_loop/__main__.py +12 -0
- froid_loop/adapters/__init__.py +3 -0
- froid_loop/adapters/base.py +254 -0
- froid_loop/adapters/entrypoints.py +63 -0
- froid_loop/adapters/env_fault.py +290 -0
- froid_loop/adapters/generic.py +2013 -0
- froid_loop/adapters/mock.py +49 -0
- froid_loop/adapters/multiplexer.py +914 -0
- froid_loop/adapters/opencode_http.py +1687 -0
- froid_loop/adapters/profile.py +650 -0
- froid_loop/adapters/psmux_backend.py +1428 -0
- froid_loop/adapters/registry.py +322 -0
- froid_loop/adapters/tmux_backend.py +35 -0
- froid_loop/adapters/tmux_base.py +630 -0
- froid_loop/checks.py +187 -0
- froid_loop/cli.py +5041 -0
- froid_loop/data/__init__.py +0 -0
- froid_loop/data/froid_loop_hook.py +228 -0
- froid_loop/data/froid_loop_probe_hook.py +88 -0
- froid_loop/data/plugins/example/plugin.toml +21 -0
- froid_loop/data/plugins/tea/plugin.toml +184 -0
- froid_loop/data/plugins/tea/tea_plugin.py +258 -0
- froid_loop/data/plugins/unity/plugin.toml +140 -0
- froid_loop/data/plugins/unity/unity_assets/FroidLoop.Unity.Editor.asmdef +16 -0
- froid_loop/data/plugins/unity/unity_assets/FroidLoop.Unity.Editor.asmdef.meta +7 -0
- froid_loop/data/plugins/unity/unity_assets/SceneAutoSaveGuard.cs +221 -0
- froid_loop/data/plugins/unity/unity_assets/SceneAutoSaveGuard.cs.meta +11 -0
- froid_loop/data/plugins/unity/unity_assets/_folders/Editor.meta +8 -0
- froid_loop/data/plugins/unity/unity_assets/_folders/FroidLoop.meta +8 -0
- froid_loop/data/plugins/unity/unity_cleanup.py +125 -0
- froid_loop/data/plugins/unity/unity_dialog_probe.py +239 -0
- froid_loop/data/plugins/unity/unity_facts.md +17 -0
- froid_loop/data/plugins/unity/unity_plugin.py +415 -0
- froid_loop/data/plugins/unity/unity_quiesce.py +234 -0
- froid_loop/data/plugins/unity/unity_ready.py +230 -0
- froid_loop/data/plugins/unity/unity_seed_assets.py +298 -0
- froid_loop/data/plugins/unity/unity_setup.py +551 -0
- froid_loop/data/plugins/unity/unity_teardown.py +362 -0
- froid_loop/data/profiles/antigravity.toml +52 -0
- froid_loop/data/profiles/claude.toml +85 -0
- froid_loop/data/profiles/codex.toml +22 -0
- froid_loop/data/profiles/copilot.toml +52 -0
- froid_loop/data/profiles/gemini.toml +26 -0
- froid_loop/data/profiles/opencode.toml +54 -0
- froid_loop/data/settings/core.toml +458 -0
- froid_loop/data/skills/README.md +93 -0
- froid_loop/data/skills/froid-loop-resolve/SKILL.md +288 -0
- froid_loop/data/skills/froid-loop-setup/SKILL.md +161 -0
- froid_loop/data/skills/froid-loop-setup/assets/module-help.csv +3 -0
- froid_loop/data/skills/froid-loop-setup/assets/module.yaml +19 -0
- froid_loop/data/skills/froid-loop-sweep/SKILL.md +100 -0
- froid_loop/data/skills/froid-loop-sweep/automation-mode.md +127 -0
- froid_loop/data/skills/froid-loop-sweep/deferred-work-format.md +302 -0
- froid_loop/data/skills/froid-loop-sweep/migration-mode.md +86 -0
- froid_loop/decisions.py +202 -0
- froid_loop/deferredwork.py +2282 -0
- froid_loop/devcontract.py +892 -0
- froid_loop/diagnostics.py +1104 -0
- froid_loop/documents.py +532 -0
- froid_loop/engine.py +7732 -0
- froid_loop/envvars.py +111 -0
- froid_loop/escalation.py +225 -0
- froid_loop/events.py +266 -0
- froid_loop/fences.py +103 -0
- froid_loop/froidconfig.py +226 -0
- froid_loop/frontmatter.py +526 -0
- froid_loop/gates.py +133 -0
- froid_loop/install.py +2936 -0
- froid_loop/journal.py +178 -0
- froid_loop/machine.py +148 -0
- froid_loop/model.py +898 -0
- froid_loop/operatoractions.py +474 -0
- froid_loop/platform_util.py +1490 -0
- froid_loop/plugins/__init__.py +64 -0
- froid_loop/plugins/bus.py +259 -0
- froid_loop/plugins/context.py +319 -0
- froid_loop/plugins/loader.py +145 -0
- froid_loop/plugins/manifest.py +279 -0
- froid_loop/plugins/model.py +296 -0
- froid_loop/plugins/registry.py +245 -0
- froid_loop/plugins/trust.py +75 -0
- froid_loop/policy.py +1569 -0
- froid_loop/probe.py +1044 -0
- froid_loop/process_host.py +408 -0
- froid_loop/recovery_flow.py +1561 -0
- froid_loop/resolve.py +283 -0
- froid_loop/runs.py +4715 -0
- froid_loop/runsetup.py +1293 -0
- froid_loop/sanitize.py +593 -0
- froid_loop/settings_schema.py +276 -0
- froid_loop/signals.py +160 -0
- froid_loop/sprintstatus.py +609 -0
- froid_loop/statemachine.py +57 -0
- froid_loop/stories.py +615 -0
- froid_loop/stories_engine.py +796 -0
- froid_loop/sweep.py +1892 -0
- froid_loop/tokens.py +196 -0
- froid_loop/tui/__init__.py +11 -0
- froid_loop/tui/app.py +1584 -0
- froid_loop/tui/data.py +840 -0
- froid_loop/tui/launch.py +1003 -0
- froid_loop/tui/screens/__init__.py +1 -0
- froid_loop/tui/screens/dashboard.py +1071 -0
- froid_loop/tui/screens/modals.py +943 -0
- froid_loop/tui/screens/settings_screen.py +477 -0
- froid_loop/tui/settings.py +135 -0
- froid_loop/tui/widgets.py +981 -0
- froid_loop/verify.py +4545 -0
- froid_loop/workspace.py +320 -0
- froid_loop/worktree_flow.py +2301 -0
- froid_loop-0.11.1.dist-info/METADATA +728 -0
- froid_loop-0.11.1.dist-info/RECORD +116 -0
- froid_loop-0.11.1.dist-info/WHEEL +4 -0
- froid_loop-0.11.1.dist-info/entry_points.txt +2 -0
- froid_loop-0.11.1.dist-info/licenses/LICENSE +30 -0
froid_loop/tokens.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""Token accounting from coding-CLI transcript files.
|
|
2
|
+
|
|
3
|
+
One parser per CLI family, selected by the profile's `usage_parser` key. All
|
|
4
|
+
parsers are tolerant — unknown lines and shapes are skipped, never fatal; a
|
|
5
|
+
transcript that yields nothing reads as None (untracked), not zero.
|
|
6
|
+
|
|
7
|
+
- claude-jsonl: ~/.claude/projects/<munged-cwd>/<session-id>.jsonl;
|
|
8
|
+
assistant entries carry an API `usage` block, summed.
|
|
9
|
+
- codex-rollout: ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl; `token_count`
|
|
10
|
+
event payloads carry CUMULATIVE totals, so the last one is
|
|
11
|
+
the session usage (sessions here are fresh per task).
|
|
12
|
+
- gemini-chat: ~/.gemini/tmp/<project>/chats/session-*.jsonl — a JSONL
|
|
13
|
+
patch stream: bare message entries (and messages inside
|
|
14
|
+
"$set" patches) carry a per-API-call `tokens` block
|
|
15
|
+
{input, output, cached, thoughts, tool, total}; the same
|
|
16
|
+
message id is re-emitted as it accretes content, so the
|
|
17
|
+
last occurrence per id wins, then unique messages are
|
|
18
|
+
summed. `input` includes the cached portion.
|
|
19
|
+
- copilot-events: ~/.copilot/session-state/<session>/events.jsonl; some
|
|
20
|
+
entries carry `data.modelMetrics.<model>.usage`
|
|
21
|
+
{inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens,
|
|
22
|
+
reasoningTokens} that is CUMULATIVE per model, so the last
|
|
23
|
+
entry bearing modelMetrics holds the session totals (summed
|
|
24
|
+
across models). reasoningTokens fold into output.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import json
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import Any
|
|
32
|
+
|
|
33
|
+
from .model import TokenUsage
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def read_usage(parser: str, transcript_path: Path) -> TokenUsage | None:
|
|
37
|
+
if parser == "claude-jsonl":
|
|
38
|
+
return tally(transcript_path)
|
|
39
|
+
if parser == "codex-rollout":
|
|
40
|
+
return tally_codex_rollout(transcript_path)
|
|
41
|
+
if parser == "gemini-chat":
|
|
42
|
+
return tally_gemini_chat(transcript_path)
|
|
43
|
+
if parser == "copilot-events":
|
|
44
|
+
return tally_copilot_events(transcript_path)
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _jsonl_entries(transcript_path: Path):
|
|
49
|
+
if not transcript_path.is_file():
|
|
50
|
+
return
|
|
51
|
+
with transcript_path.open(encoding="utf-8") as f:
|
|
52
|
+
for line in f:
|
|
53
|
+
line = line.strip()
|
|
54
|
+
if not line:
|
|
55
|
+
continue
|
|
56
|
+
try:
|
|
57
|
+
entry = json.loads(line)
|
|
58
|
+
except json.JSONDecodeError:
|
|
59
|
+
continue
|
|
60
|
+
if isinstance(entry, dict):
|
|
61
|
+
yield entry
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _int(value: Any) -> int:
|
|
65
|
+
try:
|
|
66
|
+
return int(value or 0)
|
|
67
|
+
except (TypeError, ValueError):
|
|
68
|
+
return 0
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ------------------------------------------------------------- claude-jsonl
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _usage_block(entry: dict[str, Any]) -> dict[str, Any] | None:
|
|
75
|
+
message = entry.get("message")
|
|
76
|
+
if isinstance(message, dict) and isinstance(message.get("usage"), dict):
|
|
77
|
+
return message["usage"]
|
|
78
|
+
if isinstance(entry.get("usage"), dict):
|
|
79
|
+
return entry["usage"]
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def tally(transcript_path: Path) -> TokenUsage:
|
|
84
|
+
total = TokenUsage()
|
|
85
|
+
for entry in _jsonl_entries(transcript_path):
|
|
86
|
+
usage = _usage_block(entry)
|
|
87
|
+
if not usage:
|
|
88
|
+
continue
|
|
89
|
+
total.add(
|
|
90
|
+
TokenUsage(
|
|
91
|
+
input_tokens=_int(usage.get("input_tokens")),
|
|
92
|
+
output_tokens=_int(usage.get("output_tokens")),
|
|
93
|
+
cache_read_tokens=_int(usage.get("cache_read_input_tokens")),
|
|
94
|
+
cache_creation_tokens=_int(usage.get("cache_creation_input_tokens")),
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
return total
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ----------------------------------------------------------- codex-rollout
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def tally_codex_rollout(transcript_path: Path) -> TokenUsage | None:
|
|
104
|
+
last: dict[str, Any] | None = None
|
|
105
|
+
for entry in _jsonl_entries(transcript_path):
|
|
106
|
+
payload = entry.get("payload")
|
|
107
|
+
if not isinstance(payload, dict) or payload.get("type") != "token_count":
|
|
108
|
+
continue
|
|
109
|
+
info = payload.get("info")
|
|
110
|
+
if not isinstance(info, dict):
|
|
111
|
+
continue
|
|
112
|
+
totals = info.get("total_token_usage")
|
|
113
|
+
last = totals if isinstance(totals, dict) else info
|
|
114
|
+
if last is None:
|
|
115
|
+
return None
|
|
116
|
+
cached = _int(last.get("cached_input_tokens"))
|
|
117
|
+
return TokenUsage(
|
|
118
|
+
# Codex's input_tokens includes the cached portion; split it out.
|
|
119
|
+
input_tokens=max(0, _int(last.get("input_tokens")) - cached),
|
|
120
|
+
output_tokens=_int(last.get("output_tokens")),
|
|
121
|
+
cache_read_tokens=cached,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# ------------------------------------------------------------- gemini-chat
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _gemini_messages(entry: dict[str, Any]):
|
|
129
|
+
if "id" in entry:
|
|
130
|
+
yield entry
|
|
131
|
+
set_patch = entry.get("$set")
|
|
132
|
+
if isinstance(set_patch, dict):
|
|
133
|
+
messages = set_patch.get("messages")
|
|
134
|
+
if isinstance(messages, list):
|
|
135
|
+
for message in messages:
|
|
136
|
+
if isinstance(message, dict):
|
|
137
|
+
yield message
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def tally_gemini_chat(transcript_path: Path) -> TokenUsage | None:
|
|
141
|
+
by_id: dict[str, dict[str, Any]] = {}
|
|
142
|
+
for entry in _jsonl_entries(transcript_path):
|
|
143
|
+
for message in _gemini_messages(entry):
|
|
144
|
+
tokens = message.get("tokens")
|
|
145
|
+
message_id = message.get("id")
|
|
146
|
+
if isinstance(tokens, dict) and isinstance(message_id, str):
|
|
147
|
+
by_id[message_id] = tokens
|
|
148
|
+
if not by_id:
|
|
149
|
+
return None
|
|
150
|
+
total = TokenUsage()
|
|
151
|
+
for tokens in by_id.values():
|
|
152
|
+
cached = _int(tokens.get("cached"))
|
|
153
|
+
total.add(
|
|
154
|
+
TokenUsage(
|
|
155
|
+
input_tokens=max(0, _int(tokens.get("input")) - cached),
|
|
156
|
+
output_tokens=_int(tokens.get("output")) + _int(tokens.get("thoughts")),
|
|
157
|
+
cache_read_tokens=cached,
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
return total
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# --------------------------------------------------------- copilot-events
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def tally_copilot_events(transcript_path: Path) -> TokenUsage | None:
|
|
167
|
+
# data.modelMetrics.<model>.usage is cumulative per model, so the LAST entry
|
|
168
|
+
# carrying modelMetrics holds the session totals; sum across its models.
|
|
169
|
+
# (Schema verified against a single Copilot CLI 1.0.63 events.jsonl — revisit
|
|
170
|
+
# the cumulative/multi-model assumption if a newer build disagrees.)
|
|
171
|
+
last: dict[str, Any] | None = None
|
|
172
|
+
for entry in _jsonl_entries(transcript_path):
|
|
173
|
+
data = entry.get("data")
|
|
174
|
+
if not isinstance(data, dict):
|
|
175
|
+
continue
|
|
176
|
+
metrics = data.get("modelMetrics")
|
|
177
|
+
if isinstance(metrics, dict) and metrics:
|
|
178
|
+
last = metrics
|
|
179
|
+
if last is None:
|
|
180
|
+
return None
|
|
181
|
+
total = TokenUsage()
|
|
182
|
+
for model_metrics in last.values():
|
|
183
|
+
if not isinstance(model_metrics, dict):
|
|
184
|
+
continue
|
|
185
|
+
usage = model_metrics.get("usage")
|
|
186
|
+
if not isinstance(usage, dict):
|
|
187
|
+
continue
|
|
188
|
+
total.add(
|
|
189
|
+
TokenUsage(
|
|
190
|
+
input_tokens=_int(usage.get("inputTokens")),
|
|
191
|
+
output_tokens=_int(usage.get("outputTokens")) + _int(usage.get("reasoningTokens")),
|
|
192
|
+
cache_read_tokens=_int(usage.get("cacheReadTokens")),
|
|
193
|
+
cache_creation_tokens=_int(usage.get("cacheWriteTokens")),
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
return total
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""froid-loop TUI (optional `froid-loop[tui]` extra).
|
|
2
|
+
|
|
3
|
+
Only `launch` is importable on a core-only install: `data` needs pyte and rich
|
|
4
|
+
(it never imports textual), and every other submodule may import textual. The
|
|
5
|
+
core-safe run inventory lives in `froid_loop.runs` — `data` re-exports it (#650).
|
|
6
|
+
|
|
7
|
+
It is the submodule the core CLI paths reach for (attach/park/sweep), and being
|
|
8
|
+
stdlib-only is what keeps those paths working extra-less. Every other
|
|
9
|
+
submodule is loaded lazily by the `tui` command, behind the guard that turns a
|
|
10
|
+
missing extra into an install hint.
|
|
11
|
+
"""
|