tktop-tui 0.3.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.
- tktop_tui-0.3.0/LICENSE +21 -0
- tktop_tui-0.3.0/PKG-INFO +70 -0
- tktop_tui-0.3.0/README.md +30 -0
- tktop_tui-0.3.0/pyproject.toml +35 -0
- tktop_tui-0.3.0/setup.cfg +4 -0
- tktop_tui-0.3.0/tktop_tui/__init__.py +72 -0
- tktop_tui-0.3.0/tktop_tui/tui/LICENSE +21 -0
- tktop_tui-0.3.0/tktop_tui/tui/bin/tktop +16 -0
- tktop_tui-0.3.0/tktop_tui/tui/package.json +47 -0
- tktop_tui-0.3.0/tktop_tui/tui/src/adapters/claude-code.ts +143 -0
- tktop_tui-0.3.0/tktop_tui/tui/src/adapters/codex.ts +131 -0
- tktop_tui-0.3.0/tktop_tui/tui/src/adapters/generic-jsonl.ts +170 -0
- tktop_tui-0.3.0/tktop_tui/tui/src/adapters/jsonl-tail.ts +114 -0
- tktop_tui-0.3.0/tktop_tui/tui/src/adapters/opencode.ts +124 -0
- tktop_tui-0.3.0/tktop_tui/tui/src/adapters/registry.ts +26 -0
- tktop_tui-0.3.0/tktop_tui/tui/src/dump.ts +78 -0
- tktop_tui-0.3.0/tktop_tui/tui/src/format.ts +86 -0
- tktop_tui-0.3.0/tktop_tui/tui/src/index.ts +80 -0
- tktop_tui-0.3.0/tktop_tui/tui/src/store.ts +327 -0
- tktop_tui-0.3.0/tktop_tui/tui/src/types.ts +67 -0
- tktop_tui-0.3.0/tktop_tui/tui/src/ui.ts +914 -0
- tktop_tui-0.3.0/tktop_tui.egg-info/PKG-INFO +70 -0
- tktop_tui-0.3.0/tktop_tui.egg-info/SOURCES.txt +24 -0
- tktop_tui-0.3.0/tktop_tui.egg-info/dependency_links.txt +1 -0
- tktop_tui-0.3.0/tktop_tui.egg-info/entry_points.txt +2 -0
- tktop_tui-0.3.0/tktop_tui.egg-info/top_level.txt +1 -0
tktop_tui-0.3.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AlexKay28
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
tktop_tui-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tktop-tui
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Live htop-style TUI dashboard for AI token consumption across coding agents (OpenCode, Claude Code, Codex, generic JSONL). Python wrapper that launches the tktop TUI via Bun.
|
|
5
|
+
Author: AlexKay28
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 AlexKay28
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/AlexKay28/tktop
|
|
29
|
+
Project-URL: Repository, https://github.com/AlexKay28/tktop
|
|
30
|
+
Keywords: cli,tui,ai,llm,tokens,monitoring,htop
|
|
31
|
+
Classifier: Environment :: Console
|
|
32
|
+
Classifier: Intended Audience :: Developers
|
|
33
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
34
|
+
Classifier: Operating System :: OS Independent
|
|
35
|
+
Classifier: Programming Language :: Python :: 3
|
|
36
|
+
Requires-Python: >=3.10
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
License-File: LICENSE
|
|
39
|
+
Dynamic: license-file
|
|
40
|
+
|
|
41
|
+
# tktop-tui
|
|
42
|
+
|
|
43
|
+
Python launcher for **tktop** — the live htop-style TUI dashboard for AI token
|
|
44
|
+
consumption across coding agents (OpenCode, Claude Code, Codex, generic JSONL).
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
pip install tktop-tui
|
|
48
|
+
tktop
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The wheel bundles the TypeScript TUI. On first run it stages the app into
|
|
52
|
+
`~/.local/share/tktop/<version>/`, runs `bun install` there (so read-only
|
|
53
|
+
site-packages are never touched), and execs the TUI under
|
|
54
|
+
[Bun](https://bun.sh) — install it first if you don't have it:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
curl -fsSL https://bun.sh/install | bash
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Requirements:
|
|
61
|
+
|
|
62
|
+
- Python ≥ 3.10
|
|
63
|
+
- Bun ≥ 1.1 (for the TUI runtime)
|
|
64
|
+
- network on the very first launch only (to fetch `@opentui/core`)
|
|
65
|
+
|
|
66
|
+
Features: per-session token/burn-rate table with mouse-clickable column
|
|
67
|
+
sorting, live log-scale scatterplot (colored markers + trails), grouping,
|
|
68
|
+
filtering, sparklines — all read-only, tktop never touches your harnesses.
|
|
69
|
+
|
|
70
|
+
GitHub: <https://github.com/AlexKay28/tktop>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# tktop-tui
|
|
2
|
+
|
|
3
|
+
Python launcher for **tktop** — the live htop-style TUI dashboard for AI token
|
|
4
|
+
consumption across coding agents (OpenCode, Claude Code, Codex, generic JSONL).
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
pip install tktop-tui
|
|
8
|
+
tktop
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The wheel bundles the TypeScript TUI. On first run it stages the app into
|
|
12
|
+
`~/.local/share/tktop/<version>/`, runs `bun install` there (so read-only
|
|
13
|
+
site-packages are never touched), and execs the TUI under
|
|
14
|
+
[Bun](https://bun.sh) — install it first if you don't have it:
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
curl -fsSL https://bun.sh/install | bash
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Requirements:
|
|
21
|
+
|
|
22
|
+
- Python ≥ 3.10
|
|
23
|
+
- Bun ≥ 1.1 (for the TUI runtime)
|
|
24
|
+
- network on the very first launch only (to fetch `@opentui/core`)
|
|
25
|
+
|
|
26
|
+
Features: per-session token/burn-rate table with mouse-clickable column
|
|
27
|
+
sorting, live log-scale scatterplot (colored markers + trails), grouping,
|
|
28
|
+
filtering, sparklines — all read-only, tktop never touches your harnesses.
|
|
29
|
+
|
|
30
|
+
GitHub: <https://github.com/AlexKay28/tktop>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tktop-tui"
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "Live htop-style TUI dashboard for AI token consumption across coding agents (OpenCode, Claude Code, Codex, generic JSONL). Python wrapper that launches the tktop TUI via Bun."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { file = "LICENSE" }
|
|
12
|
+
authors = [{ name = "AlexKay28" }]
|
|
13
|
+
keywords = ["cli", "tui", "ai", "llm", "tokens", "monitoring", "htop"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Environment :: Console",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
]
|
|
21
|
+
dependencies = []
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://github.com/AlexKay28/tktop"
|
|
25
|
+
Repository = "https://github.com/AlexKay28/tktop"
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
tktop = "tktop_tui:main"
|
|
29
|
+
|
|
30
|
+
[tool.setuptools]
|
|
31
|
+
packages = ["tktop_tui"]
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.package-data]
|
|
34
|
+
# the bundled TUI (staged here by python/sync.sh at build time)
|
|
35
|
+
tktop_tui = ["tui/**/*"]
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""tktop-tui: Python launcher for the tktop TUI.
|
|
2
|
+
|
|
3
|
+
`pip install tktop-tui` gives you the `tktop` command. The wheel bundles the
|
|
4
|
+
TypeScript TUI (src/ + bin/); on first run it is staged into a user cache dir
|
|
5
|
+
(~/.local/share/tktop/<version>) where `bun install` fetches its JavaScript
|
|
6
|
+
dependencies, then the TUI is exec'd under Bun. Read-only site-packages are
|
|
7
|
+
never touched.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import shutil
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
from importlib.resources import as_file, files
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
VERSION = "0.3.0"
|
|
19
|
+
HOMEPAGE = "https://github.com/AlexKay28/tktop"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def find_bun() -> str | None:
|
|
23
|
+
"""Locate bun on PATH or in its default install location."""
|
|
24
|
+
found = shutil.which("bun")
|
|
25
|
+
if found:
|
|
26
|
+
return found
|
|
27
|
+
default = Path.home() / ".bun" / "bin" / "bun"
|
|
28
|
+
return str(default) if default.is_file() and os.access(default, os.X_OK) else None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def stage_tui() -> Path:
|
|
32
|
+
"""Copy the bundled TUI into a per-version cache dir (once per version)."""
|
|
33
|
+
base = Path.home() / ".local" / "share" / "tktop" / VERSION
|
|
34
|
+
marker = base / ".installed"
|
|
35
|
+
if marker.is_file():
|
|
36
|
+
return base
|
|
37
|
+
if base.exists():
|
|
38
|
+
shutil.rmtree(base)
|
|
39
|
+
base.parent.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
src = files("tktop_tui").joinpath("tui")
|
|
41
|
+
with as_file(src) as src_path:
|
|
42
|
+
shutil.copytree(src_path, base)
|
|
43
|
+
marker.write_text("staged\n")
|
|
44
|
+
return base
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def main() -> None:
|
|
48
|
+
bun = find_bun()
|
|
49
|
+
if not bun:
|
|
50
|
+
print("tktop: bun is required but not installed.", file=sys.stderr)
|
|
51
|
+
print(" install it with: curl -fsSL https://bun.sh/install | bash", file=sys.stderr)
|
|
52
|
+
print(f" project: {HOMEPAGE}", file=sys.stderr)
|
|
53
|
+
raise SystemExit(1)
|
|
54
|
+
|
|
55
|
+
tui = stage_tui()
|
|
56
|
+
|
|
57
|
+
# fetch JS dependencies once per staged version (needs network on first run)
|
|
58
|
+
if not (tui / "node_modules").is_dir():
|
|
59
|
+
pkg = json.loads((tui / "package.json").read_text())
|
|
60
|
+
pkg.pop("devDependencies", None) # @types/bun is not needed at runtime
|
|
61
|
+
pkg.get("scripts", {}).pop("prepublishOnly", None)
|
|
62
|
+
(tui / "package.json").write_text(json.dumps(pkg))
|
|
63
|
+
r = subprocess.run([bun, "install", "--production"], cwd=tui)
|
|
64
|
+
if r.returncode != 0:
|
|
65
|
+
print("tktop: bun install failed (network reachable?).", file=sys.stderr)
|
|
66
|
+
raise SystemExit(1)
|
|
67
|
+
|
|
68
|
+
os.execv(bun, [bun, "run", str(tui / "src" / "index.ts"), *sys.argv[1:]])
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
if __name__ == "__main__":
|
|
72
|
+
main()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AlexKay28
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tktop launcher — runs the TUI via Bun.
|
|
3
|
+
# Resolves the repo root from this script's own location, so the shim works
|
|
4
|
+
# from ~/.local/bin (symlinked anywhere) and survives repo moves.
|
|
5
|
+
set -eu
|
|
6
|
+
|
|
7
|
+
REPO_ROOT="$(cd "$(dirname "$(readlink -f "$0")")/.." && pwd)"
|
|
8
|
+
export PATH="$HOME/.bun/bin:$PATH"
|
|
9
|
+
|
|
10
|
+
if ! command -v bun >/dev/null 2>&1; then
|
|
11
|
+
echo "tktop: bun is required but not installed." >&2
|
|
12
|
+
echo " install it with: curl -fsSL https://bun.sh/install | bash" >&2
|
|
13
|
+
exit 1
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
exec bun run "$REPO_ROOT/src/index.ts" "$@"
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tktop",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Live htop-style TUI dashboard for AI token consumption across harnesses (OpenCode, Claude Code, Codex, generic JSONL)",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"cli",
|
|
8
|
+
"tui",
|
|
9
|
+
"tokens",
|
|
10
|
+
"llm",
|
|
11
|
+
"ai",
|
|
12
|
+
"monitoring",
|
|
13
|
+
"opentui",
|
|
14
|
+
"htop"
|
|
15
|
+
],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+ssh://git@github.com/AlexKay28/tktop.git"
|
|
20
|
+
},
|
|
21
|
+
"bin": {
|
|
22
|
+
"tktop": "bin/tktop"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"bin/",
|
|
26
|
+
"src/",
|
|
27
|
+
"docs/",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE"
|
|
30
|
+
],
|
|
31
|
+
"engines": {
|
|
32
|
+
"bun": ">=1.1.0"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"start": "bun run src/index.ts",
|
|
36
|
+
"tktop": "bun run src/index.ts",
|
|
37
|
+
"dump": "bun run src/dump.ts",
|
|
38
|
+
"typecheck": "bun check",
|
|
39
|
+
"prepublishOnly": "bun run typecheck"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@opentui/core": "^0.8.1"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/bun": "latest"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code adapter — additive parsing of JSONL files under ~/.claude/projects.
|
|
3
|
+
* Auto-activates when ~/.claude exists. See docs/ADR-003 for caveats.
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync } from "node:fs"
|
|
6
|
+
import { homedir } from "node:os"
|
|
7
|
+
import type { Adapter, SessionInfo, TokenCounts } from "../types"
|
|
8
|
+
import { JsonlTail } from "./jsonl-tail"
|
|
9
|
+
|
|
10
|
+
interface Acc {
|
|
11
|
+
id: string
|
|
12
|
+
title: string
|
|
13
|
+
directory: string
|
|
14
|
+
model: string
|
|
15
|
+
tokens: TokenCounts
|
|
16
|
+
cost: number
|
|
17
|
+
timeCreated: number
|
|
18
|
+
timeUpdated: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function newAcc(id: string): Acc {
|
|
22
|
+
return {
|
|
23
|
+
id,
|
|
24
|
+
title: "",
|
|
25
|
+
directory: "",
|
|
26
|
+
model: "-",
|
|
27
|
+
tokens: { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0 },
|
|
28
|
+
cost: 0,
|
|
29
|
+
timeCreated: 0,
|
|
30
|
+
timeUpdated: 0,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class ClaudeCodeAdapter implements Adapter {
|
|
35
|
+
readonly harness = "claude-code"
|
|
36
|
+
lastError: string | null = null
|
|
37
|
+
|
|
38
|
+
private root: string
|
|
39
|
+
private tail: JsonlTail
|
|
40
|
+
private accs = new Map<string, Acc>()
|
|
41
|
+
private started = false
|
|
42
|
+
|
|
43
|
+
constructor(root?: string) {
|
|
44
|
+
this.root = root ?? `${homedir()}/.claude/projects`
|
|
45
|
+
this.tail = new JsonlTail(`${this.root}/**/*.jsonl`)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
available(): boolean {
|
|
49
|
+
return existsSync(this.root)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private sid(obj: any, path: string): string {
|
|
53
|
+
if (obj.sessionId) return String(obj.sessionId)
|
|
54
|
+
const m = path.match(/([0-9a-f-]{36})(?:\.jsonl)?$/i)
|
|
55
|
+
if (m?.[1]) return m[1]
|
|
56
|
+
return path
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
poll(): SessionInfo[] {
|
|
60
|
+
try {
|
|
61
|
+
if (!this.started) {
|
|
62
|
+
this.tail.refreshFiles()
|
|
63
|
+
this.started = true
|
|
64
|
+
} else if (this.accs.size === 0 || Math.random() < 0.05) {
|
|
65
|
+
// occasionally re-scan for new project files
|
|
66
|
+
this.tail.refreshFiles()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
this.tail.read((obj, path) => this.onLine(obj, path))
|
|
70
|
+
|
|
71
|
+
const out: SessionInfo[] = []
|
|
72
|
+
for (const [sid, a] of this.accs) {
|
|
73
|
+
out.push({
|
|
74
|
+
key: `${this.harness}:${sid}`,
|
|
75
|
+
harness: this.harness,
|
|
76
|
+
id: sid,
|
|
77
|
+
title: a.title || (a.directory ? a.directory.split("/").filter(Boolean).pop() || sid.slice(0, 12) : sid.slice(0, 12)),
|
|
78
|
+
directory: a.directory,
|
|
79
|
+
model: a.model || "-",
|
|
80
|
+
archived: false,
|
|
81
|
+
tokens: a.tokens,
|
|
82
|
+
cost: a.cost,
|
|
83
|
+
timeCreated: a.timeCreated || a.timeUpdated,
|
|
84
|
+
timeUpdated: a.timeUpdated,
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
this.lastError = null
|
|
88
|
+
return out
|
|
89
|
+
} catch (e) {
|
|
90
|
+
this.lastError = (e as Error).message
|
|
91
|
+
return []
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private onLine(obj: any, path: string): void {
|
|
96
|
+
if (!obj || typeof obj !== "object") return
|
|
97
|
+
const sid = this.sid(obj, path)
|
|
98
|
+
let acc = this.accs.get(sid)
|
|
99
|
+
if (!acc) {
|
|
100
|
+
acc = newAcc(sid)
|
|
101
|
+
this.accs.set(sid, acc)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const ts = obj.timestamp ? (typeof obj.timestamp === "number" ? obj.timestamp : Date.parse(obj.timestamp)) : 0
|
|
105
|
+
if (ts > acc.timeUpdated) acc.timeUpdated = ts
|
|
106
|
+
if (acc.timeCreated === 0 || (ts > 0 && ts < acc.timeCreated)) acc.timeCreated = ts
|
|
107
|
+
if (obj.cwd) acc.directory = String(obj.cwd)
|
|
108
|
+
|
|
109
|
+
if (obj.type === "summary" && obj.summary) {
|
|
110
|
+
if (!acc.title) acc.title = String(obj.summary)
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const msg = obj.message
|
|
115
|
+
if (!msg || typeof msg !== "object") return
|
|
116
|
+
|
|
117
|
+
if (obj.type === "user" && !acc.title) {
|
|
118
|
+
const c = msg.content
|
|
119
|
+
const text =
|
|
120
|
+
typeof c === "string"
|
|
121
|
+
? c
|
|
122
|
+
: Array.isArray(c)
|
|
123
|
+
? (c.find((p: any) => p?.type === "text")?.text ?? "")
|
|
124
|
+
: ""
|
|
125
|
+
if (text) acc.title = text.replace(/\s+/g, " ").slice(0, 80)
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (obj.type === "assistant") {
|
|
130
|
+
if (msg.model) acc.model = String(msg.model)
|
|
131
|
+
const u = msg.usage
|
|
132
|
+
if (u && typeof u === "object") {
|
|
133
|
+
acc.tokens.input += u.input_tokens ?? 0
|
|
134
|
+
acc.tokens.output += u.output_tokens ?? 0
|
|
135
|
+
acc.tokens.cacheRead += u.cache_read_input_tokens ?? 0
|
|
136
|
+
acc.tokens.cacheWrite += u.cache_creation_input_tokens ?? 0
|
|
137
|
+
// some history formats fold reasoning into output; record if present
|
|
138
|
+
acc.tokens.reasoning += u.reasoning_tokens ?? 0
|
|
139
|
+
}
|
|
140
|
+
if (typeof obj.costUSD === "number") acc.cost += obj.costUSD
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex CLI adapter — cumulative token_count events in
|
|
3
|
+
* ~/.codex/sessions JSONL files ("latest" semantics). See docs/ADR-003.
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync } from "node:fs"
|
|
6
|
+
import { homedir } from "node:os"
|
|
7
|
+
import type { Adapter, SessionInfo, TokenCounts } from "../types"
|
|
8
|
+
import { JsonlTail } from "./jsonl-tail"
|
|
9
|
+
|
|
10
|
+
interface Acc {
|
|
11
|
+
id: string
|
|
12
|
+
title: string
|
|
13
|
+
directory: string
|
|
14
|
+
model: string
|
|
15
|
+
tokens: TokenCounts
|
|
16
|
+
cost: number
|
|
17
|
+
timeCreated: number
|
|
18
|
+
timeUpdated: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function newAcc(id: string): Acc {
|
|
22
|
+
return {
|
|
23
|
+
id,
|
|
24
|
+
title: "",
|
|
25
|
+
directory: "",
|
|
26
|
+
model: "-",
|
|
27
|
+
tokens: { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0 },
|
|
28
|
+
cost: 0,
|
|
29
|
+
timeCreated: 0,
|
|
30
|
+
timeUpdated: 0,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class CodexAdapter implements Adapter {
|
|
35
|
+
readonly harness = "codex"
|
|
36
|
+
lastError: string | null = null
|
|
37
|
+
|
|
38
|
+
private root: string
|
|
39
|
+
private tail: JsonlTail
|
|
40
|
+
private accs = new Map<string, Acc>()
|
|
41
|
+
private started = false
|
|
42
|
+
|
|
43
|
+
constructor(root?: string) {
|
|
44
|
+
this.root = root ?? `${homedir()}/.codex/sessions`
|
|
45
|
+
this.tail = new JsonlTail(`${this.root}/**/*.jsonl`)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
available(): boolean {
|
|
49
|
+
return existsSync(this.root)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
poll(): SessionInfo[] {
|
|
53
|
+
try {
|
|
54
|
+
if (!this.started) {
|
|
55
|
+
this.tail.refreshFiles()
|
|
56
|
+
this.started = true
|
|
57
|
+
} else if (Math.random() < 0.05) {
|
|
58
|
+
this.tail.refreshFiles()
|
|
59
|
+
}
|
|
60
|
+
this.tail.read((obj, path) => this.onLine(obj, path))
|
|
61
|
+
|
|
62
|
+
const out: SessionInfo[] = []
|
|
63
|
+
for (const [sid, a] of this.accs) {
|
|
64
|
+
out.push({
|
|
65
|
+
key: `${this.harness}:${sid}`,
|
|
66
|
+
harness: this.harness,
|
|
67
|
+
id: sid,
|
|
68
|
+
title: a.title || sid.slice(0, 12),
|
|
69
|
+
directory: a.directory,
|
|
70
|
+
model: a.model || "-",
|
|
71
|
+
archived: false,
|
|
72
|
+
tokens: { ...a.tokens },
|
|
73
|
+
cost: a.cost,
|
|
74
|
+
timeCreated: a.timeCreated || a.timeUpdated,
|
|
75
|
+
timeUpdated: a.timeUpdated,
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
this.lastError = null
|
|
79
|
+
return out
|
|
80
|
+
} catch (e) {
|
|
81
|
+
this.lastError = (e as Error).message
|
|
82
|
+
return []
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private onLine(obj: any, path: string): void {
|
|
87
|
+
if (!obj || typeof obj !== "object") return
|
|
88
|
+
const payload = obj.payload
|
|
89
|
+
|
|
90
|
+
// session id: prefer payload.session_meta.id, else file stem
|
|
91
|
+
let sid: string | null = null
|
|
92
|
+
if (payload?.type === "session_meta" && payload.id) sid = String(payload.id)
|
|
93
|
+
if (!sid) {
|
|
94
|
+
const m = path.match(/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i)
|
|
95
|
+
if (m?.[1]) sid = m[1]
|
|
96
|
+
}
|
|
97
|
+
if (!sid) return
|
|
98
|
+
|
|
99
|
+
let acc = this.accs.get(sid)
|
|
100
|
+
if (!acc) {
|
|
101
|
+
acc = newAcc(sid)
|
|
102
|
+
this.accs.set(sid, acc)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const ts = obj.timestamp ? (typeof obj.timestamp === "number" ? obj.timestamp : Date.parse(obj.timestamp)) : 0
|
|
106
|
+
if (ts > acc.timeUpdated) acc.timeUpdated = ts
|
|
107
|
+
if (acc.timeCreated === 0 || (ts > 0 && ts < acc.timeCreated)) acc.timeCreated = ts
|
|
108
|
+
|
|
109
|
+
const p = payload ?? {}
|
|
110
|
+
if (p.cwd) acc.directory = String(p.cwd)
|
|
111
|
+
if (p.model) acc.model = String(p.model)
|
|
112
|
+
|
|
113
|
+
if (p.type === "token_count") {
|
|
114
|
+
const info = p.info ?? p
|
|
115
|
+
// cumulative totals: overwrite, never sum
|
|
116
|
+
const total = info.total_token_usage ?? info.totalTokenUsage ?? info.usage
|
|
117
|
+
if (total && typeof total === "object") {
|
|
118
|
+
const pick = (...names: string[]): number => {
|
|
119
|
+
for (const n of names) if (typeof total[n] === "number") return total[n]
|
|
120
|
+
return 0
|
|
121
|
+
}
|
|
122
|
+
acc.tokens.input = pick("input_tokens", "inputTokens", "input")
|
|
123
|
+
acc.tokens.cacheRead = pick("cached_input_tokens", "cachedInputTokens", "cache_read", "cached")
|
|
124
|
+
acc.tokens.output = pick("output_tokens", "outputTokens", "output")
|
|
125
|
+
acc.tokens.reasoning = pick("reasoning_output_tokens", "reasoning_tokens", "reasoningOutputTokens", "reasoning")
|
|
126
|
+
}
|
|
127
|
+
} else if (p.type === "turn_context" && p.model) {
|
|
128
|
+
acc.model = String(p.model)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|