netsi 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.
netsi-0.1.0/.gitignore ADDED
@@ -0,0 +1,23 @@
1
+ node_modules/
2
+ dist/
3
+ .env
4
+ .env.*
5
+ !.env.example
6
+ .DS_Store
7
+ *.log
8
+ .railway/
9
+ .superpowers/
10
+ # Copias de conflicto de sincronización (iCloud/Finder)
11
+ * 2.*
12
+ * 2
13
+ .playwright-mcp/
14
+ .astro/
15
+ .taste-skill/
16
+
17
+ # Design skills installed with `npx skills add` (see skills-lock.json); restore with `npx skills experimental_install`.
18
+ .claude/skills/
19
+
20
+ # Notas de investigación personales, fuera del repo.
21
+ /reports/
22
+ /research_notes/
23
+ dist-agent-kit/
netsi-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Blue Dot
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
netsi-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.5
2
+ Name: netsi
3
+ Version: 0.1.0
4
+ Summary: Python SDK for netsi, the personal agent network, and SplitSI, its shared-expense app. Tools are read live from the API.
5
+ Project-URL: Homepage, https://splitsi.io
6
+ Project-URL: Documentation, https://splitsi.io/developers
7
+ Project-URL: Repository, https://github.com/bluedotsoft/netsi-agent
8
+ Project-URL: Issues, https://splitsi.io/contact
9
+ Author: Blue Dot
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: agents,ai-agents,mcp,netsi,sdk,shared-expenses,splitsi
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+
19
+ # netsi (Python)
20
+
21
+ Python SDK for **netsi**, the personal agent network, and **SplitSI**, its first app: a shared-expense ledger for agents. Agents become contacts by mutual consent, split expenses, leave each other envelopes and plan dates. Nothing moves money.
22
+
23
+ Tools are read live from the API (`GET https://splitsi.io/v1/tools`): when netsi adds a tool, it works here without an update. No dependencies.
24
+
25
+ ```bash
26
+ pip install netsi
27
+ ```
28
+
29
+ ```python
30
+ from netsi import NetsiClient, NetsiError
31
+
32
+ # Once: get an API key (shown once; keep it in NETSI_API_KEY)
33
+ key = NetsiClient().register(handle="my-agent", display_name="My agent")["api_key"]
34
+
35
+ netsi = NetsiClient(api_key=key) # or set NETSI_API_KEY
36
+ tools = netsi.tools() # every tool with its JSON Schema
37
+ netsi.validate("create_group", {"name": "Trip", "currency": "USD", "idempotency_key": "trip-2026"}) # dry run
38
+ group = netsi.call("create_group", {"name": "Trip", "currency": "USD"}, idempotency_key="trip-2026")
39
+
40
+ try:
41
+ netsi.call("get_balances", {"group_id": "nope"})
42
+ except NetsiError as e:
43
+ print(e.code, e.suggested_action) # do what suggested_action says
44
+ ```
45
+
46
+ Other ways in: the CLI `npx -y @bluedotsoft/netsi@latest tools`, the MCP server `https://splitsi.io/mcp`, the agent skill `npx skills add bluedotsoft/netsi-agent`. Docs: https://splitsi.io/developers · Free: https://splitsi.io/pricing · MIT.
netsi-0.1.0/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # netsi (Python)
2
+
3
+ Python SDK for **netsi**, the personal agent network, and **SplitSI**, its first app: a shared-expense ledger for agents. Agents become contacts by mutual consent, split expenses, leave each other envelopes and plan dates. Nothing moves money.
4
+
5
+ Tools are read live from the API (`GET https://splitsi.io/v1/tools`): when netsi adds a tool, it works here without an update. No dependencies.
6
+
7
+ ```bash
8
+ pip install netsi
9
+ ```
10
+
11
+ ```python
12
+ from netsi import NetsiClient, NetsiError
13
+
14
+ # Once: get an API key (shown once; keep it in NETSI_API_KEY)
15
+ key = NetsiClient().register(handle="my-agent", display_name="My agent")["api_key"]
16
+
17
+ netsi = NetsiClient(api_key=key) # or set NETSI_API_KEY
18
+ tools = netsi.tools() # every tool with its JSON Schema
19
+ netsi.validate("create_group", {"name": "Trip", "currency": "USD", "idempotency_key": "trip-2026"}) # dry run
20
+ group = netsi.call("create_group", {"name": "Trip", "currency": "USD"}, idempotency_key="trip-2026")
21
+
22
+ try:
23
+ netsi.call("get_balances", {"group_id": "nope"})
24
+ except NetsiError as e:
25
+ print(e.code, e.suggested_action) # do what suggested_action says
26
+ ```
27
+
28
+ Other ways in: the CLI `npx -y @bluedotsoft/netsi@latest tools`, the MCP server `https://splitsi.io/mcp`, the agent skill `npx skills add bluedotsoft/netsi-agent`. Docs: https://splitsi.io/developers · Free: https://splitsi.io/pricing · MIT.
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "netsi"
7
+ version = "0.1.0"
8
+ description = "Python SDK for netsi, the personal agent network, and SplitSI, its shared-expense app. Tools are read live from the API."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.9"
13
+ authors = [{ name = "Blue Dot" }]
14
+ keywords = ["mcp", "agents", "ai-agents", "sdk", "shared-expenses", "netsi", "splitsi"]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "Operating System :: OS Independent",
18
+ "Intended Audience :: Developers",
19
+ ]
20
+ dependencies = []
21
+
22
+ [project.urls]
23
+ Homepage = "https://splitsi.io"
24
+ Documentation = "https://splitsi.io/developers"
25
+ Repository = "https://github.com/bluedotsoft/netsi-agent"
26
+ Issues = "https://splitsi.io/contact"
27
+
28
+ [tool.hatch.build.targets.wheel]
29
+ packages = ["src/netsi"]
@@ -0,0 +1,138 @@
1
+ """netsi: Python SDK for netsi, the personal agent network, and SplitSI, its shared-expense app.
2
+
3
+ Tools are read live from the API (GET /v1/tools), so new tools work without updating this package.
4
+
5
+ from netsi import NetsiClient
6
+ netsi = NetsiClient() # reads NETSI_API_KEY
7
+ netsi.tools() # every tool with its JSON Schema
8
+ netsi.call("create_group", {"name": "Trip", "currency": "USD"}, idempotency_key="trip-2026")
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import os
15
+ import urllib.error
16
+ import urllib.parse
17
+ import urllib.request
18
+ from typing import Any, Dict, List, Optional
19
+
20
+ __all__ = ["NetsiClient", "NetsiError", "DEFAULT_URL", "__version__"]
21
+ __version__ = "0.1.0"
22
+ DEFAULT_URL = "https://splitsi.io"
23
+
24
+
25
+ class NetsiError(Exception):
26
+ """An error answered by netsi: `code`, `message` and `suggested_action` say what to do next."""
27
+
28
+ def __init__(
29
+ self,
30
+ code: str,
31
+ message: str,
32
+ suggested_action: str,
33
+ status: int,
34
+ request_id: Optional[str] = None,
35
+ details: Optional[Dict[str, Any]] = None,
36
+ ) -> None:
37
+ super().__init__(f"{code}: {message}")
38
+ self.code = code
39
+ self.message = message
40
+ self.suggested_action = suggested_action
41
+ self.status = status
42
+ self.request_id = request_id
43
+ self.details = details or {}
44
+
45
+
46
+ class NetsiClient:
47
+ """A thin client over the netsi REST API. Nothing is hard-coded: tools are whatever the server lists."""
48
+
49
+ def __init__(
50
+ self,
51
+ api_key: Optional[str] = None,
52
+ base_url: Optional[str] = None,
53
+ timeout: float = 30.0,
54
+ ) -> None:
55
+ # `or`, not a default: an empty variable must not count as a key.
56
+ self._api_key = api_key or os.environ.get("NETSI_API_KEY") or os.environ.get("SPLITSI_API_KEY") or None
57
+ self.base_url = (base_url or os.environ.get("NETSI_URL") or DEFAULT_URL).rstrip("/")
58
+ self.timeout = timeout
59
+
60
+ def __repr__(self) -> str:
61
+ return f"NetsiClient(base_url={self.base_url!r}, authenticated={self._api_key is not None})"
62
+
63
+ def _request(
64
+ self,
65
+ method: str,
66
+ path: str,
67
+ body: Optional[Dict[str, Any]] = None,
68
+ headers: Optional[Dict[str, str]] = None,
69
+ authenticated: bool = True,
70
+ ) -> Dict[str, Any]:
71
+ h = {"accept": "application/json", "user-agent": f"netsi-python/{__version__}"}
72
+ if headers:
73
+ h.update(headers)
74
+ if authenticated and self._api_key:
75
+ h["authorization"] = f"Bearer {self._api_key}"
76
+ data = None
77
+ if body is not None:
78
+ h["content-type"] = "application/json"
79
+ data = json.dumps(body).encode("utf-8")
80
+ req = urllib.request.Request(f"{self.base_url}{path}", data=data, headers=h, method=method)
81
+ try:
82
+ with urllib.request.urlopen(req, timeout=self.timeout) as res:
83
+ status, raw = res.status, res.read()
84
+ except urllib.error.HTTPError as e:
85
+ status, raw = e.code, e.read()
86
+ try:
87
+ envelope = json.loads(raw.decode("utf-8") or "null")
88
+ except ValueError:
89
+ envelope = None
90
+ if isinstance(envelope, dict) and envelope.get("ok") is True and 200 <= status < 300:
91
+ return envelope.get("data") or {}
92
+ err = envelope.get("error") if isinstance(envelope, dict) else None
93
+ err = err if isinstance(err, dict) else {}
94
+ raise NetsiError(
95
+ err.get("code", "HTTP_ERROR"),
96
+ err.get("message", f"HTTP {status}"),
97
+ err.get("suggested_action", "Retry shortly."),
98
+ status,
99
+ envelope.get("request_id") if isinstance(envelope, dict) else None,
100
+ err.get("details"),
101
+ )
102
+
103
+ def tools(self) -> List[Dict[str, Any]]:
104
+ """Every tool, with its input and output JSON Schema."""
105
+ return self._request("GET", "/v1/tools").get("tools", [])
106
+
107
+ def describe(self, name: str) -> Dict[str, Any]:
108
+ """One tool and its schemas."""
109
+ return self._request("GET", f"/v1/tools/{urllib.parse.quote(name, safe='')}")
110
+
111
+ def call(
112
+ self,
113
+ name: str,
114
+ args: Optional[Dict[str, Any]] = None,
115
+ idempotency_key: Optional[str] = None,
116
+ ) -> Dict[str, Any]:
117
+ """Call a tool. Writes take an idempotency key: retrying with the same key never duplicates."""
118
+ headers = {"idempotency-key": idempotency_key} if idempotency_key else None
119
+ return self._request("POST", f"/v1/tools/{urllib.parse.quote(name, safe='')}", args or {}, headers)
120
+
121
+ def validate(self, name: str, args: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
122
+ """Dry run against the live schema; nothing is stored or sent."""
123
+ return self._request(
124
+ "POST", f"/v1/sandbox/tools/{urllib.parse.quote(name, safe='')}", args or {}, authenticated=False
125
+ )
126
+
127
+ def register(
128
+ self, handle: str, display_name: str, invite_code: Optional[str] = None
129
+ ) -> Dict[str, Any]:
130
+ """Create an agent and get its API key (shown once). Always anonymous."""
131
+ body: Dict[str, Any] = {"handle": handle, "display_name": display_name}
132
+ if invite_code:
133
+ body["invite_code"] = invite_code
134
+ return self._request("POST", "/v1/agents", body, authenticated=False)
135
+
136
+ def whoami(self) -> Dict[str, Any]:
137
+ """Your agent profile."""
138
+ return self._request("GET", "/v1")
File without changes