@matt82198/aesop 0.1.0-beta.3 → 0.1.0-beta.5

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.
Files changed (234) hide show
  1. package/CHANGELOG.md +79 -95
  2. package/CLAUDE-TEMPLATE.md +115 -0
  3. package/README.md +126 -245
  4. package/aesop.config.example.json +34 -26
  5. package/bin/CLAUDE.md +67 -0
  6. package/bin/cli.js +593 -36
  7. package/daemons/CLAUDE.md +36 -0
  8. package/daemons/backup-fleet.sh +92 -21
  9. package/daemons/run-watchdog.sh +95 -12
  10. package/dash/CLAUDE.md +32 -0
  11. package/dash/dash-extra.mjs +194 -21
  12. package/dash/watchdog-gui.sh +23 -6
  13. package/docs/BEHAVIORAL-PR-REVIEW.md +86 -0
  14. package/docs/CHECKPOINTING.md +100 -0
  15. package/docs/DISPATCH-MODEL.md +11 -7
  16. package/docs/FORENSICS.md +204 -0
  17. package/docs/GOVERNANCE.md +4 -2
  18. package/docs/HOOK-INSTALL.md +307 -0
  19. package/docs/HOW-THE-LOOP-WORKS.md +175 -0
  20. package/docs/MEMORY-TEMPLATE.md +61 -0
  21. package/docs/README.md +44 -0
  22. package/docs/RELEASING.md +159 -0
  23. package/docs/RELIABILITY.md +61 -0
  24. package/docs/RESTORE.md +397 -0
  25. package/docs/SCRIPTS-POLICY.md +97 -0
  26. package/docs/archive/README.md +3 -0
  27. package/docs/archive/spikes/tiered-cognition/ACTIVATION.md +125 -0
  28. package/docs/archive/spikes/tiered-cognition/DESIGN.md +287 -0
  29. package/docs/archive/spikes/tiered-cognition/FINDINGS.md +113 -0
  30. package/docs/archive/spikes/tiered-cognition/README.md +27 -0
  31. package/docs/archive/spikes/tiered-cognition/aesop-cognition.example.md +32 -0
  32. package/docs/archive/spikes/tiered-cognition/force-model-policy.merged.mjs +673 -0
  33. package/docs/archive/spikes/tiered-cognition/strip-tools-hook.mjs +434 -0
  34. package/docs/case-study-portfolio.md +61 -0
  35. package/docs/self-stats-data.json +11 -0
  36. package/docs/templates/FLEET-OPS-ANALYSIS.example.md +27 -0
  37. package/docs/templates/FLEET-OPS-RECOMMENDATIONS.example.md +24 -0
  38. package/docs/templates/PROPOSALS-LOG.example.md +64 -0
  39. package/hooks/CLAUDE.md +61 -0
  40. package/hooks/claude/force-model-policy.mjs +159 -0
  41. package/hooks/pre-push-policy.sh +753 -0
  42. package/mcp/CLAUDE.md +213 -0
  43. package/mcp/package.json +26 -0
  44. package/mcp/server.mjs +543 -0
  45. package/monitor/.signal-state.json +3 -0
  46. package/monitor/ACTIONS.log +1 -0
  47. package/monitor/BRIEF.md +24 -0
  48. package/monitor/CHARTER.md +75 -88
  49. package/monitor/CLAUDE.md +40 -0
  50. package/monitor/SIGNALS.json +54 -0
  51. package/monitor/collect-signals.mjs +613 -61
  52. package/package.json +15 -3
  53. package/scan/fleet-scan.example.mjs +292 -0
  54. package/skills/CLAUDE.md +3 -0
  55. package/skills/healthcheck/SKILL.md +44 -0
  56. package/skills/power/SKILL.md +161 -0
  57. package/state_store/CLAUDE.md +39 -0
  58. package/state_store/__init__.py +24 -0
  59. package/state_store/__pycache__/__init__.cpython-314.pyc +0 -0
  60. package/state_store/__pycache__/api.cpython-314.pyc +0 -0
  61. package/state_store/__pycache__/export.cpython-314.pyc +0 -0
  62. package/state_store/__pycache__/ingest.cpython-314.pyc +0 -0
  63. package/state_store/__pycache__/projections.cpython-314.pyc +0 -0
  64. package/state_store/__pycache__/store.cpython-314.pyc +0 -0
  65. package/state_store/api.py +35 -0
  66. package/state_store/export.py +19 -0
  67. package/state_store/ingest.py +24 -0
  68. package/state_store/projections.py +52 -0
  69. package/state_store/store.py +102 -0
  70. package/tools/CLAUDE.md +72 -0
  71. package/tools/__pycache__/alert_bridge.cpython-314.pyc +0 -0
  72. package/tools/__pycache__/buildlog.cpython-314.pyc +0 -0
  73. package/tools/__pycache__/ci_merge_wait.cpython-314.pyc +0 -0
  74. package/tools/__pycache__/ensure_state.cpython-314.pyc +0 -0
  75. package/tools/__pycache__/eod_sweep.cpython-314.pyc +0 -0
  76. package/tools/__pycache__/fleet_ledger.cpython-314.pyc +0 -0
  77. package/tools/__pycache__/fleet_prompt_extractor.cpython-314.pyc +0 -0
  78. package/tools/__pycache__/healthcheck.cpython-314.pyc +0 -0
  79. package/tools/__pycache__/heartbeat.cpython-314.pyc +0 -0
  80. package/tools/__pycache__/inbox_drain.cpython-314.pyc +0 -0
  81. package/tools/__pycache__/launch_tui.cpython-314.pyc +0 -0
  82. package/tools/__pycache__/metrics_gate.cpython-314.pyc +0 -0
  83. package/tools/__pycache__/orchestrator_status.cpython-314.pyc +0 -0
  84. package/tools/__pycache__/power_selftest.cpython-314.pyc +0 -0
  85. package/tools/__pycache__/prepublish_scan.cpython-314.pyc +0 -0
  86. package/tools/__pycache__/rotate_logs.cpython-314.pyc +0 -0
  87. package/tools/__pycache__/scanner_selftest.cpython-314.pyc +0 -0
  88. package/tools/__pycache__/secret_scan.cpython-314.pyc +0 -0
  89. package/tools/__pycache__/self_stats.cpython-314.pyc +0 -0
  90. package/tools/__pycache__/session_usage_summary.cpython-314.pyc +0 -0
  91. package/tools/__pycache__/stall_check.cpython-314.pyc +0 -0
  92. package/tools/__pycache__/transcript_replay.cpython-314.pyc +0 -0
  93. package/tools/__pycache__/transcript_timeline.cpython-314.pyc +0 -0
  94. package/tools/__pycache__/verify_dash.cpython-314.pyc +0 -0
  95. package/tools/__pycache__/verify_submit_encoding.cpython-314.pyc +0 -0
  96. package/tools/agent-forensics.sh +186 -0
  97. package/tools/alert_bridge.py +449 -0
  98. package/tools/buildlog.py +182 -0
  99. package/tools/ci_merge_wait.py +317 -0
  100. package/tools/ensure_state.py +94 -0
  101. package/tools/eod_sweep.py +239 -0
  102. package/tools/fleet_ledger.py +272 -0
  103. package/tools/fleet_prompt_extractor.py +134 -0
  104. package/tools/healthcheck.py +296 -0
  105. package/tools/heartbeat.py +131 -0
  106. package/tools/inbox_drain.py +249 -0
  107. package/tools/launch_tui.py +51 -3
  108. package/tools/lock.mjs +211 -0
  109. package/tools/metrics_gate.py +205 -0
  110. package/tools/orchestrator_status.py +99 -0
  111. package/tools/power_selftest.py +386 -0
  112. package/tools/prepublish_scan.py +84 -0
  113. package/tools/proposals.mjs +248 -0
  114. package/tools/reconstitute.sh +467 -0
  115. package/tools/rotate_logs.py +228 -0
  116. package/tools/scanner_selftest.py +303 -0
  117. package/tools/secret_scan.py +138 -31
  118. package/tools/self_stats.py +509 -0
  119. package/tools/session_usage_summary.py +198 -0
  120. package/tools/stall_check.py +172 -0
  121. package/tools/svg_to_png.mjs +50 -0
  122. package/tools/transcript_replay.py +236 -0
  123. package/tools/transcript_timeline.py +184 -0
  124. package/tools/verify_dash.py +513 -0
  125. package/tools/verify_submit_encoding.py +202 -0
  126. package/ui/CLAUDE.md +125 -0
  127. package/ui/__pycache__/agents.cpython-314.pyc +0 -0
  128. package/ui/__pycache__/collectors.cpython-314.pyc +0 -0
  129. package/ui/__pycache__/config.cpython-314.pyc +0 -0
  130. package/ui/__pycache__/cost.cpython-314.pyc +0 -0
  131. package/ui/__pycache__/csrf.cpython-314.pyc +0 -0
  132. package/ui/__pycache__/handler.cpython-314.pyc +0 -0
  133. package/ui/__pycache__/render.cpython-314.pyc +0 -0
  134. package/ui/__pycache__/serve.cpython-314.pyc +0 -0
  135. package/ui/__pycache__/sse.cpython-314.pyc +0 -0
  136. package/ui/agents.py +183 -0
  137. package/ui/api/__init__.py +84 -0
  138. package/ui/api/__pycache__/__init__.cpython-314.pyc +0 -0
  139. package/ui/api/__pycache__/submit.cpython-314.pyc +0 -0
  140. package/ui/api/__pycache__/tracker.cpython-314.pyc +0 -0
  141. package/ui/api/submit.py +97 -0
  142. package/ui/api/tracker.py +116 -0
  143. package/ui/collectors.py +735 -0
  144. package/ui/config.py +123 -0
  145. package/ui/cost.py +260 -0
  146. package/ui/csrf.py +144 -0
  147. package/ui/handler.py +676 -0
  148. package/ui/render.py +49 -0
  149. package/ui/serve.py +68 -796
  150. package/ui/sse.py +183 -0
  151. package/ui/web/.gitattributes +13 -0
  152. package/ui/web/dist/assets/index-2LZDQirC.js +9 -0
  153. package/ui/web/dist/assets/index-D4M1qyOv.css +1 -0
  154. package/ui/web/dist/index.html +14 -0
  155. package/ui/web/index.html +13 -0
  156. package/ui/web/package-lock.json +2225 -0
  157. package/ui/web/package.json +26 -0
  158. package/ui/web/src/App.test.tsx +74 -0
  159. package/ui/web/src/App.tsx +142 -0
  160. package/ui/web/src/CONTRIBUTING-UI.md +49 -0
  161. package/ui/web/src/components/AgentRow.css +187 -0
  162. package/ui/web/src/components/AgentRow.test.tsx +209 -0
  163. package/ui/web/src/components/AgentRow.tsx +207 -0
  164. package/ui/web/src/components/AgentsPanel.css +108 -0
  165. package/ui/web/src/components/AgentsPanel.test.tsx +41 -0
  166. package/ui/web/src/components/AgentsPanel.tsx +58 -0
  167. package/ui/web/src/components/AlertsPanel.css +88 -0
  168. package/ui/web/src/components/AlertsPanel.test.tsx +51 -0
  169. package/ui/web/src/components/AlertsPanel.tsx +67 -0
  170. package/ui/web/src/components/BacklogPanel.test.tsx +126 -0
  171. package/ui/web/src/components/BacklogPanel.tsx +122 -0
  172. package/ui/web/src/components/CostChart.css +110 -0
  173. package/ui/web/src/components/CostChart.test.tsx +144 -0
  174. package/ui/web/src/components/CostChart.tsx +152 -0
  175. package/ui/web/src/components/CostTable.css +93 -0
  176. package/ui/web/src/components/CostTable.test.tsx +165 -0
  177. package/ui/web/src/components/CostTable.tsx +94 -0
  178. package/ui/web/src/components/EventsFeed.css +68 -0
  179. package/ui/web/src/components/EventsFeed.test.tsx +36 -0
  180. package/ui/web/src/components/EventsFeed.tsx +31 -0
  181. package/ui/web/src/components/HealthHeader.css +137 -0
  182. package/ui/web/src/components/HealthHeader.test.tsx +278 -0
  183. package/ui/web/src/components/HealthHeader.tsx +281 -0
  184. package/ui/web/src/components/InboxForm.css +135 -0
  185. package/ui/web/src/components/InboxForm.test.tsx +208 -0
  186. package/ui/web/src/components/InboxForm.tsx +116 -0
  187. package/ui/web/src/components/MessagesTail.module.css +144 -0
  188. package/ui/web/src/components/MessagesTail.test.tsx +176 -0
  189. package/ui/web/src/components/MessagesTail.tsx +94 -0
  190. package/ui/web/src/components/ReposPanel.css +90 -0
  191. package/ui/web/src/components/ReposPanel.test.tsx +45 -0
  192. package/ui/web/src/components/ReposPanel.tsx +67 -0
  193. package/ui/web/src/components/Scorecard.css +106 -0
  194. package/ui/web/src/components/Scorecard.test.tsx +117 -0
  195. package/ui/web/src/components/Scorecard.tsx +85 -0
  196. package/ui/web/src/components/Timeline.module.css +151 -0
  197. package/ui/web/src/components/Timeline.test.tsx +215 -0
  198. package/ui/web/src/components/Timeline.tsx +99 -0
  199. package/ui/web/src/components/TrackerBoard.test.tsx +121 -0
  200. package/ui/web/src/components/TrackerBoard.tsx +107 -0
  201. package/ui/web/src/components/TrackerCard.test.tsx +180 -0
  202. package/ui/web/src/components/TrackerCard.tsx +160 -0
  203. package/ui/web/src/components/TrackerForm.test.tsx +189 -0
  204. package/ui/web/src/components/TrackerForm.tsx +144 -0
  205. package/ui/web/src/lib/api.ts +218 -0
  206. package/ui/web/src/lib/format.test.ts +89 -0
  207. package/ui/web/src/lib/format.ts +103 -0
  208. package/ui/web/src/lib/sanitizeUrl.test.ts +84 -0
  209. package/ui/web/src/lib/sanitizeUrl.ts +38 -0
  210. package/ui/web/src/lib/types.ts +230 -0
  211. package/ui/web/src/lib/useHashRoute.test.ts +60 -0
  212. package/ui/web/src/lib/useHashRoute.ts +23 -0
  213. package/ui/web/src/lib/useSSE.ts +175 -0
  214. package/ui/web/src/main.tsx +10 -0
  215. package/ui/web/src/styles/global.css +179 -0
  216. package/ui/web/src/styles/theme.css +184 -0
  217. package/ui/web/src/styles/work.css +572 -0
  218. package/ui/web/src/test/fixtures.ts +385 -0
  219. package/ui/web/src/test/setup.ts +49 -0
  220. package/ui/web/src/views/Activity.module.css +43 -0
  221. package/ui/web/src/views/Activity.test.tsx +89 -0
  222. package/ui/web/src/views/Activity.tsx +31 -0
  223. package/ui/web/src/views/Cost.css +87 -0
  224. package/ui/web/src/views/Cost.test.tsx +142 -0
  225. package/ui/web/src/views/Cost.tsx +54 -0
  226. package/ui/web/src/views/Overview.css +51 -0
  227. package/ui/web/src/views/Overview.test.tsx +76 -0
  228. package/ui/web/src/views/Overview.tsx +46 -0
  229. package/ui/web/src/views/Work.test.tsx +82 -0
  230. package/ui/web/src/views/Work.tsx +79 -0
  231. package/ui/web/src/vite-env.d.ts +10 -0
  232. package/ui/web/tsconfig.json +22 -0
  233. package/ui/web/vite.config.ts +25 -0
  234. package/ui/web/vitest.config.ts +12 -0
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Aesop UI API — inbox-submit logic (wave-10 P0 split from handler.py).
4
+
5
+ Free function: given already-validated, non-empty text, append it to the UI
6
+ inbox file. UTF-8/LF-safe (matches the encoding used to read the file back
7
+ elsewhere) and guarded against a symlinked inbox file (TOCTOU defense --
8
+ reject rather than follow a symlink another local user/process may have
9
+ planted at config.INBOX_FILE).
10
+
11
+ Reads config.INBOX_FILE live via `import config` at call time, never
12
+ `from config import INBOX_FILE` -- a frozen import goes stale after
13
+ config.reload() (breaks test-fixture isolation). See ui/CLAUDE.md.
14
+
15
+ Detects both POSIX symlinks and Windows junctions/reparse points.
16
+ """
17
+ import os
18
+ import stat
19
+ from datetime import datetime
20
+
21
+ import config
22
+
23
+
24
+ def _is_link_or_reparse(path):
25
+ """Check if path is a symlink or Windows reparse point (junction/etc).
26
+
27
+ Returns True if:
28
+ - On all platforms: path is a POSIX symlink (detected via st_mode)
29
+ - On Windows: path has the FILE_ATTRIBUTE_REPARSE_POINT attribute
30
+ (which includes junctions, symlinks, and other reparse points)
31
+
32
+ Uses os.lstat() to detect without following links (includes dangling ones).
33
+ On error (e.g., path doesn't exist), returns False to let caller's
34
+ exists() check handle it normally.
35
+ """
36
+ try:
37
+ stat_result = os.lstat(str(path))
38
+
39
+ # On POSIX, check the symlink bit in st_mode
40
+ if stat.S_ISLNK(stat_result.st_mode):
41
+ return True
42
+
43
+ # On Windows, check for reparse point attribute (junctions, symlinks, etc)
44
+ # FILE_ATTRIBUTE_REPARSE_POINT = 0x400 (1024 decimal)
45
+ # st_file_attributes only exists on Windows
46
+ if hasattr(stat_result, 'st_file_attributes'):
47
+ FILE_ATTRIBUTE_REPARSE_POINT = 0x400
48
+ if stat_result.st_file_attributes & FILE_ATTRIBUTE_REPARSE_POINT:
49
+ return True
50
+
51
+ return False
52
+ except (OSError, AttributeError):
53
+ # If we can't stat it, let it fall through to the exists() check
54
+ # (file doesn't exist, which is fine)
55
+ return False
56
+
57
+
58
+ def append_to_inbox(text):
59
+ """Append one inbox line for `text` to config.INBOX_FILE.
60
+
61
+ Creates the file (with a header comment) if it doesn't exist yet.
62
+
63
+ Args:
64
+ text: non-empty, already-stripped text to record (caller is
65
+ responsible for validating it's non-empty -- this function does
66
+ not re-check).
67
+
68
+ Returns:
69
+ (True, None) on success.
70
+ (False, (400, {"error": "..."})) if config.INBOX_FILE exists and is
71
+ a symlink (rejected for security).
72
+ """
73
+ inbox_content = f"- [{datetime.now().isoformat()}] {text}\n"
74
+
75
+ # Security: reject symlinks and Windows junctions (TOCTOU defense) FIRST —
76
+ # check link/reparse INDEPENDENT of exists(). Path.exists() follows the link,
77
+ # so a DANGLING symlink (target not yet created) returns False and would
78
+ # otherwise skip this check, then open(...,'w') would follow the link and
79
+ # create the attacker's target. Windows junctions are not detected by
80
+ # os.path.islink(), so we use _is_link_or_reparse() instead.
81
+ if _is_link_or_reparse(config.INBOX_FILE):
82
+ return False, (400, {"error": "Inbox file is a symlink (rejected for security)"})
83
+
84
+ if not config.INBOX_FILE.exists():
85
+ config.INBOX_FILE.parent.mkdir(parents=True, exist_ok=True)
86
+ # Must match the encoding (utf-8) AND newline convention (LF) of the
87
+ # append below -- text-mode write_text() with no encoding= falls back
88
+ # to the locale-preferred encoding (cp1252 on Windows), which mangles
89
+ # non-ASCII bytes like the em-dash and leaves the file as a whole not
90
+ # valid UTF-8 for anything that reads it with encoding="utf-8".
91
+ with open(config.INBOX_FILE, 'w', encoding='utf-8', newline='\n') as f:
92
+ f.write("# UI Inbox — orchestrator reads each turn / on /power\n\n")
93
+
94
+ with open(config.INBOX_FILE, 'a', encoding='utf-8') as f:
95
+ f.write(inbox_content)
96
+
97
+ return True, None
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Aesop UI API — tracker CRUD logic (wave-10 P0 split from handler.py).
4
+
5
+ Free functions: take already-parsed request input (headers/body bytes/path
6
+ params extracted by ui/handler.py) and call collectors' tracker CRUD,
7
+ returning (status_code, dict). No self/HTTP coupling -- ui/handler.py owns
8
+ parsing the raw request and writing the HTTP response bytes.
9
+
10
+ Every mutation here is CSRF-gated via api.validate_mutation() (create) or a
11
+ direct csrf.validate_csrf_request() check performed by the caller before
12
+ delete (see ui/handler.py's handle_tracker_mutate -- CSRF is checked once,
13
+ before the path is even parsed, matching the pre-split handler's ordering).
14
+
15
+ Reads collectors' tracker CRUD, which itself reads config.X live via
16
+ `import config` -- see ui/CLAUDE.md load-bearing rule.
17
+ """
18
+ import json
19
+
20
+ from . import validate_mutation
21
+ from collectors import (create_tracker_item, delete_tracker_item,
22
+ get_tracker_items, update_tracker_item)
23
+
24
+
25
+ def list_items(status=None, priority=None):
26
+ """GET /api/tracker -- list items with optional status/priority filters.
27
+
28
+ Returns:
29
+ (200, items_list) on success.
30
+ (500, error_dict) on failure.
31
+ """
32
+ try:
33
+ items = get_tracker_items(status=status, priority=priority)
34
+ return 200, items
35
+ except Exception as e:
36
+ return 500, {"error": str(e)}
37
+
38
+
39
+ def create(headers, body_bytes):
40
+ """POST /api/tracker -- create a tracker item. CSRF-gated.
41
+
42
+ Args:
43
+ headers: request headers (case-insensitive dict-like).
44
+ body_bytes: raw request body already read + bounded by the caller.
45
+
46
+ Returns:
47
+ (201, item_dict) on success.
48
+ (403, error_dict) on CSRF failure.
49
+ (400, error_dict) on invalid Content-Length or invalid JSON.
50
+ (500, error_dict) on any other failure.
51
+ """
52
+ try:
53
+ ok, result = validate_mutation(headers, body_bytes)
54
+ if not ok:
55
+ status, error = result
56
+ return status, error
57
+ item = create_tracker_item(result)
58
+ return 201, item
59
+ except json.JSONDecodeError:
60
+ return 400, {"error": "Invalid JSON"}
61
+ except Exception as e:
62
+ return 500, {"error": str(e)}
63
+
64
+
65
+ def update(item_id, body_bytes):
66
+ """POST /api/tracker/<id> (default action) -- update a tracker item.
67
+
68
+ CSRF must already have been validated by the caller before invoking this
69
+ (see ui/handler.py's handle_tracker_mutate: CSRF is checked once up front,
70
+ shared with the delete branch). This function only handles JSON parse +
71
+ the update itself, matching the pre-split handler's behavior where a
72
+ malformed JSON body on this path falls through to a generic error rather
73
+ than a dedicated "Invalid JSON" 400 (unlike create()).
74
+
75
+ Args:
76
+ item_id: tracker item id extracted from the URL path by the caller.
77
+ body_bytes: raw request body already read + bounded by the caller.
78
+
79
+ Returns:
80
+ (200, item_dict) on success.
81
+ (404, error_dict) if item_id is unknown.
82
+ (500, error_dict) on any other failure (including malformed JSON --
83
+ preserves the pre-split handler's behavior).
84
+ """
85
+ try:
86
+ body_str = (body_bytes or b'').decode('utf-8', errors='ignore')
87
+ update_data = json.loads(body_str)
88
+ item = update_tracker_item(item_id, update_data)
89
+ return 200, item
90
+ except Exception as e:
91
+ if "404" in str(e):
92
+ return 404, {"error": str(e)}
93
+ return 500, {"error": str(e)}
94
+
95
+
96
+ def delete(item_id):
97
+ """POST /api/tracker/<id>?action=delete -- soft-delete (archive) an item.
98
+
99
+ CSRF must already have been validated by the caller before invoking this
100
+ (see update()'s docstring -- same shared up-front check).
101
+
102
+ Idempotent: deleting an already-archived item succeeds again (re-sets
103
+ status to "archived"), it does not error.
104
+
105
+ Returns:
106
+ (200, item_dict) on success.
107
+ (404, error_dict) if item_id is unknown.
108
+ (500, error_dict) on any other failure.
109
+ """
110
+ try:
111
+ item = delete_tracker_item(item_id)
112
+ return 200, item
113
+ except Exception as e:
114
+ if "404" in str(e):
115
+ return 404, {"error": str(e)}
116
+ return 500, {"error": str(e)}