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,186 @@
1
+ """Book commands: search, 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_book_detail, print_book_table,
12
+ print_success, print_info,
13
+ )
14
+ from ..exceptions import AuthenticationError, APIError, NotFoundError
15
+
16
+ book_app = typer.Typer(help="Manage your book library")
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
+ @book_app.command("search")
29
+ def book_search(
30
+ title: str = typer.Argument("", help="Book title to search"),
31
+ author: Optional[str] = typer.Option(None, "--author", "-a", help="Author name"),
32
+ limit: int = typer.Option(10, "--limit", "-l", help="Max results"),
33
+ output_json: bool = typer.Option(False, "--json", help="JSON output"),
34
+ ):
35
+ """Search for books via Google Books / Open Library."""
36
+ if not title and not author:
37
+ print_error("Provide a title or --author to search.")
38
+ raise typer.Exit(code=1)
39
+
40
+ client = _get_client()
41
+ try:
42
+ results, meta = client.search_books(title=title, author=author or "", limit=limit)
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": results, "meta": meta}, indent=2, default=str))
51
+ else:
52
+ if not results:
53
+ print_error("No books found.")
54
+ else:
55
+ from rich.table import Table
56
+ from ..display import console
57
+ table = Table(title="Book Search Results")
58
+ table.add_column("#", justify="right")
59
+ table.add_column("Title", style="bold")
60
+ table.add_column("Authors")
61
+ table.add_column("Published")
62
+ table.add_column("ISBN-13")
63
+
64
+ for i, r in enumerate(results, 1):
65
+ t = r.get("title", "")
66
+ if len(t) > 45:
67
+ t = t[:42] + "..."
68
+ authors = ", ".join(r.get("authors") or []) if r.get("authors") else ""
69
+ table.add_row(
70
+ str(i), t, authors,
71
+ r.get("published_date", ""),
72
+ r.get("isbn_13", "") or "",
73
+ )
74
+ console.print(table)
75
+ print_info(f" {meta.get('count', len(results))} results. Use 'tisit book add \"Title\"' to add one.")
76
+
77
+
78
+ @book_app.command("list")
79
+ def book_list(
80
+ query: Optional[str] = typer.Option(None, "--query", "-q", help="Search title"),
81
+ sort: str = typer.Option("added_at", "--sort", "-s", help="Sort by: added_at, title"),
82
+ order: str = typer.Option("desc", "--order", "-o"),
83
+ page: int = typer.Option(1, "--page", "-p"),
84
+ per_page: int = typer.Option(20, "--per-page"),
85
+ output_json: bool = typer.Option(False, "--json", help="JSON output"),
86
+ ):
87
+ """List books in your library."""
88
+ client = _get_client()
89
+ try:
90
+ books, meta = client.list_books(
91
+ q=query, sort=sort, order=order, page=page, per_page=per_page,
92
+ )
93
+ except (AuthenticationError, APIError) as exc:
94
+ print_error(str(exc))
95
+ raise typer.Exit(code=1)
96
+ finally:
97
+ client.close()
98
+
99
+ if output_json:
100
+ typer.echo(json.dumps({"data": books, "meta": meta}, indent=2, default=str))
101
+ else:
102
+ if not books:
103
+ print_error("No books found.")
104
+ else:
105
+ print_book_table(books, meta)
106
+
107
+
108
+ @book_app.command("view")
109
+ def book_view(
110
+ book_id: int = typer.Argument(..., help="Book ID"),
111
+ output_json: bool = typer.Option(False, "--json", help="JSON output"),
112
+ ):
113
+ """View a book in detail."""
114
+ client = _get_client()
115
+ try:
116
+ book = client.get_book(book_id)
117
+ except NotFoundError:
118
+ print_error(f"Book {book_id} not found.")
119
+ raise typer.Exit(code=1)
120
+ except (AuthenticationError, APIError) as exc:
121
+ print_error(str(exc))
122
+ raise typer.Exit(code=1)
123
+ finally:
124
+ client.close()
125
+
126
+ if output_json:
127
+ typer.echo(json.dumps(book, indent=2, default=str))
128
+ else:
129
+ print_book_detail(book)
130
+
131
+
132
+ @book_app.command("add")
133
+ def book_add(
134
+ title: str = typer.Argument(..., help="Book title"),
135
+ authors: Optional[str] = typer.Option(None, "--authors", "-a", help="Authors (comma-separated)"),
136
+ isbn: Optional[str] = typer.Option(None, "--isbn", help="ISBN-13"),
137
+ output_json: bool = typer.Option(False, "--json", help="JSON output"),
138
+ ):
139
+ """Add a book to your library (async — queued for processing)."""
140
+ author_list = [a.strip() for a in authors.split(",")] if authors else None
141
+
142
+ client = _get_client()
143
+ try:
144
+ data = client.add_book(title, authors=author_list, isbn_13=isbn)
145
+ except (AuthenticationError, APIError) as exc:
146
+ print_error(str(exc))
147
+ raise typer.Exit(code=1)
148
+ finally:
149
+ client.close()
150
+
151
+ if output_json:
152
+ typer.echo(json.dumps(data, indent=2, default=str))
153
+ else:
154
+ if data.get("is_existing"):
155
+ print_success(f"Book already in library (ID: {data['book_id']}).")
156
+ else:
157
+ print_success(
158
+ f"Book added and queued for processing (ID: {data['book_id']}).\n"
159
+ f" Check status with: tisit book view {data['book_id']}"
160
+ )
161
+
162
+
163
+ @book_app.command("delete")
164
+ def book_delete(
165
+ book_id: int = typer.Argument(..., help="Book ID to remove"),
166
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
167
+ ):
168
+ """Remove a book from your library."""
169
+ if not yes:
170
+ confirm = typer.confirm(f"Remove book {book_id} from library?")
171
+ if not confirm:
172
+ raise typer.Abort()
173
+
174
+ client = _get_client()
175
+ try:
176
+ client.delete_book(book_id)
177
+ except NotFoundError:
178
+ print_error(f"Book {book_id} not found in your library.")
179
+ raise typer.Exit(code=1)
180
+ except (AuthenticationError, APIError) as exc:
181
+ print_error(str(exc))
182
+ raise typer.Exit(code=1)
183
+ finally:
184
+ client.close()
185
+
186
+ print_success(f"Book {book_id} removed from library.")
@@ -0,0 +1,76 @@
1
+ """Browse command: unified content browsing across all types."""
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,
12
+ print_note_table, print_paper_table, print_article_table,
13
+ print_video_table, print_tweet_table, print_book_table,
14
+ print_podcast_table, print_patent_table,
15
+ )
16
+ from ..exceptions import AuthenticationError, APIError
17
+
18
+ CONTENT_TYPES = ["notes", "papers", "articles", "videos", "tweets", "books", "podcasts", "patents"]
19
+
20
+
21
+ def browse(
22
+ content_type: str = typer.Option("notes", "--type", "-t", help="Content type: " + ", ".join(CONTENT_TYPES)),
23
+ query: Optional[str] = typer.Option(None, "--query", "-q", help="Search filter"),
24
+ sort: Optional[str] = typer.Option(None, "--sort", "-s", help="Sort field"),
25
+ order: str = typer.Option("desc", "--order", "-o", help="asc or desc"),
26
+ page: int = typer.Option(1, "--page", "-p"),
27
+ per_page: int = typer.Option(20, "--per-page"),
28
+ output_json: bool = typer.Option(False, "--json", help="JSON output"),
29
+ ):
30
+ """Browse content across all types."""
31
+ if content_type not in CONTENT_TYPES:
32
+ print_error(f"Invalid type. Choose from: {', '.join(CONTENT_TYPES)}")
33
+ raise typer.Exit(code=1)
34
+
35
+ token = get_token()
36
+ if not token:
37
+ print_error("Not logged in. Run: tisit login")
38
+ raise typer.Exit(code=1)
39
+
40
+ cfg = Config()
41
+ client = TisitClient(cfg.api_url, token)
42
+
43
+ params = {"order": order, "page": page, "per_page": per_page}
44
+ if query:
45
+ params["q"] = query
46
+ if sort:
47
+ params["sort"] = sort
48
+
49
+ type_map = {
50
+ "notes": ("/notes", print_note_table),
51
+ "papers": ("/papers", print_paper_table),
52
+ "articles": ("/articles", print_article_table),
53
+ "videos": ("/videos", print_video_table),
54
+ "tweets": ("/tweets", print_tweet_table),
55
+ "books": ("/books", print_book_table),
56
+ "podcasts": ("/podcasts", print_podcast_table),
57
+ "patents": ("/patents", print_patent_table),
58
+ }
59
+
60
+ endpoint, display_fn = type_map[content_type]
61
+
62
+ try:
63
+ data, meta = client._request("GET", endpoint, params=params)
64
+ except (AuthenticationError, APIError) as exc:
65
+ print_error(str(exc))
66
+ raise typer.Exit(code=1)
67
+ finally:
68
+ client.close()
69
+
70
+ if output_json:
71
+ typer.echo(json.dumps({"data": data, "meta": meta}, indent=2, default=str))
72
+ else:
73
+ if not data:
74
+ print_error(f"No {content_type} found.")
75
+ else:
76
+ display_fn(data, meta)
@@ -0,0 +1,94 @@
1
+ """Chat commands: ask (one-shot) and interactive REPL."""
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 console, print_error, print_info
11
+ from ..exceptions import AuthenticationError, APIError
12
+
13
+ chat_app = typer.Typer(help="Ask questions about your knowledge base")
14
+
15
+
16
+ def _get_client() -> TisitClient:
17
+ token = get_token()
18
+ if not token:
19
+ print_error("Not logged in. Run: tisit login")
20
+ raise typer.Exit(code=1)
21
+ cfg = Config()
22
+ return TisitClient(cfg.api_url, token)
23
+
24
+
25
+ @chat_app.command("ask")
26
+ def chat_ask(
27
+ question: str = typer.Argument(..., help="Question to ask"),
28
+ session_id: Optional[int] = typer.Option(None, "--session", "-s", help="Continue a session"),
29
+ output_json: bool = typer.Option(False, "--json", help="JSON output"),
30
+ ):
31
+ """Ask a single question (one-shot)."""
32
+ client = _get_client()
33
+ try:
34
+ data = client.chat_ask(question, session_id=session_id)
35
+ except (AuthenticationError, APIError) as exc:
36
+ print_error(str(exc))
37
+ raise typer.Exit(code=1)
38
+ finally:
39
+ client.close()
40
+
41
+ if output_json:
42
+ typer.echo(json.dumps(data, indent=2, default=str))
43
+ else:
44
+ console.print()
45
+ console.print(data.get("answer", ""))
46
+ console.print()
47
+ sources = data.get("sources_used", 0)
48
+ sid = data.get("session_id")
49
+ print_info(f" Sources: {sources} Session: {sid}")
50
+
51
+
52
+ @chat_app.callback(invoke_without_command=True)
53
+ def chat_repl(ctx: typer.Context):
54
+ """Interactive chat REPL. Type 'exit' or Ctrl+C to quit."""
55
+ if ctx.invoked_subcommand is not None:
56
+ return
57
+
58
+ client = _get_client()
59
+ session_id = None
60
+
61
+ console.print("[bold]TISIT Chat[/bold] — ask anything about your knowledge base.")
62
+ console.print("[dim]Type 'exit' or press Ctrl+C to quit.[/dim]")
63
+ console.print()
64
+
65
+ try:
66
+ while True:
67
+ try:
68
+ question = console.input("[bold cyan]> [/bold cyan]").strip()
69
+ except EOFError:
70
+ break
71
+
72
+ if not question:
73
+ continue
74
+ if question.lower() in ("exit", "quit", "q"):
75
+ break
76
+
77
+ try:
78
+ data = client.chat_ask(question, session_id=session_id)
79
+ session_id = data.get("session_id")
80
+
81
+ console.print()
82
+ console.print(data.get("answer", ""))
83
+ console.print()
84
+ sources = data.get("sources_used", 0)
85
+ console.print(f"[dim] ({sources} sources)[/dim]")
86
+ console.print()
87
+ except (AuthenticationError, APIError) as exc:
88
+ print_error(str(exc))
89
+ except KeyboardInterrupt:
90
+ console.print()
91
+ finally:
92
+ client.close()
93
+
94
+ console.print("[dim]Goodbye.[/dim]")
@@ -0,0 +1,198 @@
1
+ """Focus mode commands: list, create, view, delete, concept add."""
2
+ import json
3
+ from typing import Optional
4
+
5
+ import typer
6
+ from rich.table import Table
7
+ from rich.panel import Panel
8
+
9
+ from ..auth import get_token
10
+ from ..client import TisitClient
11
+ from ..config import Config
12
+ from ..display import console, print_error, print_success, print_info
13
+ from ..exceptions import AuthenticationError, APIError, NotFoundError
14
+
15
+ focus_app = typer.Typer(help="Focus Mode — deep dive learning")
16
+ concept_app = typer.Typer(help="Manage concepts within a bubble")
17
+ focus_app.add_typer(concept_app, name="concept")
18
+
19
+
20
+ def _get_client() -> TisitClient:
21
+ token = get_token()
22
+ if not token:
23
+ print_error("Not logged in. Run: tisit login")
24
+ raise typer.Exit(code=1)
25
+ cfg = Config()
26
+ return TisitClient(cfg.api_url, token)
27
+
28
+
29
+ @focus_app.command("list")
30
+ def focus_list(
31
+ output_json: bool = typer.Option(False, "--json", help="JSON output"),
32
+ ):
33
+ """List your topic bubbles."""
34
+ client = _get_client()
35
+ try:
36
+ bubbles = client.list_focus_bubbles()
37
+ except (AuthenticationError, APIError) as exc:
38
+ print_error(str(exc))
39
+ raise typer.Exit(code=1)
40
+ finally:
41
+ client.close()
42
+
43
+ if output_json:
44
+ typer.echo(json.dumps(bubbles, indent=2, default=str))
45
+ else:
46
+ if not bubbles:
47
+ print_info("No topic bubbles. Create one with: tisit focus create \"topic\"")
48
+ else:
49
+ table = Table(title="Topic Bubbles")
50
+ table.add_column("ID", style="cyan", justify="right")
51
+ table.add_column("Topic", style="bold")
52
+ table.add_column("Status")
53
+ table.add_column("Coverage", justify="right")
54
+ table.add_column("Mastery", justify="right")
55
+ table.add_column("Study Time", justify="right")
56
+ for b in bubbles:
57
+ study = f"{b.get('total_study_time_minutes', 0)}m"
58
+ table.add_row(
59
+ str(b["id"]),
60
+ b.get("topic_name", ""),
61
+ b.get("status", ""),
62
+ f"{b.get('coverage_score', 0):.0f}%",
63
+ f"{b.get('mastery_score', 0):.0f}%",
64
+ study,
65
+ )
66
+ console.print(table)
67
+
68
+
69
+ @focus_app.command("create")
70
+ def focus_create(
71
+ topic: str = typer.Argument(..., help="Topic to deep dive into"),
72
+ output_json: bool = typer.Option(False, "--json", help="JSON output"),
73
+ ):
74
+ """Create a new topic bubble."""
75
+ client = _get_client()
76
+ try:
77
+ data = client.create_focus_bubble(topic)
78
+ except (AuthenticationError, APIError) as exc:
79
+ print_error(str(exc))
80
+ raise typer.Exit(code=1)
81
+ finally:
82
+ client.close()
83
+
84
+ if output_json:
85
+ typer.echo(json.dumps(data, indent=2, default=str))
86
+ else:
87
+ print_success(
88
+ f"Topic bubble created (ID: {data.get('id')}).\n"
89
+ f" View with: tisit focus view {data.get('id')}"
90
+ )
91
+
92
+
93
+ @focus_app.command("view")
94
+ def focus_view(
95
+ bubble_id: int = typer.Argument(..., help="Bubble ID"),
96
+ output_json: bool = typer.Option(False, "--json", help="JSON output"),
97
+ ):
98
+ """View a topic bubble with its concepts."""
99
+ client = _get_client()
100
+ try:
101
+ data = client.get_focus_bubble(bubble_id)
102
+ except NotFoundError:
103
+ print_error(f"Topic bubble {bubble_id} not found.")
104
+ raise typer.Exit(code=1)
105
+ except (AuthenticationError, APIError) as exc:
106
+ print_error(str(exc))
107
+ raise typer.Exit(code=1)
108
+ finally:
109
+ client.close()
110
+
111
+ if output_json:
112
+ typer.echo(json.dumps(data, indent=2, default=str))
113
+ return
114
+
115
+ bubble = data.get("bubble", {})
116
+ concepts = data.get("concepts", [])
117
+
118
+ lines = [
119
+ f"[bold]{bubble.get('topic_name', '')}[/bold]",
120
+ "",
121
+ f"Status: {bubble.get('status', '')}",
122
+ f"Coverage: {bubble.get('coverage_score', 0):.0f}% Mastery: {bubble.get('mastery_score', 0):.0f}%",
123
+ f"Study Time: {bubble.get('total_study_time_minutes', 0)} minutes",
124
+ f"ID: {bubble.get('id', '')}",
125
+ ]
126
+ console.print(Panel("\n".join(lines), title="Topic Bubble", border_style="cyan"))
127
+
128
+ if concepts:
129
+ table = Table(title=f"Concepts ({len(concepts)})")
130
+ table.add_column("ID", style="cyan", justify="right")
131
+ table.add_column("Name", style="bold")
132
+ table.add_column("Category")
133
+ table.add_column("Mastery", justify="right")
134
+ table.add_column("Reviews", justify="right")
135
+ for c in concepts:
136
+ table.add_row(
137
+ str(c["id"]),
138
+ c.get("name", ""),
139
+ c.get("category", ""),
140
+ f"{c.get('mastery_level', 0):.0f}%",
141
+ str(c.get("times_reviewed", 0)),
142
+ )
143
+ console.print(table)
144
+ else:
145
+ print_info(" No concepts yet. Add with: tisit focus concept add <bubble_id> \"name\"")
146
+
147
+
148
+ @focus_app.command("delete")
149
+ def focus_delete(
150
+ bubble_id: int = typer.Argument(..., help="Bubble ID to delete"),
151
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
152
+ ):
153
+ """Delete a topic bubble."""
154
+ if not yes:
155
+ if not typer.confirm(f"Delete topic bubble {bubble_id}?"):
156
+ raise typer.Abort()
157
+
158
+ client = _get_client()
159
+ try:
160
+ client.delete_focus_bubble(bubble_id)
161
+ except NotFoundError:
162
+ print_error(f"Topic bubble {bubble_id} not found.")
163
+ raise typer.Exit(code=1)
164
+ except (AuthenticationError, APIError) as exc:
165
+ print_error(str(exc))
166
+ raise typer.Exit(code=1)
167
+ finally:
168
+ client.close()
169
+
170
+ print_success(f"Topic bubble {bubble_id} deleted.")
171
+
172
+
173
+ # ── concept subcommands ───────────────────────────────────────────
174
+
175
+ @concept_app.command("add")
176
+ def concept_add(
177
+ bubble_id: int = typer.Argument(..., help="Bubble ID"),
178
+ name: str = typer.Argument(..., help="Concept name"),
179
+ definition: Optional[str] = typer.Option(None, "--definition", "-d"),
180
+ category: str = typer.Option("term", "--category", "-c", help="term, formula, principle, process"),
181
+ output_json: bool = typer.Option(False, "--json", help="JSON output"),
182
+ ):
183
+ """Add a concept to a topic bubble."""
184
+ client = _get_client()
185
+ try:
186
+ data = client.add_focus_concept(
187
+ bubble_id, name, definition=definition, category=category
188
+ )
189
+ except (AuthenticationError, APIError) as exc:
190
+ print_error(str(exc))
191
+ raise typer.Exit(code=1)
192
+ finally:
193
+ client.close()
194
+
195
+ if output_json:
196
+ typer.echo(json.dumps(data, indent=2, default=str))
197
+ else:
198
+ print_success(f"Concept added (ID: {data.get('id')}).")
@@ -0,0 +1,126 @@
1
+ """Knowledge graph commands: stats, neighbors, paths."""
2
+ import json
3
+
4
+ import typer
5
+ from rich.table import Table
6
+
7
+ from ..auth import get_token
8
+ from ..client import TisitClient
9
+ from ..config import Config
10
+ from ..display import console, print_error, print_info
11
+ from ..exceptions import AuthenticationError, APIError
12
+
13
+ graph_app = typer.Typer(help="Knowledge graph queries")
14
+
15
+
16
+ def _get_client() -> TisitClient:
17
+ token = get_token()
18
+ if not token:
19
+ print_error("Not logged in. Run: tisit login")
20
+ raise typer.Exit(code=1)
21
+ cfg = Config()
22
+ return TisitClient(cfg.api_url, token)
23
+
24
+
25
+ @graph_app.command("stats")
26
+ def graph_stats(
27
+ output_json: bool = typer.Option(False, "--json", help="JSON output"),
28
+ ):
29
+ """Show knowledge graph statistics."""
30
+ client = _get_client()
31
+ try:
32
+ data = client.graph_stats()
33
+ except (AuthenticationError, APIError) as exc:
34
+ print_error(str(exc))
35
+ raise typer.Exit(code=1)
36
+ finally:
37
+ client.close()
38
+
39
+ if output_json:
40
+ typer.echo(json.dumps(data, indent=2, default=str))
41
+ else:
42
+ print_info(f" Nodes: {data.get('total_nodes', 0)}")
43
+ print_info(f" Edges: {data.get('total_edges', 0)}")
44
+ density = data.get('density', 0)
45
+ print_info(f" Density: {density:.4f}")
46
+ connected = "yes" if data.get("is_connected") else "no"
47
+ print_info(f" Connected: {connected}")
48
+
49
+ node_types = data.get("node_types", {})
50
+ if node_types:
51
+ console.print()
52
+ table = Table(title="Node Types")
53
+ table.add_column("Type", style="bold")
54
+ table.add_column("Count", justify="right")
55
+ for t, c in sorted(node_types.items(), key=lambda x: -x[1]):
56
+ table.add_row(t, str(c))
57
+ console.print(table)
58
+
59
+
60
+ @graph_app.command("neighbors")
61
+ def graph_neighbors(
62
+ term: str = typer.Argument(..., help="Concept to find neighbors for"),
63
+ limit: int = typer.Option(10, "--limit", "-l", help="Max neighbors"),
64
+ output_json: bool = typer.Option(False, "--json", help="JSON output"),
65
+ ):
66
+ """Show connected concepts for a term."""
67
+ client = _get_client()
68
+ try:
69
+ data, meta = client.graph_neighbors(term, limit=limit)
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({"data": data, "meta": meta}, indent=2, default=str))
78
+ else:
79
+ if not data:
80
+ print_error(f"No neighbors found for '{term}'.")
81
+ else:
82
+ table = Table(title=f"Neighbors of '{term}'")
83
+ table.add_column("Concept", style="bold")
84
+ table.add_column("Type")
85
+ table.add_column("Relationship")
86
+ table.add_column("Weight", justify="right")
87
+ for n in data:
88
+ table.add_row(
89
+ n.get("label", ""),
90
+ n.get("type", ""),
91
+ n.get("relationship", ""),
92
+ f"{n.get('weight', 0):.2f}",
93
+ )
94
+ console.print(table)
95
+
96
+
97
+ @graph_app.command("paths")
98
+ def graph_paths(
99
+ source: str = typer.Argument(..., help="Source concept"),
100
+ target: str = typer.Argument(..., help="Target concept"),
101
+ max_depth: int = typer.Option(3, "--depth", "-d", help="Max path depth"),
102
+ output_json: bool = typer.Option(False, "--json", help="JSON output"),
103
+ ):
104
+ """Find connection paths between two concepts."""
105
+ client = _get_client()
106
+ try:
107
+ data, meta = client.graph_paths(source, target, max_depth=max_depth)
108
+ except (AuthenticationError, APIError) as exc:
109
+ print_error(str(exc))
110
+ raise typer.Exit(code=1)
111
+ finally:
112
+ client.close()
113
+
114
+ if output_json:
115
+ typer.echo(json.dumps({"data": data, "meta": meta}, indent=2, default=str))
116
+ else:
117
+ paths_found = meta.get("paths_found", 0)
118
+ if paths_found == 0:
119
+ print_error(f"No paths found between '{source}' and '{target}'.")
120
+ else:
121
+ print_info(f" {paths_found} path(s) found:")
122
+ console.print()
123
+ for i, p in enumerate(data, 1):
124
+ labels = [n.get("label", "?") for n in p.get("nodes", [])]
125
+ console.print(f" [bold]{i}.[/bold] {' -> '.join(labels)}")
126
+ console.print()