octarin-cli 0.3.2 → 0.3.3
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.
package/assets/backfill.py
CHANGED
|
@@ -290,6 +290,14 @@ def parse_claude_transcript( # noqa: PLR0915 - top-down jsonl parser; splitting
|
|
|
290
290
|
:_OUTPUT_CAP
|
|
291
291
|
],
|
|
292
292
|
"status": "ok",
|
|
293
|
+
# subagent turns bill against the session model;
|
|
294
|
+
# carry it so per-model analytics can attribute
|
|
295
|
+
# their tokens
|
|
296
|
+
**(
|
|
297
|
+
{"model": model}
|
|
298
|
+
if stype == "agent" and model
|
|
299
|
+
else {}
|
|
300
|
+
),
|
|
293
301
|
}
|
|
294
302
|
)
|
|
295
303
|
prev_ts = ts
|
|
@@ -27,7 +27,6 @@ import os
|
|
|
27
27
|
import ssl
|
|
28
28
|
import subprocess
|
|
29
29
|
import sys
|
|
30
|
-
import time
|
|
31
30
|
import urllib.request
|
|
32
31
|
import uuid
|
|
33
32
|
from datetime import datetime, timezone
|
|
@@ -538,19 +537,42 @@ def post_event(event: dict) -> bool:
|
|
|
538
537
|
return False
|
|
539
538
|
|
|
540
539
|
|
|
541
|
-
def
|
|
540
|
+
def _state_file(key: str) -> Path:
|
|
541
|
+
"""Per-session state file.
|
|
542
|
+
|
|
543
|
+
One file per session key, NOT one shared JSON: with the shared file two
|
|
544
|
+
concurrent sessions raced load→save and the last writer clobbered the other
|
|
545
|
+
session's offset back, so its next fire re-read (and re-sent) transcript
|
|
546
|
+
chunks it had already shipped.
|
|
547
|
+
"""
|
|
548
|
+
return STATE_DIR / f"claude_code_state.{key[:32]}.json"
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def load_state(key: str) -> dict:
|
|
552
|
+
f = _state_file(key)
|
|
542
553
|
try:
|
|
543
|
-
|
|
554
|
+
if f.exists():
|
|
555
|
+
return {key: json.loads(f.read_text(encoding="utf-8"))}
|
|
544
556
|
except Exception:
|
|
545
557
|
return {}
|
|
558
|
+
# One-time migration: pick this session's entry out of the legacy shared file.
|
|
559
|
+
try:
|
|
560
|
+
if STATE_FILE.exists():
|
|
561
|
+
legacy = json.loads(STATE_FILE.read_text(encoding="utf-8"))
|
|
562
|
+
if key in legacy:
|
|
563
|
+
return {key: legacy[key]}
|
|
564
|
+
except Exception:
|
|
565
|
+
pass
|
|
566
|
+
return {}
|
|
546
567
|
|
|
547
568
|
|
|
548
|
-
def save_state(state: dict) -> None:
|
|
569
|
+
def save_state(state: dict, key: str) -> None:
|
|
549
570
|
try:
|
|
550
571
|
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
|
551
|
-
|
|
552
|
-
tmp
|
|
553
|
-
|
|
572
|
+
f = _state_file(key)
|
|
573
|
+
tmp = f.with_suffix(".tmp")
|
|
574
|
+
tmp.write_text(json.dumps(state.get(key) or {}, sort_keys=True), encoding="utf-8")
|
|
575
|
+
os.replace(tmp, f)
|
|
554
576
|
except Exception:
|
|
555
577
|
pass
|
|
556
578
|
|
|
@@ -561,10 +583,10 @@ def build_event(payload: dict) -> dict | None:
|
|
|
561
583
|
if not session_id or path is None:
|
|
562
584
|
return None
|
|
563
585
|
|
|
564
|
-
state = load_state()
|
|
565
586
|
key = hashlib.sha256(f"{session_id}::{path}".encode()).hexdigest()
|
|
587
|
+
state = load_state(key)
|
|
566
588
|
entries = read_new_entries(path, state, key)
|
|
567
|
-
save_state(state)
|
|
589
|
+
save_state(state, key)
|
|
568
590
|
if not entries:
|
|
569
591
|
return None
|
|
570
592
|
|
|
@@ -573,7 +595,12 @@ def build_event(payload: dict) -> dict | None:
|
|
|
573
595
|
return None
|
|
574
596
|
|
|
575
597
|
repo = Path(cwd).name if cwd else None
|
|
576
|
-
|
|
598
|
+
# Day-stable trace id (like the Cursor hook): every fire for a session
|
|
599
|
+
# upserts the SAME trace, so a re-sent chunk (offset race, retry) replaces
|
|
600
|
+
# its span rows in the ReplacingMergeTree instead of minting a new trace
|
|
601
|
+
# per second and double-counting. The date suffix splits multi-day sessions.
|
|
602
|
+
day = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
|
603
|
+
src_trace = f"{session_id}:{day}"
|
|
577
604
|
trace_id = str(uuid.uuid5(_TRACE_NAMESPACE, f"{SOURCE}:{src_trace}"))
|
|
578
605
|
times = [s["start_time"] for s in spans]
|
|
579
606
|
|
|
@@ -128,6 +128,9 @@ function toolSpanId(input) {
|
|
|
128
128
|
function fromPostToolUse(input) {
|
|
129
129
|
const tool = input.tool_name || "tool";
|
|
130
130
|
const span = baseSpan(toolSpanId(input), tool, "tool");
|
|
131
|
+
// Cursor never exposes token usage, so the backend estimates tool spans from
|
|
132
|
+
// their I/O — stamp the session model so the estimate prices at its real rate.
|
|
133
|
+
span.model = input.model || null;
|
|
131
134
|
span.input = truncate(asText(input.tool_input));
|
|
132
135
|
span.output = truncate(asText(input.tool_output));
|
|
133
136
|
span.attributes = {
|
|
@@ -144,6 +147,7 @@ function fromPostToolUse(input) {
|
|
|
144
147
|
function fromPostToolUseFailure(input) {
|
|
145
148
|
const tool = input.tool_name || "tool";
|
|
146
149
|
const span = baseSpan(toolSpanId(input), tool, "tool");
|
|
150
|
+
span.model = input.model || null;
|
|
147
151
|
span.status = "error";
|
|
148
152
|
span.error_message = input.error_message || input.failure_type || "tool failed";
|
|
149
153
|
span.input = truncate(asText(input.tool_input));
|
|
@@ -128,6 +128,9 @@ function toolSpanId(input) {
|
|
|
128
128
|
function fromPostToolUse(input) {
|
|
129
129
|
const tool = input.tool_name || "tool";
|
|
130
130
|
const span = baseSpan(toolSpanId(input), tool, "tool");
|
|
131
|
+
// Cursor never exposes token usage, so the backend estimates tool spans from
|
|
132
|
+
// their I/O — stamp the session model so the estimate prices at its real rate.
|
|
133
|
+
span.model = input.model || null;
|
|
131
134
|
span.input = truncate(asText(input.tool_input));
|
|
132
135
|
span.output = truncate(asText(input.tool_output));
|
|
133
136
|
span.attributes = {
|
|
@@ -144,6 +147,7 @@ function fromPostToolUse(input) {
|
|
|
144
147
|
function fromPostToolUseFailure(input) {
|
|
145
148
|
const tool = input.tool_name || "tool";
|
|
146
149
|
const span = baseSpan(toolSpanId(input), tool, "tool");
|
|
150
|
+
span.model = input.model || null;
|
|
147
151
|
span.status = "error";
|
|
148
152
|
span.error_message = input.error_message || input.failure_type || "tool failed";
|
|
149
153
|
span.input = truncate(asText(input.tool_input));
|
package/dist/index.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "octarin-cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Octarin's per-user CLI: install AI-coding capture (`octarin init` / `init-repo`) and authorize a machine (`octarin login`). Streams your Claude Code / Cursor / Codex usage to your Octarin workspace.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"octarin",
|