conversession 2.0.0__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.
- conversession-2.0.0/LICENSE +0 -0
- conversession-2.0.0/PKG-INFO +28 -0
- conversession-2.0.0/README.md +11 -0
- conversession-2.0.0/pyproject.toml +29 -0
- conversession-2.0.0/setup.cfg +4 -0
- conversession-2.0.0/src/conversession/__init__.py +257 -0
- conversession-2.0.0/src/conversession/__main__.py +147 -0
- conversession-2.0.0/src/conversession/models.py +9 -0
- conversession-2.0.0/src/conversession.egg-info/PKG-INFO +28 -0
- conversession-2.0.0/src/conversession.egg-info/SOURCES.txt +12 -0
- conversession-2.0.0/src/conversession.egg-info/dependency_links.txt +1 -0
- conversession-2.0.0/src/conversession.egg-info/entry_points.txt +2 -0
- conversession-2.0.0/src/conversession.egg-info/requires.txt +6 -0
- conversession-2.0.0/src/conversession.egg-info/top_level.txt +1 -0
|
File without changes
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: conversession
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Author-email: Jordi Carrera Ventura <jordi.carrera.ventura@gmail.com>
|
|
5
|
+
Classifier: Programming Language :: Python :: 3
|
|
6
|
+
Classifier: Operating System :: OS Independent
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: build
|
|
11
|
+
Requires-Dist: twine
|
|
12
|
+
Requires-Dist: openai
|
|
13
|
+
Requires-Dist: python-dotenv
|
|
14
|
+
Requires-Dist: playwright
|
|
15
|
+
Requires-Dist: beautifulsoup4
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# Conversession
|
|
19
|
+
|
|
20
|
+
Prompt management CLI application for Linux and MacOS.
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Build and publish
|
|
24
|
+
|
|
25
|
+
1. Building the package before uploading: `python -m build # from "conversession"`.
|
|
26
|
+
2. Upload the package to pypi: `python -m twine upload --repository {pypi|testpypi} dist/*`
|
|
27
|
+
|
|
28
|
+
If any dependencies are required, edit the `pyproject.toml` file, "\[project\]" field, and add a `dependencies` key with a `List\[str\]` value, where each string is a `pip`-readable dependency.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Conversession
|
|
2
|
+
|
|
3
|
+
Prompt management CLI application for Linux and MacOS.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# Build and publish
|
|
7
|
+
|
|
8
|
+
1. Building the package before uploading: `python -m build # from "conversession"`.
|
|
9
|
+
2. Upload the package to pypi: `python -m twine upload --repository {pypi|testpypi} dist/*`
|
|
10
|
+
|
|
11
|
+
If any dependencies are required, edit the `pyproject.toml` file, "\[project\]" field, and add a `dependencies` key with a `List\[str\]` value, where each string is a `pip`-readable dependency.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=75.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "conversession"
|
|
7
|
+
version = "2.0.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="Jordi Carrera Ventura", email="jordi.carrera.ventura@gmail.com" },
|
|
10
|
+
]
|
|
11
|
+
description = ""
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.11"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
]
|
|
18
|
+
dependencies = [
|
|
19
|
+
"build",
|
|
20
|
+
"twine",
|
|
21
|
+
"openai",
|
|
22
|
+
"python-dotenv",
|
|
23
|
+
"playwright",
|
|
24
|
+
"beautifulsoup4"
|
|
25
|
+
|
|
26
|
+
] # as pip-installable strings
|
|
27
|
+
|
|
28
|
+
[project.scripts]
|
|
29
|
+
conversession = "conversession.__main__:main"
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import subprocess
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import re
|
|
6
|
+
from typing import Optional
|
|
7
|
+
from urllib.parse import urlparse
|
|
8
|
+
|
|
9
|
+
import dotenv
|
|
10
|
+
from openai import OpenAI
|
|
11
|
+
from bs4 import BeautifulSoup
|
|
12
|
+
from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeoutError
|
|
13
|
+
|
|
14
|
+
from .models import LLMS
|
|
15
|
+
|
|
16
|
+
BUILT_IN_COMMANDS = [
|
|
17
|
+
"all",
|
|
18
|
+
"new",
|
|
19
|
+
"model",
|
|
20
|
+
"models",
|
|
21
|
+
"help",
|
|
22
|
+
"quit",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
SKILLS_DIR = Path(__file__).resolve().parent.parent.parent / "skills"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _get_skills_dir() -> Path:
|
|
29
|
+
SKILLS_DIR.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
return SKILLS_DIR
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def load_skills() -> list[dict]:
|
|
34
|
+
skills_dir = _get_skills_dir()
|
|
35
|
+
skills = []
|
|
36
|
+
for f in sorted(skills_dir.glob("*.md")):
|
|
37
|
+
content = f.read_text().strip()
|
|
38
|
+
if not content:
|
|
39
|
+
continue
|
|
40
|
+
name = f.stem
|
|
41
|
+
summary = content[:90].replace("\n", " ")
|
|
42
|
+
if len(content) > 90:
|
|
43
|
+
summary += "..."
|
|
44
|
+
skills.append({"name": name, "path": f, "summary": summary})
|
|
45
|
+
return skills
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def list_models(active=None) -> None:
|
|
49
|
+
for m in LLMS:
|
|
50
|
+
marker = "* " if m is active else " "
|
|
51
|
+
print(f"{marker}{m.name} ({m.shorthand}): {m.url}")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def list_skills() -> None:
|
|
55
|
+
skills = load_skills()
|
|
56
|
+
if not skills:
|
|
57
|
+
print("No skills found. Use new to create one.")
|
|
58
|
+
return
|
|
59
|
+
for s in skills:
|
|
60
|
+
print(f" {s['name']}: {s['summary']}")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _open_in_editor(filepath: Path) -> None:
|
|
64
|
+
editor = os.environ.get("EDITOR")
|
|
65
|
+
if editor:
|
|
66
|
+
subprocess.run([editor, str(filepath)])
|
|
67
|
+
elif sys.platform == "darwin":
|
|
68
|
+
subprocess.run(["open", "-t", str(filepath)])
|
|
69
|
+
elif sys.platform.startswith("linux"):
|
|
70
|
+
subprocess.run(["xdg-open", str(filepath)])
|
|
71
|
+
else:
|
|
72
|
+
subprocess.run(["vi", str(filepath)])
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def run_skill_creation_wizard() -> None:
|
|
76
|
+
skills_dir = _get_skills_dir()
|
|
77
|
+
name = input("Skill name: ").strip()
|
|
78
|
+
if not name:
|
|
79
|
+
print("Cancelled.")
|
|
80
|
+
return
|
|
81
|
+
sanitized = "".join(c for c in name if c.isalnum() or c in "-_").strip("-_")
|
|
82
|
+
if not sanitized:
|
|
83
|
+
print("Invalid name. Use letters, numbers, hyphens, and underscores.")
|
|
84
|
+
return
|
|
85
|
+
if sanitized in BUILT_IN_COMMANDS:
|
|
86
|
+
raise ValueError(
|
|
87
|
+
f"Can't create a skill whose name {sanitized}"
|
|
88
|
+
f"matches that of a built-in command: {str(BUILT_IN_COMMANDS)}")
|
|
89
|
+
filepath = skills_dir / f"{sanitized}.md"
|
|
90
|
+
if filepath.exists():
|
|
91
|
+
overwrite = input(f"Skill '{sanitized}' already exists. Edit it? [Y/n] ").strip().lower()
|
|
92
|
+
if overwrite not in ("", "y", "yes"):
|
|
93
|
+
print("Cancelled.")
|
|
94
|
+
return
|
|
95
|
+
else:
|
|
96
|
+
filepath.write_text("")
|
|
97
|
+
print(f"Created empty skill '{sanitized}'.")
|
|
98
|
+
_open_in_editor(filepath)
|
|
99
|
+
if filepath.exists() and filepath.read_text().strip():
|
|
100
|
+
print(f"Skill '{sanitized}' saved.")
|
|
101
|
+
else:
|
|
102
|
+
print(f"Skill '{sanitized}' is empty.")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _resolve_skill(name: str) -> Optional[Path]:
|
|
106
|
+
skills_dir = _get_skills_dir()
|
|
107
|
+
filepath = skills_dir / f"{name}.md"
|
|
108
|
+
return filepath if filepath.exists() else None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def edit_skill(name: str) -> None:
|
|
112
|
+
filepath = _resolve_skill(name)
|
|
113
|
+
if not filepath:
|
|
114
|
+
print(f"Skill '{name}' not found.")
|
|
115
|
+
return
|
|
116
|
+
_open_in_editor(filepath)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _set_clipboard(text: str) -> None:
|
|
120
|
+
try:
|
|
121
|
+
if sys.platform == "darwin":
|
|
122
|
+
subprocess.run(["pbcopy"], input=text, text=True)
|
|
123
|
+
elif sys.platform.startswith("linux"):
|
|
124
|
+
subprocess.run(["xclip", "-selection", "clipboard"], input=text, text=True)
|
|
125
|
+
else:
|
|
126
|
+
pass # silently skip
|
|
127
|
+
except Exception:
|
|
128
|
+
pass
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _is_url(value: str) -> bool:
|
|
132
|
+
if not value:
|
|
133
|
+
return False
|
|
134
|
+
parsed = urlparse(value)
|
|
135
|
+
return parsed.scheme in {"http", "https"} and bool(parsed.netloc)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _dismiss_common_popups(page) -> None:
|
|
139
|
+
try:
|
|
140
|
+
page.evaluate(
|
|
141
|
+
"""
|
|
142
|
+
() => {
|
|
143
|
+
const selectors = [
|
|
144
|
+
'[role="dialog"]',
|
|
145
|
+
'[aria-modal="true"]',
|
|
146
|
+
'[class*="cookie" i]',
|
|
147
|
+
'[class*="consent" i]',
|
|
148
|
+
'[class*="popup" i]',
|
|
149
|
+
'[class*="modal" i]',
|
|
150
|
+
'[class*="overlay" i]'
|
|
151
|
+
];
|
|
152
|
+
for (const sel of selectors) {
|
|
153
|
+
document.querySelectorAll(sel).forEach(el => el.remove());
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
"""
|
|
157
|
+
)
|
|
158
|
+
except Exception:
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
button_texts = [
|
|
162
|
+
"Accept all",
|
|
163
|
+
"Accept",
|
|
164
|
+
"Agree",
|
|
165
|
+
"I agree",
|
|
166
|
+
"Allow all",
|
|
167
|
+
"OK",
|
|
168
|
+
"Got it",
|
|
169
|
+
"Close",
|
|
170
|
+
]
|
|
171
|
+
for text in button_texts:
|
|
172
|
+
try:
|
|
173
|
+
locator = page.locator(f"button:has-text('{text}')").first
|
|
174
|
+
if locator.count() > 0 and locator.is_visible():
|
|
175
|
+
locator.click(timeout=1000)
|
|
176
|
+
except Exception:
|
|
177
|
+
continue
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _extract_main_text(html: str) -> str:
|
|
181
|
+
soup = BeautifulSoup(html, "html.parser")
|
|
182
|
+
for tag in soup(["script", "style", "noscript", "svg", "canvas"]):
|
|
183
|
+
tag.decompose()
|
|
184
|
+
|
|
185
|
+
candidate = (
|
|
186
|
+
soup.find("article")
|
|
187
|
+
or soup.find("main")
|
|
188
|
+
or soup.find(attrs={"role": "main"})
|
|
189
|
+
or soup.body
|
|
190
|
+
or soup
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
text = " ".join(candidate.stripped_strings)
|
|
194
|
+
text = re.sub(r"\s+", " ", text).strip()
|
|
195
|
+
return text
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def fetch_webpage_text(url: str, timeout_ms: int = 20000) -> str:
|
|
199
|
+
with sync_playwright() as p:
|
|
200
|
+
browser = p.chromium.launch(headless=True)
|
|
201
|
+
context = browser.new_context(
|
|
202
|
+
user_agent=(
|
|
203
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
|
|
204
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
205
|
+
"Chrome/124.0 Safari/537.36"
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
def _route_filter(route, request):
|
|
210
|
+
if request.resource_type in {"image", "media", "font"}:
|
|
211
|
+
route.abort()
|
|
212
|
+
else:
|
|
213
|
+
route.continue_()
|
|
214
|
+
|
|
215
|
+
context.route("**/*", _route_filter)
|
|
216
|
+
page = context.new_page()
|
|
217
|
+
page.on("dialog", lambda dialog: dialog.dismiss())
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
page.goto(url, wait_until="domcontentloaded", timeout=timeout_ms)
|
|
221
|
+
try:
|
|
222
|
+
page.wait_for_load_state("networkidle", timeout=timeout_ms)
|
|
223
|
+
except PlaywrightTimeoutError:
|
|
224
|
+
pass
|
|
225
|
+
_dismiss_common_popups(page)
|
|
226
|
+
try:
|
|
227
|
+
page.wait_for_selector("article, main, [role='main']", timeout=5000)
|
|
228
|
+
except PlaywrightTimeoutError:
|
|
229
|
+
pass
|
|
230
|
+
|
|
231
|
+
html = page.content()
|
|
232
|
+
text = _extract_main_text(html)
|
|
233
|
+
if text:
|
|
234
|
+
return text
|
|
235
|
+
|
|
236
|
+
try:
|
|
237
|
+
body_text = page.locator("body").inner_text(timeout=3000)
|
|
238
|
+
except Exception:
|
|
239
|
+
body_text = ""
|
|
240
|
+
body_text = re.sub(r"\s+", " ", body_text).strip()
|
|
241
|
+
return body_text
|
|
242
|
+
finally:
|
|
243
|
+
context.close()
|
|
244
|
+
browser.close()
|
|
245
|
+
|
|
246
|
+
def compose_prompt(name: str, target: str) -> None:
|
|
247
|
+
filepath = _resolve_skill(name)
|
|
248
|
+
if not filepath:
|
|
249
|
+
print(f"Skill '{name}' not found.")
|
|
250
|
+
return
|
|
251
|
+
|
|
252
|
+
template = filepath.read_text().strip()
|
|
253
|
+
|
|
254
|
+
parts = [template, target]
|
|
255
|
+
prompt = "\n".join(parts)
|
|
256
|
+
|
|
257
|
+
return prompt
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import tempfile
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from . import (
|
|
6
|
+
compose_prompt,
|
|
7
|
+
edit_skill,
|
|
8
|
+
list_models,
|
|
9
|
+
list_skills,
|
|
10
|
+
load_skills,
|
|
11
|
+
run_skill_creation_wizard,
|
|
12
|
+
_open_in_editor,
|
|
13
|
+
_resolve_skill,
|
|
14
|
+
_set_clipboard,
|
|
15
|
+
_is_url,
|
|
16
|
+
fetch_webpage_text,
|
|
17
|
+
)
|
|
18
|
+
from .models import LLMS
|
|
19
|
+
|
|
20
|
+
HELP = """
|
|
21
|
+
Built-in commands:
|
|
22
|
+
new Create a new skill
|
|
23
|
+
all List all skills
|
|
24
|
+
model Show active model
|
|
25
|
+
model <arg> Switch model (by name or shorthand)
|
|
26
|
+
models List all available models
|
|
27
|
+
help Show this message
|
|
28
|
+
quit Quit the application
|
|
29
|
+
|
|
30
|
+
Type <skill> to edit then open the LLM webpage.
|
|
31
|
+
Type <skill> <target> to execute a skill with context.
|
|
32
|
+
Type edit <skill> to edit a skill only (no webpage).
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
ACTIVE_MODEL = LLMS[0]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _dispatch(command: str) -> bool:
|
|
40
|
+
"""Handle one user input. Returns False if the app should quit."""
|
|
41
|
+
global ACTIVE_MODEL
|
|
42
|
+
raw = command.strip()
|
|
43
|
+
if not raw:
|
|
44
|
+
return True
|
|
45
|
+
|
|
46
|
+
# ... (Keep existing command logic: quit, help, new, all, models, model) ...
|
|
47
|
+
if raw == "quit": return False
|
|
48
|
+
if raw == "help": print(HELP); return True
|
|
49
|
+
if raw == "new": run_skill_creation_wizard(); return True
|
|
50
|
+
if raw == "all": list_skills(); return True
|
|
51
|
+
if raw == "models": list_models(ACTIVE_MODEL); return True
|
|
52
|
+
|
|
53
|
+
if raw == "model":
|
|
54
|
+
print(f"Current model: {ACTIVE_MODEL.name} ({ACTIVE_MODEL.shorthand})")
|
|
55
|
+
return True
|
|
56
|
+
if raw.startswith("model "):
|
|
57
|
+
arg = raw[6:].strip()
|
|
58
|
+
for m in LLMS:
|
|
59
|
+
if arg.lower() in (m.name.lower(), m.shorthand.lower()):
|
|
60
|
+
ACTIVE_MODEL = m
|
|
61
|
+
print(f"Switched to {ACTIVE_MODEL.name}.")
|
|
62
|
+
return True
|
|
63
|
+
print(f"Unknown model '{arg}'.")
|
|
64
|
+
return True
|
|
65
|
+
|
|
66
|
+
# Parse <edit> <skill>, <skill> <target>, or <skill>
|
|
67
|
+
rest = raw
|
|
68
|
+
first_space = rest.find(" ")
|
|
69
|
+
if first_space == -1:
|
|
70
|
+
name = rest
|
|
71
|
+
target = ""
|
|
72
|
+
else:
|
|
73
|
+
name = rest[:first_space]
|
|
74
|
+
target = rest[first_space + 1:].strip()
|
|
75
|
+
|
|
76
|
+
if name == "edit":
|
|
77
|
+
edit_skill(target)
|
|
78
|
+
return True
|
|
79
|
+
|
|
80
|
+
skills = {s["name"] for s in load_skills()}
|
|
81
|
+
if name not in skills:
|
|
82
|
+
print(f"Unknown skill '{name}'. Type `help` or `all` to see available commands.")
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# Determine initial prompt content
|
|
87
|
+
if target and _is_url(target):
|
|
88
|
+
print("Fetching webpage content...")
|
|
89
|
+
prompt = compose_prompt(name, target)
|
|
90
|
+
_set_clipboard(prompt)
|
|
91
|
+
os.system(f"open '{ACTIVE_MODEL.url}'")
|
|
92
|
+
return True
|
|
93
|
+
filepath = _resolve_skill(name)
|
|
94
|
+
if not filepath:
|
|
95
|
+
print(f"Skill '{name}' not found.")
|
|
96
|
+
return True
|
|
97
|
+
|
|
98
|
+
prompt = compose_prompt(name, target)
|
|
99
|
+
|
|
100
|
+
# Create temporary scratchpad and edit
|
|
101
|
+
with tempfile.NamedTemporaryFile(mode='w+', suffix='.md', delete=False) as tf:
|
|
102
|
+
tf.write(prompt)
|
|
103
|
+
temp_path = Path(tf.name)
|
|
104
|
+
|
|
105
|
+
if not target:
|
|
106
|
+
try:
|
|
107
|
+
_open_in_editor(temp_path)
|
|
108
|
+
|
|
109
|
+
# --- ADDED: Manual wait to ensure file is saved ---
|
|
110
|
+
input("\nFile opened in editor. Press Enter here once you have saved and closed the editor to proceed...")
|
|
111
|
+
|
|
112
|
+
# Read the final content
|
|
113
|
+
final_prompt = temp_path.read_text()
|
|
114
|
+
_set_clipboard(final_prompt)
|
|
115
|
+
print("Prompt copied to clipboard.") # Feedback for clarity
|
|
116
|
+
os.system(f"open '{ACTIVE_MODEL.url}'")
|
|
117
|
+
finally:
|
|
118
|
+
# Clean up
|
|
119
|
+
if temp_path.exists():
|
|
120
|
+
os.remove(temp_path)
|
|
121
|
+
else:
|
|
122
|
+
prompt = compose_prompt(name, target)
|
|
123
|
+
_set_clipboard(prompt)
|
|
124
|
+
os.system(f"open '{ACTIVE_MODEL.url}'")
|
|
125
|
+
|
|
126
|
+
return True
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def main() -> None:
|
|
131
|
+
try:
|
|
132
|
+
while True:
|
|
133
|
+
try:
|
|
134
|
+
command = input("\nAction: ")
|
|
135
|
+
except (EOFError, KeyboardInterrupt):
|
|
136
|
+
print()
|
|
137
|
+
break
|
|
138
|
+
if not _dispatch(command):
|
|
139
|
+
break
|
|
140
|
+
except KeyboardInterrupt:
|
|
141
|
+
print()
|
|
142
|
+
finally:
|
|
143
|
+
print("Done.")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
if __name__ == "__main__":
|
|
147
|
+
main()
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from collections import namedtuple
|
|
2
|
+
|
|
3
|
+
LLM = namedtuple("LLM", "name shorthand url")
|
|
4
|
+
|
|
5
|
+
LLMS = [
|
|
6
|
+
LLM("ChatGPT", "gpt", "https://chatgpt.com"),
|
|
7
|
+
LLM("DeepSeek", "deep", "https://chat.deepseek.com/a/chat"),
|
|
8
|
+
LLM("Gemini", "gem", "https://gemini.google.com/app"),
|
|
9
|
+
]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: conversession
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Author-email: Jordi Carrera Ventura <jordi.carrera.ventura@gmail.com>
|
|
5
|
+
Classifier: Programming Language :: Python :: 3
|
|
6
|
+
Classifier: Operating System :: OS Independent
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: build
|
|
11
|
+
Requires-Dist: twine
|
|
12
|
+
Requires-Dist: openai
|
|
13
|
+
Requires-Dist: python-dotenv
|
|
14
|
+
Requires-Dist: playwright
|
|
15
|
+
Requires-Dist: beautifulsoup4
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# Conversession
|
|
19
|
+
|
|
20
|
+
Prompt management CLI application for Linux and MacOS.
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Build and publish
|
|
24
|
+
|
|
25
|
+
1. Building the package before uploading: `python -m build # from "conversession"`.
|
|
26
|
+
2. Upload the package to pypi: `python -m twine upload --repository {pypi|testpypi} dist/*`
|
|
27
|
+
|
|
28
|
+
If any dependencies are required, edit the `pyproject.toml` file, "\[project\]" field, and add a `dependencies` key with a `List\[str\]` value, where each string is a `pip`-readable dependency.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/conversession/__init__.py
|
|
5
|
+
src/conversession/__main__.py
|
|
6
|
+
src/conversession/models.py
|
|
7
|
+
src/conversession.egg-info/PKG-INFO
|
|
8
|
+
src/conversession.egg-info/SOURCES.txt
|
|
9
|
+
src/conversession.egg-info/dependency_links.txt
|
|
10
|
+
src/conversession.egg-info/entry_points.txt
|
|
11
|
+
src/conversession.egg-info/requires.txt
|
|
12
|
+
src/conversession.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
conversession
|