aweshelf 0.1.1__tar.gz
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.
- aweshelf-0.1.1/PKG-INFO +108 -0
- aweshelf-0.1.1/README.md +87 -0
- aweshelf-0.1.1/pyproject.toml +47 -0
- aweshelf-0.1.1/setup.cfg +4 -0
- aweshelf-0.1.1/src/aweshelf/__init__.py +3 -0
- aweshelf-0.1.1/src/aweshelf/cli.py +55 -0
- aweshelf-0.1.1/src/aweshelf/commands/__init__.py +0 -0
- aweshelf-0.1.1/src/aweshelf/commands/bookmark.py +127 -0
- aweshelf-0.1.1/src/aweshelf/commands/browse.py +20 -0
- aweshelf-0.1.1/src/aweshelf/commands/list.py +75 -0
- aweshelf-0.1.1/src/aweshelf/commands/resume.py +42 -0
- aweshelf-0.1.1/src/aweshelf/commands/show.py +68 -0
- aweshelf-0.1.1/src/aweshelf/lib/__init__.py +0 -0
- aweshelf-0.1.1/src/aweshelf/lib/aweswitch.py +74 -0
- aweshelf-0.1.1/src/aweshelf/lib/discovery.py +77 -0
- aweshelf-0.1.1/src/aweshelf/lib/resume.py +10 -0
- aweshelf-0.1.1/src/aweshelf/lib/resume_target.py +87 -0
- aweshelf-0.1.1/src/aweshelf/lib/session.py +178 -0
- aweshelf-0.1.1/src/aweshelf/lib/store.py +126 -0
- aweshelf-0.1.1/src/aweshelf/tui/__init__.py +0 -0
- aweshelf-0.1.1/src/aweshelf/tui/app.py +234 -0
- aweshelf-0.1.1/src/aweshelf/types.py +32 -0
- aweshelf-0.1.1/src/aweshelf.egg-info/PKG-INFO +108 -0
- aweshelf-0.1.1/src/aweshelf.egg-info/SOURCES.txt +33 -0
- aweshelf-0.1.1/src/aweshelf.egg-info/dependency_links.txt +1 -0
- aweshelf-0.1.1/src/aweshelf.egg-info/entry_points.txt +2 -0
- aweshelf-0.1.1/src/aweshelf.egg-info/requires.txt +6 -0
- aweshelf-0.1.1/src/aweshelf.egg-info/top_level.txt +1 -0
- aweshelf-0.1.1/tests/test_aweswitch.py +148 -0
- aweshelf-0.1.1/tests/test_browse.py +49 -0
- aweshelf-0.1.1/tests/test_cli.py +197 -0
- aweshelf-0.1.1/tests/test_discovery.py +168 -0
- aweshelf-0.1.1/tests/test_resume.py +76 -0
- aweshelf-0.1.1/tests/test_session.py +89 -0
- aweshelf-0.1.1/tests/test_store.py +174 -0
aweshelf-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aweshelf
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Bookmark, categorize, and restore AI coding sessions with aweswitch profiles.
|
|
5
|
+
Author: Peng
|
|
6
|
+
License-Expression: MPL-2.0
|
|
7
|
+
Keywords: ai,agent,claude,codex,bookmark,session,aweswitch
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Topic :: Utilities
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
Requires-Dist: click>=8.1
|
|
17
|
+
Requires-Dist: textual>=0.40
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest; extra == "dev"
|
|
20
|
+
Requires-Dist: ruff; extra == "dev"
|
|
21
|
+
|
|
22
|
+
<div align="center">
|
|
23
|
+
<h1>aweshelf: Session Bookmark Manager</h1>
|
|
24
|
+
<p><strong>Bookmark, categorize, and restore AI coding sessions with aweswitch profiles.</strong></p>
|
|
25
|
+
<p>A lightweight CLI-first tool for Claude Code and Codex session management.</p>
|
|
26
|
+
<p>
|
|
27
|
+
<strong>English</strong> ·
|
|
28
|
+
<a href="./README_cn.md">简体中文</a>
|
|
29
|
+
</p>
|
|
30
|
+
<p>
|
|
31
|
+
<img src="https://img.shields.io/badge/version-0.1.1-7C3AED?style=flat-square" alt="Version">
|
|
32
|
+
<img src="https://img.shields.io/badge/python-%E2%89%A53.10-0EA5E9?style=flat-square" alt="Python">
|
|
33
|
+
</p>
|
|
34
|
+
<p>
|
|
35
|
+
<img src="https://img.shields.io/badge/status-alpha-c96a3d?style=flat-square" alt="Status">
|
|
36
|
+
<img src="https://img.shields.io/badge/install-pip-22C55E?style=flat-square" alt="pip install">
|
|
37
|
+
<img src="https://img.shields.io/badge/platform-terminal-334155?style=flat-square" alt="Platform">
|
|
38
|
+
<img src="https://img.shields.io/github/stars/Webioinfo01/aweshelf?style=flat-square" alt="GitHub stars">
|
|
39
|
+
</p>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
> Bookmark, categorize, and restore AI coding sessions with aweswitch profiles.
|
|
43
|
+
|
|
44
|
+
aweshelf lets you save your favorite Claude Code and Codex sessions, tag them with categories, and restore them instantly — including the aweswitch profile (API endpoint, model, token) that was active when you bookmarked.
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install aweshelf
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Quick Start
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# Bookmark the current project's most recent session
|
|
56
|
+
aweshelf bookmark
|
|
57
|
+
|
|
58
|
+
# List all bookmarks
|
|
59
|
+
aweshelf list
|
|
60
|
+
|
|
61
|
+
# Resume a bookmark
|
|
62
|
+
aweshelf resume aweshelf_0001
|
|
63
|
+
|
|
64
|
+
# Browse interactively
|
|
65
|
+
aweshelf browse
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Config
|
|
69
|
+
|
|
70
|
+
Bookmarks are stored at `~/.config/aweshelf/bookmarks.json`. Override with `AWESHELF_CONFIG` env var.
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"version": 1,
|
|
75
|
+
"bookmarks": [
|
|
76
|
+
{
|
|
77
|
+
"id": "aweshelf_0001",
|
|
78
|
+
"provider": "claude",
|
|
79
|
+
"session_id": "550e8400-...",
|
|
80
|
+
"title": "Fix auth middleware bug",
|
|
81
|
+
"category": "backend",
|
|
82
|
+
"project_path": "/Users/peng/Desktop/Project/my-app",
|
|
83
|
+
"aweswitch_profile": "cc-glm",
|
|
84
|
+
"bookmarked_at": "2026-05-20T14:00:00Z"
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Commands
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
aweshelf bookmark [SESSION_ID] [-t TITLE] [-c CATEGORY] [--profile PROFILE]
|
|
94
|
+
aweshelf list [-c CATEGORY] [-p PROVIDER]
|
|
95
|
+
aweshelf search QUERY
|
|
96
|
+
aweshelf recent [-n COUNT]
|
|
97
|
+
aweshelf show BOOKMARK_ID [--json]
|
|
98
|
+
aweshelf edit BOOKMARK_ID [-t TITLE] [-c CATEGORY] [--profile PROFILE]
|
|
99
|
+
aweshelf rm BOOKMARK_ID [--force]
|
|
100
|
+
aweshelf resume BOOKMARK_ID [--profile PROFILE] [--raw] [--dry-run]
|
|
101
|
+
aweshelf browse
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Development
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
python -m pytest tests/
|
|
108
|
+
```
|
aweshelf-0.1.1/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<h1>aweshelf: Session Bookmark Manager</h1>
|
|
3
|
+
<p><strong>Bookmark, categorize, and restore AI coding sessions with aweswitch profiles.</strong></p>
|
|
4
|
+
<p>A lightweight CLI-first tool for Claude Code and Codex session management.</p>
|
|
5
|
+
<p>
|
|
6
|
+
<strong>English</strong> ·
|
|
7
|
+
<a href="./README_cn.md">简体中文</a>
|
|
8
|
+
</p>
|
|
9
|
+
<p>
|
|
10
|
+
<img src="https://img.shields.io/badge/version-0.1.1-7C3AED?style=flat-square" alt="Version">
|
|
11
|
+
<img src="https://img.shields.io/badge/python-%E2%89%A53.10-0EA5E9?style=flat-square" alt="Python">
|
|
12
|
+
</p>
|
|
13
|
+
<p>
|
|
14
|
+
<img src="https://img.shields.io/badge/status-alpha-c96a3d?style=flat-square" alt="Status">
|
|
15
|
+
<img src="https://img.shields.io/badge/install-pip-22C55E?style=flat-square" alt="pip install">
|
|
16
|
+
<img src="https://img.shields.io/badge/platform-terminal-334155?style=flat-square" alt="Platform">
|
|
17
|
+
<img src="https://img.shields.io/github/stars/Webioinfo01/aweshelf?style=flat-square" alt="GitHub stars">
|
|
18
|
+
</p>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
> Bookmark, categorize, and restore AI coding sessions with aweswitch profiles.
|
|
22
|
+
|
|
23
|
+
aweshelf lets you save your favorite Claude Code and Codex sessions, tag them with categories, and restore them instantly — including the aweswitch profile (API endpoint, model, token) that was active when you bookmarked.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install aweshelf
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Bookmark the current project's most recent session
|
|
35
|
+
aweshelf bookmark
|
|
36
|
+
|
|
37
|
+
# List all bookmarks
|
|
38
|
+
aweshelf list
|
|
39
|
+
|
|
40
|
+
# Resume a bookmark
|
|
41
|
+
aweshelf resume aweshelf_0001
|
|
42
|
+
|
|
43
|
+
# Browse interactively
|
|
44
|
+
aweshelf browse
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Config
|
|
48
|
+
|
|
49
|
+
Bookmarks are stored at `~/.config/aweshelf/bookmarks.json`. Override with `AWESHELF_CONFIG` env var.
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"version": 1,
|
|
54
|
+
"bookmarks": [
|
|
55
|
+
{
|
|
56
|
+
"id": "aweshelf_0001",
|
|
57
|
+
"provider": "claude",
|
|
58
|
+
"session_id": "550e8400-...",
|
|
59
|
+
"title": "Fix auth middleware bug",
|
|
60
|
+
"category": "backend",
|
|
61
|
+
"project_path": "/Users/peng/Desktop/Project/my-app",
|
|
62
|
+
"aweswitch_profile": "cc-glm",
|
|
63
|
+
"bookmarked_at": "2026-05-20T14:00:00Z"
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Commands
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
aweshelf bookmark [SESSION_ID] [-t TITLE] [-c CATEGORY] [--profile PROFILE]
|
|
73
|
+
aweshelf list [-c CATEGORY] [-p PROVIDER]
|
|
74
|
+
aweshelf search QUERY
|
|
75
|
+
aweshelf recent [-n COUNT]
|
|
76
|
+
aweshelf show BOOKMARK_ID [--json]
|
|
77
|
+
aweshelf edit BOOKMARK_ID [-t TITLE] [-c CATEGORY] [--profile PROFILE]
|
|
78
|
+
aweshelf rm BOOKMARK_ID [--force]
|
|
79
|
+
aweshelf resume BOOKMARK_ID [--profile PROFILE] [--raw] [--dry-run]
|
|
80
|
+
aweshelf browse
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Development
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
python -m pytest tests/
|
|
87
|
+
```
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "aweshelf"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "Bookmark, categorize, and restore AI coding sessions with aweswitch profiles."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = ["click>=8.1", "textual>=0.40"]
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Peng" }
|
|
14
|
+
]
|
|
15
|
+
license = "MPL-2.0"
|
|
16
|
+
keywords = ["ai", "agent", "claude", "codex", "bookmark", "session", "aweswitch"]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 3 - Alpha",
|
|
19
|
+
"Environment :: Console",
|
|
20
|
+
"Intended Audience :: Developers",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
23
|
+
"Topic :: Utilities"
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
dev = ["pytest", "ruff"]
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
aweshelf = "aweshelf.cli:main"
|
|
31
|
+
|
|
32
|
+
[tool.setuptools.packages.find]
|
|
33
|
+
where = ["src"]
|
|
34
|
+
|
|
35
|
+
[tool.ruff]
|
|
36
|
+
target-version = "py310"
|
|
37
|
+
line-length = 100
|
|
38
|
+
|
|
39
|
+
[tool.ruff.lint]
|
|
40
|
+
select = ["E", "F", "W", "I", "UP", "B", "SIM"]
|
|
41
|
+
ignore = ["E501"]
|
|
42
|
+
|
|
43
|
+
[tool.ruff.lint.isort]
|
|
44
|
+
known-first-party = ["aweshelf"]
|
|
45
|
+
|
|
46
|
+
[tool.pytest.ini_options]
|
|
47
|
+
testpaths = ["tests"]
|
aweshelf-0.1.1/setup.cfg
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""aweshelf CLI entry point."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from aweshelf import __version__
|
|
6
|
+
from aweshelf.commands.bookmark import bookmark_command
|
|
7
|
+
from aweshelf.commands.list import list_command, search_command, recent_command
|
|
8
|
+
from aweshelf.commands.show import show_command, edit_command, rm_command
|
|
9
|
+
from aweshelf.commands.resume import resume_command
|
|
10
|
+
from aweshelf.commands.browse import browse_command
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.group(
|
|
14
|
+
name="aweshelf",
|
|
15
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
16
|
+
help="Bookmark, categorize, and restore AI coding sessions.",
|
|
17
|
+
)
|
|
18
|
+
@click.version_option(__version__, "-v", "--version", message="%(version)s")
|
|
19
|
+
def cli():
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
cli.add_command(bookmark_command)
|
|
24
|
+
cli.add_command(list_command)
|
|
25
|
+
cli.add_command(search_command)
|
|
26
|
+
cli.add_command(recent_command)
|
|
27
|
+
cli.add_command(show_command)
|
|
28
|
+
cli.add_command(edit_command)
|
|
29
|
+
cli.add_command(rm_command)
|
|
30
|
+
cli.add_command(resume_command)
|
|
31
|
+
cli.add_command(browse_command)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@cli.command("help")
|
|
35
|
+
@click.argument("command_name", required=False)
|
|
36
|
+
@click.pass_context
|
|
37
|
+
def help_command(ctx, command_name):
|
|
38
|
+
"""Display help for command."""
|
|
39
|
+
if command_name is None:
|
|
40
|
+
click.echo(ctx.parent.get_help())
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
command = cli.get_command(ctx, command_name)
|
|
44
|
+
if command is None or command.hidden:
|
|
45
|
+
raise click.ClickException(f"unknown command '{command_name}'")
|
|
46
|
+
with command.make_context(command_name, [], parent=ctx.parent, resilient_parsing=True) as command_ctx:
|
|
47
|
+
click.echo(command.get_help(command_ctx))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def main(argv=None):
|
|
51
|
+
return cli.main(args=argv, prog_name="aweshelf")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if __name__ == "__main__":
|
|
55
|
+
raise SystemExit(main())
|
|
File without changes
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Bookmark command."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from aweshelf.types import Bookmark
|
|
6
|
+
from aweshelf.lib.store import add_bookmark, list_categories, bookmark_path
|
|
7
|
+
from aweshelf.lib.discovery import find_project_sessions, find_recent_session
|
|
8
|
+
from aweshelf.lib.session import parse_session_meta
|
|
9
|
+
from aweshelf.lib.aweswitch import detect_profile, load_aweswitch_config
|
|
10
|
+
|
|
11
|
+
DEFAULT_LIST_LIMIT = 10
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def pick_session(sessions: list[dict], limit: int = DEFAULT_LIST_LIMIT) -> dict:
|
|
15
|
+
"""Let user pick a session from a numbered list."""
|
|
16
|
+
shown = sessions[:limit]
|
|
17
|
+
total = len(sessions)
|
|
18
|
+
|
|
19
|
+
if total > limit:
|
|
20
|
+
click.echo(f"Sessions in current project ({total} total, showing {limit} \u2014 use --verbose for all):\n")
|
|
21
|
+
else:
|
|
22
|
+
click.echo(f"Sessions in current project ({total} total):\n")
|
|
23
|
+
|
|
24
|
+
for i, s in enumerate(shown, 1):
|
|
25
|
+
title = s.get("title", "Untitled")
|
|
26
|
+
provider = s.get("provider", "?")
|
|
27
|
+
sid = s.get("session_id", "?")
|
|
28
|
+
click.echo(f" {i:>3}. [{provider}] {title}")
|
|
29
|
+
click.echo(f" {sid}")
|
|
30
|
+
|
|
31
|
+
if total > limit:
|
|
32
|
+
click.echo(f"\n ... and {total - limit} more")
|
|
33
|
+
click.echo()
|
|
34
|
+
|
|
35
|
+
while True:
|
|
36
|
+
choice = click.prompt("Pick a session (number)", type=int)
|
|
37
|
+
if 1 <= choice <= len(shown):
|
|
38
|
+
return sessions[choice - 1]
|
|
39
|
+
click.echo(f"Please enter 1-{len(shown)}")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def run_bookmark(
|
|
43
|
+
session_id: str | None = None,
|
|
44
|
+
title: str | None = None,
|
|
45
|
+
category: str | None = None,
|
|
46
|
+
profile: str | None = None,
|
|
47
|
+
interactive: bool = True,
|
|
48
|
+
verbose: bool = False,
|
|
49
|
+
) -> Bookmark:
|
|
50
|
+
path = bookmark_path()
|
|
51
|
+
|
|
52
|
+
if session_id is None and interactive:
|
|
53
|
+
sessions = find_project_sessions()
|
|
54
|
+
if not sessions:
|
|
55
|
+
raise SystemExit("aweshelf: no session found in current project")
|
|
56
|
+
limit = len(sessions) if verbose else DEFAULT_LIST_LIMIT
|
|
57
|
+
session = pick_session(sessions, limit)
|
|
58
|
+
session_id = session["session_id"]
|
|
59
|
+
source_path = session.get("source_path", "")
|
|
60
|
+
provider = session.get("provider", "claude")
|
|
61
|
+
auto_title = session.get("title", "")
|
|
62
|
+
project_path = session.get("project_path", "")
|
|
63
|
+
elif session_id is None:
|
|
64
|
+
session = find_recent_session()
|
|
65
|
+
if session is None:
|
|
66
|
+
raise SystemExit("aweshelf: no session found in current project")
|
|
67
|
+
session_id = session["session_id"]
|
|
68
|
+
source_path = session.get("source_path", "")
|
|
69
|
+
provider = session.get("provider", "claude")
|
|
70
|
+
auto_title = session.get("title", "")
|
|
71
|
+
project_path = session.get("project_path", "")
|
|
72
|
+
else:
|
|
73
|
+
source_path = ""
|
|
74
|
+
provider = "claude"
|
|
75
|
+
auto_title = ""
|
|
76
|
+
project_path = ""
|
|
77
|
+
|
|
78
|
+
if title is None:
|
|
79
|
+
title = auto_title or "Untitled session"
|
|
80
|
+
|
|
81
|
+
if interactive and category is None:
|
|
82
|
+
cats = list_categories(path)
|
|
83
|
+
click.echo(f"\nTitle: {title}")
|
|
84
|
+
if cats:
|
|
85
|
+
click.echo(f"Existing categories: {', '.join(cats)}")
|
|
86
|
+
cat_input = click.prompt("Category", default="", show_default=False)
|
|
87
|
+
category = cat_input if cat_input else ""
|
|
88
|
+
|
|
89
|
+
if category is None:
|
|
90
|
+
category = ""
|
|
91
|
+
|
|
92
|
+
if profile is None and source_path:
|
|
93
|
+
try:
|
|
94
|
+
meta = parse_session_meta(source_path)
|
|
95
|
+
config = load_aweswitch_config()
|
|
96
|
+
if config:
|
|
97
|
+
profile = detect_profile({"ANTHROPIC_BASE_URL": "", "ANTHROPIC_MODEL": meta.get("model", "")})
|
|
98
|
+
except Exception:
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
bookmark = Bookmark(
|
|
102
|
+
id="",
|
|
103
|
+
provider=provider,
|
|
104
|
+
session_id=session_id,
|
|
105
|
+
title=title,
|
|
106
|
+
category=category,
|
|
107
|
+
project_path=project_path,
|
|
108
|
+
aweswitch_profile=profile,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
bookmark = add_bookmark(bookmark, path)
|
|
112
|
+
return bookmark
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@click.command("bookmark")
|
|
116
|
+
@click.argument("session_id", required=False)
|
|
117
|
+
@click.option("-t", "--title", default=None, help="Bookmark title.")
|
|
118
|
+
@click.option("-c", "--category", default=None, help="Category for the bookmark.")
|
|
119
|
+
@click.option("--profile", default=None, help="aweswitch profile to use.")
|
|
120
|
+
@click.option("--verbose", is_flag=True, help="Show all sessions (no limit).")
|
|
121
|
+
def bookmark_command(session_id, title, category, profile, verbose):
|
|
122
|
+
"""Bookmark a session for quick access."""
|
|
123
|
+
try:
|
|
124
|
+
b = run_bookmark(session_id, title, category, profile, interactive=True, verbose=verbose)
|
|
125
|
+
except ValueError as exc:
|
|
126
|
+
raise click.ClickException(str(exc)) from exc
|
|
127
|
+
click.echo(f"\nBookmarked {b.id} — {b.title}")
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Browse command — TUI entry point."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from aweshelf.lib.resume_target import execute_resume
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@click.command("browse")
|
|
9
|
+
def browse_command():
|
|
10
|
+
"""Browse bookmarks interactively."""
|
|
11
|
+
try:
|
|
12
|
+
from aweshelf.tui.app import BookmarkBrowser
|
|
13
|
+
except ImportError as exc:
|
|
14
|
+
raise click.ClickException("textual is required for browse. Install with: pip install textual") from exc
|
|
15
|
+
|
|
16
|
+
app = BookmarkBrowser()
|
|
17
|
+
result = app.run()
|
|
18
|
+
|
|
19
|
+
if result is not None:
|
|
20
|
+
execute_resume(result)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""List, search, recent commands."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from aweshelf.lib.store import load_bookmarks
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def format_table(bookmarks: list) -> str:
|
|
9
|
+
if not bookmarks:
|
|
10
|
+
return "No bookmarks found."
|
|
11
|
+
|
|
12
|
+
headers = ["ID", "PROVIDER", "TITLE", "CATEGORY", "PROFILE"]
|
|
13
|
+
rows = []
|
|
14
|
+
for b in bookmarks:
|
|
15
|
+
rows.append([
|
|
16
|
+
b.id,
|
|
17
|
+
b.provider,
|
|
18
|
+
b.title[:40] + ("..." if len(b.title) > 40 else ""),
|
|
19
|
+
b.category or "-",
|
|
20
|
+
b.aweswitch_profile or "-",
|
|
21
|
+
])
|
|
22
|
+
|
|
23
|
+
widths = [len(h) for h in headers]
|
|
24
|
+
for row in rows:
|
|
25
|
+
for i, cell in enumerate(row):
|
|
26
|
+
widths[i] = max(widths[i], len(cell))
|
|
27
|
+
|
|
28
|
+
lines = []
|
|
29
|
+
header_line = " ".join(h.ljust(widths[i]) for i, h in enumerate(headers))
|
|
30
|
+
lines.append(header_line)
|
|
31
|
+
lines.append(" ".join("-" * w for w in widths))
|
|
32
|
+
for row in rows:
|
|
33
|
+
line = " ".join(cell.ljust(widths[i]) for i, cell in enumerate(row))
|
|
34
|
+
lines.append(line)
|
|
35
|
+
|
|
36
|
+
return "\n".join(lines)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@click.command("list")
|
|
40
|
+
@click.option("-c", "--category", default=None, help="Filter by category.")
|
|
41
|
+
@click.option("-p", "--provider", default=None, help="Filter by provider.")
|
|
42
|
+
def list_command(category, provider):
|
|
43
|
+
"""List all bookmarks."""
|
|
44
|
+
bookmarks = load_bookmarks()
|
|
45
|
+
if category:
|
|
46
|
+
bookmarks = [b for b in bookmarks if b.category == category]
|
|
47
|
+
if provider:
|
|
48
|
+
bookmarks = [b for b in bookmarks if b.provider == provider]
|
|
49
|
+
click.echo(format_table(bookmarks))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@click.command("search")
|
|
53
|
+
@click.argument("query")
|
|
54
|
+
def search_command(query):
|
|
55
|
+
"""Search bookmarks by title, category, session ID, project, or profile."""
|
|
56
|
+
bookmarks = load_bookmarks()
|
|
57
|
+
query_lower = query.lower()
|
|
58
|
+
results = [
|
|
59
|
+
b for b in bookmarks
|
|
60
|
+
if query_lower in b.title.lower()
|
|
61
|
+
or query_lower in b.category.lower()
|
|
62
|
+
or query_lower in b.session_id.lower()
|
|
63
|
+
or query_lower in b.project_path.lower()
|
|
64
|
+
or (b.aweswitch_profile and query_lower in b.aweswitch_profile.lower())
|
|
65
|
+
]
|
|
66
|
+
click.echo(format_table(results))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@click.command("recent")
|
|
70
|
+
@click.option("-n", "--count", default=10, help="Number of recent bookmarks.")
|
|
71
|
+
def recent_command(count):
|
|
72
|
+
"""Show recently bookmarked sessions."""
|
|
73
|
+
bookmarks = load_bookmarks()
|
|
74
|
+
bookmarks.sort(key=lambda b: b.bookmarked_at, reverse=True)
|
|
75
|
+
click.echo(format_table(bookmarks[:count]))
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Resume command."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from aweshelf.lib.resume_target import (
|
|
6
|
+
ResumeError,
|
|
7
|
+
build_resume_target,
|
|
8
|
+
format_resume_target,
|
|
9
|
+
run_resume_target,
|
|
10
|
+
)
|
|
11
|
+
from aweshelf.lib.store import find_bookmark
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.command("resume")
|
|
15
|
+
@click.argument("bookmark_id")
|
|
16
|
+
@click.option("--profile", default=None, help="Override aweswitch profile.")
|
|
17
|
+
@click.option("--raw", is_flag=True, help="Skip aweswitch, use claude/codex directly.")
|
|
18
|
+
@click.option("--dry-run", is_flag=True, help="Print the resume command without running it.")
|
|
19
|
+
def resume_command(bookmark_id, profile, raw, dry_run):
|
|
20
|
+
"""Resume a bookmarked session."""
|
|
21
|
+
b = find_bookmark(bookmark_id)
|
|
22
|
+
if b is None:
|
|
23
|
+
raise click.ClickException(f"bookmark not found: {bookmark_id}")
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
target = build_resume_target(b, profile_override=profile, raw=raw)
|
|
27
|
+
except ResumeError as exc:
|
|
28
|
+
raise click.ClickException(str(exc)) from exc
|
|
29
|
+
if target.warning:
|
|
30
|
+
click.echo(f"Warning: {target.warning}", err=True)
|
|
31
|
+
click.echo(f"Resuming {b.id} — {b.title}")
|
|
32
|
+
click.echo(f" $ {format_resume_target(target)}")
|
|
33
|
+
|
|
34
|
+
if dry_run:
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
run_resume_target(target)
|
|
39
|
+
except FileNotFoundError as exc:
|
|
40
|
+
raise click.ClickException(f"command not found: {target.argv[0]}") from exc
|
|
41
|
+
except OSError as exc:
|
|
42
|
+
raise click.ClickException(f"failed to run {target.argv[0]}: {exc}") from exc
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Show, edit, rm commands."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from aweshelf.lib.store import find_bookmark, update_bookmark, remove_bookmark
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.command("show")
|
|
11
|
+
@click.argument("bookmark_id")
|
|
12
|
+
@click.option("--json", "as_json", is_flag=True, help="Output as raw JSON.")
|
|
13
|
+
def show_command(bookmark_id, as_json):
|
|
14
|
+
"""Show bookmark details."""
|
|
15
|
+
b = find_bookmark(bookmark_id)
|
|
16
|
+
if b is None:
|
|
17
|
+
raise click.ClickException(f"bookmark not found: {bookmark_id}")
|
|
18
|
+
if as_json:
|
|
19
|
+
click.echo(json.dumps(b.to_dict(), indent=2, ensure_ascii=False))
|
|
20
|
+
else:
|
|
21
|
+
click.echo(f"ID: {b.id}")
|
|
22
|
+
click.echo(f"Provider: {b.provider}")
|
|
23
|
+
click.echo(f"Session ID: {b.session_id}")
|
|
24
|
+
click.echo(f"Title: {b.title}")
|
|
25
|
+
click.echo(f"Category: {b.category or '-'}")
|
|
26
|
+
click.echo(f"Project: {b.project_path or '-'}")
|
|
27
|
+
click.echo(f"aweswitch Profile: {b.aweswitch_profile or '-'}")
|
|
28
|
+
click.echo(f"Bookmarked at: {b.bookmarked_at}")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@click.command("edit")
|
|
32
|
+
@click.argument("bookmark_id")
|
|
33
|
+
@click.option("-t", "--title", default=None, help="New title.")
|
|
34
|
+
@click.option("-c", "--category", default=None, help="New category.")
|
|
35
|
+
@click.option("--profile", default=None, help="New aweswitch profile.")
|
|
36
|
+
def edit_command(bookmark_id, title, category, profile):
|
|
37
|
+
"""Edit a bookmark's metadata."""
|
|
38
|
+
fields = {}
|
|
39
|
+
if title is not None:
|
|
40
|
+
fields["title"] = title
|
|
41
|
+
if category is not None:
|
|
42
|
+
fields["category"] = category
|
|
43
|
+
if profile is not None:
|
|
44
|
+
fields["aweswitch_profile"] = profile
|
|
45
|
+
|
|
46
|
+
if not fields:
|
|
47
|
+
raise click.ClickException("nothing to edit; use -t, -c, or --profile")
|
|
48
|
+
|
|
49
|
+
b = update_bookmark(bookmark_id, **fields)
|
|
50
|
+
if b is None:
|
|
51
|
+
raise click.ClickException(f"bookmark not found: {bookmark_id}")
|
|
52
|
+
click.echo(f"Updated {b.id}")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@click.command("rm")
|
|
56
|
+
@click.argument("bookmark_id")
|
|
57
|
+
@click.option("--force", is_flag=True, help="Skip confirmation.")
|
|
58
|
+
def rm_command(bookmark_id, force):
|
|
59
|
+
"""Remove a bookmark."""
|
|
60
|
+
b = find_bookmark(bookmark_id)
|
|
61
|
+
if b is None:
|
|
62
|
+
raise click.ClickException(f"bookmark not found: {bookmark_id}")
|
|
63
|
+
|
|
64
|
+
if not force:
|
|
65
|
+
click.confirm(f"Remove bookmark {b.id} ({b.title})?", abort=True)
|
|
66
|
+
|
|
67
|
+
remove_bookmark(bookmark_id)
|
|
68
|
+
click.echo(f"Removed {bookmark_id}")
|
|
File without changes
|