tisit-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.
- tisit_cli/__init__.py +2 -0
- tisit_cli/auth.py +54 -0
- tisit_cli/client.py +392 -0
- tisit_cli/commands/__init__.py +0 -0
- tisit_cli/commands/article_commands.py +132 -0
- tisit_cli/commands/auth_commands.py +91 -0
- tisit_cli/commands/book_commands.py +186 -0
- tisit_cli/commands/browse_commands.py +76 -0
- tisit_cli/commands/chat_commands.py +94 -0
- tisit_cli/commands/focus_commands.py +198 -0
- tisit_cli/commands/graph_commands.py +126 -0
- tisit_cli/commands/note_commands.py +131 -0
- tisit_cli/commands/paper_commands.py +134 -0
- tisit_cli/commands/patent_commands.py +138 -0
- tisit_cli/commands/podcast_commands.py +132 -0
- tisit_cli/commands/radar_commands.py +246 -0
- tisit_cli/commands/search_commands.py +42 -0
- tisit_cli/commands/status_commands.py +50 -0
- tisit_cli/commands/tweet_commands.py +132 -0
- tisit_cli/commands/video_commands.py +132 -0
- tisit_cli/config.py +53 -0
- tisit_cli/display.py +582 -0
- tisit_cli/exceptions.py +29 -0
- tisit_cli/main.py +68 -0
- tisit_cli-0.1.0.dist-info/METADATA +114 -0
- tisit_cli-0.1.0.dist-info/RECORD +29 -0
- tisit_cli-0.1.0.dist-info/WHEEL +4 -0
- tisit_cli-0.1.0.dist-info/entry_points.txt +2 -0
- tisit_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""Radar sweep commands: status, topic add/list/delete, run, source accept/ignore."""
|
|
2
|
+
import json
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.table import Table
|
|
7
|
+
|
|
8
|
+
from ..auth import get_token
|
|
9
|
+
from ..client import TisitClient
|
|
10
|
+
from ..config import Config
|
|
11
|
+
from ..display import console, print_error, print_success, print_info
|
|
12
|
+
from ..exceptions import AuthenticationError, APIError, NotFoundError
|
|
13
|
+
|
|
14
|
+
radar_app = typer.Typer(help="Radar Sweep — intelligence monitoring")
|
|
15
|
+
topic_app = typer.Typer(help="Manage watch topics")
|
|
16
|
+
source_app = typer.Typer(help="Triage sweep sources")
|
|
17
|
+
radar_app.add_typer(topic_app, name="topic")
|
|
18
|
+
radar_app.add_typer(source_app, name="source")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _get_client() -> TisitClient:
|
|
22
|
+
token = get_token()
|
|
23
|
+
if not token:
|
|
24
|
+
print_error("Not logged in. Run: tisit login")
|
|
25
|
+
raise typer.Exit(code=1)
|
|
26
|
+
cfg = Config()
|
|
27
|
+
return TisitClient(cfg.api_url, token)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@radar_app.command("status")
|
|
31
|
+
def radar_status(
|
|
32
|
+
output_json: bool = typer.Option(False, "--json", help="JSON output"),
|
|
33
|
+
):
|
|
34
|
+
"""Show watch topics and recent sweep runs."""
|
|
35
|
+
client = _get_client()
|
|
36
|
+
try:
|
|
37
|
+
data = client.radar_status()
|
|
38
|
+
except (AuthenticationError, APIError) as exc:
|
|
39
|
+
print_error(str(exc))
|
|
40
|
+
raise typer.Exit(code=1)
|
|
41
|
+
finally:
|
|
42
|
+
client.close()
|
|
43
|
+
|
|
44
|
+
if output_json:
|
|
45
|
+
typer.echo(json.dumps(data, indent=2, default=str))
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
topics = data.get("topics", [])
|
|
49
|
+
runs = data.get("recent_runs", [])
|
|
50
|
+
|
|
51
|
+
if topics:
|
|
52
|
+
table = Table(title="Watch Topics")
|
|
53
|
+
table.add_column("ID", style="cyan", justify="right")
|
|
54
|
+
table.add_column("Topic", style="bold")
|
|
55
|
+
table.add_column("Keywords")
|
|
56
|
+
for t in topics:
|
|
57
|
+
table.add_row(
|
|
58
|
+
str(t["id"]),
|
|
59
|
+
t["topic"],
|
|
60
|
+
", ".join(t.get("keywords") or []),
|
|
61
|
+
)
|
|
62
|
+
console.print(table)
|
|
63
|
+
else:
|
|
64
|
+
print_info("No watch topics. Add one with: tisit radar topic add \"topic\"")
|
|
65
|
+
|
|
66
|
+
console.print()
|
|
67
|
+
|
|
68
|
+
if runs:
|
|
69
|
+
table = Table(title="Recent Sweep Runs")
|
|
70
|
+
table.add_column("ID", style="cyan", justify="right")
|
|
71
|
+
table.add_column("Topic")
|
|
72
|
+
table.add_column("Status")
|
|
73
|
+
table.add_column("Sources")
|
|
74
|
+
table.add_column("Notes")
|
|
75
|
+
table.add_column("When")
|
|
76
|
+
for r in runs:
|
|
77
|
+
table.add_row(
|
|
78
|
+
str(r["id"]),
|
|
79
|
+
r.get("topic_name", ""),
|
|
80
|
+
r.get("status", ""),
|
|
81
|
+
str(r.get("total_sources_fetched", 0)),
|
|
82
|
+
str(r.get("total_notes_generated", 0)),
|
|
83
|
+
(r.get("run_timestamp") or "")[:10],
|
|
84
|
+
)
|
|
85
|
+
console.print(table)
|
|
86
|
+
else:
|
|
87
|
+
print_info("No sweep runs yet.")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@radar_app.command("run")
|
|
91
|
+
def radar_run(
|
|
92
|
+
run_id: int = typer.Argument(..., help="Sweep run ID"),
|
|
93
|
+
output_json: bool = typer.Option(False, "--json", help="JSON output"),
|
|
94
|
+
):
|
|
95
|
+
"""View sweep run details and sources."""
|
|
96
|
+
client = _get_client()
|
|
97
|
+
try:
|
|
98
|
+
data = client.get_sweep_run(run_id)
|
|
99
|
+
except NotFoundError:
|
|
100
|
+
print_error(f"Sweep run {run_id} not found.")
|
|
101
|
+
raise typer.Exit(code=1)
|
|
102
|
+
except (AuthenticationError, APIError) as exc:
|
|
103
|
+
print_error(str(exc))
|
|
104
|
+
raise typer.Exit(code=1)
|
|
105
|
+
finally:
|
|
106
|
+
client.close()
|
|
107
|
+
|
|
108
|
+
if output_json:
|
|
109
|
+
typer.echo(json.dumps(data, indent=2, default=str))
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
run = data.get("run", {})
|
|
113
|
+
sources = data.get("sources", [])
|
|
114
|
+
|
|
115
|
+
print_info(
|
|
116
|
+
f"Run #{run.get('id')} — {run.get('topic_name', '')} — "
|
|
117
|
+
f"{run.get('status', '')} — {run.get('total_sources_fetched', 0)} sources"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
if sources:
|
|
121
|
+
table = Table(title="Sources")
|
|
122
|
+
table.add_column("ID", style="cyan", justify="right")
|
|
123
|
+
table.add_column("Type")
|
|
124
|
+
table.add_column("Title", style="bold")
|
|
125
|
+
table.add_column("Triage")
|
|
126
|
+
for s in sources:
|
|
127
|
+
title = s.get("title") or ""
|
|
128
|
+
if len(title) > 50:
|
|
129
|
+
title = title[:47] + "..."
|
|
130
|
+
table.add_row(
|
|
131
|
+
str(s["id"]),
|
|
132
|
+
s.get("source_type", ""),
|
|
133
|
+
title,
|
|
134
|
+
s.get("triage_status", ""),
|
|
135
|
+
)
|
|
136
|
+
console.print(table)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# ── topic subcommands ─────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
@topic_app.command("list")
|
|
142
|
+
def topic_list(
|
|
143
|
+
output_json: bool = typer.Option(False, "--json", help="JSON output"),
|
|
144
|
+
):
|
|
145
|
+
"""List your watch topics."""
|
|
146
|
+
client = _get_client()
|
|
147
|
+
try:
|
|
148
|
+
topics = client.list_watch_topics()
|
|
149
|
+
except (AuthenticationError, APIError) as exc:
|
|
150
|
+
print_error(str(exc))
|
|
151
|
+
raise typer.Exit(code=1)
|
|
152
|
+
finally:
|
|
153
|
+
client.close()
|
|
154
|
+
|
|
155
|
+
if output_json:
|
|
156
|
+
typer.echo(json.dumps(topics, indent=2, default=str))
|
|
157
|
+
else:
|
|
158
|
+
if not topics:
|
|
159
|
+
print_info("No watch topics.")
|
|
160
|
+
else:
|
|
161
|
+
table = Table(title="Watch Topics")
|
|
162
|
+
table.add_column("ID", style="cyan", justify="right")
|
|
163
|
+
table.add_column("Topic", style="bold")
|
|
164
|
+
table.add_column("Keywords")
|
|
165
|
+
for t in topics:
|
|
166
|
+
table.add_row(str(t["id"]), t["topic"], ", ".join(t.get("keywords") or []))
|
|
167
|
+
console.print(table)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@topic_app.command("add")
|
|
171
|
+
def topic_add(
|
|
172
|
+
topic: str = typer.Argument(..., help="Topic name"),
|
|
173
|
+
keywords: Optional[str] = typer.Option(None, "--keywords", "-k", help="Comma-separated keywords"),
|
|
174
|
+
output_json: bool = typer.Option(False, "--json", help="JSON output"),
|
|
175
|
+
):
|
|
176
|
+
"""Add a watch topic."""
|
|
177
|
+
kw_list = [k.strip() for k in keywords.split(",")] if keywords else None
|
|
178
|
+
|
|
179
|
+
client = _get_client()
|
|
180
|
+
try:
|
|
181
|
+
data = client.create_watch_topic(topic, keywords=kw_list)
|
|
182
|
+
except (AuthenticationError, APIError) as exc:
|
|
183
|
+
print_error(str(exc))
|
|
184
|
+
raise typer.Exit(code=1)
|
|
185
|
+
finally:
|
|
186
|
+
client.close()
|
|
187
|
+
|
|
188
|
+
if output_json:
|
|
189
|
+
typer.echo(json.dumps(data, indent=2, default=str))
|
|
190
|
+
else:
|
|
191
|
+
print_success(f"Watch topic created (ID: {data.get('id')}).")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@topic_app.command("delete")
|
|
195
|
+
def topic_delete(
|
|
196
|
+
topic_id: int = typer.Argument(..., help="Topic ID"),
|
|
197
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
|
|
198
|
+
):
|
|
199
|
+
"""Delete a watch topic."""
|
|
200
|
+
if not yes:
|
|
201
|
+
if not typer.confirm(f"Delete watch topic {topic_id}?"):
|
|
202
|
+
raise typer.Abort()
|
|
203
|
+
|
|
204
|
+
client = _get_client()
|
|
205
|
+
try:
|
|
206
|
+
client.delete_watch_topic(topic_id)
|
|
207
|
+
except NotFoundError:
|
|
208
|
+
print_error(f"Watch topic {topic_id} not found.")
|
|
209
|
+
raise typer.Exit(code=1)
|
|
210
|
+
except (AuthenticationError, APIError) as exc:
|
|
211
|
+
print_error(str(exc))
|
|
212
|
+
raise typer.Exit(code=1)
|
|
213
|
+
finally:
|
|
214
|
+
client.close()
|
|
215
|
+
|
|
216
|
+
print_success(f"Watch topic {topic_id} deleted.")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# ── source subcommands ────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
@source_app.command("accept")
|
|
222
|
+
def source_accept(source_id: int = typer.Argument(..., help="Source ID")):
|
|
223
|
+
"""Accept a sweep source for note generation."""
|
|
224
|
+
client = _get_client()
|
|
225
|
+
try:
|
|
226
|
+
client.accept_source(source_id)
|
|
227
|
+
except (AuthenticationError, APIError) as exc:
|
|
228
|
+
print_error(str(exc))
|
|
229
|
+
raise typer.Exit(code=1)
|
|
230
|
+
finally:
|
|
231
|
+
client.close()
|
|
232
|
+
print_success(f"Source {source_id} accepted.")
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@source_app.command("ignore")
|
|
236
|
+
def source_ignore(source_id: int = typer.Argument(..., help="Source ID")):
|
|
237
|
+
"""Ignore/dismiss a sweep source."""
|
|
238
|
+
client = _get_client()
|
|
239
|
+
try:
|
|
240
|
+
client.ignore_source(source_id)
|
|
241
|
+
except (AuthenticationError, APIError) as exc:
|
|
242
|
+
print_error(str(exc))
|
|
243
|
+
raise typer.Exit(code=1)
|
|
244
|
+
finally:
|
|
245
|
+
client.close()
|
|
246
|
+
print_success(f"Source {source_id} ignored.")
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Search command."""
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from ..auth import get_token
|
|
7
|
+
from ..client import TisitClient
|
|
8
|
+
from ..config import Config
|
|
9
|
+
from ..display import print_error, print_search_results
|
|
10
|
+
from ..exceptions import AuthenticationError, APIError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def search(
|
|
14
|
+
query: str = typer.Argument(..., help="Search query"),
|
|
15
|
+
type: str = typer.Option("all", "--type", "-t",
|
|
16
|
+
help="all, notes, papers, or articles"),
|
|
17
|
+
limit: int = typer.Option(10, "--limit", "-l", help="Max results (1-50)"),
|
|
18
|
+
output_json: bool = typer.Option(False, "--json", help="JSON output"),
|
|
19
|
+
):
|
|
20
|
+
"""Semantic search across your knowledge base."""
|
|
21
|
+
token = get_token()
|
|
22
|
+
if not token:
|
|
23
|
+
print_error("Not logged in. Run: tisit login")
|
|
24
|
+
raise typer.Exit(code=1)
|
|
25
|
+
|
|
26
|
+
cfg = Config()
|
|
27
|
+
client = TisitClient(cfg.api_url, token)
|
|
28
|
+
try:
|
|
29
|
+
results, meta = client.search(query, type=type, limit=limit)
|
|
30
|
+
except (AuthenticationError, APIError) as exc:
|
|
31
|
+
print_error(str(exc))
|
|
32
|
+
raise typer.Exit(code=1)
|
|
33
|
+
finally:
|
|
34
|
+
client.close()
|
|
35
|
+
|
|
36
|
+
if output_json:
|
|
37
|
+
typer.echo(json.dumps({"data": results, "meta": meta}, indent=2, default=str))
|
|
38
|
+
else:
|
|
39
|
+
if not results:
|
|
40
|
+
print_error("No results found.")
|
|
41
|
+
else:
|
|
42
|
+
print_search_results(results, meta)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Status command: system health and connection info."""
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from ..auth import get_token
|
|
7
|
+
from ..client import TisitClient
|
|
8
|
+
from ..config import Config
|
|
9
|
+
from ..display import print_error, print_success, print_info
|
|
10
|
+
from ..exceptions import APIError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def status(
|
|
14
|
+
output_json: bool = typer.Option(False, "--json", help="JSON output"),
|
|
15
|
+
):
|
|
16
|
+
"""Check system health and connection status."""
|
|
17
|
+
token = get_token()
|
|
18
|
+
cfg = Config()
|
|
19
|
+
|
|
20
|
+
result = {
|
|
21
|
+
"logged_in": token is not None,
|
|
22
|
+
"api_url": cfg.api_url,
|
|
23
|
+
"server_status": None,
|
|
24
|
+
"database": None,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if token:
|
|
28
|
+
client = TisitClient(cfg.api_url, token)
|
|
29
|
+
try:
|
|
30
|
+
data = client.status()
|
|
31
|
+
result["server_status"] = data.get("status", "ok")
|
|
32
|
+
result["database"] = data.get("database", "unknown")
|
|
33
|
+
except APIError:
|
|
34
|
+
result["server_status"] = "unreachable"
|
|
35
|
+
except Exception:
|
|
36
|
+
result["server_status"] = "error"
|
|
37
|
+
finally:
|
|
38
|
+
client.close()
|
|
39
|
+
|
|
40
|
+
if output_json:
|
|
41
|
+
typer.echo(json.dumps(result, indent=2))
|
|
42
|
+
else:
|
|
43
|
+
print_info(f" Server: {cfg.api_url}")
|
|
44
|
+
if result["logged_in"]:
|
|
45
|
+
if result["server_status"] == "ok":
|
|
46
|
+
print_success(f" Status: connected (database: {result['database']})")
|
|
47
|
+
else:
|
|
48
|
+
print_error(f" Status: {result['server_status']}")
|
|
49
|
+
else:
|
|
50
|
+
print_error(" Not logged in. Run: tisit login")
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Tweet commands: add, list, view, delete."""
|
|
2
|
+
import json
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from ..auth import get_token
|
|
8
|
+
from ..client import TisitClient
|
|
9
|
+
from ..config import Config
|
|
10
|
+
from ..display import (
|
|
11
|
+
print_error, print_tweet_detail, print_tweet_table,
|
|
12
|
+
print_success,
|
|
13
|
+
)
|
|
14
|
+
from ..exceptions import AuthenticationError, APIError, NotFoundError
|
|
15
|
+
|
|
16
|
+
tweet_app = typer.Typer(help="Manage your tweets / X posts")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _get_client() -> TisitClient:
|
|
20
|
+
token = get_token()
|
|
21
|
+
if not token:
|
|
22
|
+
print_error("Not logged in. Run: tisit login")
|
|
23
|
+
raise typer.Exit(code=1)
|
|
24
|
+
cfg = Config()
|
|
25
|
+
return TisitClient(cfg.api_url, token)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@tweet_app.command("list")
|
|
29
|
+
def tweet_list(
|
|
30
|
+
query: Optional[str] = typer.Option(None, "--query", "-q", help="Search content or author"),
|
|
31
|
+
sort: str = typer.Option("created_at", "--sort", "-s"),
|
|
32
|
+
order: str = typer.Option("desc", "--order", "-o"),
|
|
33
|
+
page: int = typer.Option(1, "--page", "-p"),
|
|
34
|
+
per_page: int = typer.Option(20, "--per-page"),
|
|
35
|
+
output_json: bool = typer.Option(False, "--json", help="JSON output"),
|
|
36
|
+
):
|
|
37
|
+
"""List your tweets."""
|
|
38
|
+
client = _get_client()
|
|
39
|
+
try:
|
|
40
|
+
tweets, meta = client.list_tweets(
|
|
41
|
+
q=query, sort=sort, order=order, page=page, per_page=per_page,
|
|
42
|
+
)
|
|
43
|
+
except (AuthenticationError, APIError) as exc:
|
|
44
|
+
print_error(str(exc))
|
|
45
|
+
raise typer.Exit(code=1)
|
|
46
|
+
finally:
|
|
47
|
+
client.close()
|
|
48
|
+
|
|
49
|
+
if output_json:
|
|
50
|
+
typer.echo(json.dumps({"data": tweets, "meta": meta}, indent=2, default=str))
|
|
51
|
+
else:
|
|
52
|
+
if not tweets:
|
|
53
|
+
print_error("No tweets found.")
|
|
54
|
+
else:
|
|
55
|
+
print_tweet_table(tweets, meta)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@tweet_app.command("view")
|
|
59
|
+
def tweet_view(
|
|
60
|
+
tweet_id: int = typer.Argument(..., help="Tweet ID"),
|
|
61
|
+
output_json: bool = typer.Option(False, "--json", help="JSON output"),
|
|
62
|
+
):
|
|
63
|
+
"""View a tweet in detail."""
|
|
64
|
+
client = _get_client()
|
|
65
|
+
try:
|
|
66
|
+
tweet = client.get_tweet(tweet_id)
|
|
67
|
+
except NotFoundError:
|
|
68
|
+
print_error(f"Tweet {tweet_id} not found.")
|
|
69
|
+
raise typer.Exit(code=1)
|
|
70
|
+
except (AuthenticationError, APIError) as exc:
|
|
71
|
+
print_error(str(exc))
|
|
72
|
+
raise typer.Exit(code=1)
|
|
73
|
+
finally:
|
|
74
|
+
client.close()
|
|
75
|
+
|
|
76
|
+
if output_json:
|
|
77
|
+
typer.echo(json.dumps(tweet, indent=2, default=str))
|
|
78
|
+
else:
|
|
79
|
+
print_tweet_detail(tweet)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@tweet_app.command("add")
|
|
83
|
+
def tweet_add(
|
|
84
|
+
url: str = typer.Argument(..., help="Tweet/X post URL"),
|
|
85
|
+
output_json: bool = typer.Option(False, "--json", help="JSON output"),
|
|
86
|
+
):
|
|
87
|
+
"""Add a tweet by URL (async — queued for processing)."""
|
|
88
|
+
client = _get_client()
|
|
89
|
+
try:
|
|
90
|
+
data = client.add_tweet(url)
|
|
91
|
+
except (AuthenticationError, APIError) as exc:
|
|
92
|
+
print_error(str(exc))
|
|
93
|
+
raise typer.Exit(code=1)
|
|
94
|
+
finally:
|
|
95
|
+
client.close()
|
|
96
|
+
|
|
97
|
+
if output_json:
|
|
98
|
+
typer.echo(json.dumps(data, indent=2, default=str))
|
|
99
|
+
else:
|
|
100
|
+
if data.get("is_duplicate"):
|
|
101
|
+
print_success(f"Tweet already exists (ID: {data['tweet_id']}).")
|
|
102
|
+
else:
|
|
103
|
+
print_success(
|
|
104
|
+
f"Tweet queued for processing (ID: {data['tweet_id']}).\n"
|
|
105
|
+
f" Check status with: tisit tweet view {data['tweet_id']}"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@tweet_app.command("delete")
|
|
110
|
+
def tweet_delete(
|
|
111
|
+
tweet_id: int = typer.Argument(..., help="Tweet ID to delete"),
|
|
112
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
|
|
113
|
+
):
|
|
114
|
+
"""Delete a tweet."""
|
|
115
|
+
if not yes:
|
|
116
|
+
confirm = typer.confirm(f"Delete tweet {tweet_id}?")
|
|
117
|
+
if not confirm:
|
|
118
|
+
raise typer.Abort()
|
|
119
|
+
|
|
120
|
+
client = _get_client()
|
|
121
|
+
try:
|
|
122
|
+
client.delete_tweet(tweet_id)
|
|
123
|
+
except NotFoundError:
|
|
124
|
+
print_error(f"Tweet {tweet_id} not found.")
|
|
125
|
+
raise typer.Exit(code=1)
|
|
126
|
+
except (AuthenticationError, APIError) as exc:
|
|
127
|
+
print_error(str(exc))
|
|
128
|
+
raise typer.Exit(code=1)
|
|
129
|
+
finally:
|
|
130
|
+
client.close()
|
|
131
|
+
|
|
132
|
+
print_success(f"Tweet {tweet_id} deleted.")
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Video commands: add, list, view, delete."""
|
|
2
|
+
import json
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from ..auth import get_token
|
|
8
|
+
from ..client import TisitClient
|
|
9
|
+
from ..config import Config
|
|
10
|
+
from ..display import (
|
|
11
|
+
print_error, print_video_detail, print_video_table,
|
|
12
|
+
print_success,
|
|
13
|
+
)
|
|
14
|
+
from ..exceptions import AuthenticationError, APIError, NotFoundError
|
|
15
|
+
|
|
16
|
+
video_app = typer.Typer(help="Manage your YouTube videos")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _get_client() -> TisitClient:
|
|
20
|
+
token = get_token()
|
|
21
|
+
if not token:
|
|
22
|
+
print_error("Not logged in. Run: tisit login")
|
|
23
|
+
raise typer.Exit(code=1)
|
|
24
|
+
cfg = Config()
|
|
25
|
+
return TisitClient(cfg.api_url, token)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@video_app.command("list")
|
|
29
|
+
def video_list(
|
|
30
|
+
query: Optional[str] = typer.Option(None, "--query", "-q", help="Search title or channel"),
|
|
31
|
+
sort: str = typer.Option("created_at", "--sort", "-s", help="Sort by: created_at, title"),
|
|
32
|
+
order: str = typer.Option("desc", "--order", "-o", help="asc or desc"),
|
|
33
|
+
page: int = typer.Option(1, "--page", "-p"),
|
|
34
|
+
per_page: int = typer.Option(20, "--per-page"),
|
|
35
|
+
output_json: bool = typer.Option(False, "--json", help="JSON output"),
|
|
36
|
+
):
|
|
37
|
+
"""List your YouTube videos."""
|
|
38
|
+
client = _get_client()
|
|
39
|
+
try:
|
|
40
|
+
videos, meta = client.list_videos(
|
|
41
|
+
q=query, sort=sort, order=order, page=page, per_page=per_page,
|
|
42
|
+
)
|
|
43
|
+
except (AuthenticationError, APIError) as exc:
|
|
44
|
+
print_error(str(exc))
|
|
45
|
+
raise typer.Exit(code=1)
|
|
46
|
+
finally:
|
|
47
|
+
client.close()
|
|
48
|
+
|
|
49
|
+
if output_json:
|
|
50
|
+
typer.echo(json.dumps({"data": videos, "meta": meta}, indent=2, default=str))
|
|
51
|
+
else:
|
|
52
|
+
if not videos:
|
|
53
|
+
print_error("No videos found.")
|
|
54
|
+
else:
|
|
55
|
+
print_video_table(videos, meta)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@video_app.command("view")
|
|
59
|
+
def video_view(
|
|
60
|
+
video_id: int = typer.Argument(..., help="Video ID"),
|
|
61
|
+
output_json: bool = typer.Option(False, "--json", help="JSON output"),
|
|
62
|
+
):
|
|
63
|
+
"""View a video in detail."""
|
|
64
|
+
client = _get_client()
|
|
65
|
+
try:
|
|
66
|
+
video = client.get_video(video_id)
|
|
67
|
+
except NotFoundError:
|
|
68
|
+
print_error(f"Video {video_id} not found.")
|
|
69
|
+
raise typer.Exit(code=1)
|
|
70
|
+
except (AuthenticationError, APIError) as exc:
|
|
71
|
+
print_error(str(exc))
|
|
72
|
+
raise typer.Exit(code=1)
|
|
73
|
+
finally:
|
|
74
|
+
client.close()
|
|
75
|
+
|
|
76
|
+
if output_json:
|
|
77
|
+
typer.echo(json.dumps(video, indent=2, default=str))
|
|
78
|
+
else:
|
|
79
|
+
print_video_detail(video)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@video_app.command("add")
|
|
83
|
+
def video_add(
|
|
84
|
+
url: str = typer.Argument(..., help="YouTube video URL"),
|
|
85
|
+
output_json: bool = typer.Option(False, "--json", help="JSON output"),
|
|
86
|
+
):
|
|
87
|
+
"""Add a YouTube video by URL (async — queued for processing)."""
|
|
88
|
+
client = _get_client()
|
|
89
|
+
try:
|
|
90
|
+
data = client.add_video(url)
|
|
91
|
+
except (AuthenticationError, APIError) as exc:
|
|
92
|
+
print_error(str(exc))
|
|
93
|
+
raise typer.Exit(code=1)
|
|
94
|
+
finally:
|
|
95
|
+
client.close()
|
|
96
|
+
|
|
97
|
+
if output_json:
|
|
98
|
+
typer.echo(json.dumps(data, indent=2, default=str))
|
|
99
|
+
else:
|
|
100
|
+
if data.get("is_duplicate"):
|
|
101
|
+
print_success(f"Video already exists (ID: {data['video_id']}).")
|
|
102
|
+
else:
|
|
103
|
+
print_success(
|
|
104
|
+
f"Video queued for processing (ID: {data['video_id']}).\n"
|
|
105
|
+
f" Check status with: tisit video view {data['video_id']}"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@video_app.command("delete")
|
|
110
|
+
def video_delete(
|
|
111
|
+
video_id: int = typer.Argument(..., help="Video ID to delete"),
|
|
112
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
|
|
113
|
+
):
|
|
114
|
+
"""Delete a video."""
|
|
115
|
+
if not yes:
|
|
116
|
+
confirm = typer.confirm(f"Delete video {video_id}?")
|
|
117
|
+
if not confirm:
|
|
118
|
+
raise typer.Abort()
|
|
119
|
+
|
|
120
|
+
client = _get_client()
|
|
121
|
+
try:
|
|
122
|
+
client.delete_video(video_id)
|
|
123
|
+
except NotFoundError:
|
|
124
|
+
print_error(f"Video {video_id} not found.")
|
|
125
|
+
raise typer.Exit(code=1)
|
|
126
|
+
except (AuthenticationError, APIError) as exc:
|
|
127
|
+
print_error(str(exc))
|
|
128
|
+
raise typer.Exit(code=1)
|
|
129
|
+
finally:
|
|
130
|
+
client.close()
|
|
131
|
+
|
|
132
|
+
print_success(f"Video {video_id} deleted.")
|
tisit_cli/config.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Configuration management — loads/saves ~/.tisit/config.toml."""
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import tomli_w
|
|
7
|
+
|
|
8
|
+
if sys.version_info >= (3, 11):
|
|
9
|
+
import tomllib
|
|
10
|
+
else:
|
|
11
|
+
import tomli as tomllib
|
|
12
|
+
|
|
13
|
+
from .exceptions import ConfigError
|
|
14
|
+
|
|
15
|
+
DEFAULT_API_URL = "https://tisit.ai"
|
|
16
|
+
CONFIG_DIR = Path.home() / ".tisit"
|
|
17
|
+
CONFIG_FILE = CONFIG_DIR / "config.toml"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Config:
|
|
21
|
+
"""TOML-backed configuration with env var overrides."""
|
|
22
|
+
|
|
23
|
+
def __init__(self):
|
|
24
|
+
self._data = {"api_url": DEFAULT_API_URL, "format": "table"}
|
|
25
|
+
self.load()
|
|
26
|
+
|
|
27
|
+
def load(self):
|
|
28
|
+
if CONFIG_FILE.exists():
|
|
29
|
+
try:
|
|
30
|
+
with open(CONFIG_FILE, "rb") as f:
|
|
31
|
+
stored = tomllib.load(f)
|
|
32
|
+
self._data.update(stored)
|
|
33
|
+
except Exception as exc:
|
|
34
|
+
raise ConfigError(f"Failed to read config: {exc}")
|
|
35
|
+
# Env var override
|
|
36
|
+
env_url = os.environ.get("TISIT_API_URL")
|
|
37
|
+
if env_url:
|
|
38
|
+
self._data["api_url"] = env_url.rstrip("/")
|
|
39
|
+
|
|
40
|
+
def save(self):
|
|
41
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
with open(CONFIG_FILE, "wb") as f:
|
|
43
|
+
tomli_w.dump(self._data, f)
|
|
44
|
+
|
|
45
|
+
def get(self, key, default=None):
|
|
46
|
+
return self._data.get(key, default)
|
|
47
|
+
|
|
48
|
+
def set(self, key, value):
|
|
49
|
+
self._data[key] = value
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def api_url(self):
|
|
53
|
+
return self._data["api_url"].rstrip("/")
|