obris-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- obris/__init__.py +0 -0
- obris/assets/icon.png +0 -0
- obris/capture.py +49 -0
- obris/cli.py +137 -0
- obris/config.py +42 -0
- obris/notify.py +70 -0
- obris/topics.py +49 -0
- obris/uploader.py +25 -0
- obris_cli-0.1.0.dist-info/METADATA +152 -0
- obris_cli-0.1.0.dist-info/RECORD +13 -0
- obris_cli-0.1.0.dist-info/WHEEL +4 -0
- obris_cli-0.1.0.dist-info/entry_points.txt +2 -0
- obris_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
obris/__init__.py
ADDED
|
File without changes
|
obris/assets/icon.png
ADDED
|
Binary file
|
obris/capture.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import sys
|
|
3
|
+
import tempfile
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def take_screenshot():
|
|
9
|
+
timestamp = datetime.now().strftime("%Y-%m-%d at %H.%M.%S")
|
|
10
|
+
filename = f"capture {timestamp}.png"
|
|
11
|
+
path = Path(tempfile.gettempdir()) / filename
|
|
12
|
+
|
|
13
|
+
if sys.platform == "darwin":
|
|
14
|
+
cmd = ["screencapture", "-i", str(path)]
|
|
15
|
+
elif sys.platform.startswith("linux"):
|
|
16
|
+
cmd = ["scrot", "-s", str(path)]
|
|
17
|
+
else:
|
|
18
|
+
raise SystemExit(f"Unsupported platform: {sys.platform}")
|
|
19
|
+
|
|
20
|
+
result = subprocess.run(cmd)
|
|
21
|
+
if result.returncode != 0:
|
|
22
|
+
raise SystemExit("Screenshot cancelled.")
|
|
23
|
+
|
|
24
|
+
if not path.exists() or path.stat().st_size == 0:
|
|
25
|
+
raise SystemExit("Screenshot cancelled.")
|
|
26
|
+
|
|
27
|
+
return path
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def prompt_name():
|
|
31
|
+
if sys.platform == "darwin":
|
|
32
|
+
result = subprocess.run(
|
|
33
|
+
[
|
|
34
|
+
"osascript",
|
|
35
|
+
"-e",
|
|
36
|
+
'display dialog "Name this capture:" default answer ""',
|
|
37
|
+
"-e",
|
|
38
|
+
"text returned of result",
|
|
39
|
+
],
|
|
40
|
+
capture_output=True,
|
|
41
|
+
text=True,
|
|
42
|
+
)
|
|
43
|
+
if result.returncode != 0:
|
|
44
|
+
raise SystemExit("Cancelled.")
|
|
45
|
+
return result.stdout.strip()
|
|
46
|
+
else:
|
|
47
|
+
import click
|
|
48
|
+
|
|
49
|
+
return click.prompt("Name this capture")
|
obris/cli.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from obris import capture, config, notify, topics, uploader
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@click.group()
|
|
9
|
+
def cli():
|
|
10
|
+
"""Obris CLI — capture and upload to your personal context layer."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@cli.command()
|
|
14
|
+
@click.option("--key", required=True, help="Your Obris API key")
|
|
15
|
+
@click.option("--base", default=None, help="API base URL (e.g. http://localhost:8000)")
|
|
16
|
+
def auth(key, base):
|
|
17
|
+
"""Save API key and detect scratch topic."""
|
|
18
|
+
cfg = config.load()
|
|
19
|
+
cfg["api_key"] = key
|
|
20
|
+
if base:
|
|
21
|
+
cfg["api_base"] = base.rstrip("/")
|
|
22
|
+
config.save(cfg)
|
|
23
|
+
|
|
24
|
+
# Find the Scratch topic
|
|
25
|
+
try:
|
|
26
|
+
all_topics = topics.list_topics()
|
|
27
|
+
except SystemExit:
|
|
28
|
+
click.echo("API key saved, but failed to fetch topics.")
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
scratch = next((t for t in all_topics if t.get("name") == "Scratch" and t.get("is_system")), None)
|
|
32
|
+
if scratch:
|
|
33
|
+
cfg["scratch_topic_id"] = scratch["id"]
|
|
34
|
+
config.save(cfg)
|
|
35
|
+
click.echo(f"Authenticated. Scratch topic: {scratch['id']}")
|
|
36
|
+
else:
|
|
37
|
+
click.echo("Authenticated. No 'Scratch' topic found — create one in the app.")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@cli.command("capture")
|
|
41
|
+
@click.option("--name", "cap_name", default=None, help="Display name for the capture")
|
|
42
|
+
@click.option("--prompt", "prompt_name", is_flag=True, help="Prompt for a name via dialog")
|
|
43
|
+
@click.option("--topic", default=None, help="Topic ID (defaults to Scratch)")
|
|
44
|
+
def capture_cmd(cap_name, prompt_name, topic):
|
|
45
|
+
"""Take a screenshot and upload it."""
|
|
46
|
+
topic_id = topic or config.get_scratch_topic_id()
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
path = capture.take_screenshot()
|
|
50
|
+
except SystemExit:
|
|
51
|
+
notify.send("Obris", "Screenshot cancelled")
|
|
52
|
+
raise
|
|
53
|
+
|
|
54
|
+
if cap_name:
|
|
55
|
+
name = cap_name
|
|
56
|
+
elif prompt_name:
|
|
57
|
+
name = capture.prompt_name()
|
|
58
|
+
if not name:
|
|
59
|
+
raise SystemExit("Name is required.")
|
|
60
|
+
else:
|
|
61
|
+
name = path.stem
|
|
62
|
+
notify.send_quiet("Obris", "Uploading...")
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
result = uploader.upload_file(topic_id, path, name)
|
|
66
|
+
except SystemExit as e:
|
|
67
|
+
notify.send("Obris", f"Upload failed")
|
|
68
|
+
raise
|
|
69
|
+
|
|
70
|
+
notify.send("Obris", f"Uploaded '{result.get('title', name)}'", url=notify.topic_url(topic_id))
|
|
71
|
+
click.echo(f"Uploaded '{result.get('title', name)}'")
|
|
72
|
+
click.echo(f" ID: {result['id']}")
|
|
73
|
+
|
|
74
|
+
path.unlink(missing_ok=True)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@cli.command()
|
|
78
|
+
@click.argument("filepath", type=click.Path(exists=True, path_type=Path))
|
|
79
|
+
@click.option("--topic", default=None, help="Topic ID (defaults to Scratch)")
|
|
80
|
+
@click.option("--name", default=None, help="Display name (defaults to filename)")
|
|
81
|
+
def upload(filepath, topic, name):
|
|
82
|
+
"""Upload a file to a topic."""
|
|
83
|
+
topic_id = topic or config.get_scratch_topic_id()
|
|
84
|
+
name = name or filepath.name
|
|
85
|
+
result = uploader.upload_file(topic_id, filepath, name)
|
|
86
|
+
click.echo(f"Uploaded '{result.get('title', name)}'")
|
|
87
|
+
click.echo(f" ID: {result['id']}")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@cli.group("topic")
|
|
91
|
+
def topic_group():
|
|
92
|
+
"""Manage topics."""
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@topic_group.command("list")
|
|
96
|
+
@click.argument("topic_id", required=False)
|
|
97
|
+
def topic_list(topic_id):
|
|
98
|
+
"""List all topics, or knowledge items in a specific topic."""
|
|
99
|
+
if topic_id:
|
|
100
|
+
items = topics.list_knowledge(topic_id)
|
|
101
|
+
if not items:
|
|
102
|
+
click.echo("No items found.")
|
|
103
|
+
return
|
|
104
|
+
click.echo(f"{'ID':<28}{'TITLE':<38}CREATED")
|
|
105
|
+
for item in items:
|
|
106
|
+
created = item.get("created_at", "")[:16].replace("T", " ")
|
|
107
|
+
click.echo(f"{item['id']:<28}{item.get('title', ''):<38}{created}")
|
|
108
|
+
else:
|
|
109
|
+
all_topics = topics.list_topics()
|
|
110
|
+
if not all_topics:
|
|
111
|
+
click.echo("No topics found.")
|
|
112
|
+
return
|
|
113
|
+
click.echo(f"{'ID':<28}{'NAME':<22}ITEMS")
|
|
114
|
+
for t in all_topics:
|
|
115
|
+
click.echo(f"{t['id']:<28}{t['name']:<22}{t.get('item_count', 0)}")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@cli.group("knowledge")
|
|
119
|
+
def knowledge_group():
|
|
120
|
+
"""Manage knowledge items."""
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@knowledge_group.command("move")
|
|
124
|
+
@click.argument("knowledge_id")
|
|
125
|
+
@click.option("--topic", required=True, help="Destination topic ID")
|
|
126
|
+
def knowledge_move(knowledge_id, topic):
|
|
127
|
+
"""Move a knowledge item to another topic."""
|
|
128
|
+
result = topics.move_knowledge(knowledge_id, topic)
|
|
129
|
+
click.echo(f"Moved to {result.get('topic_name', topic)}")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@knowledge_group.command("delete")
|
|
133
|
+
@click.argument("knowledge_id")
|
|
134
|
+
def knowledge_delete(knowledge_id):
|
|
135
|
+
"""Delete a knowledge item."""
|
|
136
|
+
topics.delete_knowledge(knowledge_id)
|
|
137
|
+
click.echo(f"Deleted {knowledge_id}")
|
obris/config.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
CONFIG_DIR = Path.home() / ".obris"
|
|
5
|
+
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
6
|
+
|
|
7
|
+
DEFAULT_CONFIG = {
|
|
8
|
+
"api_key": "",
|
|
9
|
+
"api_base": "https://api.obris.ai",
|
|
10
|
+
"scratch_topic_id": "",
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def load():
|
|
15
|
+
if not CONFIG_FILE.exists():
|
|
16
|
+
return dict(DEFAULT_CONFIG)
|
|
17
|
+
return {**DEFAULT_CONFIG, **json.loads(CONFIG_FILE.read_text())}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def save(config):
|
|
21
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
22
|
+
CONFIG_FILE.write_text(json.dumps(config, indent=2) + "\n")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_api_key():
|
|
26
|
+
cfg = load()
|
|
27
|
+
key = cfg.get("api_key")
|
|
28
|
+
if not key:
|
|
29
|
+
raise SystemExit("Not authenticated. Run: obris auth --key <key>")
|
|
30
|
+
return key
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_api_base():
|
|
34
|
+
return load().get("api_base", DEFAULT_CONFIG["api_base"])
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_scratch_topic_id():
|
|
38
|
+
cfg = load()
|
|
39
|
+
tid = cfg.get("scratch_topic_id")
|
|
40
|
+
if not tid:
|
|
41
|
+
raise SystemExit("No scratch topic configured. Run: obris auth --key <key>")
|
|
42
|
+
return tid
|
obris/notify.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
import subprocess
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
ICON_PATH = str(Path(__file__).parent / "assets" / "icon.png")
|
|
7
|
+
|
|
8
|
+
# api.obris.ai -> app.obris.ai, localhost:8000 -> localhost:3001
|
|
9
|
+
APP_BASE_MAP = {
|
|
10
|
+
"https://api.obris.ai": "https://app.obris.ai",
|
|
11
|
+
"http://localhost:8000": "http://localhost:3001",
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
_has_notifier = sys.platform == "darwin" and shutil.which("terminal-notifier") is not None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _get_app_base():
|
|
18
|
+
from obris.config import get_api_base
|
|
19
|
+
api_base = get_api_base()
|
|
20
|
+
return APP_BASE_MAP.get(api_base, api_base.replace("api.", "app."))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _osascript_notify(title, message):
|
|
24
|
+
subprocess.run([
|
|
25
|
+
"osascript", "-e",
|
|
26
|
+
f'display notification "{message}" with title "{title}"',
|
|
27
|
+
])
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def send(title, message, url=None):
|
|
31
|
+
if sys.platform != "darwin":
|
|
32
|
+
import click
|
|
33
|
+
click.echo(f"{title}: {message}")
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
if _has_notifier:
|
|
37
|
+
cmd = [
|
|
38
|
+
"terminal-notifier",
|
|
39
|
+
"-title", title,
|
|
40
|
+
"-message", message,
|
|
41
|
+
"-contentImage", ICON_PATH,
|
|
42
|
+
"-sound", "default",
|
|
43
|
+
]
|
|
44
|
+
if url:
|
|
45
|
+
cmd += ["-open", url]
|
|
46
|
+
subprocess.run(cmd)
|
|
47
|
+
else:
|
|
48
|
+
_osascript_notify(title, message)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def send_quiet(title, message):
|
|
52
|
+
"""Notification without sound (for transient states like 'Uploading...')."""
|
|
53
|
+
if sys.platform != "darwin":
|
|
54
|
+
import click
|
|
55
|
+
click.echo(f"{title}: {message}")
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
if _has_notifier:
|
|
59
|
+
subprocess.run([
|
|
60
|
+
"terminal-notifier",
|
|
61
|
+
"-title", title,
|
|
62
|
+
"-message", message,
|
|
63
|
+
"-contentImage", ICON_PATH,
|
|
64
|
+
])
|
|
65
|
+
else:
|
|
66
|
+
_osascript_notify(title, message)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def topic_url(topic_id):
|
|
70
|
+
return f"{_get_app_base()}/topics/{topic_id}"
|
obris/topics.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
|
|
3
|
+
from obris.config import get_api_base, get_api_key
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _headers():
|
|
7
|
+
return {"X-API-Key": get_api_key()}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _unwrap(data):
|
|
11
|
+
"""Handle paginated ({"results": [...]}) or plain list responses."""
|
|
12
|
+
if isinstance(data, dict) and "results" in data:
|
|
13
|
+
return data["results"]
|
|
14
|
+
return data
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def list_topics():
|
|
18
|
+
resp = requests.get(f"{get_api_base()}/v1/topics", headers=_headers())
|
|
19
|
+
if not resp.ok:
|
|
20
|
+
raise SystemExit(f"Failed to list topics ({resp.status_code}): {resp.text}")
|
|
21
|
+
return _unwrap(resp.json())
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def list_knowledge(topic_id):
|
|
25
|
+
resp = requests.get(
|
|
26
|
+
f"{get_api_base()}/v1/topics/{topic_id}/knowledge", headers=_headers()
|
|
27
|
+
)
|
|
28
|
+
if not resp.ok:
|
|
29
|
+
raise SystemExit(f"Failed to list knowledge ({resp.status_code}): {resp.text}")
|
|
30
|
+
return _unwrap(resp.json())
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def delete_knowledge(knowledge_id):
|
|
34
|
+
resp = requests.delete(
|
|
35
|
+
f"{get_api_base()}/v1/knowledge/detail/{knowledge_id}", headers=_headers()
|
|
36
|
+
)
|
|
37
|
+
if not resp.ok:
|
|
38
|
+
raise SystemExit(f"Delete failed ({resp.status_code}): {resp.text}")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def move_knowledge(knowledge_id, topic_id):
|
|
42
|
+
resp = requests.post(
|
|
43
|
+
f"{get_api_base()}/v1/knowledge/detail/{knowledge_id}/move",
|
|
44
|
+
headers=_headers(),
|
|
45
|
+
json={"topic_id": topic_id},
|
|
46
|
+
)
|
|
47
|
+
if not resp.ok:
|
|
48
|
+
raise SystemExit(f"Move failed ({resp.status_code}): {resp.text}")
|
|
49
|
+
return resp.json()
|
obris/uploader.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import mimetypes
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
|
|
5
|
+
from obris.config import get_api_base, get_api_key
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def upload_file(topic_id, filepath, name):
|
|
9
|
+
url = f"{get_api_base()}/v1/knowledge/upload"
|
|
10
|
+
headers = {"X-API-Key": get_api_key()}
|
|
11
|
+
filename = filepath.name if hasattr(filepath, "name") else str(filepath)
|
|
12
|
+
mime_type = mimetypes.guess_type(filename)[0] or "application/octet-stream"
|
|
13
|
+
|
|
14
|
+
with open(filepath, "rb") as f:
|
|
15
|
+
resp = requests.post(
|
|
16
|
+
url,
|
|
17
|
+
headers=headers,
|
|
18
|
+
files={"file": (filename, f, mime_type)},
|
|
19
|
+
data={"topic_id": topic_id, "title": name},
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
if not resp.ok:
|
|
23
|
+
raise SystemExit(f"Upload failed ({resp.status_code}): {resp.text}")
|
|
24
|
+
|
|
25
|
+
return resp.json()
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: obris-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI for capturing screenshots and managing knowledge in Obris
|
|
5
|
+
Project-URL: Homepage, https://obris.ai
|
|
6
|
+
Project-URL: Repository, https://github.com/obris-dev/obris-cli
|
|
7
|
+
Project-URL: Documentation, https://docs.obris.ai
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: ai,capture,cli,knowledge-management,obris,screenshot,second-brain
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Requires-Dist: click>=8.0
|
|
13
|
+
Requires-Dist: requests>=2.28
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# Obris CLI
|
|
17
|
+
|
|
18
|
+
A command-line tool for capturing screenshots and managing knowledge in [Obris](https://obris.ai), your personal knowledge layer for AI. Save content to organized topics so you never start another AI chat from zero again.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
Requires Python 3.10+.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install obris-cli
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or run directly with [uv](https://docs.astral.sh/uv/):
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
uvx obris-cli --help
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Optional: macOS notifications
|
|
35
|
+
|
|
36
|
+
For desktop notifications with deep linking to your uploaded content:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
brew install terminal-notifier
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Setup
|
|
43
|
+
|
|
44
|
+
### 1. Get your API key
|
|
45
|
+
|
|
46
|
+
Generate an API key from your [Obris dashboard](https://app.obris.ai/api-keys). Don't have an account? [Sign up](https://app.obris.ai/signup).
|
|
47
|
+
|
|
48
|
+
### 2. Authenticate
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
obris auth --key <your-api-key>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
This saves your key locally to `~/.obris/config.json` and detects your Scratch topic (the default destination for captures).
|
|
55
|
+
|
|
56
|
+
## Usage
|
|
57
|
+
|
|
58
|
+
### Capture a screenshot
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
obris capture # screenshot + upload to Scratch
|
|
62
|
+
obris capture --name "my pic" # explicit name
|
|
63
|
+
obris capture --prompt # prompt for a name via dialog
|
|
64
|
+
obris capture --topic <id> # upload to a specific topic
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Upload a file
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
obris upload photo.png # upload to Scratch
|
|
71
|
+
obris upload photo.png --topic <id> # upload to a specific topic
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Manage topics
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
obris topic list # list all topics
|
|
78
|
+
obris topic list <topic_id> # list knowledge in a topic
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Manage knowledge
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
obris knowledge move <id> --topic <id> # move to another topic
|
|
85
|
+
obris knowledge delete <id> # delete a knowledge item
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Hotkeys
|
|
89
|
+
|
|
90
|
+
Bind keyboard shortcuts to capture commands using any automation tool — [Alfred](https://www.alfredapp.com/), [Raycast](https://www.raycast.com/), [Keyboard Maestro](https://www.keyboardmaestro.com/), macOS Shortcuts, etc.
|
|
91
|
+
|
|
92
|
+
### Example: Alfred
|
|
93
|
+
|
|
94
|
+
Create a workflow with a **Hotkey** trigger connected to a **Run Script** action (language: `/bin/zsh`):
|
|
95
|
+
|
|
96
|
+
**Quick capture:**
|
|
97
|
+
|
|
98
|
+
```zsh
|
|
99
|
+
/full/path/to/obris capture
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Capture with name prompt:**
|
|
103
|
+
|
|
104
|
+
```zsh
|
|
105
|
+
/full/path/to/obris capture --prompt
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
> **Tip:** Use the full path to the `obris` binary since Alfred doesn't load your shell profile. Run `which obris` to find it.
|
|
109
|
+
|
|
110
|
+
## Platform support
|
|
111
|
+
|
|
112
|
+
| Platform | Capture | Upload / Topics / Knowledge |
|
|
113
|
+
|----------|---------|----------------------------|
|
|
114
|
+
| macOS | Yes | Yes |
|
|
115
|
+
| Linux | Yes | Yes |
|
|
116
|
+
| Windows | No | Yes |
|
|
117
|
+
|
|
118
|
+
## Development
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
git clone https://github.com/obris-dev/obris-cli.git
|
|
122
|
+
cd obris-cli
|
|
123
|
+
uv sync
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Run commands locally without installing:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
uv run obris auth --key <your-api-key> --base http://localhost:8000
|
|
130
|
+
uv run obris capture
|
|
131
|
+
uv run obris topic list
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Publishing
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
make publish # build and publish to PyPI
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Privacy Policy
|
|
141
|
+
|
|
142
|
+
This CLI sends your Obris API key to the Obris API (`api.obris.ai`) to authenticate requests. It uploads files and retrieves topic and knowledge data from your account. No data is stored beyond the local config file at `~/.obris/config.json`.
|
|
143
|
+
|
|
144
|
+
For the full privacy policy, see [obris.ai/privacy](https://obris.ai/privacy).
|
|
145
|
+
|
|
146
|
+
## Support
|
|
147
|
+
|
|
148
|
+
For issues or questions, contact [support@obris.ai](mailto:support@obris.ai) or open an issue on [GitHub](https://github.com/obris-dev/obris-cli/issues).
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
MIT
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
obris/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
obris/capture.py,sha256=p3FDHDHNllo-D3htYH9OHr6G__H0UTrCAXYFuPH8MAc,1327
|
|
3
|
+
obris/cli.py,sha256=CMVGRkhkF5RrYew-i8Rq1guvuQA2uBWJw7mrv2MJ80M,4463
|
|
4
|
+
obris/config.py,sha256=0EXugY6s_CtHCv4FrcPczrMXt8qw54HB3nlMPy04be8,968
|
|
5
|
+
obris/notify.py,sha256=hSfYLRIcasnHy8F5HlwfVFER-hXBaXEBPFEUimpi_4U,1788
|
|
6
|
+
obris/topics.py,sha256=uE8Pbd-yl7jKBGy0N3EmvWjRVRojSxU3rYd5iT_j-Kc,1416
|
|
7
|
+
obris/uploader.py,sha256=oL4RPcLscuN7dkR4SESihM-siQM2oXONfmc41hQWSxs,735
|
|
8
|
+
obris/assets/icon.png,sha256=Bj88FinJLxlnPuINM_b8plw1Q6akHnqsWS4RkhfydYM,12340
|
|
9
|
+
obris_cli-0.1.0.dist-info/METADATA,sha256=jTfXULIOH_mvT_BSE64TROEYSXveI7dm9LaJAcLNhjo,3912
|
|
10
|
+
obris_cli-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
11
|
+
obris_cli-0.1.0.dist-info/entry_points.txt,sha256=7ujVi_V6PQxISgOZ1Jyc7WYympakAYsxaDdngWmcKug,40
|
|
12
|
+
obris_cli-0.1.0.dist-info/licenses/LICENSE,sha256=HDIIGoyIe7XW8UG0Q-TVBneWsfDijfUhdelvyCgbJBM,1061
|
|
13
|
+
obris_cli-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Obris
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|