aline-ai 0.5.3__py3-none-any.whl → 0.5.4__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.5.3.dist-info → aline_ai-0.5.4.dist-info}/METADATA +1 -1
- {aline_ai-0.5.3.dist-info → aline_ai-0.5.4.dist-info}/RECORD +12 -12
- realign/__init__.py +1 -1
- realign/claude_hooks/permission_request_hook.py +35 -0
- realign/cli.py +17 -0
- realign/commands/add.py +303 -0
- realign/dashboard/tmux_manager.py +41 -17
- realign/dashboard/widgets/terminal_panel.py +146 -0
- {aline_ai-0.5.3.dist-info → aline_ai-0.5.4.dist-info}/WHEEL +0 -0
- {aline_ai-0.5.3.dist-info → aline_ai-0.5.4.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.5.3.dist-info → aline_ai-0.5.4.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.5.3.dist-info → aline_ai-0.5.4.dist-info}/top_level.txt +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
aline_ai-0.5.
|
|
2
|
-
realign/__init__.py,sha256=
|
|
1
|
+
aline_ai-0.5.4.dist-info/licenses/LICENSE,sha256=H8wTqV5IF1oHw_HbBtS1PSDU8G_q81yblEIL_JfV8Vo,1077
|
|
2
|
+
realign/__init__.py,sha256=IahJlCDpOv2BJxOCpmx8UfhzQxweVN7I9N7YQyT7zMo,1623
|
|
3
3
|
realign/claude_detector.py,sha256=hU5OcFO7JH9BCVbmajAmz4TIP-EQuvz9VlpsRYuSoVM,2792
|
|
4
|
-
realign/cli.py,sha256=
|
|
4
|
+
realign/cli.py,sha256=81zMPACOurb9YzDKdhHyNmnye5lHOv3dbNSuXmKqJ7Y,29783
|
|
5
5
|
realign/codex_detector.py,sha256=vDjRWHycjzX9Pavv8IwrznMf5oyFKBn4FvqYYy3I0s4,4141
|
|
6
6
|
realign/config.py,sha256=fDwXstNF80yNSUOtNJYAqkDEWZOQkzNC7cN0-_2W0KU,12223
|
|
7
7
|
realign/context.py,sha256=ttQL4_Q9FNv6JA85aRslBuu97LeJPoKAgRbShaj_UiU,10006
|
|
@@ -24,7 +24,7 @@ realign/adapters/codex.py,sha256=o9XyZEPDVqskHgXSkCDovu3yJM8x6HTIIobU90MGE3s,207
|
|
|
24
24
|
realign/adapters/gemini.py,sha256=Oaucgz6l_6Cb0GDdt0ant_Pun6B1CfrDMN9B4irIHXc,2636
|
|
25
25
|
realign/adapters/registry.py,sha256=gJf9MSfd0clt653eBfcM17snrSDXeQDwVXal0N2NrHo,3303
|
|
26
26
|
realign/claude_hooks/__init__.py,sha256=-2CiH5UIjPQzok2pBQ8yIVXB5oFxkzLhFXQ8NDDVy0c,594
|
|
27
|
-
realign/claude_hooks/permission_request_hook.py,sha256=
|
|
27
|
+
realign/claude_hooks/permission_request_hook.py,sha256=jMN7UtL6bMqHObUCP5A5ysvFrooDEcd9KxtmF2-3nCw,6448
|
|
28
28
|
realign/claude_hooks/permission_request_hook_installer.py,sha256=B05ey_7OT3tlJBGzBlmy6DFJdW3OGMZCCrk1HSXl0Cs,7800
|
|
29
29
|
realign/claude_hooks/stop_hook.py,sha256=bcWr9jCDZErijSzVSP-kdfmoZ6DHYc6hkRaJ_A2PmQ8,11989
|
|
30
30
|
realign/claude_hooks/stop_hook_installer.py,sha256=BjzabUrAPLCA0m_8ZQWqDW4eCujdPTzlwUNdVGxtRk4,7238
|
|
@@ -32,7 +32,7 @@ realign/claude_hooks/terminal_state.py,sha256=ZvdQ-ZmqEltdMoNk3lXVsbpvbAQEmf2hxT
|
|
|
32
32
|
realign/claude_hooks/user_prompt_submit_hook.py,sha256=WD-UavhBTueN2TPfnZrnPC7DFYGEeptjUEF21EJn7Qo,10312
|
|
33
33
|
realign/claude_hooks/user_prompt_submit_hook_installer.py,sha256=2xLF8yZcE7Iwib9gU-xCkA1NWxNH9Nc5CFKPYK7rtXw,5371
|
|
34
34
|
realign/commands/__init__.py,sha256=sx_ck55oxaoiF4N3LugG0ZXwonUDxeEZ5uHbBKCC7K8,89
|
|
35
|
-
realign/commands/add.py,sha256=
|
|
35
|
+
realign/commands/add.py,sha256=nkkHETnNprCMKoJuqirddVzJjAiLew7oatLSt3kJOV4,23620
|
|
36
36
|
realign/commands/config.py,sha256=rVwWUgLQDoRh25bjNzsN2eC77aiaPB5D77UtxgS3RlY,6798
|
|
37
37
|
realign/commands/context.py,sha256=tD5jQG4kXPfV57PrS1Du3JUzvY6RCbNbxsOQWdomrPg,7057
|
|
38
38
|
realign/commands/export_shares.py,sha256=6omUtPK8OJsWfrzeQAx5LbM8-lOiONZtWa9btpVumVQ,136179
|
|
@@ -45,7 +45,7 @@ realign/commands/watcher.py,sha256=6EBzIc439ClqS4UG8iGd_tfb5MQMPeieAgjCc-M-NEI,1
|
|
|
45
45
|
realign/commands/worker.py,sha256=LErEjB9T9_XatuLTS9Wn0BbSFc3ah0O9lFTzQOin4qU,22673
|
|
46
46
|
realign/dashboard/__init__.py,sha256=QZkHTsGityH8UkF8rmvA3xW7dMXNe0swEWr443qfgCM,128
|
|
47
47
|
realign/dashboard/app.py,sha256=SOlQ6QHt01-Jj5Daa5xyigrNhR-gXC3jupH24lcbeLY,11935
|
|
48
|
-
realign/dashboard/tmux_manager.py,sha256=
|
|
48
|
+
realign/dashboard/tmux_manager.py,sha256=zptH81f62tita2h0Yj3HUMcbBQoDNxHDlID4s6KQXAk,22022
|
|
49
49
|
realign/dashboard/screens/__init__.py,sha256=x42K31sqL5KVMtufOnZjG8LnFN7hQyN5-z8CySqbwlM,304
|
|
50
50
|
realign/dashboard/screens/create_event.py,sha256=nPMZMOekduxLXBTjMmLJEQMhb33RXWRIY0uHbD5fmmM,5484
|
|
51
51
|
realign/dashboard/screens/event_detail.py,sha256=WJFO7pryO0DZIMtyA-IOriFYtSLZ5Ri5AVSzjYiW1BQ,20842
|
|
@@ -59,7 +59,7 @@ realign/dashboard/widgets/header.py,sha256=ESejMT53T3sbtRrlCGJk8smUv0ts8binkshzC
|
|
|
59
59
|
realign/dashboard/widgets/openable_table.py,sha256=GeJPDEYp0kRHShqvmPMzAePpYXRZHUNqcWNnxqsqxjA,1963
|
|
60
60
|
realign/dashboard/widgets/search_panel.py,sha256=D0NXVIdNXpcnnVETfA44Muue4CyZq5XkMj4vfpNb3LQ,8635
|
|
61
61
|
realign/dashboard/widgets/sessions_table.py,sha256=CfSy93AbAqGGDFxEQW4u59twZDoVnHyuMmPcSOZTiW4,21852
|
|
62
|
-
realign/dashboard/widgets/terminal_panel.py,sha256=
|
|
62
|
+
realign/dashboard/widgets/terminal_panel.py,sha256=obVB9ONnR2Voun_druPiyVG-hd6gf4idXN9iOU84Jq4,29704
|
|
63
63
|
realign/dashboard/widgets/watcher_panel.py,sha256=ThQfVi_GOP-KR1GDNh5WsSmPJ11TEiP_fuuyxd2sdEk,19662
|
|
64
64
|
realign/dashboard/widgets/worker_panel.py,sha256=ufJYpW0nIPcek7GHa1nuMkrpNU9AY0WvtnWzhNZneW0,18547
|
|
65
65
|
realign/db/__init__.py,sha256=dqFKTskcVA7qCn7JAXeUz_c5T0_g--e9BAVokmk2Ys0,1874
|
|
@@ -86,8 +86,8 @@ realign/triggers/next_turn_trigger.py,sha256=CU6jsIxA_uoV-ROIke0iwEh4-on8vuataLG
|
|
|
86
86
|
realign/triggers/registry.py,sha256=AVlMm5xjzWLCnJMuzvfw4hMdNGlLqSTsgd3VCOZ-cHs,3799
|
|
87
87
|
realign/triggers/turn_status.py,sha256=tPHUB3NnnBfMVCIYdrFt5W1840IUGEZ-3v2GEtC981g,5987
|
|
88
88
|
realign/triggers/turn_summary.py,sha256=f3hEUshgv9skJ9AbfWpoYs417lsv_HK2A_vpPjgryO4,4467
|
|
89
|
-
aline_ai-0.5.
|
|
90
|
-
aline_ai-0.5.
|
|
91
|
-
aline_ai-0.5.
|
|
92
|
-
aline_ai-0.5.
|
|
93
|
-
aline_ai-0.5.
|
|
89
|
+
aline_ai-0.5.4.dist-info/METADATA,sha256=hpo9KnzjijOm4LWcK9gS851AwfbHhETS3DFarY-FONk,1597
|
|
90
|
+
aline_ai-0.5.4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
91
|
+
aline_ai-0.5.4.dist-info/entry_points.txt,sha256=TvYELpMoWsUTcQdMV8tBHxCbEf_LbK4sESqK3r8PM6Y,78
|
|
92
|
+
aline_ai-0.5.4.dist-info/top_level.txt,sha256=yIL3s2xv9nf1GwD5n71Aq_JEIV4AfzCIDNKBzewuRm4,8
|
|
93
|
+
aline_ai-0.5.4.dist-info/RECORD,,
|
realign/__init__.py
CHANGED
|
@@ -25,7 +25,37 @@ stdin JSON format:
|
|
|
25
25
|
import os
|
|
26
26
|
import sys
|
|
27
27
|
import json
|
|
28
|
+
import time
|
|
28
29
|
import subprocess
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_signal_dir() -> Path:
|
|
34
|
+
"""Get the signal directory for permission requests."""
|
|
35
|
+
signal_dir = Path.home() / ".aline" / ".signals" / "permission_request"
|
|
36
|
+
signal_dir.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
return signal_dir
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def write_signal_file(terminal_id: str, tool_name: str = "") -> None:
|
|
41
|
+
"""Write a signal file to notify the dashboard of a permission request."""
|
|
42
|
+
try:
|
|
43
|
+
signal_dir = get_signal_dir()
|
|
44
|
+
timestamp_ms = int(time.time() * 1000)
|
|
45
|
+
signal_file = signal_dir / f"{terminal_id}_{timestamp_ms}.signal"
|
|
46
|
+
tmp_file = signal_dir / f"{terminal_id}_{timestamp_ms}.signal.tmp"
|
|
47
|
+
|
|
48
|
+
signal_data = {
|
|
49
|
+
"terminal_id": terminal_id,
|
|
50
|
+
"tool_name": tool_name,
|
|
51
|
+
"timestamp": time.time(),
|
|
52
|
+
"hook_event": "PermissionRequest",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
tmp_file.write_text(json.dumps(signal_data, indent=2))
|
|
56
|
+
tmp_file.replace(signal_file)
|
|
57
|
+
except Exception:
|
|
58
|
+
pass # Best effort
|
|
29
59
|
|
|
30
60
|
|
|
31
61
|
def main():
|
|
@@ -142,6 +172,11 @@ def main():
|
|
|
142
172
|
except Exception:
|
|
143
173
|
pass
|
|
144
174
|
|
|
175
|
+
# Write signal file to notify dashboard (triggers file watcher refresh)
|
|
176
|
+
if terminal_id:
|
|
177
|
+
tool_name = data.get("tool_name", "")
|
|
178
|
+
write_signal_file(terminal_id, tool_name)
|
|
179
|
+
|
|
145
180
|
# Exit 0 - don't block the permission request
|
|
146
181
|
sys.exit(0)
|
|
147
182
|
|
realign/cli.py
CHANGED
|
@@ -157,6 +157,23 @@ def add_skills_cli(
|
|
|
157
157
|
raise typer.Exit(code=exit_code)
|
|
158
158
|
|
|
159
159
|
|
|
160
|
+
@add_app.command(name="skills-dev")
|
|
161
|
+
def add_skills_dev_cli(
|
|
162
|
+
force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing skills"),
|
|
163
|
+
):
|
|
164
|
+
"""Install developer skills from skill-dev/ directory.
|
|
165
|
+
|
|
166
|
+
Scans skill-dev/ for SKILL.md files and installs them to ~/.claude/skills/.
|
|
167
|
+
This is for developer use only.
|
|
168
|
+
|
|
169
|
+
Examples:
|
|
170
|
+
aline add skills-dev # Install dev skills
|
|
171
|
+
aline add skills-dev --force # Reinstall/update dev skills
|
|
172
|
+
"""
|
|
173
|
+
exit_code = add.add_skills_dev_command(force=force)
|
|
174
|
+
raise typer.Exit(code=exit_code)
|
|
175
|
+
|
|
176
|
+
|
|
160
177
|
@context_app.command(name="load")
|
|
161
178
|
def context_load_cli(
|
|
162
179
|
sessions: Optional[str] = typer.Option(
|
realign/commands/add.py
CHANGED
|
@@ -251,10 +251,233 @@ Use this skill when the user wants to:
|
|
|
251
251
|
- Send Slack updates about completed work
|
|
252
252
|
"""
|
|
253
253
|
|
|
254
|
+
# Aline Import History Sessions skill definition for Claude Code
|
|
255
|
+
# Installed to ~/.claude/skills/aline-import-history-sessions/SKILL.md
|
|
256
|
+
ALINE_IMPORT_HISTORY_SESSIONS_SKILL_MD = """---
|
|
257
|
+
name: aline-import-history-sessions
|
|
258
|
+
description: Guide users through importing Claude Code session history into Aline database. Use this for first-time setup, onboarding new users, or when users want to selectively import historical sessions. Provides interactive workflow with progress checking.
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
# Aline Import History Sessions Skill
|
|
262
|
+
|
|
263
|
+
This skill guides users through the process of importing Claude Code session history into Aline's database. It provides an interactive, step-by-step workflow to help users discover, select, and import their historical sessions.
|
|
264
|
+
|
|
265
|
+
## Workflow Overview
|
|
266
|
+
|
|
267
|
+
```
|
|
268
|
+
Analyze Unimported Sessions → Present Summary → User Selection → Import Sessions → Verify Success → Continue or Finish
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Step-by-Step Guide
|
|
272
|
+
|
|
273
|
+
### Step 1: Analyze Current Status
|
|
274
|
+
|
|
275
|
+
First, list all sessions to understand what hasn't been imported yet:
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
aline watcher session list --detect-turns
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
**Internal analysis (do NOT expose status terminology to user):**
|
|
282
|
+
- Count sessions with status `new` → these are "unimported sessions"
|
|
283
|
+
- Count sessions with status `partial` → these have "updates available"
|
|
284
|
+
- Count sessions with status `tracked` → these are "already imported"
|
|
285
|
+
|
|
286
|
+
Parse the output to extract:
|
|
287
|
+
- Total number of unimported sessions
|
|
288
|
+
- Their session IDs (use these for import, NOT index numbers)
|
|
289
|
+
- Project paths they belong to
|
|
290
|
+
|
|
291
|
+
### Step 2: Present Summary to User
|
|
292
|
+
|
|
293
|
+
Present a user-friendly summary WITHOUT mentioning internal status labels:
|
|
294
|
+
|
|
295
|
+
Example:
|
|
296
|
+
> "I found **47 sessions** in your Claude Code history:
|
|
297
|
+
> - **12 sessions** haven't been imported yet
|
|
298
|
+
> - **3 sessions** have updates since last import
|
|
299
|
+
> - **32 sessions** are already fully imported
|
|
300
|
+
>
|
|
301
|
+
> The unimported sessions span these projects:
|
|
302
|
+
> - `/Users/you/Projects/ProjectA` (5 sessions)
|
|
303
|
+
> - `/Users/you/Projects/ProjectB` (7 sessions)"
|
|
304
|
+
|
|
305
|
+
### Step 3: Ask User Import Preferences
|
|
306
|
+
|
|
307
|
+
Use `AskUserQuestion` to understand what the user wants to import:
|
|
308
|
+
|
|
309
|
+
```json
|
|
310
|
+
{
|
|
311
|
+
"questions": [{
|
|
312
|
+
"header": "Import scope",
|
|
313
|
+
"question": "Which sessions would you like to import?",
|
|
314
|
+
"options": [
|
|
315
|
+
{"label": "All unimported (Recommended)", "description": "Import all 12 sessions that haven't been imported yet"},
|
|
316
|
+
{"label": "Include updates", "description": "Import unimported sessions + update 3 sessions with new content"},
|
|
317
|
+
{"label": "Select by project", "description": "Choose which project's sessions to import"},
|
|
318
|
+
{"label": "Select specific", "description": "I'll review and pick individual sessions"}
|
|
319
|
+
],
|
|
320
|
+
"multiSelect": false
|
|
321
|
+
}]
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Step 4: Handle User Selection
|
|
326
|
+
|
|
327
|
+
#### If "All unimported":
|
|
328
|
+
Confirm the import with session count:
|
|
329
|
+
```json
|
|
330
|
+
{
|
|
331
|
+
"questions": [{
|
|
332
|
+
"header": "Confirm",
|
|
333
|
+
"question": "Ready to import 12 sessions. Proceed?",
|
|
334
|
+
"options": [
|
|
335
|
+
{"label": "Yes, import", "description": "Start importing all unimported sessions"},
|
|
336
|
+
{"label": "Let me review first", "description": "Show me the session list to review"}
|
|
337
|
+
],
|
|
338
|
+
"multiSelect": false
|
|
339
|
+
}]
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
#### If "Select by project":
|
|
344
|
+
List the projects with unimported sessions and ask:
|
|
345
|
+
```json
|
|
346
|
+
{
|
|
347
|
+
"questions": [{
|
|
348
|
+
"header": "Project",
|
|
349
|
+
"question": "Which project's sessions should I import?",
|
|
350
|
+
"options": [
|
|
351
|
+
{"label": "ProjectA", "description": "5 unimported sessions"},
|
|
352
|
+
{"label": "ProjectB", "description": "7 unimported sessions"},
|
|
353
|
+
{"label": "Current directory", "description": "Import sessions from the current working directory"}
|
|
354
|
+
],
|
|
355
|
+
"multiSelect": true
|
|
356
|
+
}]
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
#### If "Select specific":
|
|
361
|
+
Show the session list with details (project path, turn count, last modified) and let user specify which ones. When user provides selection, map their choice back to session IDs.
|
|
362
|
+
|
|
363
|
+
### Step 5: Execute Import
|
|
364
|
+
|
|
365
|
+
**IMPORTANT: Always use session_id for imports, NOT index numbers.** Index numbers can change between list operations and cause wrong sessions to be imported.
|
|
366
|
+
|
|
367
|
+
```bash
|
|
368
|
+
# Import by session ID (PREFERRED - always use this)
|
|
369
|
+
aline watcher session import abc12345-6789-...
|
|
370
|
+
|
|
371
|
+
# Import multiple by session ID
|
|
372
|
+
aline watcher session import abc12345,def67890,ghi11111
|
|
373
|
+
|
|
374
|
+
# With force flag (re-import already tracked)
|
|
375
|
+
aline watcher session import abc12345 --force
|
|
376
|
+
|
|
377
|
+
# With regenerate flag (update summaries)
|
|
378
|
+
aline watcher session import abc12345 --regenerate
|
|
379
|
+
|
|
380
|
+
# Synchronous import to wait for completion
|
|
381
|
+
aline watcher session import abc12345 --sync
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
For importing multiple sessions, collect all the session IDs from your analysis and pass them comma-separated.
|
|
385
|
+
|
|
386
|
+
### Step 6: Verify Import Success
|
|
387
|
+
|
|
388
|
+
After import, check the status again:
|
|
389
|
+
|
|
390
|
+
```bash
|
|
391
|
+
aline watcher session list --detect-turns
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
Verify:
|
|
395
|
+
- Previously unimported sessions should now show as imported
|
|
396
|
+
- Check for any errors in the import output
|
|
397
|
+
- Report success/failure count to user
|
|
398
|
+
|
|
399
|
+
### Step 7: Ask If User Is Satisfied
|
|
400
|
+
|
|
401
|
+
```json
|
|
402
|
+
{
|
|
403
|
+
"questions": [{
|
|
404
|
+
"header": "Continue?",
|
|
405
|
+
"question": "Successfully imported X sessions. What would you like to do next?",
|
|
406
|
+
"options": [
|
|
407
|
+
{"label": "Import more", "description": "Select additional sessions to import"},
|
|
408
|
+
{"label": "View imported", "description": "Show details of imported sessions"},
|
|
409
|
+
{"label": "Done", "description": "Finish the import process"}
|
|
410
|
+
],
|
|
411
|
+
"multiSelect": false
|
|
412
|
+
}]
|
|
413
|
+
}
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
If user wants to import more, loop back to Step 1.
|
|
417
|
+
|
|
418
|
+
### Step 8: Final Summary
|
|
419
|
+
|
|
420
|
+
When the user is done, provide a summary:
|
|
421
|
+
- Total sessions imported in this session
|
|
422
|
+
- Sessions that were updated
|
|
423
|
+
- Any errors encountered
|
|
424
|
+
- Next steps: suggest `aline search` to explore their imported history
|
|
425
|
+
|
|
426
|
+
## Command Reference
|
|
427
|
+
|
|
428
|
+
| Command | Purpose |
|
|
429
|
+
|---------|---------|
|
|
430
|
+
| `aline watcher session list` | List all discovered sessions with status |
|
|
431
|
+
| `aline watcher session list --detect-turns` | Include turn counts in listing |
|
|
432
|
+
| `aline watcher session list -p N -n M` | Paginate: page N with M items per page |
|
|
433
|
+
| `aline watcher session import <session_id>` | Import by session ID (recommended) |
|
|
434
|
+
| `aline watcher session import <id1>,<id2>` | Import multiple sessions by ID |
|
|
435
|
+
| `aline watcher session import <id> -f` | Force re-import |
|
|
436
|
+
| `aline watcher session import <id> -r` | Regenerate LLM summaries |
|
|
437
|
+
| `aline watcher session import <id> --sync` | Synchronous import (wait for completion) |
|
|
438
|
+
| `aline watcher session show <session_id>` | View details of a specific session |
|
|
439
|
+
|
|
440
|
+
## Important: Use Session IDs, Not Index Numbers
|
|
441
|
+
|
|
442
|
+
**Always use session_id (UUID) for import operations.**
|
|
443
|
+
|
|
444
|
+
Why:
|
|
445
|
+
- Index numbers are assigned dynamically and can change between list commands
|
|
446
|
+
- Using wrong index could import unintended sessions
|
|
447
|
+
- Session IDs are stable and unique identifiers
|
|
448
|
+
|
|
449
|
+
Example session ID format: `e58f67bf-ebba-47bd-9371-5ef9e06697d3`
|
|
450
|
+
|
|
451
|
+
You can use UUID prefix for convenience: `e58f67bf` (first 8 characters)
|
|
452
|
+
|
|
453
|
+
## Error Handling
|
|
454
|
+
|
|
455
|
+
- **No sessions found**: Check if Claude Code history exists at `~/.claude/projects/`. Suggest running some Claude Code sessions first.
|
|
456
|
+
- **Import fails**: Check disk space, database permissions at `~/.aline/db/aline.db`
|
|
457
|
+
- **Partial import**: Some sessions may fail due to corrupted JSONL files. Report specific errors and suggest `--force` retry.
|
|
458
|
+
- **Watcher not running**: If async import doesn't complete, suggest `aline watcher start` or use `--sync` flag.
|
|
459
|
+
|
|
460
|
+
## Tips for Large Imports
|
|
461
|
+
|
|
462
|
+
- For many sessions (50+), import in batches to track progress
|
|
463
|
+
- Use `--sync` flag to see real-time progress for smaller batches
|
|
464
|
+
- Check `~/.aline/.logs/watcher.log` for detailed import logs
|
|
465
|
+
|
|
466
|
+
## When to Use This Skill
|
|
467
|
+
|
|
468
|
+
Use this skill when:
|
|
469
|
+
- User is setting up Aline for the first time
|
|
470
|
+
- User wants to import historical Claude Code sessions
|
|
471
|
+
- User asks "how do I import my history?" or similar
|
|
472
|
+
- User wants to selectively import specific project sessions
|
|
473
|
+
- User needs to re-import or update existing sessions
|
|
474
|
+
"""
|
|
475
|
+
|
|
254
476
|
# Registry of all skills to install
|
|
255
477
|
SKILLS_REGISTRY: dict[str, str] = {
|
|
256
478
|
"aline": ALINE_SKILL_MD,
|
|
257
479
|
"aline-share": ALINE_SHARE_SKILL_MD,
|
|
480
|
+
"aline-import-history-sessions": ALINE_IMPORT_HISTORY_SESSIONS_SKILL_MD,
|
|
258
481
|
}
|
|
259
482
|
|
|
260
483
|
|
|
@@ -365,3 +588,83 @@ def add_skills_command(force: bool = False) -> int:
|
|
|
365
588
|
|
|
366
589
|
return 1 if failed_skills else 0
|
|
367
590
|
|
|
591
|
+
|
|
592
|
+
def add_skills_dev_command(force: bool = False) -> int:
|
|
593
|
+
"""Install developer skills from skill-dev/ directory.
|
|
594
|
+
|
|
595
|
+
Scans the skill-dev/ folder in the project root for SKILL.md files
|
|
596
|
+
and installs them to ~/.claude/skills/
|
|
597
|
+
|
|
598
|
+
This is for developer use only - skills in development that are not
|
|
599
|
+
yet bundled into the package.
|
|
600
|
+
|
|
601
|
+
Args:
|
|
602
|
+
force: Overwrite existing skills if they exist
|
|
603
|
+
|
|
604
|
+
Returns:
|
|
605
|
+
Exit code (0 for success, 1 for failure)
|
|
606
|
+
"""
|
|
607
|
+
# Find skill-dev directory relative to this file's package location
|
|
608
|
+
# Go up from src/realign/commands/add.py to project root
|
|
609
|
+
package_root = Path(__file__).parent.parent.parent.parent
|
|
610
|
+
skill_dev_dir = package_root / "skill-dev"
|
|
611
|
+
|
|
612
|
+
if not skill_dev_dir.exists():
|
|
613
|
+
console.print(f"[red]skill-dev/ directory not found at:[/red] {skill_dev_dir}")
|
|
614
|
+
console.print("[dim]This command is for developer use only.[/dim]")
|
|
615
|
+
return 1
|
|
616
|
+
|
|
617
|
+
claude_skill_root = Path.home() / ".claude" / "skills"
|
|
618
|
+
installed_skills: list[str] = []
|
|
619
|
+
skipped_skills: list[str] = []
|
|
620
|
+
failed_skills: list[tuple[str, str]] = []
|
|
621
|
+
|
|
622
|
+
# Scan skill-dev for directories containing SKILL.md
|
|
623
|
+
for skill_dir in skill_dev_dir.iterdir():
|
|
624
|
+
if not skill_dir.is_dir():
|
|
625
|
+
continue
|
|
626
|
+
|
|
627
|
+
skill_file = skill_dir / "SKILL.md"
|
|
628
|
+
if not skill_file.exists():
|
|
629
|
+
continue
|
|
630
|
+
|
|
631
|
+
skill_name = skill_dir.name
|
|
632
|
+
dest_path = claude_skill_root / skill_name / "SKILL.md"
|
|
633
|
+
|
|
634
|
+
# Check if skill already exists
|
|
635
|
+
if dest_path.exists() and not force:
|
|
636
|
+
skipped_skills.append(skill_name)
|
|
637
|
+
continue
|
|
638
|
+
|
|
639
|
+
try:
|
|
640
|
+
skill_content = skill_file.read_text(encoding="utf-8")
|
|
641
|
+
_install_skill_to_path(claude_skill_root, skill_name, skill_content)
|
|
642
|
+
installed_skills.append(skill_name)
|
|
643
|
+
except Exception as e:
|
|
644
|
+
failed_skills.append((skill_name, str(e)))
|
|
645
|
+
|
|
646
|
+
if not installed_skills and not skipped_skills and not failed_skills:
|
|
647
|
+
console.print("[yellow]No skills found in skill-dev/[/yellow]")
|
|
648
|
+
console.print("[dim]Each skill should be in its own directory with a SKILL.md file.[/dim]")
|
|
649
|
+
return 0
|
|
650
|
+
|
|
651
|
+
# Report results
|
|
652
|
+
for skill_name in installed_skills:
|
|
653
|
+
skill_path = claude_skill_root / skill_name / "SKILL.md"
|
|
654
|
+
console.print(f"[green]✓[/green] Installed: [cyan]{skill_path}[/cyan]")
|
|
655
|
+
|
|
656
|
+
for skill_name in skipped_skills:
|
|
657
|
+
skill_path = claude_skill_root / skill_name / "SKILL.md"
|
|
658
|
+
console.print(f"[yellow]⊘[/yellow] Already exists: [dim]{skill_path}[/dim]")
|
|
659
|
+
|
|
660
|
+
for skill_name, error in failed_skills:
|
|
661
|
+
console.print(f"[red]✗[/red] Failed to install {skill_name}: {error}")
|
|
662
|
+
|
|
663
|
+
if skipped_skills and not installed_skills:
|
|
664
|
+
console.print("[dim]Use --force to overwrite existing skills[/dim]")
|
|
665
|
+
elif installed_skills:
|
|
666
|
+
skill_names = ", ".join(f"/{s}" for s in installed_skills)
|
|
667
|
+
console.print(f"[dim]Restart Claude Code to activate: {skill_names}[/dim]")
|
|
668
|
+
|
|
669
|
+
return 1 if failed_skills else 0
|
|
670
|
+
|
|
@@ -14,6 +14,7 @@ import shutil
|
|
|
14
14
|
import stat
|
|
15
15
|
import subprocess
|
|
16
16
|
import sys
|
|
17
|
+
import time
|
|
17
18
|
import uuid
|
|
18
19
|
from dataclasses import dataclass
|
|
19
20
|
from pathlib import Path
|
|
@@ -39,6 +40,7 @@ OPT_SESSION_ID = "@aline_session_id"
|
|
|
39
40
|
OPT_TRANSCRIPT_PATH = "@aline_transcript_path"
|
|
40
41
|
OPT_CONTEXT_ID = "@aline_context_id"
|
|
41
42
|
OPT_ATTENTION = "@aline_attention"
|
|
43
|
+
OPT_CREATED_AT = "@aline_created_at"
|
|
42
44
|
|
|
43
45
|
|
|
44
46
|
@dataclass(frozen=True)
|
|
@@ -53,6 +55,7 @@ class InnerWindow:
|
|
|
53
55
|
transcript_path: str | None = None
|
|
54
56
|
context_id: str | None = None
|
|
55
57
|
attention: str | None = None # "permission_request", "stop", or None
|
|
58
|
+
created_at: float | None = None # Unix timestamp when window was created
|
|
56
59
|
|
|
57
60
|
|
|
58
61
|
def tmux_available() -> bool:
|
|
@@ -484,6 +487,8 @@ def list_inner_windows() -> list[InnerWindow]:
|
|
|
484
487
|
+ OPT_CONTEXT_ID
|
|
485
488
|
+ "}\t#{"
|
|
486
489
|
+ OPT_ATTENTION
|
|
490
|
+
+ "}\t#{"
|
|
491
|
+
+ OPT_CREATED_AT
|
|
487
492
|
+ "}",
|
|
488
493
|
],
|
|
489
494
|
capture=True,
|
|
@@ -505,6 +510,13 @@ def list_inner_windows() -> list[InnerWindow]:
|
|
|
505
510
|
transcript_path = parts[7] if len(parts) > 7 and parts[7] else None
|
|
506
511
|
context_id = parts[8] if len(parts) > 8 and parts[8] else None
|
|
507
512
|
attention = parts[9] if len(parts) > 9 and parts[9] else None
|
|
513
|
+
created_at_str = parts[10] if len(parts) > 10 and parts[10] else None
|
|
514
|
+
created_at: float | None = None
|
|
515
|
+
if created_at_str:
|
|
516
|
+
try:
|
|
517
|
+
created_at = float(created_at_str)
|
|
518
|
+
except ValueError:
|
|
519
|
+
pass
|
|
508
520
|
|
|
509
521
|
if terminal_id:
|
|
510
522
|
persisted = state.get(terminal_id) or {}
|
|
@@ -535,8 +547,11 @@ def list_inner_windows() -> list[InnerWindow]:
|
|
|
535
547
|
transcript_path=transcript_path,
|
|
536
548
|
context_id=context_id,
|
|
537
549
|
attention=attention,
|
|
550
|
+
created_at=created_at,
|
|
538
551
|
)
|
|
539
552
|
)
|
|
553
|
+
# Sort by creation time (newest first). Windows without created_at go to the bottom.
|
|
554
|
+
windows.sort(key=lambda w: w.created_at if w.created_at is not None else 0, reverse=True)
|
|
540
555
|
return windows
|
|
541
556
|
|
|
542
557
|
|
|
@@ -551,11 +566,6 @@ def set_inner_window_options(window_id: str, options: dict[str, str]) -> bool:
|
|
|
551
566
|
return ok
|
|
552
567
|
|
|
553
568
|
|
|
554
|
-
def clear_attention(window_id: str) -> bool:
|
|
555
|
-
"""Clear attention state on a window."""
|
|
556
|
-
return set_inner_window_options(window_id, {OPT_ATTENTION: ""})
|
|
557
|
-
|
|
558
|
-
|
|
559
569
|
def kill_inner_window(window_id: str) -> bool:
|
|
560
570
|
if not ensure_inner_session():
|
|
561
571
|
return False
|
|
@@ -576,6 +586,9 @@ def create_inner_window(
|
|
|
576
586
|
existing = list_inner_windows()
|
|
577
587
|
name = _unique_name((w.window_name for w in existing), base_name)
|
|
578
588
|
|
|
589
|
+
# Record creation time before creating the window
|
|
590
|
+
created_at = time.time()
|
|
591
|
+
|
|
579
592
|
proc = _run_inner_tmux(
|
|
580
593
|
[
|
|
581
594
|
"new-window",
|
|
@@ -598,18 +611,18 @@ def create_inner_window(
|
|
|
598
611
|
return None
|
|
599
612
|
window_id, window_name = (created[0].split("\t", 1) + [""])[:2]
|
|
600
613
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
614
|
+
# Always set options including the creation timestamp
|
|
615
|
+
opts: dict[str, str] = {OPT_CREATED_AT: str(created_at)}
|
|
616
|
+
if terminal_id:
|
|
617
|
+
opts[OPT_TERMINAL_ID] = terminal_id
|
|
618
|
+
if provider:
|
|
619
|
+
opts[OPT_PROVIDER] = provider
|
|
620
|
+
if context_id:
|
|
621
|
+
opts[OPT_CONTEXT_ID] = context_id
|
|
622
|
+
opts.setdefault(OPT_SESSION_TYPE, "")
|
|
623
|
+
opts.setdefault(OPT_SESSION_ID, "")
|
|
624
|
+
opts.setdefault(OPT_TRANSCRIPT_PATH, "")
|
|
625
|
+
set_inner_window_options(window_id, opts)
|
|
613
626
|
|
|
614
627
|
_run_inner_tmux(["select-window", "-t", window_id])
|
|
615
628
|
|
|
@@ -620,6 +633,7 @@ def create_inner_window(
|
|
|
620
633
|
terminal_id=terminal_id,
|
|
621
634
|
provider=provider,
|
|
622
635
|
context_id=context_id,
|
|
636
|
+
created_at=created_at,
|
|
623
637
|
)
|
|
624
638
|
|
|
625
639
|
|
|
@@ -629,6 +643,16 @@ def select_inner_window(window_id: str) -> bool:
|
|
|
629
643
|
return _run_inner_tmux(["select-window", "-t", window_id]).returncode == 0
|
|
630
644
|
|
|
631
645
|
|
|
646
|
+
def clear_attention(window_id: str) -> bool:
|
|
647
|
+
"""Clear the attention state for a window (e.g., after user acknowledges permission request)."""
|
|
648
|
+
if not ensure_inner_session():
|
|
649
|
+
return False
|
|
650
|
+
return (
|
|
651
|
+
_run_inner_tmux(["set-option", "-w", "-t", window_id, OPT_ATTENTION, ""]).returncode
|
|
652
|
+
== 0
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
|
|
632
656
|
def get_active_claude_context_id() -> str | None:
|
|
633
657
|
"""Return the active inner tmux window's Claude ALINE_CONTEXT_ID (if any)."""
|
|
634
658
|
try:
|
|
@@ -13,16 +13,127 @@ import re
|
|
|
13
13
|
import shlex
|
|
14
14
|
import subprocess
|
|
15
15
|
from pathlib import Path
|
|
16
|
+
from typing import Callable
|
|
16
17
|
|
|
17
18
|
from textual.app import ComposeResult
|
|
18
19
|
from textual.containers import Container, Horizontal, Vertical, VerticalScroll
|
|
20
|
+
from textual.message import Message
|
|
19
21
|
from textual.widgets import Button, Static
|
|
20
22
|
from rich.text import Text
|
|
21
23
|
|
|
22
24
|
from .. import tmux_manager
|
|
23
25
|
|
|
24
26
|
|
|
27
|
+
# Signal directory for permission request notifications
|
|
28
|
+
PERMISSION_SIGNAL_DIR = Path.home() / ".aline" / ".signals" / "permission_request"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class _SignalFileWatcher:
|
|
32
|
+
"""Watches for new signal files in the permission_request directory.
|
|
33
|
+
|
|
34
|
+
Uses OS-native file watching via asyncio when available,
|
|
35
|
+
otherwise falls back to checking directory mtime.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, callback: Callable[[], None]) -> None:
|
|
39
|
+
self._callback = callback
|
|
40
|
+
self._running = False
|
|
41
|
+
self._task: asyncio.Task | None = None
|
|
42
|
+
self._last_mtime: float = 0
|
|
43
|
+
self._seen_files: set[str] = set()
|
|
44
|
+
|
|
45
|
+
def start(self) -> None:
|
|
46
|
+
if self._running:
|
|
47
|
+
return
|
|
48
|
+
self._running = True
|
|
49
|
+
# Initialize seen files
|
|
50
|
+
self._scan_existing_files()
|
|
51
|
+
self._task = asyncio.create_task(self._watch_loop())
|
|
52
|
+
|
|
53
|
+
def stop(self) -> None:
|
|
54
|
+
self._running = False
|
|
55
|
+
if self._task:
|
|
56
|
+
self._task.cancel()
|
|
57
|
+
self._task = None
|
|
58
|
+
|
|
59
|
+
def _scan_existing_files(self) -> None:
|
|
60
|
+
"""Record existing signal files so we only react to new ones."""
|
|
61
|
+
try:
|
|
62
|
+
if PERMISSION_SIGNAL_DIR.exists():
|
|
63
|
+
self._seen_files = {
|
|
64
|
+
f.name for f in PERMISSION_SIGNAL_DIR.iterdir()
|
|
65
|
+
if f.suffix == ".signal"
|
|
66
|
+
}
|
|
67
|
+
except Exception:
|
|
68
|
+
self._seen_files = set()
|
|
69
|
+
|
|
70
|
+
async def _watch_loop(self) -> None:
|
|
71
|
+
"""Watch for new signal files using directory mtime checks."""
|
|
72
|
+
try:
|
|
73
|
+
while self._running:
|
|
74
|
+
# Wait a bit before checking (reduces CPU usage)
|
|
75
|
+
await asyncio.sleep(0.5)
|
|
76
|
+
|
|
77
|
+
if not self._running:
|
|
78
|
+
break
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
if not PERMISSION_SIGNAL_DIR.exists():
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
# Check if directory was modified
|
|
85
|
+
current_mtime = PERMISSION_SIGNAL_DIR.stat().st_mtime
|
|
86
|
+
if current_mtime <= self._last_mtime:
|
|
87
|
+
continue
|
|
88
|
+
self._last_mtime = current_mtime
|
|
89
|
+
|
|
90
|
+
# Check for new signal files
|
|
91
|
+
current_files = {
|
|
92
|
+
f.name for f in PERMISSION_SIGNAL_DIR.iterdir()
|
|
93
|
+
if f.suffix == ".signal"
|
|
94
|
+
}
|
|
95
|
+
new_files = current_files - self._seen_files
|
|
96
|
+
|
|
97
|
+
if new_files:
|
|
98
|
+
self._seen_files = current_files
|
|
99
|
+
# New signal file detected - trigger callback
|
|
100
|
+
self._callback()
|
|
101
|
+
# Clean up old signal files (keep last 10)
|
|
102
|
+
self._cleanup_old_signals()
|
|
103
|
+
|
|
104
|
+
except Exception:
|
|
105
|
+
pass # Ignore errors, keep watching
|
|
106
|
+
|
|
107
|
+
except asyncio.CancelledError:
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
def _cleanup_old_signals(self) -> None:
|
|
111
|
+
"""Remove old signal files to prevent directory from growing."""
|
|
112
|
+
try:
|
|
113
|
+
if not PERMISSION_SIGNAL_DIR.exists():
|
|
114
|
+
return
|
|
115
|
+
files = sorted(
|
|
116
|
+
PERMISSION_SIGNAL_DIR.glob("*.signal"),
|
|
117
|
+
key=lambda f: f.stat().st_mtime,
|
|
118
|
+
reverse=True
|
|
119
|
+
)
|
|
120
|
+
# Keep only the 10 most recent
|
|
121
|
+
for f in files[10:]:
|
|
122
|
+
try:
|
|
123
|
+
f.unlink()
|
|
124
|
+
except Exception:
|
|
125
|
+
pass
|
|
126
|
+
except Exception:
|
|
127
|
+
pass
|
|
128
|
+
|
|
129
|
+
|
|
25
130
|
class TerminalPanel(Container, can_focus=True):
|
|
131
|
+
"""Terminal controls panel with permission request notifications."""
|
|
132
|
+
|
|
133
|
+
class PermissionRequestDetected(Message):
|
|
134
|
+
"""Posted when a new permission request signal file is detected."""
|
|
135
|
+
pass
|
|
136
|
+
|
|
26
137
|
DEFAULT_CSS = """
|
|
27
138
|
TerminalPanel {
|
|
28
139
|
height: 100%;
|
|
@@ -179,6 +290,7 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
179
290
|
super().__init__()
|
|
180
291
|
self._refresh_lock = asyncio.Lock()
|
|
181
292
|
self._expanded_window_id: str | None = None
|
|
293
|
+
self._signal_watcher: _SignalFileWatcher | None = None
|
|
182
294
|
|
|
183
295
|
def compose(self) -> ComposeResult:
|
|
184
296
|
controls_enabled = self.supported()
|
|
@@ -205,6 +317,40 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
205
317
|
exclusive=True,
|
|
206
318
|
)
|
|
207
319
|
)
|
|
320
|
+
# Start watching for permission request signals
|
|
321
|
+
self._start_signal_watcher()
|
|
322
|
+
|
|
323
|
+
def on_hide(self) -> None:
|
|
324
|
+
# Stop watching when panel is hidden
|
|
325
|
+
self._stop_signal_watcher()
|
|
326
|
+
|
|
327
|
+
def _start_signal_watcher(self) -> None:
|
|
328
|
+
"""Start watching for permission request signal files."""
|
|
329
|
+
if self._signal_watcher is not None:
|
|
330
|
+
return
|
|
331
|
+
self._signal_watcher = _SignalFileWatcher(self._on_permission_signal)
|
|
332
|
+
self._signal_watcher.start()
|
|
333
|
+
|
|
334
|
+
def _stop_signal_watcher(self) -> None:
|
|
335
|
+
"""Stop watching for permission request signal files."""
|
|
336
|
+
if self._signal_watcher is not None:
|
|
337
|
+
self._signal_watcher.stop()
|
|
338
|
+
self._signal_watcher = None
|
|
339
|
+
|
|
340
|
+
def _on_permission_signal(self) -> None:
|
|
341
|
+
"""Called when a new permission request signal is detected."""
|
|
342
|
+
# Post message to trigger refresh on the main thread
|
|
343
|
+
self.post_message(self.PermissionRequestDetected())
|
|
344
|
+
|
|
345
|
+
def on_terminal_panel_permission_request_detected(
|
|
346
|
+
self, event: PermissionRequestDetected
|
|
347
|
+
) -> None:
|
|
348
|
+
"""Handle permission request detection - refresh the terminal list."""
|
|
349
|
+
self.run_worker(
|
|
350
|
+
self.refresh_data(),
|
|
351
|
+
group="terminal-panel-refresh",
|
|
352
|
+
exclusive=True,
|
|
353
|
+
)
|
|
208
354
|
|
|
209
355
|
async def refresh_data(self) -> None:
|
|
210
356
|
async with self._refresh_lock:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|