@kauakdev/kauak 0.0.0-stage → 0.1.1

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.
@@ -0,0 +1,344 @@
1
+ # Context meters for a remote floor: the part of context.ts that has
2
+ # to run where the agents' transcripts are. The bridge starts it over SSH
3
+ # (`python3 -c ...`, so nothing is installed on the remote machine) and keeps
4
+ # it running; it keeps its own caches between requests.
5
+ #
6
+ # Protocol, one JSON object per line:
7
+ # in: {"id": 1, "panes": [{"pane_id", "agent", "session": {"kind", "value"} | null, "pids": [...], "cwd"}]}
8
+ # out: {"id": 1, "result": {pane_id: {"usage": {"used", "max"} | null, "pid": int | null}}}
9
+ # `session` is Herdr's pane.agent_session; `pids` are the pane's foreground
10
+ # processes (pane.process_info), for a pane without a session. `pid` in the
11
+ # answer is the agent's: Claude Code's, or the first Codex process.
12
+ #
13
+ # Keep the reading rules in step with context.ts.
14
+
15
+ import datetime
16
+ import json
17
+ import os
18
+ import re
19
+ import subprocess
20
+ import sys
21
+ import time
22
+
23
+ CLAUDE_DIR = os.environ.get("CLAUDE_CONFIG_DIR") or os.path.expanduser("~/.claude")
24
+ CODEX_DIR = os.environ.get("CODEX_HOME") or os.path.expanduser("~/.codex")
25
+ MISS_S = 15
26
+ FIRST_READ_MAX = 32 * 1024 * 1024
27
+ CLAUDE_WINDOW = 1_000_000
28
+ CLAUDE_WINDOW_SMALL = 200_000
29
+ CLAUDE_SMALL_MODEL = re.compile(r"^claude-(?:3|haiku)|^claude-(?:opus|sonnet)-4(?:-[015])?(?:-\d{8})?$")
30
+ SESSION_ID = re.compile(r"^[A-Za-z0-9-]{1,64}$")
31
+
32
+ found = {} # "kind:id" -> transcript path
33
+ misses = {} # "kind:id" -> when to look again
34
+ transcripts = {} # path -> Transcript
35
+ rollouts = {} # rollout path -> its session_meta: {cwd, originator, source}
36
+
37
+
38
+ class ClaudeLog:
39
+ def __init__(self):
40
+ self.used = None
41
+ self.model = None # the model of the last call
42
+ self.model_id = None # Claude Code's name for it, "[1m]" when it asked for 1M
43
+
44
+ def wants(self, line):
45
+ return b'"usage"' in line or b'"compact_boundary"' in line or b'"modelId"' in line
46
+
47
+ def add(self, e):
48
+ if e.get("isSidechain"):
49
+ return
50
+ kind = e.get("type")
51
+ if kind == "assistant":
52
+ m = e.get("message") or {}
53
+ u = m.get("usage")
54
+ if not u or m.get("model") == "<synthetic>":
55
+ return
56
+ used = (u.get("input_tokens") or 0) + (u.get("cache_creation_input_tokens") or 0) + (u.get("cache_read_input_tokens") or 0)
57
+ if used > 0:
58
+ self.used = used
59
+ if isinstance(m.get("model"), str):
60
+ self.model = m["model"]
61
+ elif kind == "system" and e.get("subtype") == "compact_boundary":
62
+ post = (e.get("compactMetadata") or {}).get("postTokens")
63
+ self.used = post if isinstance(post, int) else None
64
+ elif kind == "attachment" and (e.get("attachment") or {}).get("type") == "model":
65
+ self.model_id = ((e["attachment"].get("identity") or {}).get("modelId")) or self.model_id
66
+
67
+ def usage(self):
68
+ if self.used is None:
69
+ return None
70
+ small = (self.used <= CLAUDE_WINDOW_SMALL and "[1m]" not in (self.model_id or "").lower()
71
+ and CLAUDE_SMALL_MODEL.match(self.model or self.model_id or "") is not None)
72
+ return {"used": self.used, "max": CLAUDE_WINDOW_SMALL if small else CLAUDE_WINDOW}
73
+
74
+
75
+ class CodexLog:
76
+ def __init__(self):
77
+ self.used = None
78
+ self.max = None
79
+
80
+ def wants(self, line):
81
+ return b'"token_count"' in line
82
+
83
+ def add(self, e):
84
+ p = e.get("payload") or {}
85
+ info = p.get("info") if e.get("type") == "event_msg" and p.get("type") == "token_count" else None
86
+ if not info:
87
+ return
88
+ total = (info.get("last_token_usage") or {}).get("total_tokens")
89
+ if isinstance(total, int):
90
+ self.used = total
91
+ if isinstance(info.get("model_context_window"), int):
92
+ self.max = info["model_context_window"]
93
+
94
+ def usage(self):
95
+ return {"used": self.used, "max": self.max} if self.used is not None and self.max else None
96
+
97
+
98
+ class Transcript:
99
+ """One transcript, read incrementally from where the last read stopped."""
100
+
101
+ def __init__(self, path, kind):
102
+ self.path = path
103
+ self.log_class = ClaudeLog if kind == "claude" else CodexLog
104
+ self.reset()
105
+
106
+ def reset(self):
107
+ self.log = self.log_class()
108
+ self.offset = 0
109
+ self.rest = b""
110
+ self.skip_first = False
111
+
112
+ def usage(self):
113
+ size = os.stat(self.path).st_size
114
+ if size < self.offset: # rewritten: start over
115
+ self.reset()
116
+ if size > self.offset:
117
+ self.read_to(size)
118
+ return self.log.usage()
119
+
120
+ def read_to(self, size):
121
+ start = self.offset
122
+ if start == 0 and size > FIRST_READ_MAX:
123
+ start = size - FIRST_READ_MAX
124
+ self.skip_first = True
125
+ with open(self.path, "rb") as f:
126
+ f.seek(start)
127
+ left = size - start
128
+ while left > 0:
129
+ chunk = f.read(min(1 << 20, left))
130
+ if not chunk:
131
+ break
132
+ left -= len(chunk)
133
+ self.offset = size - left
134
+ lines = (self.rest + chunk).split(b"\n")
135
+ self.rest = lines.pop()
136
+ for line in lines:
137
+ if self.skip_first: # started mid-line
138
+ self.skip_first = False
139
+ continue
140
+ if self.log.wants(line):
141
+ try:
142
+ self.log.add(json.loads(line))
143
+ except Exception:
144
+ pass
145
+
146
+
147
+ def session_file(pid):
148
+ """Claude Code's record of a running instance: ~/.claude/sessions/<pid>.json."""
149
+ if not isinstance(pid, int):
150
+ return None
151
+ try:
152
+ os.kill(pid, 0)
153
+ except ProcessLookupError:
154
+ return None
155
+ except PermissionError:
156
+ pass
157
+ try:
158
+ with open(os.path.join(CLAUDE_DIR, "sessions", f"{pid}.json"), encoding="utf-8") as f:
159
+ meta = json.load(f)
160
+ except Exception:
161
+ return None
162
+ return meta if meta.get("pid") == pid and isinstance(meta.get("sessionId"), str) else None
163
+
164
+
165
+ def find_claude(sid, cwd):
166
+ projects = os.path.join(CLAUDE_DIR, "projects")
167
+ if cwd:
168
+ guess = os.path.join(projects, re.sub(r"[^A-Za-z0-9]", "-", cwd), f"{sid}.jsonl")
169
+ if os.path.exists(guess):
170
+ return guess
171
+ try:
172
+ dirs = os.listdir(projects)
173
+ except OSError:
174
+ return None
175
+ for d in dirs:
176
+ path = os.path.join(projects, d, f"{sid}.jsonl")
177
+ if os.path.exists(path):
178
+ return path
179
+ return None
180
+
181
+
182
+ def find_codex(sid):
183
+ def ls(d):
184
+ try:
185
+ return sorted(os.listdir(d), reverse=True)
186
+ except OSError:
187
+ return []
188
+ root = os.path.join(CODEX_DIR, "sessions")
189
+ for y in ls(root):
190
+ for m in ls(os.path.join(root, y)):
191
+ for d in ls(os.path.join(root, y, m)):
192
+ day = os.path.join(root, y, m, d)
193
+ for name in ls(day):
194
+ if name.endswith(f"-{sid}.jsonl"):
195
+ return os.path.join(day, name)
196
+ return None
197
+
198
+
199
+ def find(kind, sid, cwd=None):
200
+ if not isinstance(sid, str) or not SESSION_ID.match(sid):
201
+ return None
202
+ key = f"{kind}:{sid}"
203
+ known = found.get(key)
204
+ if known and os.path.exists(known):
205
+ return known
206
+ if misses.get(key, 0) > time.time():
207
+ return None
208
+ path = find_claude(sid, cwd) if kind == "claude" else find_codex(sid)
209
+ if path:
210
+ found[key] = path
211
+ misses.pop(key, None)
212
+ else:
213
+ misses[key] = time.time() + MISS_S
214
+ return path
215
+
216
+
217
+ def process_start(pids):
218
+ """The earliest-started of `pids` and when it started (s), from `ps`."""
219
+ if not pids:
220
+ return None
221
+ try:
222
+ out = subprocess.run(["ps", "-o", "pid=,lstart=", "-p", ",".join(str(p) for p in pids)],
223
+ capture_output=True, text=True, env={**os.environ, "LC_ALL": "C"}, timeout=5).stdout
224
+ except (OSError, subprocess.SubprocessError):
225
+ return None
226
+ best = None
227
+ for line in out.splitlines():
228
+ parts = line.split(None, 1)
229
+ if len(parts) != 2:
230
+ continue
231
+ try:
232
+ at = time.mktime(time.strptime(" ".join(parts[1].split()), "%a %b %d %H:%M:%S %Y"))
233
+ except ValueError:
234
+ continue
235
+ if best is None or at < best[1]:
236
+ best = (int(parts[0]), at)
237
+ return best
238
+
239
+
240
+ def rollout_meta(path):
241
+ """A rollout's first line, session_meta; {} if it is something else, None while it is being written."""
242
+ try:
243
+ with open(path, "rb") as f:
244
+ head = f.read(1 << 20)
245
+ except OSError:
246
+ return None
247
+ nl = head.find(b"\n")
248
+ if nl == -1:
249
+ return None
250
+ try:
251
+ e = json.loads(head[:nl])
252
+ except ValueError:
253
+ return {}
254
+ p = e.get("payload") if isinstance(e, dict) and e.get("type") == "session_meta" else None
255
+ return {"cwd": p.get("cwd"), "originator": p.get("originator"), "source": p.get("source")} if isinstance(p, dict) else {}
256
+
257
+
258
+ def codex_by_folder(cwd, since):
259
+ """The newest terminal Codex rollout for `cwd` written since `since` (when Codex started), from the day it started on."""
260
+ best, best_time = None, 0
261
+ day = datetime.date.fromtimestamp(since) - datetime.timedelta(days=1) # time zones
262
+ last = datetime.date.today() + datetime.timedelta(days=1)
263
+ for _ in range(31):
264
+ if day > last:
265
+ break
266
+ d = os.path.join(CODEX_DIR, "sessions", f"{day.year:04d}", f"{day.month:02d}", f"{day.day:02d}")
267
+ day += datetime.timedelta(days=1)
268
+ try:
269
+ names = os.listdir(d)
270
+ except OSError:
271
+ continue
272
+ for name in names:
273
+ if not (name.startswith("rollout-") and name.endswith(".jsonl")):
274
+ continue
275
+ path = os.path.join(d, name)
276
+ try:
277
+ mtime = os.stat(path).st_mtime
278
+ except OSError:
279
+ continue
280
+ if mtime < since or mtime <= best_time:
281
+ continue
282
+ meta = rollouts.get(path)
283
+ if meta is None:
284
+ meta = rollout_meta(path)
285
+ if meta is not None:
286
+ rollouts[path] = meta
287
+ # A subagent's rollout has an object for `source`.
288
+ if meta and meta.get("cwd") == cwd and meta.get("originator") == "codex-tui" and isinstance(meta.get("source"), str):
289
+ best, best_time = path, mtime
290
+ return best
291
+
292
+
293
+ def pane_usage(pane, used):
294
+ agent, s, pid, path = pane.get("agent"), pane.get("session"), None, None
295
+ if s and isinstance(s.get("value"), str):
296
+ if s.get("kind") == "path":
297
+ path = s["value"] if s["value"].endswith(".jsonl") else None
298
+ elif s.get("kind") == "id":
299
+ path = find(agent, s["value"], pane.get("cwd"))
300
+ elif agent == "codex":
301
+ # The pid is kept even before the first rollout shows up, so the next read looks again.
302
+ started = process_start(pane.get("pids") or [])
303
+ if started:
304
+ pid = started[0]
305
+ path = codex_by_folder(pane.get("cwd"), started[1])
306
+ elif agent == "claude":
307
+ for p in pane.get("pids") or []:
308
+ meta = session_file(p)
309
+ if meta:
310
+ pid = p
311
+ path = find("claude", meta["sessionId"], meta.get("cwd"))
312
+ break
313
+ usage = None
314
+ if path:
315
+ used.add(path)
316
+ t = transcripts.get(path)
317
+ if not t:
318
+ t = transcripts[path] = Transcript(path, agent)
319
+ try:
320
+ usage = t.usage()
321
+ except OSError:
322
+ pass
323
+ return {"usage": usage, "pid": pid}
324
+
325
+
326
+ def main():
327
+ for line in sys.stdin:
328
+ try:
329
+ req = json.loads(line)
330
+ except ValueError:
331
+ continue
332
+ used, result = set(), {}
333
+ for pane in req.get("panes") or []:
334
+ try:
335
+ result[pane["pane_id"]] = pane_usage(pane, used)
336
+ except Exception:
337
+ pass
338
+ for path in [p for p in transcripts if p not in used]:
339
+ del transcripts[path]
340
+ sys.stdout.write(json.dumps({"id": req.get("id"), "result": result}) + "\n")
341
+ sys.stdout.flush()
342
+
343
+
344
+ main()
@@ -0,0 +1,85 @@
1
+ # Printers for a remote floor: what diffs.ts needs from the checkouts
2
+ # on that machine, which is git and the files themselves. The bridge starts it
3
+ # over SSH like context_remote.py (ssh/remote.ts); all the deciding (what
4
+ # changed, the diffs) stays in diffs.ts.
5
+ #
6
+ # Protocol, one JSON object per line (b64: base64 of the bytes):
7
+ # in: {"id", "op": "git", "cwd", "args": [...], "input": b64 | null, "max": int}
8
+ # out: {"id", "result": {"ok": bool, "out": b64, "truncated": bool, "err": str}}
9
+ # in: {"id", "op": "stat", "root", "paths": [...]}
10
+ # out: {"id", "result": {path: "size:mtime" | "other" | null}}
11
+ # in: {"id", "op": "read", "root", "paths": [...], "max": int}
12
+ # out: {"id", "result": {path: b64 | "large" | null}}
13
+ # "other" is something that is not a regular file (a directory, a symlink);
14
+ # null, nothing there.
15
+
16
+ import base64
17
+ import json
18
+ import os
19
+ import stat
20
+ import subprocess
21
+ import sys
22
+
23
+ GIT_TIMEOUT_S = 15
24
+
25
+
26
+ def b64(data):
27
+ return base64.b64encode(data).decode("ascii")
28
+
29
+
30
+ def run_git(req):
31
+ env = dict(os.environ, GIT_OPTIONAL_LOCKS="0")
32
+ data = base64.b64decode(req["input"]) if req.get("input") is not None else b""
33
+ try:
34
+ p = subprocess.run(["git", "-c", "core.quotepath=off"] + list(req["args"]), cwd=req["cwd"], env=env,
35
+ input=data, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=GIT_TIMEOUT_S)
36
+ except (OSError, subprocess.SubprocessError) as e:
37
+ return {"ok": False, "out": "", "truncated": False, "err": str(e)}
38
+ out, cap = p.stdout, int(req.get("max") or len(p.stdout))
39
+ err = p.stderr.decode("utf-8", "replace").strip()
40
+ return {"ok": p.returncode == 0, "out": b64(out[:cap]), "truncated": len(out) > cap, "err": err}
41
+
42
+
43
+ def stat_paths(req):
44
+ result = {}
45
+ for path in req["paths"]:
46
+ try:
47
+ st = os.lstat(os.path.join(req["root"], path))
48
+ except OSError:
49
+ result[path] = None
50
+ continue
51
+ result[path] = f"{st.st_size}:{st.st_mtime_ns}" if stat.S_ISREG(st.st_mode) else "other"
52
+ return result
53
+
54
+
55
+ def read_paths(req):
56
+ result, cap = {}, int(req["max"])
57
+ for path in req["paths"]:
58
+ try:
59
+ with open(os.path.join(req["root"], path), "rb") as f:
60
+ data = f.read(cap + 1)
61
+ except OSError:
62
+ result[path] = None
63
+ continue
64
+ result[path] = "large" if len(data) > cap else b64(data)
65
+ return result
66
+
67
+
68
+ OPS = {"git": run_git, "stat": stat_paths, "read": read_paths}
69
+
70
+
71
+ def main():
72
+ for line in sys.stdin:
73
+ try:
74
+ req = json.loads(line)
75
+ except ValueError:
76
+ continue
77
+ try:
78
+ result = OPS[req["op"]](req)
79
+ except Exception:
80
+ result = None
81
+ sys.stdout.write(json.dumps({"id": req.get("id"), "result": result}) + "\n")
82
+ sys.stdout.flush()
83
+
84
+
85
+ main()