cli-consumption 0.1.0__tar.gz → 0.1.1__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.
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/PKG-INFO +2 -1
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/README.md +1 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/docs/provider-support.md +24 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/pyproject.toml +1 -1
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/src/cli_consumption/adapters/__init__.py +2 -0
- cli_consumption-0.1.1/src/cli_consumption/adapters/mistral_vibe.py +281 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/src/cli_consumption/cli.py +7 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/tests/test_cli.py +38 -0
- cli_consumption-0.1.1/tests/test_mistral_vibe_adapter.py +207 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/uv.lock +1 -1
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/.agents/skills/add-cli-adapter/SKILL.md +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/.agents/skills/add-cli-adapter/agents/openai.yaml +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/.agents/skills/audit-usage-privacy/SKILL.md +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/.agents/skills/audit-usage-privacy/agents/openai.yaml +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/.agents/skills/evolve-storage-schema/SKILL.md +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/.agents/skills/evolve-storage-schema/agents/openai.yaml +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/.agents/skills/yeet-github/SKILL.md +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/.agents/skills/yeet-github/agents/openai.yaml +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/.agents/skills/yolo/SKILL.md +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/.agents/skills/yolo/agents/openai.yaml +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/.github/workflows/ci.yml +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/.github/workflows/release.yaml +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/.gitignore +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/.pre-commit-config.yaml +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/.python-version +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/AGENTS.md +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/CONTRIBUTING.md +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/LICENSE +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/NOTICE +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/docs/architecture.md +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/docs/privacy.md +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/docs/roadmap.md +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/src/cli_consumption/__init__.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/src/cli_consumption/__main__.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/src/cli_consumption/adapters/_shared.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/src/cli_consumption/adapters/aider.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/src/cli_consumption/adapters/amazon_q.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/src/cli_consumption/adapters/amp.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/src/cli_consumption/adapters/base.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/src/cli_consumption/adapters/claude.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/src/cli_consumption/adapters/cline.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/src/cli_consumption/adapters/codex.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/src/cli_consumption/adapters/continue_cli.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/src/cli_consumption/adapters/copilot.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/src/cli_consumption/adapters/crush.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/src/cli_consumption/adapters/cursor.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/src/cli_consumption/adapters/gemini.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/src/cli_consumption/adapters/goose.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/src/cli_consumption/adapters/grok.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/src/cli_consumption/adapters/kilo.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/src/cli_consumption/adapters/kimi.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/src/cli_consumption/adapters/opencode.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/src/cli_consumption/adapters/openhands.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/src/cli_consumption/adapters/pi.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/src/cli_consumption/adapters/plandex.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/src/cli_consumption/adapters/qwen.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/src/cli_consumption/api.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/src/cli_consumption/dashboard.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/src/cli_consumption/exporting.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/src/cli_consumption/models.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/src/cli_consumption/py.typed +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/src/cli_consumption/storage.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/src/cli_consumption/sync.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/tests/conftest.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/tests/test_aider_adapter.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/tests/test_amazon_q_adapter.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/tests/test_amp_adapter.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/tests/test_api.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/tests/test_claude_adapter.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/tests/test_cline_adapter.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/tests/test_codex_adapter.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/tests/test_continue_adapter.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/tests/test_copilot_adapter.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/tests/test_crush_adapter.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/tests/test_cursor_adapter.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/tests/test_gemini_adapter.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/tests/test_goose_adapter.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/tests/test_grok_adapter.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/tests/test_kilo_adapter.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/tests/test_kimi_adapter.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/tests/test_opencode_adapter.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/tests/test_openhands_adapter.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/tests/test_pi_adapter.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/tests/test_plandex_adapter.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/tests/test_qwen_adapter.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/tests/test_storage_and_exports.py +0 -0
- {cli_consumption-0.1.0 → cli_consumption-0.1.1}/tests/test_sync.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: cli-consumption
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Analyze and consolidate AI coding CLI consumption across machines.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Guillaume-Lombardo/cli-consumption
|
|
6
6
|
Project-URL: Documentation, https://github.com/Guillaume-Lombardo/cli-consumption#readme
|
|
@@ -95,6 +95,7 @@ Use the provider name below with `--provider` to select one CLI explicitly.
|
|
|
95
95
|
| Grok Build | `grok` | `~/.grok/sessions/` | Per-prompt aggregates, reasoning effort, TTFT, and auto-compactions; no costs or subagent relationships. |
|
|
96
96
|
| Kilo Code | `kilo` | `~/.local/share/kilo/kilo.db` | CLI SQLite store only; excludes legacy IDE tasks, cloud sessions, subagents, context windows, and costs. |
|
|
97
97
|
| Kimi Code CLI | `kimi` | `~/.kimi/sessions/` | Wire v1 events, context windows, and compactions; selected model is not persisted and is reported as `unknown`. |
|
|
98
|
+
| Mistral Vibe CLI | `mistral-vibe` | `~/.vibe/logs/session/` | Session-level token aggregates, user turns, tools, and compactions; no per-message timestamps or historical model attribution. |
|
|
98
99
|
| OpenCode | `opencode` | `~/.local/share/opencode/opencode.db` | SQLite v2 only; no legacy storage, child sessions, context windows, or costs. |
|
|
99
100
|
| OpenHands CLI | `openhands` | `~/.openhands/conversations/` | SDK persistence with context windows, reasoning effort, and condensations; excludes cloud-only conversations and delegates. |
|
|
100
101
|
| Pi | `pi` | `~/.pi/agent/sessions/` | Counts all persisted branches; no branch relationships, custom-directory auto-detection, context windows, or provider-reported durations. |
|
|
@@ -67,6 +67,7 @@ Use the provider name below with `--provider` to select one CLI explicitly.
|
|
|
67
67
|
| Grok Build | `grok` | `~/.grok/sessions/` | Per-prompt aggregates, reasoning effort, TTFT, and auto-compactions; no costs or subagent relationships. |
|
|
68
68
|
| Kilo Code | `kilo` | `~/.local/share/kilo/kilo.db` | CLI SQLite store only; excludes legacy IDE tasks, cloud sessions, subagents, context windows, and costs. |
|
|
69
69
|
| Kimi Code CLI | `kimi` | `~/.kimi/sessions/` | Wire v1 events, context windows, and compactions; selected model is not persisted and is reported as `unknown`. |
|
|
70
|
+
| Mistral Vibe CLI | `mistral-vibe` | `~/.vibe/logs/session/` | Session-level token aggregates, user turns, tools, and compactions; no per-message timestamps or historical model attribution. |
|
|
70
71
|
| OpenCode | `opencode` | `~/.local/share/opencode/opencode.db` | SQLite v2 only; no legacy storage, child sessions, context windows, or costs. |
|
|
71
72
|
| OpenHands CLI | `openhands` | `~/.openhands/conversations/` | SDK persistence with context windows, reasoning effort, and condensations; excludes cloud-only conversations and delegates. |
|
|
72
73
|
| Pi | `pi` | `~/.pi/agent/sessions/` | Counts all persisted branches; no branch relationships, custom-directory auto-detection, context windows, or provider-reported durations. |
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
| Grok Build | Supported (core) | Local session summary and update/event JSONL |
|
|
18
18
|
| Kilo Code | Supported (core) | Local SQLite session store |
|
|
19
19
|
| Kimi Code CLI | Supported (core) | Local Wire event JSONL |
|
|
20
|
+
| Mistral Vibe CLI | Supported (core) | Local session metadata JSON and message JSONL |
|
|
20
21
|
| OpenCode | Supported (core) | Local SQLite v2 session store |
|
|
21
22
|
| OpenHands CLI | Supported (core) | Local SDK conversation state and event JSON |
|
|
22
23
|
| Pi | Supported (core) | Local session JSONL v1-v3 |
|
|
@@ -31,6 +32,29 @@ It does not mean that token counters are equivalent to invoices.
|
|
|
31
32
|
ingests each metadata-only snapshot independently. With no explicit source it checks
|
|
32
33
|
the local default homes; repeated `--source` paths are filtered by detected format.
|
|
33
34
|
|
|
35
|
+
## Mistral Vibe CLI
|
|
36
|
+
|
|
37
|
+
Mistral Vibe CLI reads top-level session directories under
|
|
38
|
+
`~/.vibe/logs/session/`, when interaction logging is enabled. The adapter was
|
|
39
|
+
qualified against Mistral Vibe 2.24.5 and its current `meta.json` plus
|
|
40
|
+
`messages.jsonl` format in August 2026. It extracts stable session and user-message
|
|
41
|
+
identifiers, session timestamps, the latest active model alias, cumulative token
|
|
42
|
+
usage, function names from assistant tool calls, and compaction markers. Working
|
|
43
|
+
directories are inspected only for explicit project mappings. Titles, prompts,
|
|
44
|
+
responses, reasoning, tool arguments/results, system instructions, usernames, Git
|
|
45
|
+
metadata, environment values other than the mapped working directory, prices,
|
|
46
|
+
arbitrary configuration, and raw messages are discarded.
|
|
47
|
+
|
|
48
|
+
Vibe persists prompt, cached-prompt, and completion counters only as cumulative
|
|
49
|
+
session statistics. The adapter therefore emits one unattributed aggregate model
|
|
50
|
+
call, subtracts cached prompt tokens from uncached input, and does not assign token
|
|
51
|
+
usage to individual turns. The persisted active model is the latest selection and is
|
|
52
|
+
not attributed historically to turns. Message records have no timestamps, so turns,
|
|
53
|
+
tools, and compactions do not expose event times or durations. Child-agent sessions,
|
|
54
|
+
model changes, cache writes, reasoning tokens, context-window samples, costs, tool
|
|
55
|
+
outcomes, and provider-reported latency are not collected. Vibe's internal session
|
|
56
|
+
format can change without notice, and local token events are not billing data.
|
|
57
|
+
|
|
34
58
|
## Codex
|
|
35
59
|
|
|
36
60
|
Codex additionally exposes provider-reported turn duration and TTFT, model context
|
|
@@ -13,6 +13,7 @@ from cli_consumption.adapters.goose import GooseAdapter
|
|
|
13
13
|
from cli_consumption.adapters.grok import GrokAdapter
|
|
14
14
|
from cli_consumption.adapters.kilo import KiloAdapter
|
|
15
15
|
from cli_consumption.adapters.kimi import KimiAdapter
|
|
16
|
+
from cli_consumption.adapters.mistral_vibe import MistralVibeAdapter
|
|
16
17
|
from cli_consumption.adapters.opencode import OpenCodeAdapter
|
|
17
18
|
from cli_consumption.adapters.openhands import OpenHandsAdapter
|
|
18
19
|
from cli_consumption.adapters.pi import PiAdapter
|
|
@@ -35,6 +36,7 @@ __all__ = [
|
|
|
35
36
|
"GrokAdapter",
|
|
36
37
|
"KiloAdapter",
|
|
37
38
|
"KimiAdapter",
|
|
39
|
+
"MistralVibeAdapter",
|
|
38
40
|
"OpenCodeAdapter",
|
|
39
41
|
"OpenHandsAdapter",
|
|
40
42
|
"PiAdapter",
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from cli_consumption.adapters._shared import (
|
|
11
|
+
add_tokens,
|
|
12
|
+
counter,
|
|
13
|
+
iso,
|
|
14
|
+
label,
|
|
15
|
+
mapping,
|
|
16
|
+
project,
|
|
17
|
+
timestamp,
|
|
18
|
+
tokens,
|
|
19
|
+
)
|
|
20
|
+
from cli_consumption.models import Snapshot, empty_tokens
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(slots=True)
|
|
24
|
+
class _Candidate:
|
|
25
|
+
machine: str
|
|
26
|
+
external_id: str
|
|
27
|
+
metadata: dict[str, Any]
|
|
28
|
+
messages: list[dict[str, Any]]
|
|
29
|
+
event_count: int
|
|
30
|
+
digest: str
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class MistralVibeAdapter:
|
|
34
|
+
"""Read Mistral Vibe session logs without retaining conversation content."""
|
|
35
|
+
|
|
36
|
+
name = "mistral-vibe"
|
|
37
|
+
|
|
38
|
+
def collect(
|
|
39
|
+
self,
|
|
40
|
+
sources: list[tuple[str, Path]],
|
|
41
|
+
project_mappings: list[tuple[str, str]] | None = None,
|
|
42
|
+
) -> Snapshot:
|
|
43
|
+
selected: dict[str, _Candidate] = {}
|
|
44
|
+
duplicates = malformed = 0
|
|
45
|
+
for machine, home in sources:
|
|
46
|
+
sessions = home / "logs" / "session"
|
|
47
|
+
if not sessions.is_dir():
|
|
48
|
+
raise ValueError(
|
|
49
|
+
f"Missing Mistral Vibe session log directory: {sessions}"
|
|
50
|
+
)
|
|
51
|
+
for metadata_path in sorted(sessions.glob("*/meta.json")):
|
|
52
|
+
candidate, invalid = _read_candidate(machine, metadata_path)
|
|
53
|
+
malformed += invalid
|
|
54
|
+
if candidate is None:
|
|
55
|
+
continue
|
|
56
|
+
previous = selected.get(candidate.external_id)
|
|
57
|
+
if previous is None:
|
|
58
|
+
selected[candidate.external_id] = candidate
|
|
59
|
+
continue
|
|
60
|
+
duplicates += 1
|
|
61
|
+
if _rank(candidate) > _rank(previous):
|
|
62
|
+
selected[candidate.external_id] = candidate
|
|
63
|
+
|
|
64
|
+
snapshot = Snapshot(
|
|
65
|
+
provider=self.name,
|
|
66
|
+
duplicate_conversations=duplicates,
|
|
67
|
+
malformed_records=malformed,
|
|
68
|
+
)
|
|
69
|
+
for candidate in sorted(selected.values(), key=lambda item: item.external_id):
|
|
70
|
+
self._normalize(snapshot, candidate, project_mappings or [])
|
|
71
|
+
return snapshot
|
|
72
|
+
|
|
73
|
+
def _normalize(
|
|
74
|
+
self,
|
|
75
|
+
snapshot: Snapshot,
|
|
76
|
+
source: _Candidate,
|
|
77
|
+
mappings: list[tuple[str, str]],
|
|
78
|
+
) -> None:
|
|
79
|
+
conversation_id = f"mistral-vibe:{source.external_id}"
|
|
80
|
+
started_at = timestamp(source.metadata.get("start_time"))
|
|
81
|
+
ended_at = timestamp(source.metadata.get("end_time"))
|
|
82
|
+
active_turn: dict[str, Any] | None = None
|
|
83
|
+
turn_number = 0
|
|
84
|
+
tool_number = 0
|
|
85
|
+
compaction_number = 0
|
|
86
|
+
|
|
87
|
+
for message in source.messages:
|
|
88
|
+
role = message.get("role")
|
|
89
|
+
if role == "user" and message.get("injected") is not True:
|
|
90
|
+
if active_turn is not None:
|
|
91
|
+
active_turn["status"] = "completed"
|
|
92
|
+
turn_number += 1
|
|
93
|
+
active_turn = _turn(conversation_id, message, turn_number)
|
|
94
|
+
snapshot.turns.append(active_turn)
|
|
95
|
+
snapshot.turn_settings.append(
|
|
96
|
+
{
|
|
97
|
+
"id": f"{conversation_id}:settings:{turn_number}",
|
|
98
|
+
"conversation_id": conversation_id,
|
|
99
|
+
"turn_id": active_turn["id"],
|
|
100
|
+
"model": None,
|
|
101
|
+
"effort": None,
|
|
102
|
+
"collaboration_mode": None,
|
|
103
|
+
"service_tier": None,
|
|
104
|
+
"context_window_tokens": None,
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
if message.get("context_boundary") == "compaction":
|
|
109
|
+
compaction_number += 1
|
|
110
|
+
snapshot.compaction_events.append(
|
|
111
|
+
{
|
|
112
|
+
"id": f"{conversation_id}:compaction:{compaction_number}",
|
|
113
|
+
"conversation_id": conversation_id,
|
|
114
|
+
"turn_id": active_turn["id"] if active_turn else None,
|
|
115
|
+
"sequence": compaction_number,
|
|
116
|
+
"timestamp": None,
|
|
117
|
+
}
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
if role != "assistant":
|
|
121
|
+
continue
|
|
122
|
+
if active_turn is not None:
|
|
123
|
+
active_turn["status"] = "completed"
|
|
124
|
+
raw_tool_calls = message.get("tool_calls")
|
|
125
|
+
if not isinstance(raw_tool_calls, list):
|
|
126
|
+
continue
|
|
127
|
+
for raw_call in raw_tool_calls:
|
|
128
|
+
function = mapping(mapping(raw_call).get("function"))
|
|
129
|
+
tool_name = label(function.get("name"))
|
|
130
|
+
if tool_name is None:
|
|
131
|
+
snapshot.malformed_records += 1
|
|
132
|
+
continue
|
|
133
|
+
tool_number += 1
|
|
134
|
+
if active_turn is not None:
|
|
135
|
+
active_turn["tool_calls"] += 1
|
|
136
|
+
snapshot.tool_calls.append(
|
|
137
|
+
{
|
|
138
|
+
"id": f"{conversation_id}:tool:{tool_number}",
|
|
139
|
+
"conversation_id": conversation_id,
|
|
140
|
+
"turn_id": active_turn["id"] if active_turn else None,
|
|
141
|
+
"sequence": tool_number,
|
|
142
|
+
"timestamp": None,
|
|
143
|
+
"tool_name": tool_name,
|
|
144
|
+
"outer_tool_name": tool_name,
|
|
145
|
+
}
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
stats = mapping(source.metadata.get("stats"))
|
|
149
|
+
prompt_tokens = counter(stats.get("session_prompt_tokens"))
|
|
150
|
+
cached_tokens = min(prompt_tokens, counter(stats.get("session_cached_tokens")))
|
|
151
|
+
completion_tokens = counter(stats.get("session_completion_tokens"))
|
|
152
|
+
usage = tokens(
|
|
153
|
+
uncached=prompt_tokens - cached_tokens,
|
|
154
|
+
cached=cached_tokens,
|
|
155
|
+
visible=completion_tokens,
|
|
156
|
+
)
|
|
157
|
+
config = mapping(source.metadata.get("config"))
|
|
158
|
+
model = label(config.get("active_model"), 255) or "unknown"
|
|
159
|
+
model_calls = 0
|
|
160
|
+
totals = empty_tokens()
|
|
161
|
+
if usage["total_tokens"] > 0 or counter(stats.get("steps")) > 0:
|
|
162
|
+
model_calls = 1
|
|
163
|
+
add_tokens(totals, usage)
|
|
164
|
+
snapshot.model_calls.append(
|
|
165
|
+
{
|
|
166
|
+
"id": f"{conversation_id}:model:aggregate",
|
|
167
|
+
"conversation_id": conversation_id,
|
|
168
|
+
"turn_id": None,
|
|
169
|
+
"sequence": 1,
|
|
170
|
+
"timestamp": iso(ended_at),
|
|
171
|
+
"model": model,
|
|
172
|
+
**usage,
|
|
173
|
+
}
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
directory = mapping(source.metadata.get("environment")).get("working_directory")
|
|
177
|
+
project_name, project_source = project(
|
|
178
|
+
directory if isinstance(directory, str) else None, mappings
|
|
179
|
+
)
|
|
180
|
+
snapshot.conversations.append(
|
|
181
|
+
{
|
|
182
|
+
"id": conversation_id,
|
|
183
|
+
"provider": self.name,
|
|
184
|
+
"external_id": source.external_id,
|
|
185
|
+
"source_machine": source.machine,
|
|
186
|
+
"project": project_name,
|
|
187
|
+
"project_source": project_source,
|
|
188
|
+
"started_at": iso(started_at),
|
|
189
|
+
"ended_at": iso(ended_at),
|
|
190
|
+
"duration_seconds": _duration(started_at, ended_at),
|
|
191
|
+
"source": "local-session-json",
|
|
192
|
+
"models": [model] if model_calls else [],
|
|
193
|
+
"iterations": turn_number,
|
|
194
|
+
"model_calls": model_calls,
|
|
195
|
+
"tool_calls": tool_number,
|
|
196
|
+
"compactions": compaction_number,
|
|
197
|
+
"event_count": source.event_count,
|
|
198
|
+
"content_hash": source.digest,
|
|
199
|
+
**totals,
|
|
200
|
+
}
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _read_candidate(machine: str, metadata_path: Path) -> tuple[_Candidate | None, int]:
|
|
205
|
+
digest = hashlib.sha256()
|
|
206
|
+
try:
|
|
207
|
+
metadata_bytes = metadata_path.read_bytes()
|
|
208
|
+
digest.update(metadata_bytes)
|
|
209
|
+
metadata = json.loads(metadata_bytes)
|
|
210
|
+
except (OSError, json.JSONDecodeError, UnicodeDecodeError):
|
|
211
|
+
return None, 1
|
|
212
|
+
if not isinstance(metadata, dict):
|
|
213
|
+
return None, 1
|
|
214
|
+
external_id = label(metadata.get("session_id"), 512)
|
|
215
|
+
if external_id is None:
|
|
216
|
+
return None, 1
|
|
217
|
+
|
|
218
|
+
messages: list[dict[str, Any]] = []
|
|
219
|
+
malformed = 0
|
|
220
|
+
messages_path = metadata_path.parent / "messages.jsonl"
|
|
221
|
+
try:
|
|
222
|
+
with messages_path.open("rb") as handle:
|
|
223
|
+
for line in handle:
|
|
224
|
+
digest.update(line)
|
|
225
|
+
if not line.strip():
|
|
226
|
+
continue
|
|
227
|
+
try:
|
|
228
|
+
message = json.loads(line)
|
|
229
|
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
230
|
+
malformed += 1
|
|
231
|
+
continue
|
|
232
|
+
if not isinstance(message, dict) or not isinstance(
|
|
233
|
+
message.get("role"), str
|
|
234
|
+
):
|
|
235
|
+
malformed += 1
|
|
236
|
+
continue
|
|
237
|
+
messages.append(message)
|
|
238
|
+
except OSError:
|
|
239
|
+
malformed += 1
|
|
240
|
+
|
|
241
|
+
return (
|
|
242
|
+
_Candidate(
|
|
243
|
+
machine=machine,
|
|
244
|
+
external_id=external_id,
|
|
245
|
+
metadata=metadata,
|
|
246
|
+
messages=messages,
|
|
247
|
+
event_count=len(messages),
|
|
248
|
+
digest=digest.hexdigest(),
|
|
249
|
+
),
|
|
250
|
+
malformed,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _turn(
|
|
255
|
+
conversation_id: str, message: dict[str, Any], sequence: int
|
|
256
|
+
) -> dict[str, Any]:
|
|
257
|
+
external_id = label(message.get("message_id"), 512) or f"turn-{sequence}"
|
|
258
|
+
return {
|
|
259
|
+
"id": f"{conversation_id}:{external_id}",
|
|
260
|
+
"conversation_id": conversation_id,
|
|
261
|
+
"external_id": external_id,
|
|
262
|
+
"started_at": None,
|
|
263
|
+
"ended_at": None,
|
|
264
|
+
"status": "in-progress",
|
|
265
|
+
"duration_ms": None,
|
|
266
|
+
"time_to_first_token_ms": None,
|
|
267
|
+
"model_calls": 0,
|
|
268
|
+
"tool_calls": 0,
|
|
269
|
+
**empty_tokens(),
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _duration(started_at: datetime | None, ended_at: datetime | None) -> float | None:
|
|
274
|
+
if started_at is None or ended_at is None:
|
|
275
|
+
return None
|
|
276
|
+
return max(0.0, (ended_at - started_at).total_seconds())
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _rank(candidate: _Candidate) -> tuple[int, str, str]:
|
|
280
|
+
ended_at = timestamp(candidate.metadata.get("end_time"))
|
|
281
|
+
return candidate.event_count, iso(ended_at) or "", candidate.digest
|
|
@@ -24,6 +24,7 @@ from cli_consumption.adapters import (
|
|
|
24
24
|
GrokAdapter,
|
|
25
25
|
KiloAdapter,
|
|
26
26
|
KimiAdapter,
|
|
27
|
+
MistralVibeAdapter,
|
|
27
28
|
OpenCodeAdapter,
|
|
28
29
|
OpenHandsAdapter,
|
|
29
30
|
PiAdapter,
|
|
@@ -83,6 +84,7 @@ def providers() -> None:
|
|
|
83
84
|
typer.echo("cline supported")
|
|
84
85
|
typer.echo("kilo supported")
|
|
85
86
|
typer.echo("kimi supported")
|
|
87
|
+
typer.echo("mistral-vibe supported")
|
|
86
88
|
typer.echo("opencode supported")
|
|
87
89
|
typer.echo("openhands supported")
|
|
88
90
|
typer.echo("pi supported")
|
|
@@ -282,6 +284,11 @@ def _collect_snapshots(
|
|
|
282
284
|
"cline": (ClineAdapter, ".cline/data", "sessions/sessions.db"),
|
|
283
285
|
"kilo": (KiloAdapter, ".local/share/kilo", "kilo.db"),
|
|
284
286
|
"kimi": (KimiAdapter, ".kimi", "sessions/*/*/wire.jsonl"),
|
|
287
|
+
"mistral-vibe": (
|
|
288
|
+
MistralVibeAdapter,
|
|
289
|
+
".vibe",
|
|
290
|
+
"logs/session/*/meta.json",
|
|
291
|
+
),
|
|
285
292
|
"opencode": (
|
|
286
293
|
OpenCodeAdapter,
|
|
287
294
|
".local/share/opencode",
|
|
@@ -37,6 +37,7 @@ def test_provider_status_is_explicit() -> None:
|
|
|
37
37
|
assert "cline supported" in result.stdout
|
|
38
38
|
assert "kilo supported" in result.stdout
|
|
39
39
|
assert "kimi supported" in result.stdout
|
|
40
|
+
assert "mistral-vibe supported" in result.stdout
|
|
40
41
|
assert "opencode supported" in result.stdout
|
|
41
42
|
assert "openhands supported" in result.stdout
|
|
42
43
|
assert "pi supported" in result.stdout
|
|
@@ -172,6 +173,43 @@ def test_collects_continue_cli(tmp_path: Path) -> None:
|
|
|
172
173
|
assert "1 written" in result.stdout
|
|
173
174
|
|
|
174
175
|
|
|
176
|
+
def test_collects_mistral_vibe_cli(tmp_path: Path) -> None:
|
|
177
|
+
home = tmp_path / "vibe"
|
|
178
|
+
session_dir = home / "logs" / "session" / "session_cli"
|
|
179
|
+
session_dir.mkdir(parents=True)
|
|
180
|
+
(session_dir / "meta.json").write_text(
|
|
181
|
+
json.dumps(
|
|
182
|
+
{
|
|
183
|
+
"session_id": "vibe-cli",
|
|
184
|
+
"start_time": "2026-08-27T10:00:00Z",
|
|
185
|
+
"stats": {"steps": 0},
|
|
186
|
+
}
|
|
187
|
+
),
|
|
188
|
+
encoding="utf-8",
|
|
189
|
+
)
|
|
190
|
+
(session_dir / "messages.jsonl").write_text(
|
|
191
|
+
json.dumps({"role": "user", "message_id": "message-cli"}) + "\n",
|
|
192
|
+
encoding="utf-8",
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
result = runner.invoke(
|
|
196
|
+
app,
|
|
197
|
+
[
|
|
198
|
+
"collect",
|
|
199
|
+
"--provider",
|
|
200
|
+
"mistral-vibe",
|
|
201
|
+
"--source",
|
|
202
|
+
f"desktop={home}",
|
|
203
|
+
"--database",
|
|
204
|
+
str(tmp_path / "mistral-vibe.sqlite"),
|
|
205
|
+
],
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
assert result.exit_code == 0, result.output
|
|
209
|
+
assert "Ingestion mistral-vibe" in result.stdout
|
|
210
|
+
assert "1 written" in result.stdout
|
|
211
|
+
|
|
212
|
+
|
|
175
213
|
def test_collects_openhands_cli(tmp_path: Path) -> None:
|
|
176
214
|
home = tmp_path / "openhands"
|
|
177
215
|
conversation = home / "conversations" / "openhands-cli"
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from cli_consumption.adapters.mistral_vibe import MistralVibeAdapter
|
|
9
|
+
from cli_consumption.dashboard import generate_dashboard
|
|
10
|
+
from cli_consumption.exporting import export_csv
|
|
11
|
+
from cli_consumption.storage import (
|
|
12
|
+
TABLES,
|
|
13
|
+
create_database_engine,
|
|
14
|
+
ingest_snapshot,
|
|
15
|
+
read_table,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
CANARY = "mistral vibe privacy canary secret"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def session(home: Path, *, extra: bool = False) -> Path:
|
|
22
|
+
session_dir = home / "logs" / "session" / "session_20260827_vibe0001"
|
|
23
|
+
session_dir.mkdir(parents=True)
|
|
24
|
+
metadata = {
|
|
25
|
+
"session_id": "vibe-session-1",
|
|
26
|
+
"start_time": "2026-08-27T10:00:00Z",
|
|
27
|
+
"end_time": "2026-08-27T10:05:00Z",
|
|
28
|
+
"git_commit": CANARY,
|
|
29
|
+
"git_branch": CANARY,
|
|
30
|
+
"username": CANARY,
|
|
31
|
+
"environment": {
|
|
32
|
+
"working_directory": f"/srv/work/acme/{CANARY}",
|
|
33
|
+
"secret": CANARY,
|
|
34
|
+
},
|
|
35
|
+
"title": CANARY,
|
|
36
|
+
"stats": {
|
|
37
|
+
"steps": 3,
|
|
38
|
+
"session_prompt_tokens": 100,
|
|
39
|
+
"session_completion_tokens": 20,
|
|
40
|
+
"session_cached_tokens": 40,
|
|
41
|
+
"input_price_per_million": 99,
|
|
42
|
+
},
|
|
43
|
+
"config": {
|
|
44
|
+
"active_model": "devstral-medium-latest",
|
|
45
|
+
"api_key": CANARY,
|
|
46
|
+
"system_prompt": CANARY,
|
|
47
|
+
},
|
|
48
|
+
"system_prompt": {"role": "system", "content": CANARY},
|
|
49
|
+
"tools_available": [{"function": {"name": CANARY}}],
|
|
50
|
+
}
|
|
51
|
+
(session_dir / "meta.json").write_text(json.dumps(metadata), encoding="utf-8")
|
|
52
|
+
messages = [
|
|
53
|
+
{
|
|
54
|
+
"role": "user",
|
|
55
|
+
"message_id": "user-1",
|
|
56
|
+
"content": CANARY,
|
|
57
|
+
"resources": [{"path": CANARY}],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"role": "assistant",
|
|
61
|
+
"message_id": "assistant-1",
|
|
62
|
+
"content": CANARY,
|
|
63
|
+
"reasoning_content": CANARY,
|
|
64
|
+
"tool_calls": [
|
|
65
|
+
{
|
|
66
|
+
"id": "call-1",
|
|
67
|
+
"function": {
|
|
68
|
+
"name": "bash",
|
|
69
|
+
"arguments": json.dumps({"secret": CANARY}),
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"role": "tool",
|
|
76
|
+
"tool_call_id": "call-1",
|
|
77
|
+
"name": "bash",
|
|
78
|
+
"content": CANARY,
|
|
79
|
+
"tool_result": {"output": CANARY},
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"role": "assistant",
|
|
83
|
+
"injected": True,
|
|
84
|
+
"context_boundary": "compaction",
|
|
85
|
+
"content": CANARY,
|
|
86
|
+
},
|
|
87
|
+
{"role": "user", "message_id": "user-2", "content": CANARY},
|
|
88
|
+
{
|
|
89
|
+
"role": "assistant",
|
|
90
|
+
"content": CANARY,
|
|
91
|
+
"tool_calls": [{"function": {"name": CANARY, "arguments": CANARY}}],
|
|
92
|
+
},
|
|
93
|
+
]
|
|
94
|
+
if extra:
|
|
95
|
+
messages.append(
|
|
96
|
+
{"role": "assistant", "message_id": "assistant-3", "content": CANARY}
|
|
97
|
+
)
|
|
98
|
+
(session_dir / "messages.jsonl").write_text(
|
|
99
|
+
"".join(json.dumps(message) + "\n" for message in messages),
|
|
100
|
+
encoding="utf-8",
|
|
101
|
+
)
|
|
102
|
+
return session_dir
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_collects_session_aggregate_usage_turns_tools_and_compactions(
|
|
106
|
+
tmp_path: Path,
|
|
107
|
+
) -> None:
|
|
108
|
+
home = tmp_path / "vibe"
|
|
109
|
+
session(home)
|
|
110
|
+
|
|
111
|
+
snapshot = MistralVibeAdapter().collect(
|
|
112
|
+
[("laptop", home)], [("acme", "/srv/work/acme")]
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
conversation = snapshot.conversations[0]
|
|
116
|
+
assert conversation["id"] == "mistral-vibe:vibe-session-1"
|
|
117
|
+
assert conversation["project"] == "acme"
|
|
118
|
+
assert conversation["project_source"] == "mapping"
|
|
119
|
+
assert conversation["models"] == ["devstral-medium-latest"]
|
|
120
|
+
assert conversation["iterations"] == 2
|
|
121
|
+
assert conversation["model_calls"] == 1
|
|
122
|
+
assert conversation["tool_calls"] == 1
|
|
123
|
+
assert conversation["compactions"] == 1
|
|
124
|
+
assert conversation["duration_seconds"] == 300
|
|
125
|
+
assert conversation["input_tokens"] == 100
|
|
126
|
+
assert conversation["uncached_input_tokens"] == 60
|
|
127
|
+
assert conversation["cached_input_tokens"] == 40
|
|
128
|
+
assert conversation["output_tokens"] == 20
|
|
129
|
+
assert conversation["visible_output_tokens"] == 20
|
|
130
|
+
assert conversation["total_tokens"] == 120
|
|
131
|
+
assert [turn["status"] for turn in snapshot.turns] == [
|
|
132
|
+
"completed",
|
|
133
|
+
"completed",
|
|
134
|
+
]
|
|
135
|
+
assert all(turn["model_calls"] == 0 for turn in snapshot.turns)
|
|
136
|
+
assert snapshot.model_calls[0]["turn_id"] is None
|
|
137
|
+
assert snapshot.tool_calls[0]["tool_name"] == "bash"
|
|
138
|
+
assert snapshot.compaction_events[0]["turn_id"] == snapshot.turns[0]["id"]
|
|
139
|
+
assert all(setting["model"] is None for setting in snapshot.turn_settings)
|
|
140
|
+
assert snapshot.malformed_records == 1
|
|
141
|
+
assert CANARY not in json.dumps(snapshot.to_dict())
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_deduplicates_and_tolerates_malformed_or_partial_sessions(
|
|
145
|
+
tmp_path: Path,
|
|
146
|
+
) -> None:
|
|
147
|
+
first, second = tmp_path / "first", tmp_path / "second"
|
|
148
|
+
session(first)
|
|
149
|
+
path = session(second, extra=True) / "messages.jsonl"
|
|
150
|
+
with path.open("a", encoding="utf-8") as handle:
|
|
151
|
+
handle.write("not-json\n[]\n")
|
|
152
|
+
|
|
153
|
+
snapshot = MistralVibeAdapter().collect([("desktop", first), ("laptop", second)])
|
|
154
|
+
|
|
155
|
+
assert snapshot.duplicate_conversations == 1
|
|
156
|
+
assert snapshot.malformed_records == 3
|
|
157
|
+
assert snapshot.conversations[0]["source_machine"] == "laptop"
|
|
158
|
+
assert CANARY not in json.dumps(snapshot.to_dict())
|
|
159
|
+
assert (
|
|
160
|
+
snapshot.to_dict()
|
|
161
|
+
== MistralVibeAdapter()
|
|
162
|
+
.collect([("desktop", first), ("laptop", second)])
|
|
163
|
+
.to_dict()
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
partial = tmp_path / "partial"
|
|
167
|
+
partial_dir = partial / "logs" / "session" / "session_partial"
|
|
168
|
+
partial_dir.mkdir(parents=True)
|
|
169
|
+
(partial_dir / "meta.json").write_text(
|
|
170
|
+
json.dumps(
|
|
171
|
+
{
|
|
172
|
+
"session_id": "partial-session",
|
|
173
|
+
"start_time": "2026-08-27T11:00:00Z",
|
|
174
|
+
}
|
|
175
|
+
),
|
|
176
|
+
encoding="utf-8",
|
|
177
|
+
)
|
|
178
|
+
partial_snapshot = MistralVibeAdapter().collect([("machine", partial)])
|
|
179
|
+
assert partial_snapshot.malformed_records == 1
|
|
180
|
+
assert partial_snapshot.conversations[0]["model_calls"] == 0
|
|
181
|
+
|
|
182
|
+
with pytest.raises(ValueError, match="Missing Mistral Vibe session log directory"):
|
|
183
|
+
MistralVibeAdapter().collect([("machine", tmp_path / "missing")])
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def test_privacy_canary_is_absent_and_ingestion_is_idempotent(tmp_path: Path) -> None:
|
|
187
|
+
home = tmp_path / "vibe"
|
|
188
|
+
session(home)
|
|
189
|
+
snapshot = MistralVibeAdapter().collect([("machine", home)])
|
|
190
|
+
assert CANARY not in json.dumps(snapshot.to_dict())
|
|
191
|
+
|
|
192
|
+
engine = create_database_engine(tmp_path / "usage.sqlite")
|
|
193
|
+
try:
|
|
194
|
+
first = ingest_snapshot(engine, snapshot)
|
|
195
|
+
second = ingest_snapshot(engine, snapshot)
|
|
196
|
+
assert (first.written, first.skipped) == (1, 0)
|
|
197
|
+
assert (second.written, second.skipped) == (0, 1)
|
|
198
|
+
rows = {name: read_table(engine, name) for name in TABLES}
|
|
199
|
+
assert CANARY not in json.dumps(rows)
|
|
200
|
+
output = tmp_path / "reports"
|
|
201
|
+
paths = export_csv(engine, output)
|
|
202
|
+
dashboard = output / "dashboard.html"
|
|
203
|
+
generate_dashboard(engine, dashboard)
|
|
204
|
+
assert all(CANARY not in path.read_text() for path in paths)
|
|
205
|
+
assert CANARY not in dashboard.read_text()
|
|
206
|
+
finally:
|
|
207
|
+
engine.dispose()
|
|
File without changes
|
{cli_consumption-0.1.0 → cli_consumption-0.1.1}/.agents/skills/add-cli-adapter/agents/openai.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cli_consumption-0.1.0 → cli_consumption-0.1.1}/.agents/skills/evolve-storage-schema/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cli_consumption-0.1.0 → cli_consumption-0.1.1}/.agents/skills/yeet-github/agents/openai.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cli_consumption-0.1.0 → cli_consumption-0.1.1}/src/cli_consumption/adapters/continue_cli.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|