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.
- suggestionbox_connect-2.0.0/CHANGELOG.md +25 -0
- suggestionbox_connect-2.0.0/LICENSE +8 -0
- suggestionbox_connect-2.0.0/MANIFEST.in +3 -0
- suggestionbox_connect-2.0.0/PKG-INFO +139 -0
- suggestionbox_connect-2.0.0/README.md +110 -0
- suggestionbox_connect-2.0.0/pyproject.toml +46 -0
- suggestionbox_connect-2.0.0/setup.cfg +4 -0
- suggestionbox_connect-2.0.0/suggestionbox_connect/__init__.py +1 -0
- suggestionbox_connect-2.0.0/suggestionbox_connect/cli.py +143 -0
- suggestionbox_connect-2.0.0/suggestionbox_connect/client.py +289 -0
- suggestionbox_connect-2.0.0/suggestionbox_connect/integrations.py +97 -0
- suggestionbox_connect-2.0.0/suggestionbox_connect/mcp_server.py +144 -0
- suggestionbox_connect-2.0.0/suggestionbox_connect.egg-info/PKG-INFO +139 -0
- suggestionbox_connect-2.0.0/suggestionbox_connect.egg-info/SOURCES.txt +16 -0
- suggestionbox_connect-2.0.0/suggestionbox_connect.egg-info/dependency_links.txt +1 -0
- suggestionbox_connect-2.0.0/suggestionbox_connect.egg-info/entry_points.txt +3 -0
- suggestionbox_connect-2.0.0/suggestionbox_connect.egg-info/requires.txt +1 -0
- suggestionbox_connect-2.0.0/suggestionbox_connect.egg-info/top_level.txt +1 -0
|
@@ -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,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 @@
|
|
|
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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
keyring<26,>=25
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
suggestionbox_connect
|