airweave-cli 0.1.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Airweave
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.
@@ -0,0 +1,194 @@
1
+ Metadata-Version: 2.1
2
+ Name: airweave-cli
3
+ Version: 0.1.0
4
+ Summary: The Airweave CLI for developers and AI agents.
5
+ License: MIT
6
+ Keywords: airweave,cli,search,ai,agents
7
+ Author: Airweave
8
+ Author-email: hello@airweave.ai
9
+ Requires-Python: >=3.9,<4.0
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Classifier: Typing :: Typed
22
+ Requires-Dist: airweave-sdk (>=0.9)
23
+ Requires-Dist: rich (>=13,<14)
24
+ Requires-Dist: typer[all] (>=0.12,<0.13)
25
+ Description-Content-Type: text/markdown
26
+
27
+ # Airweave CLI
28
+
29
+ [![pypi](https://img.shields.io/pypi/v/airweave-cli)](https://pypi.python.org/pypi/airweave-cli)
30
+
31
+ A command-line interface for [Airweave](https://airweave.ai) — search across your connected sources from any terminal. Built for developers and AI agents.
32
+
33
+ ## Installation
34
+
35
+ ```sh
36
+ pip install airweave-cli
37
+ ```
38
+
39
+ This installs the `airweave` binary.
40
+
41
+ ## Quick Start
42
+
43
+ ```sh
44
+ # Set your API key
45
+ export AIRWEAVE_API_KEY="your-api-key"
46
+
47
+ # Search a collection
48
+ airweave search "quarterly revenue figures" --collection finance-data
49
+
50
+ # List collections
51
+ airweave collections list
52
+ ```
53
+
54
+ ## Authentication
55
+
56
+ The CLI resolves credentials in this order:
57
+
58
+ 1. `AIRWEAVE_API_KEY` environment variable
59
+ 2. `~/.airweave/config.json` (saved via `airweave auth login`)
60
+
61
+ For interactive setup:
62
+
63
+ ```sh
64
+ airweave auth login
65
+ ```
66
+
67
+ This prompts for your API key, validates it, and saves it locally.
68
+
69
+ ### Self-hosted
70
+
71
+ Point the CLI at your own Airweave instance:
72
+
73
+ ```sh
74
+ export AIRWEAVE_BASE_URL="https://airweave.internal.corp.com"
75
+ ```
76
+
77
+ Or set it during `airweave auth login`.
78
+
79
+ ## Commands
80
+
81
+ ### Search
82
+
83
+ ```sh
84
+ airweave search "<query>" --collection <id> --top-k 5 --format json
85
+ ```
86
+
87
+ | Flag | Short | Default | Description |
88
+ |------|-------|---------|-------------|
89
+ | `--collection` | `-c` | `$AIRWEAVE_COLLECTION` | Collection readable ID |
90
+ | `--top-k` | `-k` | `10` | Number of results |
91
+ | `--format` | `-f` | `json` | Output format: `json` or `text` |
92
+
93
+ JSON output (default) writes pure JSON to stdout — nothing else. Pipe it:
94
+
95
+ ```sh
96
+ airweave search "query" -c my-collection | jq '.results[0].md_content'
97
+ ```
98
+
99
+ ### Auth
100
+
101
+ ```sh
102
+ airweave auth login # interactive setup
103
+ airweave auth status # show current auth state
104
+ airweave auth logout # clear saved credentials
105
+ ```
106
+
107
+ ### Collections
108
+
109
+ ```sh
110
+ airweave collections list # list all
111
+ airweave collections create --name "My Data" # create
112
+ airweave collections get my-data-x7k9m # get details
113
+ ```
114
+
115
+ ### Sources
116
+
117
+ ```sh
118
+ airweave sources list --collection my-data # list source connections
119
+ airweave sources sync <source-connection-id> # trigger sync
120
+ airweave sources sync <id> --force # full re-sync
121
+ ```
122
+
123
+ ## Agent Usage
124
+
125
+ The CLI is designed for non-interactive use by AI agents. Every command:
126
+
127
+ - Outputs clean JSON to stdout (default)
128
+ - Sends all errors to stderr
129
+ - Uses correct exit codes (0 = success, 1 = error)
130
+
131
+ ### Environment variables
132
+
133
+ ```sh
134
+ export AIRWEAVE_API_KEY="sk-..."
135
+ export AIRWEAVE_COLLECTION="my-knowledge-base"
136
+ export AIRWEAVE_BASE_URL="https://api.airweave.ai" # optional
137
+ ```
138
+
139
+ ### Piping
140
+
141
+ ```sh
142
+ # Get the top result's content
143
+ airweave search "how to reset password" | jq -r '.results[0].md_content'
144
+
145
+ # Get the AI-generated answer
146
+ airweave search "what is our refund policy" | jq -r '.completion'
147
+
148
+ # List collection IDs
149
+ airweave collections list | jq -r '.[].readable_id'
150
+ ```
151
+
152
+ ### CI / Cloud Functions
153
+
154
+ ```yaml
155
+ # GitHub Actions example
156
+ - name: Search Airweave
157
+ env:
158
+ AIRWEAVE_API_KEY: ${{ secrets.AIRWEAVE_API_KEY }}
159
+ AIRWEAVE_COLLECTION: docs
160
+ run: |
161
+ result=$(airweave search "deployment guide")
162
+ echo "$result" | jq '.results[:3]'
163
+ ```
164
+
165
+ No SDK imports, no MCP server, no config files — just an env var and a shell.
166
+
167
+ ## Configuration
168
+
169
+ Config file location: `~/.airweave/config.json`
170
+
171
+ ```json
172
+ {
173
+ "api_key": "sk-...",
174
+ "base_url": "https://api.airweave.ai",
175
+ "collection": "my-default-collection"
176
+ }
177
+ ```
178
+
179
+ Resolution order for all settings:
180
+
181
+ 1. CLI flag (e.g. `--collection`)
182
+ 2. Environment variable (e.g. `AIRWEAVE_COLLECTION`)
183
+ 3. Config file
184
+ 4. Default / error
185
+
186
+ ## Contributing
187
+
188
+ ```sh
189
+ git clone https://github.com/airweave-ai/cli.git
190
+ cd cli
191
+ poetry install
192
+ poetry run airweave --help
193
+ ```
194
+
@@ -0,0 +1,167 @@
1
+ # Airweave CLI
2
+
3
+ [![pypi](https://img.shields.io/pypi/v/airweave-cli)](https://pypi.python.org/pypi/airweave-cli)
4
+
5
+ A command-line interface for [Airweave](https://airweave.ai) — search across your connected sources from any terminal. Built for developers and AI agents.
6
+
7
+ ## Installation
8
+
9
+ ```sh
10
+ pip install airweave-cli
11
+ ```
12
+
13
+ This installs the `airweave` binary.
14
+
15
+ ## Quick Start
16
+
17
+ ```sh
18
+ # Set your API key
19
+ export AIRWEAVE_API_KEY="your-api-key"
20
+
21
+ # Search a collection
22
+ airweave search "quarterly revenue figures" --collection finance-data
23
+
24
+ # List collections
25
+ airweave collections list
26
+ ```
27
+
28
+ ## Authentication
29
+
30
+ The CLI resolves credentials in this order:
31
+
32
+ 1. `AIRWEAVE_API_KEY` environment variable
33
+ 2. `~/.airweave/config.json` (saved via `airweave auth login`)
34
+
35
+ For interactive setup:
36
+
37
+ ```sh
38
+ airweave auth login
39
+ ```
40
+
41
+ This prompts for your API key, validates it, and saves it locally.
42
+
43
+ ### Self-hosted
44
+
45
+ Point the CLI at your own Airweave instance:
46
+
47
+ ```sh
48
+ export AIRWEAVE_BASE_URL="https://airweave.internal.corp.com"
49
+ ```
50
+
51
+ Or set it during `airweave auth login`.
52
+
53
+ ## Commands
54
+
55
+ ### Search
56
+
57
+ ```sh
58
+ airweave search "<query>" --collection <id> --top-k 5 --format json
59
+ ```
60
+
61
+ | Flag | Short | Default | Description |
62
+ |------|-------|---------|-------------|
63
+ | `--collection` | `-c` | `$AIRWEAVE_COLLECTION` | Collection readable ID |
64
+ | `--top-k` | `-k` | `10` | Number of results |
65
+ | `--format` | `-f` | `json` | Output format: `json` or `text` |
66
+
67
+ JSON output (default) writes pure JSON to stdout — nothing else. Pipe it:
68
+
69
+ ```sh
70
+ airweave search "query" -c my-collection | jq '.results[0].md_content'
71
+ ```
72
+
73
+ ### Auth
74
+
75
+ ```sh
76
+ airweave auth login # interactive setup
77
+ airweave auth status # show current auth state
78
+ airweave auth logout # clear saved credentials
79
+ ```
80
+
81
+ ### Collections
82
+
83
+ ```sh
84
+ airweave collections list # list all
85
+ airweave collections create --name "My Data" # create
86
+ airweave collections get my-data-x7k9m # get details
87
+ ```
88
+
89
+ ### Sources
90
+
91
+ ```sh
92
+ airweave sources list --collection my-data # list source connections
93
+ airweave sources sync <source-connection-id> # trigger sync
94
+ airweave sources sync <id> --force # full re-sync
95
+ ```
96
+
97
+ ## Agent Usage
98
+
99
+ The CLI is designed for non-interactive use by AI agents. Every command:
100
+
101
+ - Outputs clean JSON to stdout (default)
102
+ - Sends all errors to stderr
103
+ - Uses correct exit codes (0 = success, 1 = error)
104
+
105
+ ### Environment variables
106
+
107
+ ```sh
108
+ export AIRWEAVE_API_KEY="sk-..."
109
+ export AIRWEAVE_COLLECTION="my-knowledge-base"
110
+ export AIRWEAVE_BASE_URL="https://api.airweave.ai" # optional
111
+ ```
112
+
113
+ ### Piping
114
+
115
+ ```sh
116
+ # Get the top result's content
117
+ airweave search "how to reset password" | jq -r '.results[0].md_content'
118
+
119
+ # Get the AI-generated answer
120
+ airweave search "what is our refund policy" | jq -r '.completion'
121
+
122
+ # List collection IDs
123
+ airweave collections list | jq -r '.[].readable_id'
124
+ ```
125
+
126
+ ### CI / Cloud Functions
127
+
128
+ ```yaml
129
+ # GitHub Actions example
130
+ - name: Search Airweave
131
+ env:
132
+ AIRWEAVE_API_KEY: ${{ secrets.AIRWEAVE_API_KEY }}
133
+ AIRWEAVE_COLLECTION: docs
134
+ run: |
135
+ result=$(airweave search "deployment guide")
136
+ echo "$result" | jq '.results[:3]'
137
+ ```
138
+
139
+ No SDK imports, no MCP server, no config files — just an env var and a shell.
140
+
141
+ ## Configuration
142
+
143
+ Config file location: `~/.airweave/config.json`
144
+
145
+ ```json
146
+ {
147
+ "api_key": "sk-...",
148
+ "base_url": "https://api.airweave.ai",
149
+ "collection": "my-default-collection"
150
+ }
151
+ ```
152
+
153
+ Resolution order for all settings:
154
+
155
+ 1. CLI flag (e.g. `--collection`)
156
+ 2. Environment variable (e.g. `AIRWEAVE_COLLECTION`)
157
+ 3. Config file
158
+ 4. Default / error
159
+
160
+ ## Contributing
161
+
162
+ ```sh
163
+ git clone https://github.com/airweave-ai/cli.git
164
+ cd cli
165
+ poetry install
166
+ poetry run airweave --help
167
+ ```
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
File without changes
@@ -0,0 +1,105 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from enum import Enum
6
+
7
+ import typer
8
+ from rich.console import Console
9
+
10
+ from airweave_cli.config import (
11
+ clear_config,
12
+ load_config,
13
+ save_config,
14
+ )
15
+
16
+ app = typer.Typer(name="auth", help="Manage authentication.", no_args_is_help=True)
17
+
18
+ stderr = Console(stderr=True)
19
+ stdout = Console()
20
+
21
+
22
+ class OutputFormat(str, Enum):
23
+ json = "json"
24
+ text = "text"
25
+
26
+
27
+ @app.command()
28
+ def login() -> None:
29
+ """Interactive login: save your API key to ~/.airweave/config.json."""
30
+ api_key = typer.prompt("API key", hide_input=True)
31
+ base_url = typer.prompt(
32
+ "Base URL",
33
+ default="https://api.airweave.ai",
34
+ show_default=True,
35
+ )
36
+ collection = typer.prompt(
37
+ "Default collection (readable_id)",
38
+ default="",
39
+ show_default=False,
40
+ )
41
+
42
+ # Validate the key
43
+ try:
44
+ from airweave import AirweaveSDK
45
+
46
+ client = AirweaveSDK(api_key=api_key, base_url=base_url)
47
+ client.collections.list(limit=1)
48
+ except Exception as exc:
49
+ stderr.print(f"[red]Authentication failed:[/red] {exc}")
50
+ raise typer.Exit(code=1)
51
+
52
+ cfg = load_config()
53
+ cfg["api_key"] = api_key
54
+ if base_url and base_url != "https://api.airweave.ai":
55
+ cfg["base_url"] = base_url
56
+ if collection:
57
+ cfg["collection"] = collection
58
+ save_config(cfg)
59
+
60
+ stderr.print("[green]Logged in.[/green] Config saved to ~/.airweave/config.json")
61
+
62
+
63
+ @app.command()
64
+ def status(
65
+ format: OutputFormat = typer.Option(
66
+ OutputFormat.text, "--format", "-f", help="Output format."
67
+ ),
68
+ ) -> None:
69
+ """Show current authentication state."""
70
+ env_key = os.environ.get("AIRWEAVE_API_KEY")
71
+ cfg = load_config()
72
+
73
+ info = {
74
+ "api_key_source": "env" if env_key else ("config" if cfg.get("api_key") else "none"),
75
+ "api_key_set": bool(env_key or cfg.get("api_key")),
76
+ "base_url": os.environ.get("AIRWEAVE_BASE_URL")
77
+ or cfg.get("base_url", "https://api.airweave.ai"),
78
+ "base_url_source": "env"
79
+ if os.environ.get("AIRWEAVE_BASE_URL")
80
+ else ("config" if cfg.get("base_url") else "default"),
81
+ "collection": os.environ.get("AIRWEAVE_COLLECTION") or cfg.get("collection"),
82
+ "collection_source": "env"
83
+ if os.environ.get("AIRWEAVE_COLLECTION")
84
+ else ("config" if cfg.get("collection") else "none"),
85
+ }
86
+
87
+ if format == OutputFormat.json:
88
+ typer.echo(json.dumps(info, indent=2))
89
+ return
90
+
91
+ stdout.print()
92
+ key_display = "****" if info["api_key_set"] else "[red]not set[/red]"
93
+ stdout.print(f" API key: {key_display} [dim]({info['api_key_source']})[/dim]")
94
+ stdout.print(f" Base URL: {info['base_url']} [dim]({info['base_url_source']})[/dim]")
95
+ coll_display = info["collection"] or "[red]not set[/red]"
96
+ coll_source = f" [dim]({info['collection_source']})[/dim]" if info["collection"] else ""
97
+ stdout.print(f" Collection: {coll_display}{coll_source}")
98
+ stdout.print()
99
+
100
+
101
+ @app.command()
102
+ def logout() -> None:
103
+ """Clear saved credentials from ~/.airweave/config.json."""
104
+ clear_config()
105
+ stderr.print("Logged out. Config cleared.")
@@ -0,0 +1,116 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from enum import Enum
5
+ from typing import Optional
6
+
7
+ import typer
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+
11
+ from airweave_cli.config import get_client, serialize
12
+
13
+ app = typer.Typer(name="collections", help="Manage collections.", no_args_is_help=True)
14
+
15
+ stderr = Console(stderr=True)
16
+ stdout = Console()
17
+
18
+
19
+ class OutputFormat(str, Enum):
20
+ json = "json"
21
+ text = "text"
22
+
23
+
24
+ @app.command("list")
25
+ def list_collections(
26
+ format: OutputFormat = typer.Option(
27
+ OutputFormat.json, "--format", "-f", help="Output format."
28
+ ),
29
+ ) -> None:
30
+ """List all collections."""
31
+ client = get_client()
32
+ try:
33
+ collections = client.collections.list()
34
+ except Exception as exc:
35
+ stderr.print(f"[red]Error:[/red] {exc}")
36
+ raise typer.Exit(code=1)
37
+
38
+ if format == OutputFormat.json:
39
+ typer.echo(json.dumps(serialize(collections), indent=2, default=str))
40
+ return
41
+
42
+ if not collections:
43
+ stderr.print("[yellow]No collections found.[/yellow]")
44
+ return
45
+
46
+ table = Table(title="Collections")
47
+ table.add_column("Name", style="bold")
48
+ table.add_column("Readable ID")
49
+ table.add_column("Status")
50
+ table.add_column("Created")
51
+
52
+ for c in collections:
53
+ table.add_row(
54
+ c.name,
55
+ c.readable_id,
56
+ str(c.status) if c.status else "-",
57
+ c.created_at.strftime("%Y-%m-%d %H:%M") if c.created_at else "-",
58
+ )
59
+
60
+ stdout.print(table)
61
+
62
+
63
+ @app.command()
64
+ def create(
65
+ name: str = typer.Option(..., "--name", "-n", help="Collection name."),
66
+ readable_id: Optional[str] = typer.Option(
67
+ None, "--readable-id", "-r", help="Custom readable ID (auto-generated if omitted)."
68
+ ),
69
+ format: OutputFormat = typer.Option(
70
+ OutputFormat.json, "--format", "-f", help="Output format."
71
+ ),
72
+ ) -> None:
73
+ """Create a new collection."""
74
+ client = get_client()
75
+ kwargs = {"name": name}
76
+ if readable_id:
77
+ kwargs["readable_id"] = readable_id
78
+
79
+ try:
80
+ collection = client.collections.create(**kwargs)
81
+ except Exception as exc:
82
+ stderr.print(f"[red]Error:[/red] {exc}")
83
+ raise typer.Exit(code=1)
84
+
85
+ if format == OutputFormat.json:
86
+ typer.echo(json.dumps(serialize(collection), indent=2, default=str))
87
+ return
88
+
89
+ stdout.print(f"[green]Created:[/green] {collection.name} ({collection.readable_id})")
90
+
91
+
92
+ @app.command()
93
+ def get(
94
+ readable_id: str = typer.Argument(..., help="Collection readable_id."),
95
+ format: OutputFormat = typer.Option(
96
+ OutputFormat.json, "--format", "-f", help="Output format."
97
+ ),
98
+ ) -> None:
99
+ """Get details of a collection."""
100
+ client = get_client()
101
+ try:
102
+ collection = client.collections.get(readable_id)
103
+ except Exception as exc:
104
+ stderr.print(f"[red]Error:[/red] {exc}")
105
+ raise typer.Exit(code=1)
106
+
107
+ if format == OutputFormat.json:
108
+ typer.echo(json.dumps(serialize(collection), indent=2, default=str))
109
+ return
110
+
111
+ stdout.print(f"[bold]{collection.name}[/bold] ({collection.readable_id})")
112
+ stdout.print(f" ID: {collection.id}")
113
+ stdout.print(f" Status: {collection.status}")
114
+ stdout.print(f" Vectors: {collection.vector_size}d ({collection.embedding_model_name})")
115
+ stdout.print(f" Created: {collection.created_at}")
116
+ stdout.print(f" Modified: {collection.modified_at}")
@@ -0,0 +1,87 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from enum import Enum
5
+ from typing import Optional
6
+
7
+ import typer
8
+ from rich.console import Console
9
+ from rich.markdown import Markdown
10
+ from rich.panel import Panel
11
+ from rich.text import Text
12
+
13
+ from airweave_cli.config import get_client, resolve_collection, serialize
14
+
15
+ stderr = Console(stderr=True)
16
+ stdout = Console()
17
+
18
+
19
+ class OutputFormat(str, Enum):
20
+ json = "json"
21
+ text = "text"
22
+
23
+
24
+ def search(
25
+ query: str = typer.Argument(..., help="Search query."),
26
+ collection: Optional[str] = typer.Option(
27
+ None, "--collection", "-c", help="Collection readable_id."
28
+ ),
29
+ top_k: int = typer.Option(10, "--top-k", "-k", help="Number of results to return."),
30
+ format: OutputFormat = typer.Option(
31
+ OutputFormat.json, "--format", "-f", help="Output format."
32
+ ),
33
+ ) -> None:
34
+ """Search a collection. This is the core command.
35
+
36
+ Pipe JSON output directly: airweave search "query" | jq '.results[0]'
37
+ """
38
+ coll = resolve_collection(collection)
39
+ client = get_client()
40
+
41
+ try:
42
+ from airweave import SearchRequest
43
+
44
+ response = client.collections.search(
45
+ coll,
46
+ request=SearchRequest(query=query, limit=top_k),
47
+ )
48
+ except Exception as exc:
49
+ stderr.print(f"[red]Error:[/red] {exc}")
50
+ raise typer.Exit(code=1)
51
+
52
+ if format == OutputFormat.json:
53
+ typer.echo(json.dumps(serialize(response), indent=2, default=str))
54
+ return
55
+
56
+ # Rich text output
57
+ results = response.results
58
+ if not results:
59
+ stderr.print("[yellow]No results found.[/yellow]")
60
+ raise typer.Exit(code=0)
61
+
62
+ if response.completion:
63
+ stdout.print(Panel(Markdown(response.completion), title="Answer", border_style="green"))
64
+ stdout.print()
65
+
66
+ for i, result in enumerate(results, 1):
67
+ meta = result.get("system_metadata") or {}
68
+ source = meta.get("source_name") or result.get("source_name", "unknown")
69
+ score = result.get("score")
70
+ name = result.get("name", "")
71
+ content = result.get("textual_representation") or result.get("md_content", "")
72
+ source_fields = result.get("source_fields") or {}
73
+ url = source_fields.get("web_url") or source_fields.get("url") or result.get("url")
74
+
75
+ title_parts = [f"[bold]#{i}[/bold] {source}"]
76
+ if score is not None:
77
+ title_parts.append(f"[dim]score={score:.3f}[/dim]")
78
+ title = Text.from_markup(" ".join(title_parts))
79
+
80
+ snippet = content[:500] + ("..." if len(content) > 500 else "") if content else ""
81
+ body = Markdown(snippet) if snippet else Text.from_markup("[dim]no content[/dim]")
82
+
83
+ subtitle = url or None
84
+ if name and not snippet:
85
+ body = Text(name)
86
+
87
+ stdout.print(Panel(body, title=title, subtitle=subtitle, border_style="blue"))
@@ -0,0 +1,96 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from enum import Enum
5
+ from typing import Optional
6
+
7
+ import typer
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+
11
+ from airweave_cli.config import get_client, resolve_collection, serialize
12
+
13
+ app = typer.Typer(name="sources", help="Manage source connections.", no_args_is_help=True)
14
+
15
+ stderr = Console(stderr=True)
16
+ stdout = Console()
17
+
18
+
19
+ class OutputFormat(str, Enum):
20
+ json = "json"
21
+ text = "text"
22
+
23
+
24
+ @app.command("list")
25
+ def list_sources(
26
+ collection: Optional[str] = typer.Option(
27
+ None, "--collection", "-c", help="Collection readable_id."
28
+ ),
29
+ format: OutputFormat = typer.Option(
30
+ OutputFormat.json, "--format", "-f", help="Output format."
31
+ ),
32
+ ) -> None:
33
+ """List source connections for a collection."""
34
+ coll = resolve_collection(collection)
35
+ client = get_client()
36
+
37
+ try:
38
+ sources = client.source_connections.list(collection=coll)
39
+ except Exception as exc:
40
+ stderr.print(f"[red]Error:[/red] {exc}")
41
+ raise typer.Exit(code=1)
42
+
43
+ if format == OutputFormat.json:
44
+ typer.echo(json.dumps(serialize(sources), indent=2, default=str))
45
+ return
46
+
47
+ if not sources:
48
+ stderr.print("[yellow]No source connections found.[/yellow]")
49
+ return
50
+
51
+ table = Table(title="Source Connections")
52
+ table.add_column("Name", style="bold")
53
+ table.add_column("Source")
54
+ table.add_column("ID")
55
+ table.add_column("Status")
56
+ table.add_column("Entities")
57
+
58
+ for s in sources:
59
+ table.add_row(
60
+ s.name,
61
+ s.short_name,
62
+ s.id,
63
+ str(s.status) if s.status else "-",
64
+ str(s.entity_count) if s.entity_count is not None else "-",
65
+ )
66
+
67
+ stdout.print(table)
68
+
69
+
70
+ @app.command()
71
+ def sync(
72
+ source_connection_id: str = typer.Argument(..., help="Source connection ID (UUID)."),
73
+ force: bool = typer.Option(
74
+ False, "--force", help="Force a full re-sync instead of incremental."
75
+ ),
76
+ format: OutputFormat = typer.Option(
77
+ OutputFormat.json, "--format", "-f", help="Output format."
78
+ ),
79
+ ) -> None:
80
+ """Trigger a sync for a source connection."""
81
+ client = get_client()
82
+
83
+ try:
84
+ job = client.source_connections.run(
85
+ source_connection_id,
86
+ force_full_sync=force if force else None,
87
+ )
88
+ except Exception as exc:
89
+ stderr.print(f"[red]Error:[/red] {exc}")
90
+ raise typer.Exit(code=1)
91
+
92
+ if format == OutputFormat.json:
93
+ typer.echo(json.dumps(serialize(job), indent=2, default=str))
94
+ return
95
+
96
+ stdout.print(f"[green]Sync started.[/green] Job ID: {job.id} Status: {job.status}")
@@ -0,0 +1,105 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+ from typing import Any, Dict, Optional
7
+
8
+ import typer
9
+
10
+ CONFIG_DIR = Path.home() / ".airweave"
11
+ CONFIG_PATH = CONFIG_DIR / "config.json"
12
+
13
+ DEFAULT_BASE_URL = "https://api.airweave.ai"
14
+
15
+
16
+ def load_config() -> Dict[str, Any]:
17
+ if not CONFIG_PATH.exists():
18
+ return {}
19
+ try:
20
+ return json.loads(CONFIG_PATH.read_text())
21
+ except (json.JSONDecodeError, OSError):
22
+ return {}
23
+
24
+
25
+ def save_config(data: Dict[str, Any]) -> None:
26
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
27
+ CONFIG_DIR.chmod(0o700)
28
+ CONFIG_PATH.write_text(json.dumps(data, indent=2) + "\n")
29
+
30
+
31
+ def clear_config() -> None:
32
+ if CONFIG_PATH.exists():
33
+ CONFIG_PATH.unlink()
34
+
35
+
36
+ def _fail(message: str) -> None:
37
+ typer.echo(message, err=True)
38
+ raise typer.Exit(code=1)
39
+
40
+
41
+ def resolve_api_key() -> str:
42
+ key = os.environ.get("AIRWEAVE_API_KEY")
43
+ if key:
44
+ return key
45
+
46
+ cfg = load_config()
47
+ key = cfg.get("api_key")
48
+ if key:
49
+ return key
50
+
51
+ _fail(
52
+ "No API key found. Set AIRWEAVE_API_KEY or run: airweave auth login"
53
+ )
54
+ return "" # unreachable, keeps type checkers happy
55
+
56
+
57
+ def resolve_base_url() -> str:
58
+ url = os.environ.get("AIRWEAVE_BASE_URL")
59
+ if url:
60
+ return url
61
+
62
+ cfg = load_config()
63
+ url = cfg.get("base_url")
64
+ if url:
65
+ return url
66
+
67
+ return DEFAULT_BASE_URL
68
+
69
+
70
+ def resolve_collection(flag: Optional[str] = None) -> str:
71
+ if flag:
72
+ return flag
73
+
74
+ coll = os.environ.get("AIRWEAVE_COLLECTION")
75
+ if coll:
76
+ return coll
77
+
78
+ cfg = load_config()
79
+ coll = cfg.get("collection")
80
+ if coll:
81
+ return coll
82
+
83
+ _fail(
84
+ "No collection specified. Use --collection, set AIRWEAVE_COLLECTION, "
85
+ "or run: airweave auth login"
86
+ )
87
+ return ""
88
+
89
+
90
+ def get_client():
91
+ from airweave import AirweaveSDK
92
+
93
+ return AirweaveSDK(
94
+ api_key=resolve_api_key(),
95
+ base_url=resolve_base_url(),
96
+ )
97
+
98
+
99
+ def serialize(obj: Any) -> Any:
100
+ """Convert an SDK model (or list of models) to a JSON-serializable dict."""
101
+ if isinstance(obj, list):
102
+ return [serialize(item) for item in obj]
103
+ if hasattr(obj, "dict"):
104
+ return obj.dict()
105
+ return obj
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ import typer
4
+
5
+ from airweave_cli import __version__
6
+ from airweave_cli.commands.auth import app as auth_app
7
+ from airweave_cli.commands.collections import app as collections_app
8
+ from airweave_cli.commands.search import search
9
+ from airweave_cli.commands.sources import app as sources_app
10
+
11
+ app = typer.Typer(
12
+ name="airweave",
13
+ help="Airweave CLI — search across your connected sources.",
14
+ no_args_is_help=True,
15
+ )
16
+
17
+
18
+ def _version_callback(value: bool) -> None:
19
+ if value:
20
+ typer.echo(f"airweave-cli {__version__}")
21
+ raise typer.Exit()
22
+
23
+
24
+ @app.callback()
25
+ def main(
26
+ version: bool = typer.Option(
27
+ False,
28
+ "--version",
29
+ "-v",
30
+ help="Show version and exit.",
31
+ callback=_version_callback,
32
+ is_eager=True,
33
+ ),
34
+ ) -> None:
35
+ pass
36
+
37
+
38
+ app.add_typer(auth_app, name="auth")
39
+ app.add_typer(collections_app, name="collections")
40
+ app.add_typer(sources_app, name="sources")
41
+ app.command()(search)
42
+
43
+
44
+ if __name__ == "__main__":
45
+ app()
@@ -0,0 +1,57 @@
1
+ [project]
2
+ name = "airweave-cli"
3
+
4
+ [tool.poetry]
5
+ name = "airweave-cli"
6
+ version = "0.1.0"
7
+ description = "The Airweave CLI for developers and AI agents."
8
+ readme = "README.md"
9
+ license = "MIT"
10
+ authors = ["Airweave <hello@airweave.ai>"]
11
+ keywords = ["airweave", "cli", "search", "ai", "agents"]
12
+
13
+ classifiers = [
14
+ "Intended Audience :: Developers",
15
+ "Programming Language :: Python",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.9",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Operating System :: OS Independent",
23
+ "Topic :: Software Development :: Libraries :: Python Modules",
24
+ "Typing :: Typed",
25
+ ]
26
+ packages = [
27
+ { include = "airweave_cli" }
28
+ ]
29
+
30
+ [project.urls]
31
+ Repository = "https://github.com/airweave-ai/cli"
32
+
33
+ [tool.poetry.dependencies]
34
+ python = "^3.9"
35
+ typer = {version = "^0.12", extras = ["all"]}
36
+ rich = "^13"
37
+ airweave-sdk = ">=0.9"
38
+
39
+ [tool.poetry.group.dev.dependencies]
40
+ pytest = "^8.0"
41
+ ruff = ">=0.11"
42
+
43
+ [tool.poetry.scripts]
44
+ airweave = "airweave_cli.main:app"
45
+
46
+ [tool.ruff]
47
+ line-length = 100
48
+
49
+ [tool.ruff.lint]
50
+ select = ["E", "F", "I"]
51
+
52
+ [tool.pytest.ini_options]
53
+ testpaths = ["tests"]
54
+
55
+ [build-system]
56
+ requires = ["poetry-core"]
57
+ build-backend = "poetry.core.masonry.api"