interlatch 0.22.0__py3-none-any.whl

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.
Files changed (118) hide show
  1. chronicle/__init__.py +17 -0
  2. chronicle/__main__.py +3 -0
  3. chronicle/access.py +93 -0
  4. chronicle/agents.py +23 -0
  5. chronicle/analyze.py +498 -0
  6. chronicle/antigravity_parser.py +311 -0
  7. chronicle/artifacts.py +621 -0
  8. chronicle/bob_parser.py +259 -0
  9. chronicle/cases.py +75 -0
  10. chronicle/chat_import.py +325 -0
  11. chronicle/chatgpt_export.py +127 -0
  12. chronicle/claude_export.py +129 -0
  13. chronicle/cli.py +2980 -0
  14. chronicle/codex_cloud.py +138 -0
  15. chronicle/codex_parser.py +753 -0
  16. chronicle/config.py +627 -0
  17. chronicle/connectors.py +666 -0
  18. chronicle/container.py +202 -0
  19. chronicle/copilot_parser.py +494 -0
  20. chronicle/db.py +598 -0
  21. chronicle/desktop.py +543 -0
  22. chronicle/diagram.py +488 -0
  23. chronicle/digest.py +194 -0
  24. chronicle/export_md.py +210 -0
  25. chronicle/friction.py +841 -0
  26. chronicle/glossary.py +522 -0
  27. chronicle/groups.py +181 -0
  28. chronicle/hooks.py +169 -0
  29. chronicle/hub.py +2291 -0
  30. chronicle/i18n.py +37 -0
  31. chronicle/i18n_ja.py +421 -0
  32. chronicle/ingest.py +1190 -0
  33. chronicle/install.py +590 -0
  34. chronicle/instructions.py +699 -0
  35. chronicle/ladder.py +244 -0
  36. chronicle/learning.py +170 -0
  37. chronicle/llm.py +570 -0
  38. chronicle/mcp_server.py +403 -0
  39. chronicle/menubar.py +771 -0
  40. chronicle/migrate.py +580 -0
  41. chronicle/mirror.py +453 -0
  42. chronicle/news.py +110 -0
  43. chronicle/notify.py +92 -0
  44. chronicle/parser.py +908 -0
  45. chronicle/people.py +536 -0
  46. chronicle/pg.py +122 -0
  47. chronicle/pricing.py +168 -0
  48. chronicle/providers.py +591 -0
  49. chronicle/redact.py +50 -0
  50. chronicle/reviews.py +251 -0
  51. chronicle/screen.py +308 -0
  52. chronicle/search.py +238 -0
  53. chronicle/server.py +3009 -0
  54. chronicle/session_export.py +178 -0
  55. chronicle/statusline.py +258 -0
  56. chronicle/suggest.py +643 -0
  57. chronicle/synthesize.py +365 -0
  58. chronicle/systems.py +1265 -0
  59. chronicle/tailnet.py +97 -0
  60. chronicle/team_store.py +450 -0
  61. chronicle/update.py +404 -0
  62. chronicle/util.py +217 -0
  63. chronicle/views.py +249 -0
  64. chronicle/web/PLEX-OFL.txt +93 -0
  65. chronicle/web/app.css +2350 -0
  66. chronicle/web/app.js +7587 -0
  67. chronicle/web/art-book.webp +0 -0
  68. chronicle/web/art-camera.webp +0 -0
  69. chronicle/web/art-chip.webp +0 -0
  70. chronicle/web/art-clock.webp +0 -0
  71. chronicle/web/art-cloud.webp +0 -0
  72. chronicle/web/art-coins.webp +0 -0
  73. chronicle/web/art-container.webp +0 -0
  74. chronicle/web/art-dashboard.webp +0 -0
  75. chronicle/web/art-dex.webp +0 -0
  76. chronicle/web/art-document.webp +0 -0
  77. chronicle/web/art-flag.webp +0 -0
  78. chronicle/web/art-folder.webp +0 -0
  79. chronicle/web/art-gateway.webp +0 -0
  80. chronicle/web/art-gears.webp +0 -0
  81. chronicle/web/art-glitch.webp +0 -0
  82. chronicle/web/art-globe.webp +0 -0
  83. chronicle/web/art-laptop.webp +0 -0
  84. chronicle/web/art-log.webp +0 -0
  85. chronicle/web/art-lumi.webp +0 -0
  86. chronicle/web/art-magnifier.webp +0 -0
  87. chronicle/web/art-pip.webp +0 -0
  88. chronicle/web/art-plug.webp +0 -0
  89. chronicle/web/art-rack.webp +0 -0
  90. chronicle/web/art-tink.webp +0 -0
  91. chronicle/web/boot.js +25 -0
  92. chronicle/web/favicon.png +0 -0
  93. chronicle/web/i18n.js +50 -0
  94. chronicle/web/icon-180.png +0 -0
  95. chronicle/web/icon-192.png +0 -0
  96. chronicle/web/icon-512.png +0 -0
  97. chronicle/web/icon.png +0 -0
  98. chronicle/web/index.html +92 -0
  99. chronicle/web/ja.js +1880 -0
  100. chronicle/web/learning.js +438 -0
  101. chronicle/web/manifest.webmanifest +15 -0
  102. chronicle/web/panel.css +175 -0
  103. chronicle/web/panel.html +14 -0
  104. chronicle/web/panel.js +241 -0
  105. chronicle/web/plex-mono-400.woff2 +0 -0
  106. chronicle/web/plex-mono-500.woff2 +0 -0
  107. chronicle/web/plex-mono-600.woff2 +0 -0
  108. chronicle/web/plex-sans-400.woff2 +0 -0
  109. chronicle/web/plex-sans-500.woff2 +0 -0
  110. chronicle/web/plex-sans-600.woff2 +0 -0
  111. chronicle/web/plex-sans-700.woff2 +0 -0
  112. chronicle/web/rough.js +2 -0
  113. chronicle/worker.py +390 -0
  114. interlatch-0.22.0.dist-info/METADATA +221 -0
  115. interlatch-0.22.0.dist-info/RECORD +118 -0
  116. interlatch-0.22.0.dist-info/WHEEL +4 -0
  117. interlatch-0.22.0.dist-info/entry_points.txt +3 -0
  118. interlatch-0.22.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,138 @@
1
+ """Codex Cloud tasks (chatgpt.com/codex), read through the codex CLI with the user's own Codex login.
2
+
3
+ `codex cloud list --json` gives each task's title, environment (the repository), status, change counts and last
4
+ update; `codex cloud diff <id>` gives its diff. The CLI does not give a task's conversation, so a cloud task becomes
5
+ a session without prompts: the title, the files it changed and the diff, with a link to the task. Interlatch never
6
+ reads Codex's login state itself; the CLI does the talking.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import logging
13
+ import re
14
+ import subprocess
15
+ import time
16
+
17
+ from .copilot_parser import _Builder, finish_session
18
+ from .hooks import internal_env
19
+ from .parser import FileStat, ParsedSession
20
+ from .util import safe_text
21
+
22
+ log = logging.getLogger("chronicle.codex_cloud")
23
+
24
+ CLOUD_PARSER_VERSION = 1
25
+ STATUS_KEY = "codex-cloud:status" # kv: the last listing's outcome, for the Sources page
26
+ MAX_PAGES = 25 # 20 tasks a page
27
+ _DIFF_FILE_RE = re.compile(r"^diff --git a/(.+?) b/(.+)$")
28
+
29
+
30
+ class CloudError(Exception):
31
+ """The codex CLI could not list tasks (not logged in, offline, an old CLI without `codex cloud`)."""
32
+
33
+
34
+ def _run(binary: str, *args: str, timeout: float = 120) -> str:
35
+ try:
36
+ proc = subprocess.run([binary, "cloud", *args], capture_output=True, text=True, timeout=timeout,
37
+ stdin=subprocess.DEVNULL, env=internal_env())
38
+ except subprocess.TimeoutExpired as exc:
39
+ raise CloudError(f"codex cloud {args[0]} timed out") from exc
40
+ except OSError as exc:
41
+ raise CloudError(f"could not run codex: {exc}") from exc
42
+ if proc.returncode:
43
+ msg = (proc.stderr or proc.stdout).strip().splitlines()
44
+ raise CloudError(msg[-1][:300] if msg else f"codex cloud {args[0]} exited with {proc.returncode}")
45
+ return proc.stdout
46
+
47
+
48
+ def list_tasks(binary: str) -> list[dict]:
49
+ """Every task the account can see, newest first."""
50
+ tasks, cursor = [], None
51
+ for _ in range(MAX_PAGES):
52
+ out = _run(binary, "list", "--json", "--limit", "20", *(["--cursor", cursor] if cursor else []))
53
+ try:
54
+ page = json.loads(out)
55
+ except ValueError as exc:
56
+ raise CloudError("codex cloud list did not return JSON (update the Codex CLI)") from exc
57
+ tasks += [t for t in page.get("tasks") or [] if isinstance(t, dict) and t.get("id")]
58
+ cursor = page.get("cursor")
59
+ if not cursor:
60
+ break
61
+ return tasks
62
+
63
+
64
+ def task_diff(binary: str, task_id: str) -> str | None:
65
+ """The task's diff, or None when it has none yet (still running) or the CLI fails."""
66
+ try:
67
+ return _run(binary, "diff", task_id) or None
68
+ except CloudError as exc:
69
+ log.info("no diff for %s: %s", task_id, exc)
70
+ return None
71
+
72
+
73
+ def diff_files(diff: str | None) -> dict[str, tuple[int, int]]:
74
+ """Lines added and removed per file of a unified git diff."""
75
+ files: dict[str, list[int]] = {}
76
+ cur = None
77
+ for line in (diff or "").splitlines():
78
+ m = _DIFF_FILE_RE.match(line)
79
+ if m:
80
+ cur = files.setdefault(m.group(2), [0, 0])
81
+ elif cur is None or line.startswith(("+++", "---")):
82
+ continue
83
+ elif line.startswith("+"):
84
+ cur[0] += 1
85
+ elif line.startswith("-"):
86
+ cur[1] += 1
87
+ return {p: (a, r) for p, (a, r) in files.items()}
88
+
89
+
90
+ def _ms(ts: str | None) -> str | None:
91
+ """Codex Cloud times have nanoseconds; the rest of Interlatch stores milliseconds ("…:28.652Z")."""
92
+ return re.sub(r"(\.\d{3})\d+", r"\1", ts) if ts else None
93
+
94
+
95
+ def task_signature(task: dict) -> str:
96
+ return f"{task.get('updated_at')}|{task.get('status')}|{task.get('attempt_total')}|v{CLOUD_PARSER_VERSION}"
97
+
98
+
99
+ def parse_cloud_task(task: dict, diff: str | None, project_path: str | None) -> ParsedSession:
100
+ ps = ParsedSession(id=task["id"])
101
+ ps.project_path = project_path
102
+ ps.entrypoint = "codex:cloud"
103
+ ps.custom_title = safe_text(task.get("title") or "") or None
104
+ ps.artifacts.append({"title": "Open in Codex Cloud", "url": task.get("url"), "ts": task.get("updated_at")})
105
+ ts = _ms(task.get("updated_at"))
106
+ b = _Builder(ps)
107
+ env = task.get("environment_label") or task.get("environment_id") or "no environment"
108
+ s = task.get("summary") or {}
109
+ head = f"Codex Cloud task · {env} · {task.get('status') or 'unknown'}"
110
+ if s.get("files_changed"):
111
+ head += f" · +{s.get('lines_added', 0)} −{s.get('lines_removed', 0)} in {s['files_changed']} files"
112
+ if (task.get("attempt_total") or 1) > 1:
113
+ head += f" · {task['attempt_total']} attempts"
114
+ b.event(ts, "system", "meta", head)
115
+ files = diff_files(diff)
116
+ for path, (added, removed) in files.items():
117
+ ps.files[path] = FileStat(edits=1, lines_added=added, lines_removed=removed)
118
+ if files:
119
+ listing = "\n".join(f"- {p} (+{a} −{r})" for p, (a, r) in sorted(files.items()))
120
+ b.event(ts, "assistant", "text", f"Changed {len(files)} file{'s' if len(files) != 1 else ''}:\n{listing}")
121
+ b.event(ts, "assistant", "text", f"```diff\n{diff.rstrip()}\n```")
122
+ b.event(ts, "system", "meta", "Codex's CLI does not share a cloud task's conversation; open the task for it.")
123
+ return finish_session(ps, [ts] if ts else [])
124
+
125
+
126
+ def save_status(conn, **status) -> None:
127
+ from .db import kv_set
128
+
129
+ kv_set(conn, STATUS_KEY, json.dumps({**status, "checked_at": time.time()}))
130
+
131
+
132
+ def load_status(conn) -> dict:
133
+ from .db import kv_get
134
+
135
+ try:
136
+ return json.loads(kv_get(conn, STATUS_KEY) or "{}")
137
+ except ValueError:
138
+ return {}