suggestionbox-connect 2.0.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,25 @@
1
+ # SuggestionBox Connect changelog
2
+
3
+ ## 2.0.0 — 2026-08-28
4
+
5
+ - Identify the exact SuggestionBox bound to each Inbox credential using the
6
+ `suggestionbox` connection contract.
7
+ - Rename the public client connection fields to `suggestionbox_name` and
8
+ `suggestionbox_slug`.
9
+ - Require installed 1.x clients to upgrade; the removed `organization`
10
+ response and client attributes have no compatibility aliases.
11
+
12
+ ## 1.0.0 — 2026-08-17
13
+
14
+ - Install SuggestionBox Connect as `suggestionbox-connect`, with the `sb`
15
+ command-line tool and `suggestionbox-mcp` AI-assistant connector included.
16
+ - Connect and verify separate command-line, Codex, and Claude credentials using
17
+ the operating-system credential store.
18
+ - Register the local MCP server and required operating-system environment with
19
+ one `sb connect codex` or `sb connect claude` command.
20
+ - Add private Inbox items with a title, body, or both without copying one field
21
+ into the other.
22
+ - Mark privately recorded requests with `--received` and accept body content
23
+ from standard input.
24
+ - Inspect connection status and remove a saved credential without exposing it.
25
+ - Run the add-only `suggestionbox-mcp` STDIO server from the same package.
@@ -0,0 +1,8 @@
1
+ Copyright 2026 KnowledgeTack LLC. All rights reserved.
2
+
3
+ This software is proprietary. You may install and use it only to access the
4
+ SuggestionBox.io service in accordance with the SuggestionBox.io Terms of
5
+ Service at https://suggestionbox.io/terms/.
6
+
7
+ No other license or rights are granted. The software is provided subject to
8
+ the disclaimers and limitations of liability in those Terms.
@@ -0,0 +1,3 @@
1
+ include CHANGELOG.md
2
+ include LICENSE
3
+ include README.md
@@ -0,0 +1,139 @@
1
+ Metadata-Version: 2.4
2
+ Name: suggestionbox-connect
3
+ Version: 2.0.0
4
+ Summary: Connect your terminal and AI assistant to your private SuggestionBox.io Inbox.
5
+ Author: KnowledgeTack LLC
6
+ License-Expression: LicenseRef-Proprietary
7
+ Project-URL: Homepage, https://suggestionbox.io/
8
+ Project-URL: Changelog, https://pypi.org/project/suggestionbox-connect/#history
9
+ Project-URL: Terms, https://suggestionbox.io/terms/
10
+ Project-URL: Support, https://suggestionbox.io/terms/#contact
11
+ Keywords: suggestionbox,suggestions,cli,mcp,ai,product-management
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: End Users/Desktop
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Office/Business
23
+ Classifier: Topic :: Utilities
24
+ Requires-Python: <3.15,>=3.10
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: keyring<26,>=25
28
+ Dynamic: license-file
29
+
30
+ # SuggestionBox Connect
31
+
32
+ Connect your terminal and AI assistant to your private SuggestionBox.io Inbox.
33
+ One installation provides the `sb` command-line tool and the
34
+ `suggestionbox-mcp` AI-assistant connector.
35
+
36
+ ## Install
37
+
38
+ Install SuggestionBox Connect in an isolated environment with
39
+ [pipx](https://pipx.pypa.io/):
40
+
41
+ ```bash
42
+ pipx install suggestionbox-connect
43
+ ```
44
+
45
+ SuggestionBox supports Python 3.10 through 3.14 on Linux, macOS, and Windows.
46
+
47
+ ## Connect
48
+
49
+ 1. Sign in to SuggestionBox.io and open **Inbox → Inbox settings**.
50
+ 2. In **Command line**, create a credential for this computer and copy it.
51
+ 3. Run:
52
+
53
+ ```bash
54
+ sb connect
55
+ ```
56
+
57
+ Paste the credential into the hidden prompt. The CLI verifies it before saving
58
+ it in your operating system's credential store and names the exact
59
+ SuggestionBox it belongs to. The credential can add private
60
+ Inbox items only; it cannot read your Inbox or publish suggestions.
61
+
62
+ Confirm the connection at any time:
63
+
64
+ ```bash
65
+ sb status
66
+ ```
67
+
68
+ ## Add to Inbox
69
+
70
+ Use a title, a body, or both:
71
+
72
+ ```bash
73
+ sb add --title "Remember Inbox filters between visits"
74
+ sb add --body "Show which CSV column failed validation."
75
+ sb add --title "Scheduled exports" --body "Allow a weekly delivery schedule."
76
+ ```
77
+
78
+ Pipe a longer body from another command:
79
+
80
+ ```bash
81
+ printf '%s' "Add a dry-run option" | sb add --body -
82
+ ```
83
+
84
+ Use `--received` when recording something that came from another person:
85
+
86
+ ```bash
87
+ sb add --received --title "Customer requested a printable invoice"
88
+ ```
89
+
90
+ Every successful command prints the private Inbox item ID and its owner-only
91
+ review URL.
92
+
93
+ ## Disconnect, upgrade, or uninstall
94
+
95
+ ```bash
96
+ sb disconnect
97
+ pipx upgrade suggestionbox-connect
98
+ pipx uninstall suggestionbox-connect
99
+ ```
100
+
101
+ Deleting the credential in Inbox settings immediately prevents later use from
102
+ this computer. `sb disconnect` removes the local copy but does not delete the
103
+ server-side credential.
104
+
105
+ ## Automation
106
+
107
+ For ephemeral automation, supply `SUGGESTIONBOX_INBOX_TOKEN` through the
108
+ automation platform's secret store. Do not put an Inbox credential in a command,
109
+ URL, project file, prompt, or log.
110
+
111
+ ## AI-assistant tool
112
+
113
+ The same distribution includes the add-only `suggestionbox-mcp` STDIO
114
+ server. Create and copy a separate **AI assistant** credential for each
115
+ assistant, then run the matching command:
116
+
117
+ ```bash
118
+ sb connect codex
119
+ sb connect claude
120
+ ```
121
+
122
+ Each command verifies and securely saves the credential, installs the MCP
123
+ server in that assistant, and handles operating-system details internally.
124
+ Start a new assistant session afterward. The MCP server cannot read or publish
125
+ suggestions and acts only when the assistant invokes its single `add_to_inbox`
126
+ tool.
127
+
128
+ Check or remove either connection with:
129
+
130
+ ```bash
131
+ sb status codex
132
+ sb disconnect codex
133
+ sb status claude
134
+ sb disconnect claude
135
+ ```
136
+
137
+ Use of this client and SuggestionBox.io is governed by the
138
+ [SuggestionBox.io Terms of Service](https://suggestionbox.io/terms/). For help,
139
+ contact [support@suggestionbox.io](mailto:support@suggestionbox.io).
@@ -0,0 +1,110 @@
1
+ # SuggestionBox Connect
2
+
3
+ Connect your terminal and AI assistant to your private SuggestionBox.io Inbox.
4
+ One installation provides the `sb` command-line tool and the
5
+ `suggestionbox-mcp` AI-assistant connector.
6
+
7
+ ## Install
8
+
9
+ Install SuggestionBox Connect in an isolated environment with
10
+ [pipx](https://pipx.pypa.io/):
11
+
12
+ ```bash
13
+ pipx install suggestionbox-connect
14
+ ```
15
+
16
+ SuggestionBox supports Python 3.10 through 3.14 on Linux, macOS, and Windows.
17
+
18
+ ## Connect
19
+
20
+ 1. Sign in to SuggestionBox.io and open **Inbox → Inbox settings**.
21
+ 2. In **Command line**, create a credential for this computer and copy it.
22
+ 3. Run:
23
+
24
+ ```bash
25
+ sb connect
26
+ ```
27
+
28
+ Paste the credential into the hidden prompt. The CLI verifies it before saving
29
+ it in your operating system's credential store and names the exact
30
+ SuggestionBox it belongs to. The credential can add private
31
+ Inbox items only; it cannot read your Inbox or publish suggestions.
32
+
33
+ Confirm the connection at any time:
34
+
35
+ ```bash
36
+ sb status
37
+ ```
38
+
39
+ ## Add to Inbox
40
+
41
+ Use a title, a body, or both:
42
+
43
+ ```bash
44
+ sb add --title "Remember Inbox filters between visits"
45
+ sb add --body "Show which CSV column failed validation."
46
+ sb add --title "Scheduled exports" --body "Allow a weekly delivery schedule."
47
+ ```
48
+
49
+ Pipe a longer body from another command:
50
+
51
+ ```bash
52
+ printf '%s' "Add a dry-run option" | sb add --body -
53
+ ```
54
+
55
+ Use `--received` when recording something that came from another person:
56
+
57
+ ```bash
58
+ sb add --received --title "Customer requested a printable invoice"
59
+ ```
60
+
61
+ Every successful command prints the private Inbox item ID and its owner-only
62
+ review URL.
63
+
64
+ ## Disconnect, upgrade, or uninstall
65
+
66
+ ```bash
67
+ sb disconnect
68
+ pipx upgrade suggestionbox-connect
69
+ pipx uninstall suggestionbox-connect
70
+ ```
71
+
72
+ Deleting the credential in Inbox settings immediately prevents later use from
73
+ this computer. `sb disconnect` removes the local copy but does not delete the
74
+ server-side credential.
75
+
76
+ ## Automation
77
+
78
+ For ephemeral automation, supply `SUGGESTIONBOX_INBOX_TOKEN` through the
79
+ automation platform's secret store. Do not put an Inbox credential in a command,
80
+ URL, project file, prompt, or log.
81
+
82
+ ## AI-assistant tool
83
+
84
+ The same distribution includes the add-only `suggestionbox-mcp` STDIO
85
+ server. Create and copy a separate **AI assistant** credential for each
86
+ assistant, then run the matching command:
87
+
88
+ ```bash
89
+ sb connect codex
90
+ sb connect claude
91
+ ```
92
+
93
+ Each command verifies and securely saves the credential, installs the MCP
94
+ server in that assistant, and handles operating-system details internally.
95
+ Start a new assistant session afterward. The MCP server cannot read or publish
96
+ suggestions and acts only when the assistant invokes its single `add_to_inbox`
97
+ tool.
98
+
99
+ Check or remove either connection with:
100
+
101
+ ```bash
102
+ sb status codex
103
+ sb disconnect codex
104
+ sb status claude
105
+ sb disconnect claude
106
+ ```
107
+
108
+ Use of this client and SuggestionBox.io is governed by the
109
+ [SuggestionBox.io Terms of Service](https://suggestionbox.io/terms/). For help,
110
+ contact [support@suggestionbox.io](mailto:support@suggestionbox.io).
@@ -0,0 +1,46 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "suggestionbox-connect"
7
+ dynamic = ["version"]
8
+ description = "Connect your terminal and AI assistant to your private SuggestionBox.io Inbox."
9
+ readme = "README.md"
10
+ authors = [{ name = "KnowledgeTack LLC" }]
11
+ requires-python = ">=3.10,<3.15"
12
+ license = "LicenseRef-Proprietary"
13
+ license-files = ["LICENSE"]
14
+ dependencies = ["keyring>=25,<26"]
15
+ keywords = ["suggestionbox", "suggestions", "cli", "mcp", "ai", "product-management"]
16
+ classifiers = [
17
+ "Development Status :: 5 - Production/Stable",
18
+ "Environment :: Console",
19
+ "Intended Audience :: End Users/Desktop",
20
+ "Operating System :: OS Independent",
21
+ "Programming Language :: Python :: 3 :: Only",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Programming Language :: Python :: 3.14",
27
+ "Topic :: Office/Business",
28
+ "Topic :: Utilities",
29
+ ]
30
+
31
+ [project.urls]
32
+ Homepage = "https://suggestionbox.io/"
33
+ Changelog = "https://pypi.org/project/suggestionbox-connect/#history"
34
+ Terms = "https://suggestionbox.io/terms/"
35
+ Support = "https://suggestionbox.io/terms/#contact"
36
+
37
+ [project.scripts]
38
+ sb = "suggestionbox_connect.cli:main"
39
+ suggestionbox-mcp = "suggestionbox_connect.mcp_server:main"
40
+
41
+ [tool.setuptools.dynamic]
42
+ version = { attr = "suggestionbox_connect.__version__" }
43
+
44
+ [tool.setuptools.packages.find]
45
+ where = ["."]
46
+ include = ["suggestionbox_connect*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ __version__ = "2.0.0"
@@ -0,0 +1,143 @@
1
+ import argparse
2
+ import getpass
3
+ import os
4
+ import sys
5
+
6
+ from suggestionbox_connect import __version__
7
+ from suggestionbox_connect.client import (
8
+ InboxClient,
9
+ InboxClientError,
10
+ InboxValidationError,
11
+ credential_environment_name,
12
+ delete_token,
13
+ save_token,
14
+ )
15
+ from suggestionbox_connect.integrations import (
16
+ ASSISTANT_LABELS,
17
+ register_assistant,
18
+ unregister_assistant,
19
+ )
20
+
21
+
22
+ def parser():
23
+ root = argparse.ArgumentParser(
24
+ prog="sb", description="Add a private thought to your SuggestionBox.io Inbox."
25
+ )
26
+ root.add_argument("--version", action="version", version=f"sb {__version__}")
27
+ commands = root.add_subparsers(dest="command", required=True)
28
+ add = commands.add_parser("add", help="Add one private unfinished thought.")
29
+ add.add_argument("--title", default="", help="Add an optional short title.")
30
+ add.add_argument(
31
+ "--body",
32
+ default="",
33
+ help="Add an optional body. Use - to read the body from standard input.",
34
+ )
35
+ add.add_argument("--source-url", default="")
36
+ add.add_argument(
37
+ "--received",
38
+ action="store_true",
39
+ help="Mark privately as received from someone.",
40
+ )
41
+ connect = commands.add_parser(
42
+ "connect",
43
+ help="Verify and save an Inbox credential securely.",
44
+ )
45
+ connect.add_argument(
46
+ "target",
47
+ choices=tuple(ASSISTANT_LABELS),
48
+ nargs="?",
49
+ help="Connect Codex or Claude and install its MCP server.",
50
+ )
51
+ status = commands.add_parser(
52
+ "status",
53
+ help="Verify the saved connection without reading Inbox contents.",
54
+ )
55
+ status.add_argument(
56
+ "target",
57
+ choices=tuple(ASSISTANT_LABELS),
58
+ nargs="?",
59
+ help="Check the command-line, Codex, or Claude connection.",
60
+ )
61
+ disconnect = commands.add_parser(
62
+ "disconnect",
63
+ help="Remove a saved Inbox credential from this device.",
64
+ )
65
+ disconnect.add_argument(
66
+ "target",
67
+ choices=tuple(ASSISTANT_LABELS),
68
+ nargs="?",
69
+ help="Disconnect command line, Codex, or Claude.",
70
+ )
71
+ return root
72
+
73
+
74
+ def main(argv=None):
75
+ args = parser().parse_args(argv)
76
+ try:
77
+ if args.command == "connect":
78
+ profile = args.target or "cli"
79
+ try:
80
+ token = getpass.getpass(
81
+ "Paste the Inbox credential (input hidden): "
82
+ ).strip()
83
+ except (EOFError, KeyboardInterrupt):
84
+ raise InboxValidationError("Connection canceled.") from None
85
+ expected_adapter = "cli" if profile == "cli" else "mcp"
86
+ connection = InboxClient(token=token, profile=profile).connection(
87
+ expected_adapter=expected_adapter
88
+ )
89
+ save_token(token, profile=profile)
90
+ if args.target:
91
+ register_assistant(args.target)
92
+ label = ASSISTANT_LABELS[args.target]
93
+ print(
94
+ f"{label} connected to {connection.suggestionbox_name} · "
95
+ f"{connection.credential_label}. Start a new {label} session."
96
+ )
97
+ else:
98
+ print(
99
+ f"Connected to {connection.suggestionbox_name} · "
100
+ f"{connection.credential_label}"
101
+ )
102
+ return 0
103
+ if args.command == "status":
104
+ profile = args.target or "cli"
105
+ expected_adapter = "cli" if profile == "cli" else "mcp"
106
+ connection = InboxClient(profile=profile).connection(
107
+ expected_adapter=expected_adapter
108
+ )
109
+ print(
110
+ f"Connected to {connection.suggestionbox_name} · "
111
+ f"{connection.credential_label}"
112
+ )
113
+ return 0
114
+ if args.command == "disconnect":
115
+ profile = args.target or "cli"
116
+ removed = delete_token(profile=profile)
117
+ if args.target:
118
+ unregister_assistant(args.target)
119
+ environment_name = credential_environment_name(profile)
120
+ if os.environ.get(environment_name, "").strip():
121
+ print(
122
+ f"Saved connection removed. {environment_name} is still set "
123
+ "for this process."
124
+ )
125
+ else:
126
+ print("Disconnected." if removed else "No saved connection.")
127
+ return 0
128
+ body = sys.stdin.read() if args.body == "-" else args.body
129
+ result = InboxClient().add(
130
+ title=args.title,
131
+ body=body,
132
+ source_url=args.source_url,
133
+ origin_hint="received" if args.received else "",
134
+ )
135
+ print(f"Added to your Inbox · {result.inbox_item_id} · {result.review_url}")
136
+ return 0
137
+ except InboxClientError as error:
138
+ print(str(error), file=sys.stderr)
139
+ return error.exit_code
140
+
141
+
142
+ if __name__ == "__main__":
143
+ raise SystemExit(main())
@@ -0,0 +1,289 @@
1
+ import json
2
+ import os
3
+ import time
4
+ import urllib.error
5
+ import urllib.parse
6
+ import urllib.request
7
+ from dataclasses import dataclass
8
+ from uuid import uuid4
9
+
10
+ from suggestionbox_connect import __version__
11
+
12
+
13
+ DEFAULT_API_URL = "https://suggestionbox.io/api/v1/inbox-items/"
14
+ PROFILE_ENVIRONMENT_NAMES = {
15
+ "cli": "SUGGESTIONBOX_INBOX_TOKEN",
16
+ "codex": "SUGGESTIONBOX_INBOX_CODEX_TOKEN",
17
+ "claude": "SUGGESTIONBOX_INBOX_CLAUDE_TOKEN",
18
+ }
19
+
20
+
21
+ class InboxClientError(RuntimeError):
22
+ exit_code = 6
23
+
24
+
25
+ class InboxAuthenticationError(InboxClientError):
26
+ exit_code = 3
27
+
28
+
29
+ class InboxValidationError(InboxClientError):
30
+ exit_code = 2
31
+
32
+
33
+ class InboxRateLimitError(InboxClientError):
34
+ exit_code = 4
35
+
36
+
37
+ class InboxNetworkError(InboxClientError):
38
+ exit_code = 5
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class InboxResult:
43
+ inbox_item_id: str
44
+ state: str
45
+ review_url: str
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class InboxConnection:
50
+ suggestionbox_name: str
51
+ suggestionbox_slug: str
52
+ credential_label: str
53
+ adapter: str
54
+
55
+
56
+ def configured_token(profile="cli"):
57
+ environment_name = credential_environment_name(profile)
58
+ token = os.environ.get(environment_name, "").strip()
59
+ if token:
60
+ return token
61
+ try:
62
+ import keyring
63
+
64
+ return keyring.get_password("suggestionbox.io", f"inbox-token:{profile}") or ""
65
+ except Exception:
66
+ return ""
67
+
68
+
69
+ def save_token(token, *, profile="cli"):
70
+ environment_name = credential_environment_name(profile)
71
+ if not token.startswith("sb_inbox_"):
72
+ raise InboxValidationError("That is not a SuggestionBox Inbox credential.")
73
+ try:
74
+ import keyring
75
+
76
+ keyring.set_password("suggestionbox.io", f"inbox-token:{profile}", token)
77
+ except Exception as error:
78
+ raise InboxValidationError(
79
+ "A secure operating-system credential store is unavailable. "
80
+ f"Use {environment_name} for an ephemeral process instead."
81
+ ) from error
82
+
83
+
84
+ def delete_token(*, profile="cli"):
85
+ credential_environment_name(profile)
86
+ try:
87
+ import keyring
88
+ from keyring.errors import PasswordDeleteError
89
+
90
+ try:
91
+ keyring.delete_password("suggestionbox.io", f"inbox-token:{profile}")
92
+ except PasswordDeleteError:
93
+ return False
94
+ return True
95
+ except Exception as error:
96
+ raise InboxValidationError(
97
+ "The operating-system credential store is unavailable."
98
+ ) from error
99
+
100
+
101
+ def credential_environment_name(profile):
102
+ try:
103
+ return PROFILE_ENVIRONMENT_NAMES[profile]
104
+ except KeyError:
105
+ raise InboxValidationError("Choose command line, Codex, or Claude.") from None
106
+
107
+
108
+ class InboxClient:
109
+ def __init__(
110
+ self, *, token=None, profile="cli", api_url=None, timeout=8, opener=None
111
+ ):
112
+ selected_token = configured_token(profile) if token is None else token
113
+ self.token = selected_token.strip()
114
+ self.profile = profile
115
+ self.api_url = (
116
+ api_url or os.environ.get("SUGGESTIONBOX_INBOX_API_URL") or DEFAULT_API_URL
117
+ ).strip()
118
+ parsed_api_url = urllib.parse.urlsplit(self.api_url)
119
+ if (
120
+ parsed_api_url.scheme not in {"http", "https"}
121
+ or not parsed_api_url.hostname
122
+ or parsed_api_url.username
123
+ or parsed_api_url.password
124
+ or parsed_api_url.path != "/api/v1/inbox-items/"
125
+ or parsed_api_url.query
126
+ or parsed_api_url.fragment
127
+ or (
128
+ parsed_api_url.scheme != "https"
129
+ and parsed_api_url.hostname not in {"localhost", "127.0.0.1", "::1"}
130
+ )
131
+ ):
132
+ raise InboxValidationError(
133
+ "The Inbox API URL must use HTTPS; HTTP is allowed only for localhost."
134
+ )
135
+ self.timeout = timeout
136
+ self.opener = opener or urllib.request.urlopen
137
+
138
+ @property
139
+ def credential_url(self):
140
+ parsed = urllib.parse.urlsplit(self.api_url)
141
+ return urllib.parse.urlunsplit(
142
+ (
143
+ parsed.scheme,
144
+ parsed.netloc,
145
+ "/api/v1/inbox-credentials/current/",
146
+ "",
147
+ "",
148
+ )
149
+ )
150
+
151
+ def _authorization_header(self):
152
+ if not self.token:
153
+ environment_name = credential_environment_name(self.profile)
154
+ connect_command = (
155
+ "sb connect" if self.profile == "cli" else f"sb connect {self.profile}"
156
+ )
157
+ raise InboxAuthenticationError(
158
+ f"No Inbox credential is connected. Run `{connect_command}` or set "
159
+ f"{environment_name} for this process."
160
+ )
161
+ return f"Bearer {self.token}"
162
+
163
+ def _open_json(self, request):
164
+ for attempt in range(3):
165
+ try:
166
+ with self.opener(request, timeout=self.timeout) as response:
167
+ try:
168
+ return json.loads(response.read().decode("utf-8"))
169
+ except (UnicodeDecodeError, json.JSONDecodeError, TypeError):
170
+ raise InboxClientError(
171
+ "SuggestionBox.io returned an invalid response."
172
+ ) from None
173
+ except urllib.error.HTTPError as error:
174
+ try:
175
+ detail = json.loads(error.read().decode("utf-8")).get(
176
+ "detail", "Request failed."
177
+ )
178
+ except Exception:
179
+ detail = "Request failed."
180
+ if error.code in {401, 403}:
181
+ raise InboxAuthenticationError(detail) from None
182
+ if error.code in {400, 409, 410, 413, 415, 422}:
183
+ raise InboxValidationError(detail) from None
184
+ if error.code == 429:
185
+ raise InboxRateLimitError(detail) from None
186
+ if 500 <= error.code < 600 and attempt < 2:
187
+ time.sleep(0.15 * (attempt + 1))
188
+ continue
189
+ raise InboxClientError(detail) from None
190
+ except (urllib.error.URLError, TimeoutError, OSError) as error:
191
+ if attempt < 2:
192
+ time.sleep(0.15 * (attempt + 1))
193
+ continue
194
+ raise InboxNetworkError(
195
+ "SuggestionBox.io could not be reached."
196
+ ) from error
197
+ raise InboxNetworkError("SuggestionBox.io could not be reached.")
198
+
199
+ def connection(self, *, expected_adapter=None):
200
+ request = urllib.request.Request(
201
+ self.credential_url,
202
+ method="GET",
203
+ headers={
204
+ "Authorization": self._authorization_header(),
205
+ "Accept": "application/json",
206
+ "User-Agent": f"suggestionbox-connect/{__version__}",
207
+ },
208
+ )
209
+ data = self._open_json(request)
210
+ try:
211
+ connection = InboxConnection(
212
+ suggestionbox_name=data["suggestionbox"]["name"],
213
+ suggestionbox_slug=data["suggestionbox"]["slug"],
214
+ credential_label=data["credential"]["label"],
215
+ adapter=data["credential"]["adapter"],
216
+ )
217
+ except (KeyError, TypeError):
218
+ raise InboxClientError(
219
+ "SuggestionBox.io returned an invalid response."
220
+ ) from None
221
+ if not all(
222
+ isinstance(value, str) and value.strip()
223
+ for value in (
224
+ connection.suggestionbox_name,
225
+ connection.suggestionbox_slug,
226
+ connection.credential_label,
227
+ connection.adapter,
228
+ )
229
+ ):
230
+ raise InboxClientError("SuggestionBox.io returned an invalid response.")
231
+ if expected_adapter and connection.adapter != expected_adapter:
232
+ expected_label = (
233
+ "AI assistant" if expected_adapter == "mcp" else "Command line"
234
+ )
235
+ raise InboxAuthenticationError(
236
+ f"That credential is not for {expected_label}. Create the matching credential in Inbox settings."
237
+ )
238
+ return connection
239
+
240
+ def add(
241
+ self,
242
+ *,
243
+ title="",
244
+ body="",
245
+ source_url="",
246
+ origin_hint="",
247
+ idempotency_key=None,
248
+ ):
249
+ authorization = self._authorization_header()
250
+ if not isinstance(title, str) or not isinstance(body, str):
251
+ raise InboxValidationError("Title and body must be text.")
252
+ title = title.strip()
253
+ body = body.strip()
254
+ if not title and not body:
255
+ raise InboxValidationError("Enter a title or body to add.")
256
+ if len(title) > 160:
257
+ raise InboxValidationError("Keep the title under 160 characters.")
258
+ if len(body) > 10_000:
259
+ raise InboxValidationError("Keep the body under 10,000 characters.")
260
+ payload = {"title": title, "body": body}
261
+ if source_url:
262
+ payload["source_url"] = source_url
263
+ if origin_hint:
264
+ payload["origin_hint"] = origin_hint
265
+ body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
266
+ idempotency_key = idempotency_key or str(uuid4())
267
+ request = urllib.request.Request(
268
+ self.api_url,
269
+ data=body,
270
+ method="POST",
271
+ headers={
272
+ "Authorization": authorization,
273
+ "Content-Type": "application/json",
274
+ "Accept": "application/json",
275
+ "Idempotency-Key": idempotency_key,
276
+ "User-Agent": f"suggestionbox-connect/{__version__}",
277
+ },
278
+ )
279
+ data = self._open_json(request)
280
+ try:
281
+ return InboxResult(
282
+ inbox_item_id=data["inbox_item_id"],
283
+ state=data["state"],
284
+ review_url=data["review_url"],
285
+ )
286
+ except (KeyError, TypeError):
287
+ raise InboxClientError(
288
+ "SuggestionBox.io returned an invalid response."
289
+ ) from None
@@ -0,0 +1,97 @@
1
+ import os
2
+ import shutil
3
+ import subprocess
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from suggestionbox_connect.client import InboxValidationError
8
+
9
+
10
+ MCP_SERVER_NAME = "suggestionbox"
11
+ ASSISTANT_LABELS = {"codex": "Codex", "claude": "Claude"}
12
+
13
+
14
+ def _required_executable(name):
15
+ executable = shutil.which(name)
16
+ if executable:
17
+ return executable
18
+ label = ASSISTANT_LABELS.get(name, name)
19
+ raise InboxValidationError(f"{label} is not installed or is not available on PATH.")
20
+
21
+
22
+ def _mcp_executable():
23
+ executable = shutil.which("suggestionbox-mcp")
24
+ if executable:
25
+ return executable
26
+ filename = "suggestionbox-mcp.exe" if os.name == "nt" else "suggestionbox-mcp"
27
+ sibling = Path(sys.executable).with_name(filename)
28
+ if sibling.is_file():
29
+ return str(sibling)
30
+ raise InboxValidationError(
31
+ "suggestionbox-mcp is not installed. Reinstall SuggestionBox Connect."
32
+ )
33
+
34
+
35
+ def _mcp_environment_arguments():
36
+ arguments = []
37
+ api_url = os.environ.get("SUGGESTIONBOX_INBOX_API_URL", "").strip()
38
+ if api_url:
39
+ arguments.extend(["--env", f"SUGGESTIONBOX_INBOX_API_URL={api_url}"])
40
+ dbus_address = os.environ.get("DBUS_SESSION_BUS_ADDRESS", "").strip()
41
+ if dbus_address:
42
+ arguments.extend(["--env", f"DBUS_SESSION_BUS_ADDRESS={dbus_address}"])
43
+ return arguments
44
+
45
+
46
+ def _run(command):
47
+ try:
48
+ return subprocess.run(
49
+ command,
50
+ capture_output=True,
51
+ check=False,
52
+ text=True,
53
+ timeout=15,
54
+ )
55
+ except (OSError, subprocess.TimeoutExpired) as error:
56
+ raise InboxValidationError(
57
+ "The AI assistant could not be configured. Try again."
58
+ ) from error
59
+
60
+
61
+ def register_assistant(target):
62
+ if target not in ASSISTANT_LABELS:
63
+ raise InboxValidationError("Choose Codex or Claude.")
64
+ host = _required_executable(target)
65
+ mcp_server = _mcp_executable()
66
+ remove_command = [host, "mcp", "remove"]
67
+ if target == "claude":
68
+ remove_command.extend(["--scope", "user"])
69
+ remove_command.append(MCP_SERVER_NAME)
70
+ _run(remove_command)
71
+ command = [host, "mcp", "add"]
72
+ if target == "claude":
73
+ command.extend(["--scope", "user"])
74
+ command.append(MCP_SERVER_NAME)
75
+ command.extend(_mcp_environment_arguments())
76
+ command.extend(["--", mcp_server, "--profile", target])
77
+ result = _run(command)
78
+ if result.returncode:
79
+ detail = (result.stderr or result.stdout).strip()
80
+ label = ASSISTANT_LABELS[target]
81
+ message = f"{label} could not be configured."
82
+ if detail:
83
+ message = f"{message} {detail}"
84
+ raise InboxValidationError(message)
85
+
86
+
87
+ def unregister_assistant(target):
88
+ if target not in ASSISTANT_LABELS:
89
+ raise InboxValidationError("Choose Codex or Claude.")
90
+ host = shutil.which(target)
91
+ if not host:
92
+ return
93
+ command = [host, "mcp", "remove"]
94
+ if target == "claude":
95
+ command.extend(["--scope", "user"])
96
+ command.append(MCP_SERVER_NAME)
97
+ _run(command)
@@ -0,0 +1,144 @@
1
+ import argparse
2
+ import json
3
+ import sys
4
+
5
+ from suggestionbox_connect import __version__
6
+ from suggestionbox_connect.client import InboxClient, InboxClientError
7
+
8
+
9
+ TOOL = {
10
+ "name": "add_to_inbox",
11
+ "description": (
12
+ "Store one private unfinished thought in the user's SuggestionBox.io Inbox. "
13
+ "This never publishes. Call only when the user explicitly asks to save or add the thought."
14
+ ),
15
+ "inputSchema": {
16
+ "type": "object",
17
+ "properties": {
18
+ "title": {"type": "string", "maxLength": 160},
19
+ "body": {"type": "string", "maxLength": 10000},
20
+ "source_url": {"type": "string"},
21
+ "origin_hint": {"type": "string", "enum": ["", "owner", "received"]},
22
+ },
23
+ "anyOf": [{"required": ["title"]}, {"required": ["body"]}],
24
+ "additionalProperties": False,
25
+ },
26
+ }
27
+
28
+
29
+ def response(request_id, result=None, error=None):
30
+ payload = {"jsonrpc": "2.0", "id": request_id}
31
+ if error is not None:
32
+ payload["error"] = error
33
+ else:
34
+ payload["result"] = result
35
+ return payload
36
+
37
+
38
+ def handle(message, client=None, profile="codex"):
39
+ client = client or InboxClient(profile=profile)
40
+ request_id = message.get("id")
41
+ method = message.get("method")
42
+ if method == "initialize":
43
+ requested = message.get("params", {}).get("protocolVersion", "2025-06-18")
44
+ return response(
45
+ request_id,
46
+ {
47
+ "protocolVersion": requested,
48
+ "capabilities": {"tools": {"listChanged": False}},
49
+ "serverInfo": {
50
+ "name": "suggestionbox-mcp",
51
+ "version": __version__,
52
+ },
53
+ "instructions": (
54
+ "Use add_to_inbox only when the user explicitly asks to save an unfinished "
55
+ "thought. It is private and never publishes."
56
+ ),
57
+ },
58
+ )
59
+ if method == "notifications/initialized":
60
+ return None
61
+ if method == "ping":
62
+ return response(request_id, {})
63
+ if method == "shutdown":
64
+ return response(request_id, {})
65
+ if method == "tools/list":
66
+ return response(request_id, {"tools": [TOOL]})
67
+ if method == "tools/call":
68
+ params = message.get("params", {})
69
+ if params.get("name") != "add_to_inbox":
70
+ return response(
71
+ request_id, error={"code": -32602, "message": "Unknown tool."}
72
+ )
73
+ arguments = params.get("arguments", {})
74
+ allowed_arguments = {"title", "body", "source_url", "origin_hint"}
75
+ if (
76
+ not isinstance(arguments, dict)
77
+ or set(arguments) - allowed_arguments
78
+ or not isinstance(arguments.get("title", ""), str)
79
+ or not isinstance(arguments.get("body", ""), str)
80
+ or not (
81
+ arguments.get("title", "").strip() or arguments.get("body", "").strip()
82
+ )
83
+ or not isinstance(arguments.get("source_url", ""), str)
84
+ or arguments.get("origin_hint", "") not in {"", "owner", "received"}
85
+ ):
86
+ return response(
87
+ request_id,
88
+ error={"code": -32602, "message": "Invalid tool arguments."},
89
+ )
90
+ try:
91
+ result = client.add(
92
+ title=arguments.get("title", ""),
93
+ body=arguments.get("body", ""),
94
+ source_url=arguments.get("source_url", ""),
95
+ origin_hint=arguments.get("origin_hint", ""),
96
+ )
97
+ except InboxClientError as error:
98
+ return response(
99
+ request_id,
100
+ {
101
+ "content": [{"type": "text", "text": str(error)}],
102
+ "isError": True,
103
+ },
104
+ )
105
+ structured = {
106
+ "inbox_item_id": result.inbox_item_id,
107
+ "state": result.state,
108
+ "review_url": result.review_url,
109
+ }
110
+ return response(
111
+ request_id,
112
+ {
113
+ "content": [{"type": "text", "text": "Added to your Inbox"}],
114
+ "structuredContent": structured,
115
+ "isError": False,
116
+ },
117
+ )
118
+ return response(request_id, error={"code": -32601, "message": "Method not found."})
119
+
120
+
121
+ def main(argv=None):
122
+ parser = argparse.ArgumentParser(prog="suggestionbox-mcp")
123
+ parser.add_argument("--profile", choices=("codex", "claude"), default="codex")
124
+ args = parser.parse_args(argv)
125
+ client = InboxClient(profile=args.profile)
126
+ for line in sys.stdin:
127
+ try:
128
+ message = json.loads(line)
129
+ output = handle(message, client=client)
130
+ if output is not None:
131
+ print(json.dumps(output, separators=(",", ":")), flush=True)
132
+ except Exception:
133
+ print(
134
+ json.dumps(
135
+ response(
136
+ None, error={"code": -32700, "message": "Invalid request."}
137
+ )
138
+ ),
139
+ flush=True,
140
+ )
141
+
142
+
143
+ if __name__ == "__main__":
144
+ main()
@@ -0,0 +1,139 @@
1
+ Metadata-Version: 2.4
2
+ Name: suggestionbox-connect
3
+ Version: 2.0.0
4
+ Summary: Connect your terminal and AI assistant to your private SuggestionBox.io Inbox.
5
+ Author: KnowledgeTack LLC
6
+ License-Expression: LicenseRef-Proprietary
7
+ Project-URL: Homepage, https://suggestionbox.io/
8
+ Project-URL: Changelog, https://pypi.org/project/suggestionbox-connect/#history
9
+ Project-URL: Terms, https://suggestionbox.io/terms/
10
+ Project-URL: Support, https://suggestionbox.io/terms/#contact
11
+ Keywords: suggestionbox,suggestions,cli,mcp,ai,product-management
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: End Users/Desktop
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Office/Business
23
+ Classifier: Topic :: Utilities
24
+ Requires-Python: <3.15,>=3.10
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: keyring<26,>=25
28
+ Dynamic: license-file
29
+
30
+ # SuggestionBox Connect
31
+
32
+ Connect your terminal and AI assistant to your private SuggestionBox.io Inbox.
33
+ One installation provides the `sb` command-line tool and the
34
+ `suggestionbox-mcp` AI-assistant connector.
35
+
36
+ ## Install
37
+
38
+ Install SuggestionBox Connect in an isolated environment with
39
+ [pipx](https://pipx.pypa.io/):
40
+
41
+ ```bash
42
+ pipx install suggestionbox-connect
43
+ ```
44
+
45
+ SuggestionBox supports Python 3.10 through 3.14 on Linux, macOS, and Windows.
46
+
47
+ ## Connect
48
+
49
+ 1. Sign in to SuggestionBox.io and open **Inbox → Inbox settings**.
50
+ 2. In **Command line**, create a credential for this computer and copy it.
51
+ 3. Run:
52
+
53
+ ```bash
54
+ sb connect
55
+ ```
56
+
57
+ Paste the credential into the hidden prompt. The CLI verifies it before saving
58
+ it in your operating system's credential store and names the exact
59
+ SuggestionBox it belongs to. The credential can add private
60
+ Inbox items only; it cannot read your Inbox or publish suggestions.
61
+
62
+ Confirm the connection at any time:
63
+
64
+ ```bash
65
+ sb status
66
+ ```
67
+
68
+ ## Add to Inbox
69
+
70
+ Use a title, a body, or both:
71
+
72
+ ```bash
73
+ sb add --title "Remember Inbox filters between visits"
74
+ sb add --body "Show which CSV column failed validation."
75
+ sb add --title "Scheduled exports" --body "Allow a weekly delivery schedule."
76
+ ```
77
+
78
+ Pipe a longer body from another command:
79
+
80
+ ```bash
81
+ printf '%s' "Add a dry-run option" | sb add --body -
82
+ ```
83
+
84
+ Use `--received` when recording something that came from another person:
85
+
86
+ ```bash
87
+ sb add --received --title "Customer requested a printable invoice"
88
+ ```
89
+
90
+ Every successful command prints the private Inbox item ID and its owner-only
91
+ review URL.
92
+
93
+ ## Disconnect, upgrade, or uninstall
94
+
95
+ ```bash
96
+ sb disconnect
97
+ pipx upgrade suggestionbox-connect
98
+ pipx uninstall suggestionbox-connect
99
+ ```
100
+
101
+ Deleting the credential in Inbox settings immediately prevents later use from
102
+ this computer. `sb disconnect` removes the local copy but does not delete the
103
+ server-side credential.
104
+
105
+ ## Automation
106
+
107
+ For ephemeral automation, supply `SUGGESTIONBOX_INBOX_TOKEN` through the
108
+ automation platform's secret store. Do not put an Inbox credential in a command,
109
+ URL, project file, prompt, or log.
110
+
111
+ ## AI-assistant tool
112
+
113
+ The same distribution includes the add-only `suggestionbox-mcp` STDIO
114
+ server. Create and copy a separate **AI assistant** credential for each
115
+ assistant, then run the matching command:
116
+
117
+ ```bash
118
+ sb connect codex
119
+ sb connect claude
120
+ ```
121
+
122
+ Each command verifies and securely saves the credential, installs the MCP
123
+ server in that assistant, and handles operating-system details internally.
124
+ Start a new assistant session afterward. The MCP server cannot read or publish
125
+ suggestions and acts only when the assistant invokes its single `add_to_inbox`
126
+ tool.
127
+
128
+ Check or remove either connection with:
129
+
130
+ ```bash
131
+ sb status codex
132
+ sb disconnect codex
133
+ sb status claude
134
+ sb disconnect claude
135
+ ```
136
+
137
+ Use of this client and SuggestionBox.io is governed by the
138
+ [SuggestionBox.io Terms of Service](https://suggestionbox.io/terms/). For help,
139
+ contact [support@suggestionbox.io](mailto:support@suggestionbox.io).
@@ -0,0 +1,16 @@
1
+ CHANGELOG.md
2
+ LICENSE
3
+ MANIFEST.in
4
+ README.md
5
+ pyproject.toml
6
+ suggestionbox_connect/__init__.py
7
+ suggestionbox_connect/cli.py
8
+ suggestionbox_connect/client.py
9
+ suggestionbox_connect/integrations.py
10
+ suggestionbox_connect/mcp_server.py
11
+ suggestionbox_connect.egg-info/PKG-INFO
12
+ suggestionbox_connect.egg-info/SOURCES.txt
13
+ suggestionbox_connect.egg-info/dependency_links.txt
14
+ suggestionbox_connect.egg-info/entry_points.txt
15
+ suggestionbox_connect.egg-info/requires.txt
16
+ suggestionbox_connect.egg-info/top_level.txt
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ sb = suggestionbox_connect.cli:main
3
+ suggestionbox-mcp = suggestionbox_connect.mcp_server:main
@@ -0,0 +1 @@
1
+ suggestionbox_connect