stash-sdk 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,48 @@
1
+ Metadata-Version: 2.4
2
+ Name: stash-sdk
3
+ Version: 0.1.0
4
+ Summary: Python SDK for Stash — shared memory for AI agents
5
+ Author: Fergana Labs
6
+ License: MIT
7
+ Project-URL: Homepage, https://joinstash.ai
8
+ Project-URL: Repository, https://github.com/Fergana-Labs/stash
9
+ Keywords: stash,ai,agents,memory,sdk
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Requires-Python: >=3.11
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: httpx>=0.27.0
20
+
21
+ # stash-sdk
22
+
23
+ Python client for [Stash](https://joinstash.ai) — shared memory for AI
24
+ agents. One dependency (httpx), fully typed.
25
+
26
+ ```bash
27
+ pip install stash-sdk
28
+ ```
29
+
30
+ ```python
31
+ from stash_sdk import Stash
32
+
33
+ stash = Stash() # reads STASH_API_KEY (and optional STASH_URL) from env
34
+
35
+ stash.push_event(
36
+ agent_name="my-agent",
37
+ event_type="user_message",
38
+ content="Can you rebook my Lisbon trip?",
39
+ session_id="sam:trip-114",
40
+ )
41
+ hits = stash.search_events("Lisbon")
42
+ ```
43
+
44
+ Everything the API offers is a method on `Stash`: events and transcripts,
45
+ pages and folders, files, tables, and Skills. Errors raise `StashError`
46
+ with the API's status code and detail — nothing fails silently.
47
+
48
+ The full API reference lives at [joinstash.ai/docs](https://joinstash.ai/docs).
@@ -0,0 +1,28 @@
1
+ # stash-sdk
2
+
3
+ Python client for [Stash](https://joinstash.ai) — shared memory for AI
4
+ agents. One dependency (httpx), fully typed.
5
+
6
+ ```bash
7
+ pip install stash-sdk
8
+ ```
9
+
10
+ ```python
11
+ from stash_sdk import Stash
12
+
13
+ stash = Stash() # reads STASH_API_KEY (and optional STASH_URL) from env
14
+
15
+ stash.push_event(
16
+ agent_name="my-agent",
17
+ event_type="user_message",
18
+ content="Can you rebook my Lisbon trip?",
19
+ session_id="sam:trip-114",
20
+ )
21
+ hits = stash.search_events("Lisbon")
22
+ ```
23
+
24
+ Everything the API offers is a method on `Stash`: events and transcripts,
25
+ pages and folders, files, tables, and Skills. Errors raise `StashError`
26
+ with the API's status code and detail — nothing fails silently.
27
+
28
+ The full API reference lives at [joinstash.ai/docs](https://joinstash.ai/docs).
@@ -0,0 +1,32 @@
1
+ [project]
2
+ name = "stash-sdk"
3
+ version = "0.1.0"
4
+ description = "Python SDK for Stash — shared memory for AI agents"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = { text = "MIT" }
8
+ authors = [{ name = "Fergana Labs" }]
9
+ keywords = ["stash", "ai", "agents", "memory", "sdk"]
10
+ classifiers = [
11
+ "Development Status :: 4 - Beta",
12
+ "Intended Audience :: Developers",
13
+ "License :: OSI Approved :: MIT License",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.11",
16
+ "Programming Language :: Python :: 3.12",
17
+ "Topic :: Software Development :: Libraries :: Python Modules",
18
+ ]
19
+ dependencies = [
20
+ "httpx>=0.27.0",
21
+ ]
22
+
23
+ [project.urls]
24
+ Homepage = "https://joinstash.ai"
25
+ Repository = "https://github.com/Fergana-Labs/stash"
26
+
27
+ [build-system]
28
+ requires = ["setuptools>=68.0"]
29
+ build-backend = "setuptools.build_meta"
30
+
31
+ [tool.setuptools.packages.find]
32
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ from .client import Stash, StashError
2
+
3
+ __all__ = ["Stash", "StashError"]
@@ -0,0 +1,457 @@
1
+ """Stash Python SDK — shared memory for AI agents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import mimetypes
6
+ import os
7
+ from pathlib import Path
8
+
9
+ import httpx
10
+
11
+ DEFAULT_BASE_URL = "https://api.joinstash.ai"
12
+
13
+
14
+ class StashError(Exception):
15
+ def __init__(self, status_code: int, detail: str | list):
16
+ self.status_code = status_code
17
+ self.detail = detail
18
+ super().__init__(f"[{status_code}] {detail}")
19
+
20
+
21
+ class Stash:
22
+ """Client for the Stash API.
23
+
24
+ Args:
25
+ api_key: Stash API key (``st_...``). Falls back to ``STASH_API_KEY`` env var.
26
+ base_url: API base URL. Falls back to ``STASH_URL`` env var, then production.
27
+ timeout: Default request timeout in seconds.
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ api_key: str | None = None,
33
+ base_url: str | None = None,
34
+ timeout: int = 30,
35
+ ):
36
+ self.api_key = api_key or os.environ.get("STASH_API_KEY", "")
37
+ base = (base_url or os.environ.get("STASH_URL", "")).rstrip("/") or DEFAULT_BASE_URL
38
+ self._http = httpx.Client(base_url=base, timeout=timeout)
39
+
40
+ def close(self) -> None:
41
+ self._http.close()
42
+
43
+ def __enter__(self):
44
+ return self
45
+
46
+ def __exit__(self, *args):
47
+ self.close()
48
+
49
+ # --- internals ---
50
+
51
+ def _headers(self) -> dict[str, str]:
52
+ if not self.api_key:
53
+ return {}
54
+ return {"Authorization": f"Bearer {self.api_key}"}
55
+
56
+ def _request(self, method: str, path: str, **kwargs) -> httpx.Response:
57
+ headers = kwargs.pop("headers", {})
58
+ headers.update(self._headers())
59
+ resp = self._http.request(method, path, headers=headers, **kwargs)
60
+ if not resp.is_success:
61
+ detail: str | list = resp.text
62
+ try:
63
+ detail = resp.json().get("detail", resp.text)
64
+ except Exception:
65
+ pass
66
+ raise StashError(resp.status_code, detail)
67
+ return resp
68
+
69
+ def _get(self, path: str, **params) -> dict | list:
70
+ return self._request("GET", path, params=params).json()
71
+
72
+ def _post(self, path: str, json=None, **kwargs) -> dict:
73
+ resp = self._request("POST", path, json=json, **kwargs)
74
+ return {} if resp.status_code == 204 else resp.json()
75
+
76
+ def _put(self, path: str, json=None) -> dict:
77
+ return self._request("PUT", path, json=json).json()
78
+
79
+ def _patch(self, path: str, json=None) -> dict:
80
+ return self._request("PATCH", path, json=json).json()
81
+
82
+ def _delete(self, path: str) -> None:
83
+ self._request("DELETE", path)
84
+
85
+ def _list(self, path: str, key: str, **params) -> list:
86
+ data = self._get(path, **params)
87
+ return data.get(key, data) if isinstance(data, dict) else data
88
+
89
+ def _upload(self, path: str, file_path: str | Path) -> dict:
90
+ p = Path(file_path)
91
+ content_type = mimetypes.guess_type(p.name)[0] or "application/octet-stream"
92
+ with open(p, "rb") as f:
93
+ resp = self._request(
94
+ "POST",
95
+ path,
96
+ files={"file": (p.name, f, content_type)},
97
+ timeout=300,
98
+ )
99
+ return resp.json()
100
+
101
+ # =========================================================================
102
+ # Auth
103
+ # =========================================================================
104
+
105
+ def register(self, name: str, description: str = "", password: str | None = None) -> dict:
106
+ body: dict = {"name": name, "description": description}
107
+ if password:
108
+ body["password"] = password
109
+ return self._post("/api/v1/users/register", json=body)
110
+
111
+ def login(self, name: str, password: str) -> dict:
112
+ return self._post("/api/v1/users/login", json={"name": name, "password": password})
113
+
114
+ def whoami(self) -> dict:
115
+ return self._get("/api/v1/users/me")
116
+
117
+ def list_api_keys(self) -> list:
118
+ return self._get("/api/v1/users/me/keys")
119
+
120
+ def revoke_api_key(self, key_id: str) -> None:
121
+ self._delete(f"/api/v1/users/me/keys/{key_id}")
122
+
123
+ # =========================================================================
124
+ # Discover (public Skills)
125
+ # =========================================================================
126
+
127
+ def list_discover_skills(
128
+ self,
129
+ query: str = "",
130
+ sort: str = "trending",
131
+ limit: int = 48,
132
+ ) -> dict:
133
+ params: dict = {"sort": sort, "limit": limit}
134
+ if query:
135
+ params["q"] = query
136
+ return self._get("/api/v1/discover/skills", **params)
137
+
138
+ # =========================================================================
139
+ # Skills
140
+ # =========================================================================
141
+
142
+ def list_skills(self) -> list:
143
+ return self._list("/api/v1/me/skills", "skills")
144
+
145
+ def publish_skill_folder(
146
+ self,
147
+ folder_id: str,
148
+ title: str | None = None,
149
+ description: str = "",
150
+ discoverable: bool = False,
151
+ ) -> dict:
152
+ body = {
153
+ "folder_id": folder_id,
154
+ "description": description,
155
+ "discoverable": discoverable,
156
+ }
157
+ if title:
158
+ body["title"] = title
159
+ return self._post("/api/v1/me/skills", json=body)
160
+
161
+ def update_skill(self, skill_id: str, **fields) -> dict:
162
+ return self._patch(f"/api/v1/skills/{skill_id}", json=fields)
163
+
164
+ def unpublish_skill(self, skill_id: str) -> None:
165
+ self._delete(f"/api/v1/skills/{skill_id}")
166
+
167
+ def get_public_skill(self, slug: str) -> dict:
168
+ return self._get(f"/api/v1/skills/{slug}")
169
+
170
+ def get_skill_text(self, slug: str) -> str:
171
+ resp = self._request("GET", f"/api/v1/skills/{slug}", params={"format": "text"})
172
+ return resp.text
173
+
174
+ def fork_skill(self, slug: str) -> dict:
175
+ return self._post(f"/api/v1/skills/{slug}/add-to-stash")
176
+
177
+ # =========================================================================
178
+ # Aggregate
179
+ # =========================================================================
180
+
181
+ def all_pages(self) -> list:
182
+ return self._list("/api/v1/me/pages", "pages")
183
+
184
+ def all_events(
185
+ self,
186
+ agent_name: str | None = None,
187
+ event_type: str | None = None,
188
+ limit: int = 50,
189
+ ) -> list:
190
+ params: dict = {"limit": limit}
191
+ if agent_name:
192
+ params["agent_name"] = agent_name
193
+ if event_type:
194
+ params["event_type"] = event_type
195
+ return self._list("/api/v1/me/session-events", "events", **params)
196
+
197
+ def all_tables(self) -> list:
198
+ return self._list("/api/v1/me/tables", "tables")
199
+
200
+ # =========================================================================
201
+ # Folders (user-scoped, nestable)
202
+ # =========================================================================
203
+
204
+ def list_folders(self) -> list:
205
+ return self._list("/api/v1/me/folders", "folders")
206
+
207
+ def create_folder(
208
+ self,
209
+ name: str,
210
+ parent_folder_id: str | None = None,
211
+ ) -> dict:
212
+ body: dict = {"name": name}
213
+ if parent_folder_id:
214
+ body["parent_folder_id"] = parent_folder_id
215
+ return self._post("/api/v1/me/folders", json=body)
216
+
217
+ def delete_folder(self, folder_id: str) -> None:
218
+ self._delete(f"/api/v1/me/folders/{folder_id}")
219
+
220
+ def get_tree(self) -> dict:
221
+ return self._get("/api/v1/me/tree")
222
+
223
+ # =========================================================================
224
+ # Pages (user-scoped)
225
+ # =========================================================================
226
+
227
+ def create_page(
228
+ self,
229
+ name: str,
230
+ content: str = "",
231
+ folder_id: str | None = None,
232
+ content_type: str = "markdown",
233
+ content_html: str = "",
234
+ ) -> dict:
235
+ body: dict = {
236
+ "name": name,
237
+ "content": content,
238
+ "content_type": content_type,
239
+ "content_html": content_html,
240
+ }
241
+ if folder_id:
242
+ body["folder_id"] = folder_id
243
+ return self._post("/api/v1/me/pages/new", json=body)
244
+
245
+ def list_pages(self) -> list:
246
+ return self._list("/api/v1/me/pages", "pages")
247
+
248
+ def get_page(self, page_id: str) -> dict:
249
+ return self._get(f"/api/v1/pages/{page_id}")
250
+
251
+ def update_page(self, page_id: str, **kwargs) -> dict:
252
+ return self._patch(f"/api/v1/me/pages/{page_id}", json=kwargs)
253
+
254
+ def delete_page(self, page_id: str) -> None:
255
+ self._delete(f"/api/v1/me/pages/{page_id}")
256
+
257
+ # =========================================================================
258
+ # Session events
259
+ # =========================================================================
260
+
261
+ def push_event(
262
+ self,
263
+ agent_name: str,
264
+ event_type: str,
265
+ content: str,
266
+ session_id: str | None = None,
267
+ tool_name: str | None = None,
268
+ metadata: dict | None = None,
269
+ attachments: list[dict] | None = None,
270
+ created_at: str | None = None,
271
+ ) -> dict:
272
+ body: dict = {
273
+ "agent_name": agent_name,
274
+ "event_type": event_type,
275
+ "content": content,
276
+ }
277
+ if session_id:
278
+ body["session_id"] = session_id
279
+ if tool_name:
280
+ body["tool_name"] = tool_name
281
+ if metadata:
282
+ body["metadata"] = metadata
283
+ if attachments:
284
+ body["attachments"] = attachments
285
+ if created_at:
286
+ body["created_at"] = created_at
287
+ return self._post("/api/v1/me/sessions/events", json=body)
288
+
289
+ def push_events_batch(self, events: list[dict]) -> list:
290
+ body: dict = {"events": events}
291
+ return self._post("/api/v1/me/sessions/events/batch", json=body)
292
+
293
+ def query_events(
294
+ self,
295
+ agent_name: str | None = None,
296
+ event_type: str | None = None,
297
+ limit: int = 50,
298
+ after: str | None = None,
299
+ ) -> list:
300
+ params: dict = {"limit": limit}
301
+ if agent_name:
302
+ params["agent_name"] = agent_name
303
+ if event_type:
304
+ params["event_type"] = event_type
305
+ if after:
306
+ params["after"] = after
307
+ return self._list("/api/v1/me/sessions/events", "events", **params)
308
+
309
+ def search_events(self, query: str, limit: int = 50) -> list:
310
+ return self._list(
311
+ "/api/v1/me/sessions/events/search",
312
+ "events",
313
+ q=query,
314
+ limit=limit,
315
+ )
316
+
317
+ def list_agent_names(self) -> list:
318
+ data = self._get("/api/v1/me/sessions/agent-names")
319
+ return data.get("agent_names", []) if isinstance(data, dict) else data
320
+
321
+ def upload_transcript(
322
+ self,
323
+ session_id: str,
324
+ transcript_path: str | Path,
325
+ agent_name: str,
326
+ cwd: str = "",
327
+ replace: bool = False,
328
+ ) -> dict:
329
+ import gzip as _gzip
330
+
331
+ with open(transcript_path, "rb") as f:
332
+ raw = f.read()
333
+ body = _gzip.compress(raw)
334
+ name = os.path.basename(str(transcript_path))
335
+ if not name.endswith(".gz"):
336
+ name += ".gz"
337
+ resp = self._request(
338
+ "POST",
339
+ "/api/v1/me/transcripts",
340
+ data={
341
+ "session_id": session_id,
342
+ "agent_name": agent_name,
343
+ "cwd": cwd,
344
+ "replace": str(replace).lower(),
345
+ },
346
+ files={"file": (name, body, "application/gzip")},
347
+ timeout=120,
348
+ )
349
+ return resp.json()
350
+
351
+ # =========================================================================
352
+ # Files
353
+ # =========================================================================
354
+
355
+ def upload_file(self, file_path: str | Path) -> dict:
356
+ return self._upload("/api/v1/me/files", file_path)
357
+
358
+ def list_files(self) -> list:
359
+ return self._list("/api/v1/me/files", "files")
360
+
361
+ def get_file(self, file_id: str) -> dict:
362
+ return self._get(f"/api/v1/me/files/{file_id}")
363
+
364
+ def delete_file(self, file_id: str) -> None:
365
+ self._delete(f"/api/v1/me/files/{file_id}")
366
+
367
+ def get_file_text(self, file_id: str) -> dict:
368
+ return self._get(f"/api/v1/me/files/{file_id}/text")
369
+
370
+ # =========================================================================
371
+ # Webhooks
372
+ # =========================================================================
373
+
374
+ def set_webhook(self, url: str, secret: str | None = None) -> dict:
375
+ body: dict = {"url": url}
376
+ if secret:
377
+ body["secret"] = secret
378
+ return self._post("/api/v1/me/webhooks", json=body)
379
+
380
+ # =========================================================================
381
+ # Tables
382
+ # =========================================================================
383
+
384
+ def create_table(
385
+ self,
386
+ name: str,
387
+ description: str = "",
388
+ columns: list | None = None,
389
+ ) -> dict:
390
+ return self._post(
391
+ "/api/v1/me/tables",
392
+ json={"name": name, "description": description, "columns": columns or []},
393
+ )
394
+
395
+ def list_tables(self) -> list:
396
+ return self._list("/api/v1/me/tables", "tables")
397
+
398
+ def get_table(self, table_id: str) -> dict:
399
+ return self._get(f"/api/v1/me/tables/{table_id}")
400
+
401
+ def update_table(self, table_id: str, **kwargs) -> dict:
402
+ return self._patch(f"/api/v1/me/tables/{table_id}", json=kwargs)
403
+
404
+ def delete_table(self, table_id: str) -> None:
405
+ self._delete(f"/api/v1/me/tables/{table_id}")
406
+
407
+ def list_table_rows(
408
+ self,
409
+ table_id: str,
410
+ limit: int = 50,
411
+ offset: int = 0,
412
+ sort_by: str = "",
413
+ sort_order: str = "asc",
414
+ filters: str = "",
415
+ ) -> dict:
416
+ params: dict = {"limit": limit, "offset": offset, "sort_order": sort_order}
417
+ if sort_by:
418
+ params["sort_by"] = sort_by
419
+ if filters:
420
+ params["filters"] = filters
421
+ return self._get(f"/api/v1/me/tables/{table_id}/rows", **params)
422
+
423
+ def insert_table_row(self, table_id: str, data: dict) -> dict:
424
+ return self._post(f"/api/v1/me/tables/{table_id}/rows", json={"data": data})
425
+
426
+ def insert_table_rows_batch(self, table_id: str, rows: list[dict]) -> dict:
427
+ return self._post(
428
+ f"/api/v1/me/tables/{table_id}/rows/batch",
429
+ json={"rows": [{"data": r} for r in rows]},
430
+ )
431
+
432
+ def update_table_row(self, table_id: str, row_id: str, data: dict) -> dict:
433
+ return self._patch(
434
+ f"/api/v1/me/tables/{table_id}/rows/{row_id}",
435
+ json={"data": data},
436
+ )
437
+
438
+ def delete_table_row(self, table_id: str, row_id: str) -> None:
439
+ self._delete(f"/api/v1/me/tables/{table_id}/rows/{row_id}")
440
+
441
+ def add_table_column(
442
+ self,
443
+ table_id: str,
444
+ name: str,
445
+ col_type: str = "text",
446
+ options: list | None = None,
447
+ ) -> dict:
448
+ body: dict = {"name": name, "type": col_type}
449
+ if options:
450
+ body["options"] = options
451
+ return self._post(f"/api/v1/me/tables/{table_id}/columns", json=body)
452
+
453
+ def delete_table_column(self, table_id: str, column_id: str) -> dict:
454
+ return self._request(
455
+ "DELETE",
456
+ f"/api/v1/me/tables/{table_id}/columns/{column_id}",
457
+ ).json()
File without changes
@@ -0,0 +1,48 @@
1
+ Metadata-Version: 2.4
2
+ Name: stash-sdk
3
+ Version: 0.1.0
4
+ Summary: Python SDK for Stash — shared memory for AI agents
5
+ Author: Fergana Labs
6
+ License: MIT
7
+ Project-URL: Homepage, https://joinstash.ai
8
+ Project-URL: Repository, https://github.com/Fergana-Labs/stash
9
+ Keywords: stash,ai,agents,memory,sdk
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Requires-Python: >=3.11
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: httpx>=0.27.0
20
+
21
+ # stash-sdk
22
+
23
+ Python client for [Stash](https://joinstash.ai) — shared memory for AI
24
+ agents. One dependency (httpx), fully typed.
25
+
26
+ ```bash
27
+ pip install stash-sdk
28
+ ```
29
+
30
+ ```python
31
+ from stash_sdk import Stash
32
+
33
+ stash = Stash() # reads STASH_API_KEY (and optional STASH_URL) from env
34
+
35
+ stash.push_event(
36
+ agent_name="my-agent",
37
+ event_type="user_message",
38
+ content="Can you rebook my Lisbon trip?",
39
+ session_id="sam:trip-114",
40
+ )
41
+ hits = stash.search_events("Lisbon")
42
+ ```
43
+
44
+ Everything the API offers is a method on `Stash`: events and transcripts,
45
+ pages and folders, files, tables, and Skills. Errors raise `StashError`
46
+ with the API's status code and detail — nothing fails silently.
47
+
48
+ The full API reference lives at [joinstash.ai/docs](https://joinstash.ai/docs).
@@ -0,0 +1,10 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/stash_sdk/__init__.py
4
+ src/stash_sdk/client.py
5
+ src/stash_sdk/py.typed
6
+ src/stash_sdk.egg-info/PKG-INFO
7
+ src/stash_sdk.egg-info/SOURCES.txt
8
+ src/stash_sdk.egg-info/dependency_links.txt
9
+ src/stash_sdk.egg-info/requires.txt
10
+ src/stash_sdk.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ httpx>=0.27.0
@@ -0,0 +1 @@
1
+ stash_sdk