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.
@@ -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("/")