aline-ai 0.7.3__py3-none-any.whl → 0.7.5__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.
- {aline_ai-0.7.3.dist-info → aline_ai-0.7.5.dist-info}/METADATA +1 -1
- {aline_ai-0.7.3.dist-info → aline_ai-0.7.5.dist-info}/RECORD +32 -27
- realign/__init__.py +1 -1
- realign/adapters/codex.py +30 -2
- realign/claude_hooks/stop_hook.py +176 -21
- realign/codex_home.py +71 -0
- realign/codex_hooks/__init__.py +16 -0
- realign/codex_hooks/notify_hook.py +511 -0
- realign/codex_hooks/notify_hook_installer.py +247 -0
- realign/commands/doctor.py +125 -0
- realign/commands/import_shares.py +30 -10
- realign/commands/init.py +16 -0
- realign/commands/sync_agent.py +230 -52
- realign/commands/upgrade.py +117 -0
- realign/commit_pipeline.py +1024 -0
- realign/config.py +3 -11
- realign/dashboard/app.py +150 -0
- realign/dashboard/diagnostics.py +274 -0
- realign/dashboard/screens/create_agent.py +2 -1
- realign/dashboard/screens/create_agent_info.py +40 -77
- realign/dashboard/tmux_manager.py +354 -20
- realign/dashboard/widgets/agents_panel.py +817 -202
- realign/dashboard/widgets/config_panel.py +52 -121
- realign/dashboard/widgets/header.py +1 -1
- realign/db/sqlite_db.py +59 -1
- realign/logging_config.py +51 -6
- realign/watcher_core.py +742 -393
- realign/worker_core.py +206 -15
- {aline_ai-0.7.3.dist-info → aline_ai-0.7.5.dist-info}/WHEEL +0 -0
- {aline_ai-0.7.3.dist-info → aline_ai-0.7.5.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.7.3.dist-info → aline_ai-0.7.5.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.7.3.dist-info → aline_ai-0.7.5.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Codex notify hook installer (best-effort).
|
|
3
|
+
|
|
4
|
+
We primarily support the Rust Codex CLI which reads CODEX_HOME/config.toml and
|
|
5
|
+
supports `notify = "command args..."` to run a script when a turn finishes.
|
|
6
|
+
|
|
7
|
+
For legacy Codex config.yaml/config.json formats, we can only set `notify: true`
|
|
8
|
+
to enable built-in notifications; there is no guaranteed script hook in that format.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import re
|
|
15
|
+
import shutil
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
from ..logging_config import setup_logger
|
|
22
|
+
|
|
23
|
+
logger = setup_logger("realign.codex_hooks.installer", "codex_hooks_installer.log")
|
|
24
|
+
|
|
25
|
+
ALINE_HOOK_MARKER = "aline-codex-notify-hook"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_notify_hook_script_path() -> Path:
|
|
29
|
+
return Path(__file__).parent / "notify_hook.py"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_notify_hook_command_parts() -> list[str]:
|
|
33
|
+
script_path = get_notify_hook_script_path()
|
|
34
|
+
return [sys.executable, str(script_path)]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _toml_escape(s: str) -> str:
|
|
38
|
+
return s.replace("\\", "\\\\").replace('"', '\\"')
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _format_notify_toml(cmd: list[str]) -> str:
|
|
42
|
+
# Codex CLI expects notify as a string (shell command), not an array.
|
|
43
|
+
# NOTE: we intentionally do not attempt complex quoting here; the common case is
|
|
44
|
+
# paths without spaces (e.g. /opt/homebrew/...).
|
|
45
|
+
command_str = " ".join(cmd)
|
|
46
|
+
return f"notify = \"{_toml_escape(command_str)}\" # {ALINE_HOOK_MARKER}\n"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _update_toml_linewise(path: Path, *, cmd: list[str]) -> bool:
|
|
50
|
+
"""
|
|
51
|
+
Update config.toml in a minimal, formatting-preserving way (line-based).
|
|
52
|
+
|
|
53
|
+
Returns True if the file was written/updated.
|
|
54
|
+
"""
|
|
55
|
+
desired = _format_notify_toml(cmd)
|
|
56
|
+
existing = ""
|
|
57
|
+
try:
|
|
58
|
+
existing = path.read_text(encoding="utf-8")
|
|
59
|
+
except FileNotFoundError:
|
|
60
|
+
existing = ""
|
|
61
|
+
except Exception:
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
lines = existing.splitlines(keepends=True) if existing else []
|
|
65
|
+
out: list[str] = []
|
|
66
|
+
replaced = False
|
|
67
|
+
|
|
68
|
+
for line in lines:
|
|
69
|
+
stripped = line.lstrip()
|
|
70
|
+
if stripped.startswith("notify ="):
|
|
71
|
+
if not replaced:
|
|
72
|
+
out.append(desired)
|
|
73
|
+
replaced = True
|
|
74
|
+
else:
|
|
75
|
+
# Drop duplicate notify lines.
|
|
76
|
+
continue
|
|
77
|
+
else:
|
|
78
|
+
out.append(line)
|
|
79
|
+
|
|
80
|
+
if not replaced:
|
|
81
|
+
if out and not out[-1].endswith("\n"):
|
|
82
|
+
out[-1] = out[-1] + "\n"
|
|
83
|
+
if out and out[-1].strip():
|
|
84
|
+
out.append("\n")
|
|
85
|
+
out.append(desired)
|
|
86
|
+
|
|
87
|
+
new_text = "".join(out)
|
|
88
|
+
if new_text == existing:
|
|
89
|
+
return True
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
93
|
+
path.write_text(new_text, encoding="utf-8")
|
|
94
|
+
return True
|
|
95
|
+
except Exception:
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _ensure_legacy_notify_enabled(codex_home: Path) -> list[Path]:
|
|
100
|
+
"""
|
|
101
|
+
Best-effort for legacy Codex config formats (YAML/JSON):
|
|
102
|
+
set `notify: true` / `"notify": true` if the file exists.
|
|
103
|
+
"""
|
|
104
|
+
updated: list[Path] = []
|
|
105
|
+
yaml_path = codex_home / "config.yaml"
|
|
106
|
+
json_path = codex_home / "config.json"
|
|
107
|
+
|
|
108
|
+
if yaml_path.exists():
|
|
109
|
+
try:
|
|
110
|
+
raw = yaml_path.read_text(encoding="utf-8")
|
|
111
|
+
if "notify:" in raw:
|
|
112
|
+
# Minimal replace: notify: <anything> -> notify: true
|
|
113
|
+
out_lines: list[str] = []
|
|
114
|
+
for line in raw.splitlines():
|
|
115
|
+
if line.strip().startswith("notify:"):
|
|
116
|
+
out_lines.append("notify: true")
|
|
117
|
+
else:
|
|
118
|
+
out_lines.append(line)
|
|
119
|
+
new_raw = "\n".join(out_lines) + ("\n" if raw.endswith("\n") else "")
|
|
120
|
+
else:
|
|
121
|
+
new_raw = raw + ("\n" if raw and not raw.endswith("\n") else "") + "notify: true\n"
|
|
122
|
+
if new_raw != raw:
|
|
123
|
+
yaml_path.write_text(new_raw, encoding="utf-8")
|
|
124
|
+
updated.append(yaml_path)
|
|
125
|
+
except Exception:
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
if json_path.exists():
|
|
129
|
+
try:
|
|
130
|
+
obj = json.loads(json_path.read_text(encoding="utf-8") or "{}")
|
|
131
|
+
if isinstance(obj, dict):
|
|
132
|
+
if obj.get("notify") is not True:
|
|
133
|
+
obj["notify"] = True
|
|
134
|
+
json_path.write_text(json.dumps(obj, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
135
|
+
updated.append(json_path)
|
|
136
|
+
except Exception:
|
|
137
|
+
pass
|
|
138
|
+
|
|
139
|
+
return updated
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def ensure_notify_hook_installed_for_codex_home(
|
|
143
|
+
codex_home: Path, *, quiet: bool = True
|
|
144
|
+
) -> bool:
|
|
145
|
+
"""
|
|
146
|
+
Ensure the notify hook is installed for a given CODEX_HOME.
|
|
147
|
+
|
|
148
|
+
- Rust CLI: writes/updates CODEX_HOME/config.toml notify command.
|
|
149
|
+
- Legacy: enables notify=true if config.yaml/config.json exist.
|
|
150
|
+
"""
|
|
151
|
+
codex_home = Path(codex_home).expanduser()
|
|
152
|
+
cmd = get_notify_hook_command_parts()
|
|
153
|
+
|
|
154
|
+
ok = False
|
|
155
|
+
toml_path = codex_home / "config.toml"
|
|
156
|
+
if _update_toml_linewise(toml_path, cmd=cmd):
|
|
157
|
+
ok = True
|
|
158
|
+
if not quiet:
|
|
159
|
+
print(f"[Aline] Codex notify hook installed: {toml_path}", file=sys.stderr)
|
|
160
|
+
_ensure_legacy_notify_enabled(codex_home)
|
|
161
|
+
return ok
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def ensure_global_codex_notify_hook_installed(*, quiet: bool = True) -> bool:
|
|
165
|
+
"""Best-effort: install notify hook into default global CODEX_HOME (~/.codex)."""
|
|
166
|
+
return ensure_notify_hook_installed_for_codex_home(Path.home() / ".codex", quiet=quiet)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def ensure_all_aline_codex_homes_notify_hook_installed(*, quiet: bool = True) -> int:
|
|
170
|
+
"""
|
|
171
|
+
Best-effort: install notify hook into every Aline-managed CODEX_HOME under ~/.aline/codex_homes.
|
|
172
|
+
Returns number of homes updated.
|
|
173
|
+
"""
|
|
174
|
+
try:
|
|
175
|
+
from ..codex_home import aline_codex_homes_dir
|
|
176
|
+
|
|
177
|
+
root = aline_codex_homes_dir()
|
|
178
|
+
except Exception:
|
|
179
|
+
root = Path.home() / ".aline" / "codex_homes"
|
|
180
|
+
|
|
181
|
+
if not root.exists():
|
|
182
|
+
return 0
|
|
183
|
+
|
|
184
|
+
updated = 0
|
|
185
|
+
for child in root.iterdir():
|
|
186
|
+
if not child.is_dir():
|
|
187
|
+
continue
|
|
188
|
+
# Layouts:
|
|
189
|
+
# - <terminal_id>/
|
|
190
|
+
# - agent-<id>/<terminal_id>/
|
|
191
|
+
if child.name.startswith("agent-"):
|
|
192
|
+
try:
|
|
193
|
+
for grandchild in child.iterdir():
|
|
194
|
+
if grandchild.is_dir():
|
|
195
|
+
if ensure_notify_hook_installed_for_codex_home(grandchild, quiet=quiet):
|
|
196
|
+
updated += 1
|
|
197
|
+
except Exception:
|
|
198
|
+
continue
|
|
199
|
+
else:
|
|
200
|
+
if ensure_notify_hook_installed_for_codex_home(child, quiet=quiet):
|
|
201
|
+
updated += 1
|
|
202
|
+
return updated
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def codex_cli_supports_notify_hook(*, timeout_seconds: float = 0.5) -> Optional[bool]:
|
|
206
|
+
"""
|
|
207
|
+
Best-effort detect whether the installed `codex` binary supports the Rust notify hook.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
- True: looks like Rust Codex CLI (supports config.toml + notify command)
|
|
211
|
+
- False: looks like legacy Codex (no reliable script notify hook)
|
|
212
|
+
- None: codex binary not found
|
|
213
|
+
"""
|
|
214
|
+
if shutil.which("codex") is None:
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
def run(args: list[str]) -> str:
|
|
218
|
+
try:
|
|
219
|
+
proc = subprocess.run(
|
|
220
|
+
args,
|
|
221
|
+
text=True,
|
|
222
|
+
capture_output=True,
|
|
223
|
+
check=False,
|
|
224
|
+
timeout=float(timeout_seconds),
|
|
225
|
+
)
|
|
226
|
+
return f"{proc.stdout}\n{proc.stderr}".strip().lower()
|
|
227
|
+
except Exception:
|
|
228
|
+
return ""
|
|
229
|
+
|
|
230
|
+
help_out = run(["codex", "--help"])
|
|
231
|
+
ver_out = run(["codex", "--version"])
|
|
232
|
+
out = (help_out + "\n" + ver_out).lower()
|
|
233
|
+
|
|
234
|
+
# Strong positive indicators for Rust CLI.
|
|
235
|
+
if "config.toml" in out and "notify" in out:
|
|
236
|
+
return True
|
|
237
|
+
# Avoid false positives (e.g. "trusted" contains the substring "rust").
|
|
238
|
+
if "codex-rs" in out or re.search(r"\brust\b", out):
|
|
239
|
+
return True
|
|
240
|
+
|
|
241
|
+
# Legacy indicators (YAML/JSON config keys from older docs/CLI).
|
|
242
|
+
if "config.yaml" in out or "approvalmode" in out or "fullautoerrormode" in out:
|
|
243
|
+
return False
|
|
244
|
+
|
|
245
|
+
# Conservative default: if we cannot confirm, treat as unsupported so we don't
|
|
246
|
+
# claim Codex integration works when it won't.
|
|
247
|
+
return False
|
realign/commands/doctor.py
CHANGED
|
@@ -131,6 +131,27 @@ def _update_claude_hooks(*, verbose: bool) -> Tuple[list[str], list[str]]:
|
|
|
131
131
|
return hooks_updated, hooks_failed
|
|
132
132
|
|
|
133
133
|
|
|
134
|
+
def _update_codex_notify_hook(*, verbose: bool) -> Tuple[int, int]:
|
|
135
|
+
"""
|
|
136
|
+
Ensure Codex notify hook is installed:
|
|
137
|
+
- global ~/.codex
|
|
138
|
+
- all Aline-managed ~/.aline/codex_homes/*
|
|
139
|
+
"""
|
|
140
|
+
try:
|
|
141
|
+
from ..codex_hooks.notify_hook_installer import (
|
|
142
|
+
ensure_all_aline_codex_homes_notify_hook_installed,
|
|
143
|
+
ensure_global_codex_notify_hook_installed,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
ok_global = 1 if ensure_global_codex_notify_hook_installed(quiet=not verbose) else 0
|
|
147
|
+
ok_homes = int(ensure_all_aline_codex_homes_notify_hook_installed(quiet=not verbose))
|
|
148
|
+
return ok_global, ok_homes
|
|
149
|
+
except Exception as e:
|
|
150
|
+
if verbose:
|
|
151
|
+
console.print(f" [yellow]Codex notify hook update failed: {e}[/yellow]")
|
|
152
|
+
return 0, 0
|
|
153
|
+
|
|
154
|
+
|
|
134
155
|
def _update_skills(*, verbose: bool) -> int:
|
|
135
156
|
from .add import add_skills_command
|
|
136
157
|
|
|
@@ -302,6 +323,66 @@ def _check_llm_error_turns(
|
|
|
302
323
|
db.close()
|
|
303
324
|
|
|
304
325
|
|
|
326
|
+
def _check_watcher_backlog(
|
|
327
|
+
config: ReAlignConfig,
|
|
328
|
+
*,
|
|
329
|
+
verbose: bool,
|
|
330
|
+
fix: bool,
|
|
331
|
+
) -> Tuple[int, int]:
|
|
332
|
+
"""
|
|
333
|
+
Check for backlog sessions that changed since the watcher last run.
|
|
334
|
+
|
|
335
|
+
Uses the same 2-phase startup scan logic as the watcher:
|
|
336
|
+
1) stat previously persisted session paths (fast)
|
|
337
|
+
2) full scan of watch paths (complete)
|
|
338
|
+
"""
|
|
339
|
+
from ..db.sqlite_db import SQLiteDatabase
|
|
340
|
+
from ..watcher_core import DialogueWatcher
|
|
341
|
+
|
|
342
|
+
db_path = Path(config.sqlite_db_path).expanduser()
|
|
343
|
+
if not db_path.exists():
|
|
344
|
+
return 0, 0
|
|
345
|
+
|
|
346
|
+
watcher = DialogueWatcher()
|
|
347
|
+
candidates, _sizes, _mtimes, report = watcher._startup_scan_collect_candidates()
|
|
348
|
+
|
|
349
|
+
if verbose:
|
|
350
|
+
console.print(
|
|
351
|
+
f" [dim]Tracked paths: {report.prev_paths} (missing: {report.prev_missing}, changed: {report.prev_changed})[/dim]"
|
|
352
|
+
)
|
|
353
|
+
console.print(
|
|
354
|
+
f" [dim]Scan paths: {report.scan_paths} (new: {report.scan_new}, changed: {report.scan_changed})[/dim]"
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
if not candidates:
|
|
358
|
+
return 0, 0
|
|
359
|
+
|
|
360
|
+
if not fix:
|
|
361
|
+
return len(candidates), 0
|
|
362
|
+
|
|
363
|
+
db = SQLiteDatabase(str(db_path))
|
|
364
|
+
try:
|
|
365
|
+
enqueued = 0
|
|
366
|
+
for session_file in candidates:
|
|
367
|
+
try:
|
|
368
|
+
db.enqueue_session_process_job(
|
|
369
|
+
session_file_path=session_file,
|
|
370
|
+
session_id=session_file.stem,
|
|
371
|
+
workspace_path=None,
|
|
372
|
+
session_type=watcher._detect_session_type(session_file),
|
|
373
|
+
source_event="doctor_backlog_scan",
|
|
374
|
+
priority=getattr(watcher, "_startup_scan_priority", 5),
|
|
375
|
+
)
|
|
376
|
+
enqueued += 1
|
|
377
|
+
except Exception as e:
|
|
378
|
+
if verbose:
|
|
379
|
+
console.print(f" [yellow]Failed to enqueue {session_file}: {e}[/yellow]")
|
|
380
|
+
continue
|
|
381
|
+
return len(candidates), enqueued
|
|
382
|
+
finally:
|
|
383
|
+
db.close()
|
|
384
|
+
|
|
385
|
+
|
|
305
386
|
def run_doctor(
|
|
306
387
|
*,
|
|
307
388
|
restart_daemons: bool,
|
|
@@ -389,6 +470,34 @@ def run_doctor(
|
|
|
389
470
|
if hooks_failed:
|
|
390
471
|
console.print(f" [yellow]![/yellow] Failed hooks: {', '.join(hooks_failed)}")
|
|
391
472
|
|
|
473
|
+
# 3b. Update Codex notify hook
|
|
474
|
+
console.print("\n[bold]3b. Updating Codex notify hook...[/bold]")
|
|
475
|
+
try:
|
|
476
|
+
ok_global, ok_homes = _update_codex_notify_hook(verbose=verbose)
|
|
477
|
+
if ok_global:
|
|
478
|
+
console.print(" [green]✓[/green] Updated global Codex config (~/.codex)")
|
|
479
|
+
else:
|
|
480
|
+
console.print(" [dim]Global Codex config not updated (may be missing or unwritable)[/dim]")
|
|
481
|
+
if ok_homes:
|
|
482
|
+
console.print(f" [green]✓[/green] Updated {ok_homes} Aline CODEX_HOME(s)")
|
|
483
|
+
else:
|
|
484
|
+
console.print(" [dim]No Aline CODEX_HOME(s) updated[/dim]")
|
|
485
|
+
|
|
486
|
+
# If Codex exists but is legacy, warn: Aline expects the Rust notify hook.
|
|
487
|
+
try:
|
|
488
|
+
from ..codex_hooks.notify_hook_installer import codex_cli_supports_notify_hook
|
|
489
|
+
|
|
490
|
+
supported = codex_cli_supports_notify_hook()
|
|
491
|
+
if supported is False:
|
|
492
|
+
console.print(" [yellow]![/yellow] Codex CLI does not support notify hook.")
|
|
493
|
+
console.print(
|
|
494
|
+
" [dim]Tip: update to the Rust Codex CLI to enable reliable, event-driven Codex imports.[/dim]"
|
|
495
|
+
)
|
|
496
|
+
except Exception:
|
|
497
|
+
pass
|
|
498
|
+
except Exception as e:
|
|
499
|
+
console.print(f" [yellow]![/yellow] Codex notify hook update failed: {e}")
|
|
500
|
+
|
|
392
501
|
# 4. Update skills
|
|
393
502
|
console.print("\n[bold]4. Updating skills...[/bold]")
|
|
394
503
|
try:
|
|
@@ -446,6 +555,22 @@ def run_doctor(
|
|
|
446
555
|
except Exception as e:
|
|
447
556
|
console.print(f" [yellow]![/yellow] Failed to repair associations: {e}")
|
|
448
557
|
|
|
558
|
+
# 5c. Check backlog sessions (watcher startup scan semantics)
|
|
559
|
+
console.print("\n[bold]5c. Checking watcher backlog sessions...[/bold]")
|
|
560
|
+
try:
|
|
561
|
+
backlog_count, _ = _check_watcher_backlog(config, verbose=verbose, fix=False)
|
|
562
|
+
if backlog_count == 0:
|
|
563
|
+
console.print(" [green]✓[/green] No backlog sessions detected")
|
|
564
|
+
else:
|
|
565
|
+
console.print(f" [yellow]![/yellow] Found {backlog_count} backlog session(s) to process")
|
|
566
|
+
if auto_fix or typer.confirm("\n Do you want to enqueue these for processing now?", default=True):
|
|
567
|
+
_, enqueued = _check_watcher_backlog(config, verbose=verbose, fix=True)
|
|
568
|
+
console.print(f" [green]✓[/green] Enqueued {enqueued} session_process job(s)")
|
|
569
|
+
else:
|
|
570
|
+
console.print(" [dim]Skipped enqueueing backlog sessions[/dim]")
|
|
571
|
+
except Exception as e:
|
|
572
|
+
console.print(f" [yellow]![/yellow] Failed to check watcher backlog: {e}")
|
|
573
|
+
|
|
449
574
|
# 6. Restart/ensure daemons
|
|
450
575
|
if restart_daemons:
|
|
451
576
|
console.print("\n[bold]6. Checking daemons...[/bold]")
|
|
@@ -47,6 +47,7 @@ else:
|
|
|
47
47
|
def download_share_data(
|
|
48
48
|
share_url: str,
|
|
49
49
|
password: Optional[str] = None,
|
|
50
|
+
cache_buster: Optional[str] = None,
|
|
50
51
|
) -> Dict[str, Any]:
|
|
51
52
|
"""
|
|
52
53
|
Download share data from a share URL.
|
|
@@ -62,7 +63,10 @@ def download_share_data(
|
|
|
62
63
|
{"success": False, "error": str} on failure
|
|
63
64
|
"""
|
|
64
65
|
if not HTTPX_AVAILABLE:
|
|
65
|
-
return {
|
|
66
|
+
return {
|
|
67
|
+
"success": False,
|
|
68
|
+
"error": "httpx package not installed. Install with: pip install httpx",
|
|
69
|
+
}
|
|
66
70
|
|
|
67
71
|
share_id = extract_share_id(share_url)
|
|
68
72
|
if not share_id:
|
|
@@ -101,14 +105,16 @@ def download_share_data(
|
|
|
101
105
|
|
|
102
106
|
# Download export data
|
|
103
107
|
try:
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
108
|
+
export_url = f"{backend_url}/api/share/{share_id}/export"
|
|
109
|
+
if cache_buster:
|
|
110
|
+
export_url = f"{export_url}?cache_bust={cache_buster}"
|
|
111
|
+
|
|
112
|
+
export_response = httpx.get(export_url, headers=headers, timeout=30.0)
|
|
107
113
|
export_data = export_response.json()
|
|
108
114
|
|
|
109
115
|
if export_response.status_code == 413 or export_data.get("needs_chunked_download"):
|
|
110
116
|
total_chunks = export_data.get("total_chunks", 1)
|
|
111
|
-
raw_data = _download_chunks(backend_url, share_id, headers, total_chunks)
|
|
117
|
+
raw_data = _download_chunks(backend_url, share_id, headers, total_chunks, cache_buster)
|
|
112
118
|
conversation_data = json.loads(raw_data)
|
|
113
119
|
export_data = {
|
|
114
120
|
"success": True,
|
|
@@ -494,7 +500,9 @@ def import_v2_data(
|
|
|
494
500
|
share_url=share_url,
|
|
495
501
|
commit_hashes=[],
|
|
496
502
|
# V18: user identity (with backward compatibility for old format)
|
|
497
|
-
created_by=event_data.get("created_by")
|
|
503
|
+
created_by=event_data.get("created_by")
|
|
504
|
+
or event_data.get("uid")
|
|
505
|
+
or event_data.get("creator_id"),
|
|
498
506
|
shared_by=config.uid, # Current user is the importer
|
|
499
507
|
)
|
|
500
508
|
|
|
@@ -603,7 +611,9 @@ def import_session_with_turns(
|
|
|
603
611
|
summary_locked_until=None,
|
|
604
612
|
summary_error=None,
|
|
605
613
|
# V18: user identity (with backward compatibility for old format)
|
|
606
|
-
created_by=session_data.get("created_by")
|
|
614
|
+
created_by=session_data.get("created_by")
|
|
615
|
+
or session_data.get("uid")
|
|
616
|
+
or session_data.get("creator_id"),
|
|
607
617
|
shared_by=config.uid, # Current user is the importer
|
|
608
618
|
)
|
|
609
619
|
|
|
@@ -833,7 +843,11 @@ def generate_content_hash(messages: List[Dict]) -> str:
|
|
|
833
843
|
|
|
834
844
|
|
|
835
845
|
def _download_chunks(
|
|
836
|
-
backend_url: str,
|
|
846
|
+
backend_url: str,
|
|
847
|
+
share_id: str,
|
|
848
|
+
headers: Dict[str, str],
|
|
849
|
+
total_chunks: int,
|
|
850
|
+
cache_buster: Optional[str] = None,
|
|
837
851
|
) -> str:
|
|
838
852
|
"""
|
|
839
853
|
Download data in chunks and combine them.
|
|
@@ -854,8 +868,11 @@ def _download_chunks(
|
|
|
854
868
|
task = progress.add_task("[cyan]Downloading chunks...", total=total_chunks)
|
|
855
869
|
|
|
856
870
|
for i in range(total_chunks):
|
|
871
|
+
url = f"{backend_url}/api/share/{share_id}/export?chunk={i}"
|
|
872
|
+
if cache_buster:
|
|
873
|
+
url = f"{url}&cache_bust={cache_buster}"
|
|
857
874
|
chunk_response = httpx.get(
|
|
858
|
-
|
|
875
|
+
url,
|
|
859
876
|
headers=headers,
|
|
860
877
|
timeout=60.0,
|
|
861
878
|
)
|
|
@@ -876,8 +893,11 @@ def _download_chunks(
|
|
|
876
893
|
for i in range(total_chunks):
|
|
877
894
|
print(f"Downloading chunk {i + 1}/{total_chunks}...")
|
|
878
895
|
|
|
896
|
+
url = f"{backend_url}/api/share/{share_id}/export?chunk={i}"
|
|
897
|
+
if cache_buster:
|
|
898
|
+
url = f"{url}&cache_bust={cache_buster}"
|
|
879
899
|
chunk_response = httpx.get(
|
|
880
|
-
|
|
900
|
+
url,
|
|
881
901
|
headers=headers,
|
|
882
902
|
timeout=60.0,
|
|
883
903
|
)
|
realign/commands/init.py
CHANGED
|
@@ -836,6 +836,22 @@ def init_command(
|
|
|
836
836
|
console.print(f" Tmux: [cyan]{result.get('tmux_conf', 'N/A')}[/cyan]")
|
|
837
837
|
console.print(f" Skills: [cyan]{result.get('skills_path', 'N/A')}[/cyan]")
|
|
838
838
|
|
|
839
|
+
# Codex compatibility note (best-effort).
|
|
840
|
+
# We rely on the Rust Codex CLI notify hook to avoid expensive polling. If the installed
|
|
841
|
+
# Codex binary is legacy/unsupported, warn and suggest upgrading.
|
|
842
|
+
try:
|
|
843
|
+
from ..codex_hooks.notify_hook_installer import codex_cli_supports_notify_hook
|
|
844
|
+
|
|
845
|
+
supported = codex_cli_supports_notify_hook()
|
|
846
|
+
if supported is False:
|
|
847
|
+
console.print("\n[yellow]![/yellow] Codex CLI detected but does not support notify hook.")
|
|
848
|
+
console.print(
|
|
849
|
+
"[dim]Tip: update to the Rust Codex CLI to enable reliable, event-driven Codex imports (no polling).[/dim]"
|
|
850
|
+
)
|
|
851
|
+
# If Codex isn't installed (None), stay silent.
|
|
852
|
+
except Exception:
|
|
853
|
+
pass
|
|
854
|
+
|
|
839
855
|
hooks_installed = result.get("hooks_installed") or []
|
|
840
856
|
if hooks_installed:
|
|
841
857
|
console.print(f" Hooks: [cyan]{', '.join(hooks_installed)}[/cyan]")
|