@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.
- package/LICENSE +202 -0
- package/README.md +186 -2
- package/bin/kauak.js +12 -0
- package/bridge/config.js +34 -0
- package/bridge/context_remote.py +344 -0
- package/bridge/diffs_remote.py +85 -0
- package/bridge/server.js +2387 -0
- package/cli/commands/serve.js +97 -0
- package/cli/errors.js +2 -0
- package/cli/main.js +101 -0
- package/dist/THIRD_PARTY_LICENSES.txt +183 -0
- package/dist/assets/BufferResource-CHYsmqd5.js +195 -0
- package/dist/assets/CanvasRenderer-DY1TnK1M.js +1 -0
- package/dist/assets/Filter-IrwvDh3w.js +1 -0
- package/dist/assets/RenderTargetSystem-CBR4meCp.js +184 -0
- package/dist/assets/WebGLRenderer-DprtkQRW.js +156 -0
- package/dist/assets/WebGPURenderer-ByQ-10KB.js +42 -0
- package/dist/assets/browserAll-B-9xDvMq.js +14 -0
- package/dist/assets/contours-DG3HeJNa.svg +68 -0
- package/dist/assets/demo-DyZsh87s.css +1 -0
- package/dist/assets/demo-mFndvU9t.js +5 -0
- package/dist/assets/index-BxvDL43s.css +32 -0
- package/dist/assets/index-Do5XwO_P.js +349 -0
- package/dist/assets/webworkerAll-gM2Z784I.js +83 -0
- package/dist/index.html +160 -0
- package/dist/kauak.png +0 -0
- package/package.json +24 -4
|
@@ -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()
|