govinbox 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,110 @@
1
+ Metadata-Version: 2.4
2
+ Name: govinbox
3
+ Version: 0.1.0
4
+ Summary: GovInbox CLI: federal set-aside contract opportunities from your terminal or your AI agent
5
+ Author: Artifex Innovations LLC
6
+ License: Proprietary
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+
10
+ # govinbox CLI
11
+
12
+ Federal set-aside contract opportunities from your terminal. Or your AI agent's terminal.
13
+
14
+ The CLI is a thin wrapper over the versioned GovInbox REST API. All the logic lives on the server behind `/v1/`. This tool only builds requests and prints answers. It has zero dependencies beyond Python itself.
15
+
16
+ ## Install
17
+
18
+ Coming soon: `pip install govinbox`. Until the first release, install straight from the repo:
19
+
20
+ ```bash
21
+ pip install git+https://github.com/AUsernameForYou/setaside-radar.git#subdirectory=cli
22
+ ```
23
+
24
+ Requires Python 3.10 or newer. That is the whole install. No API keys to configure by hand, no virtualenv dance.
25
+
26
+ ## Quickstart
27
+
28
+ ```bash
29
+ govinbox login
30
+ # paste your API token (from your trial confirmation email or account page)
31
+
32
+ govinbox search --set-aside sdvosb --naics 5415 --limit 10
33
+
34
+ govinbox opportunity <notice-id>
35
+
36
+ govinbox digest
37
+ ```
38
+
39
+ ## Commands
40
+
41
+ Every command takes `--json` for machine-readable output (agents, pipes, scripts). Without it you get readable tables.
42
+
43
+ | Command | What it does |
44
+ |---|---|
45
+ | `govinbox login` | Save your API token on this machine (stored at `~/.config/govinbox/credentials`, readable only by you). The token is verified against the server; if it fails, nothing is kept. |
46
+ | `govinbox logout` | Remove the saved token. |
47
+ | `govinbox whoami` | Check the saved token is valid and show how many watchlists you have. |
48
+ | `govinbox search` | Search opportunities. Filters: `--set-aside` (sdvosb, 8a, hubzone, wosb, veteran, indian, smallbiz), `--naics` (prefix like 5415), `--keyword`, `--agency` (e.g. "veterans affairs"), `--days` (default 7), `--limit` (default 25). |
49
+ | `govinbox opportunity <id>` | Full details for one opportunity, including the SAM.gov link and early signals. |
50
+ | `govinbox watchlists` | List your saved searches. |
51
+ | `govinbox watchlist <id>` | Show one saved search. |
52
+ | `govinbox digest [--watchlist <id>] [--days N]` | New matches for your watchlists, plain-English lines, newest first. |
53
+ | `govinbox version` | CLI version, API endpoint, and server version. |
54
+
55
+ Add `--debug` to any command to see full error details.
56
+
57
+ ## Examples
58
+
59
+ ```bash
60
+ # SDVOSB IT opportunities posted in the last 3 days
61
+ govinbox search --set-aside sdvosb --naics 5415 --days 3
62
+
63
+ # Pipe titles into another tool (this is the agent pattern)
64
+ govinbox search --keyword cybersecurity --json | jq -r '.items[].title'
65
+
66
+ # Everything due within the week, as JSON
67
+ govinbox search --limit 100 --json | jq '[.items[] | select(.days_left <= 7)]'
68
+
69
+ # This morning's digest for one watchlist
70
+ govinbox digest --watchlist abc123 --days 1
71
+
72
+ # Full detail plus raw data for scripting
73
+ govinbox opportunity abc123 --json > opp.json
74
+ ```
75
+
76
+ ## Auth
77
+
78
+ `govinbox login` prompts for your API token and saves it to `~/.config/govinbox/credentials` with owner-only permissions (0600). Nothing is ever printed back.
79
+
80
+ Two overrides, for CI and agents:
81
+
82
+ - `GOVINBOX_API_TOKEN` environment variable: used instead of the saved token.
83
+ - `GOVINBOX_API_URL` environment variable: point at a staging server instead of `https://api.getgovinbox.com`.
84
+
85
+ If your token is rejected you will see:
86
+
87
+ > Your API token was rejected. Run `govinbox login` to refresh your token.
88
+
89
+ That is always the fix. If it keeps failing, check that you pasted the full token.
90
+
91
+ ## How versioning works
92
+
93
+ The CLI never contains business logic, so it rarely needs to change. The API is versioned (`/v1/`), and every request sends an `X-GovInbox-CLI-Version` header identifying the client. The server does not act on the header today; it exists so the server can warn outdated clients in the future. `govinbox version` compares your CLI against the server's reported version.
94
+
95
+ ## Exit codes
96
+
97
+ Scripts can rely on these:
98
+
99
+ | Code | Meaning |
100
+ |---|---|
101
+ | 0 | Success |
102
+ | 1 | General error (bad arguments from the server, network failure, unexpected error) |
103
+ | 2 | Not logged in, or the token was rejected (`govinbox login` is the fix) |
104
+ | 3 | Subscription lapsed (HTTP 402) |
105
+ | 4 | Rate limited (HTTP 429; the message tells you when to retry) |
106
+ | 5 | Not found (HTTP 404) |
107
+ | 6 | Bad request (HTTP 400) |
108
+ | 130 | Cancelled with Ctrl-C |
109
+
110
+ Errors print one plain-English line to stderr and never a traceback, unless you pass `--debug`.
@@ -0,0 +1,101 @@
1
+ # govinbox CLI
2
+
3
+ Federal set-aside contract opportunities from your terminal. Or your AI agent's terminal.
4
+
5
+ The CLI is a thin wrapper over the versioned GovInbox REST API. All the logic lives on the server behind `/v1/`. This tool only builds requests and prints answers. It has zero dependencies beyond Python itself.
6
+
7
+ ## Install
8
+
9
+ Coming soon: `pip install govinbox`. Until the first release, install straight from the repo:
10
+
11
+ ```bash
12
+ pip install git+https://github.com/AUsernameForYou/setaside-radar.git#subdirectory=cli
13
+ ```
14
+
15
+ Requires Python 3.10 or newer. That is the whole install. No API keys to configure by hand, no virtualenv dance.
16
+
17
+ ## Quickstart
18
+
19
+ ```bash
20
+ govinbox login
21
+ # paste your API token (from your trial confirmation email or account page)
22
+
23
+ govinbox search --set-aside sdvosb --naics 5415 --limit 10
24
+
25
+ govinbox opportunity <notice-id>
26
+
27
+ govinbox digest
28
+ ```
29
+
30
+ ## Commands
31
+
32
+ Every command takes `--json` for machine-readable output (agents, pipes, scripts). Without it you get readable tables.
33
+
34
+ | Command | What it does |
35
+ |---|---|
36
+ | `govinbox login` | Save your API token on this machine (stored at `~/.config/govinbox/credentials`, readable only by you). The token is verified against the server; if it fails, nothing is kept. |
37
+ | `govinbox logout` | Remove the saved token. |
38
+ | `govinbox whoami` | Check the saved token is valid and show how many watchlists you have. |
39
+ | `govinbox search` | Search opportunities. Filters: `--set-aside` (sdvosb, 8a, hubzone, wosb, veteran, indian, smallbiz), `--naics` (prefix like 5415), `--keyword`, `--agency` (e.g. "veterans affairs"), `--days` (default 7), `--limit` (default 25). |
40
+ | `govinbox opportunity <id>` | Full details for one opportunity, including the SAM.gov link and early signals. |
41
+ | `govinbox watchlists` | List your saved searches. |
42
+ | `govinbox watchlist <id>` | Show one saved search. |
43
+ | `govinbox digest [--watchlist <id>] [--days N]` | New matches for your watchlists, plain-English lines, newest first. |
44
+ | `govinbox version` | CLI version, API endpoint, and server version. |
45
+
46
+ Add `--debug` to any command to see full error details.
47
+
48
+ ## Examples
49
+
50
+ ```bash
51
+ # SDVOSB IT opportunities posted in the last 3 days
52
+ govinbox search --set-aside sdvosb --naics 5415 --days 3
53
+
54
+ # Pipe titles into another tool (this is the agent pattern)
55
+ govinbox search --keyword cybersecurity --json | jq -r '.items[].title'
56
+
57
+ # Everything due within the week, as JSON
58
+ govinbox search --limit 100 --json | jq '[.items[] | select(.days_left <= 7)]'
59
+
60
+ # This morning's digest for one watchlist
61
+ govinbox digest --watchlist abc123 --days 1
62
+
63
+ # Full detail plus raw data for scripting
64
+ govinbox opportunity abc123 --json > opp.json
65
+ ```
66
+
67
+ ## Auth
68
+
69
+ `govinbox login` prompts for your API token and saves it to `~/.config/govinbox/credentials` with owner-only permissions (0600). Nothing is ever printed back.
70
+
71
+ Two overrides, for CI and agents:
72
+
73
+ - `GOVINBOX_API_TOKEN` environment variable: used instead of the saved token.
74
+ - `GOVINBOX_API_URL` environment variable: point at a staging server instead of `https://api.getgovinbox.com`.
75
+
76
+ If your token is rejected you will see:
77
+
78
+ > Your API token was rejected. Run `govinbox login` to refresh your token.
79
+
80
+ That is always the fix. If it keeps failing, check that you pasted the full token.
81
+
82
+ ## How versioning works
83
+
84
+ The CLI never contains business logic, so it rarely needs to change. The API is versioned (`/v1/`), and every request sends an `X-GovInbox-CLI-Version` header identifying the client. The server does not act on the header today; it exists so the server can warn outdated clients in the future. `govinbox version` compares your CLI against the server's reported version.
85
+
86
+ ## Exit codes
87
+
88
+ Scripts can rely on these:
89
+
90
+ | Code | Meaning |
91
+ |---|---|
92
+ | 0 | Success |
93
+ | 1 | General error (bad arguments from the server, network failure, unexpected error) |
94
+ | 2 | Not logged in, or the token was rejected (`govinbox login` is the fix) |
95
+ | 3 | Subscription lapsed (HTTP 402) |
96
+ | 4 | Rate limited (HTTP 429; the message tells you when to retry) |
97
+ | 5 | Not found (HTTP 404) |
98
+ | 6 | Bad request (HTTP 400) |
99
+ | 130 | Cancelled with Ctrl-C |
100
+
101
+ Errors print one plain-English line to stderr and never a traceback, unless you pass `--debug`.
@@ -0,0 +1,8 @@
1
+ """GovInbox public CLI: thin wrapper over the versioned GovInbox REST API.
2
+
3
+ All business logic lives server-side behind /v1/. This package only builds
4
+ requests, sends them with the caller's API token, and pretty-prints responses.
5
+ """
6
+
7
+ __version__ = "0.1.0"
8
+ __all__ = ["__version__"]
@@ -0,0 +1,6 @@
1
+ """Allows `python -m govinbox ...` as an alternative to the `govinbox` script."""
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(main())
@@ -0,0 +1,70 @@
1
+ """Token storage for the GovInbox CLI.
2
+
3
+ The token lives in ~/.config/govinbox/credentials (mode 0600). A
4
+ GOVINBOX_API_TOKEN environment variable overrides the file, which is handy
5
+ for CI and for agents that inject secrets via the environment.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import os
12
+ import stat
13
+ from pathlib import Path
14
+
15
+ CONFIG_DIR = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "govinbox"
16
+ CREDENTIALS_FILE = CONFIG_DIR / "credentials"
17
+ ENV_TOKEN = "GOVINBOX_API_TOKEN"
18
+
19
+
20
+ def get_token() -> str | None:
21
+ """Return the stored token, or None when the user has not logged in."""
22
+ env = os.environ.get(ENV_TOKEN)
23
+ if env:
24
+ return env.strip() or None
25
+ try:
26
+ data = json.loads(CREDENTIALS_FILE.read_text())
27
+ except (FileNotFoundError, json.JSONDecodeError):
28
+ return None
29
+ token = data.get("token")
30
+ return token.strip() if token else None
31
+
32
+
33
+ def token_source() -> str:
34
+ """Where the current token came from, for user-facing messages."""
35
+ if os.environ.get(ENV_TOKEN):
36
+ return f"environment variable {ENV_TOKEN}"
37
+ return str(CREDENTIALS_FILE)
38
+
39
+
40
+ def save_token(token: str) -> Path:
41
+ """Persist the token with owner-only permissions. Returns the file path.
42
+
43
+ Uses os.open with mode 0600 so the file is never briefly world-readable
44
+ between creation and chmod.
45
+ """
46
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
47
+ fd = os.open(CREDENTIALS_FILE, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
48
+ try:
49
+ os.write(fd, (json.dumps({"token": token.strip()}) + "\n").encode("utf-8"))
50
+ finally:
51
+ os.close(fd)
52
+ # Belt and suspenders: enforce 0600 even if the file already existed with
53
+ # wider permissions.
54
+ os.chmod(CREDENTIALS_FILE, stat.S_IRUSR | stat.S_IWUSR)
55
+ return CREDENTIALS_FILE
56
+
57
+
58
+ def clear_token() -> bool:
59
+ """Remove the stored token. True when something was removed."""
60
+ try:
61
+ CREDENTIALS_FILE.unlink()
62
+ return True
63
+ except FileNotFoundError:
64
+ return False
65
+
66
+
67
+ def mask(token: str) -> str:
68
+ """Last four characters, for confirming which token is in use."""
69
+ t = token.strip()
70
+ return "…" + t[-4:] if len(t) > 4 else "…"