caudate-cli 0.1.0__py3-none-any.whl

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 (153) hide show
  1. api/__init__.py +5 -0
  2. api/anthropic_compat.py +1518 -0
  3. api/artifact_viewer.py +366 -0
  4. api/caudate_middleware.py +618 -0
  5. api/forge_bootstrapper_routes.py +377 -0
  6. api/forge_routes.py +630 -0
  7. api/forge_system_routes.py +294 -0
  8. api/openai_compat.py +1993 -0
  9. api/server.py +667 -0
  10. api/storyboard_page.py +677 -0
  11. caudate_cli-0.1.0.dist-info/METADATA +354 -0
  12. caudate_cli-0.1.0.dist-info/RECORD +153 -0
  13. caudate_cli-0.1.0.dist-info/WHEEL +5 -0
  14. caudate_cli-0.1.0.dist-info/entry_points.txt +2 -0
  15. caudate_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  16. caudate_cli-0.1.0.dist-info/top_level.txt +14 -0
  17. cognos_mcp/__init__.py +4 -0
  18. cognos_mcp/bridge.py +41 -0
  19. cognos_mcp/client.py +70 -0
  20. cognos_mcp/config.py +49 -0
  21. cognos_mcp/server.py +66 -0
  22. config.py +82 -0
  23. core/__init__.py +0 -0
  24. core/agent.py +468 -0
  25. core/agentic_loop.py +731 -0
  26. core/anthropic_auth.py +91 -0
  27. core/background.py +113 -0
  28. core/banner.py +134 -0
  29. core/bootstrap.py +292 -0
  30. core/citations.py +131 -0
  31. core/compaction.py +109 -0
  32. core/constitution.py +198 -0
  33. core/diff_viewer.py +87 -0
  34. core/export.py +85 -0
  35. core/file_refs.py +119 -0
  36. core/files.py +199 -0
  37. core/hooks.py +209 -0
  38. core/image.py +599 -0
  39. core/input.py +91 -0
  40. core/loop.py +238 -0
  41. core/memory_md.py +147 -0
  42. core/notifications.py +99 -0
  43. core/ownership.py +181 -0
  44. core/paste.py +81 -0
  45. core/permissions.py +210 -0
  46. core/plan_mode.py +215 -0
  47. core/sandbox_prompt.py +185 -0
  48. core/scheduler.py +195 -0
  49. core/schemas.py +202 -0
  50. core/session.py +90 -0
  51. core/settings.py +132 -0
  52. core/skills.py +398 -0
  53. core/slash_commands.py +977 -0
  54. core/statusline.py +61 -0
  55. core/subagent.py +300 -0
  56. core/thinking.py +50 -0
  57. core/updater.py +122 -0
  58. core/usage.py +109 -0
  59. core/worktree.py +93 -0
  60. execution/__init__.py +0 -0
  61. execution/executor.py +329 -0
  62. execution/plugins.py +108 -0
  63. execution/tools/__init__.py +0 -0
  64. execution/tools/agent_tool.py +107 -0
  65. execution/tools/agentic_tool.py +297 -0
  66. execution/tools/artifact_tool.py +191 -0
  67. execution/tools/ask_user_question_tool.py +137 -0
  68. execution/tools/base.py +81 -0
  69. execution/tools/calculator_tool.py +137 -0
  70. execution/tools/cognos_card_tool.py +124 -0
  71. execution/tools/cron_tool.py +215 -0
  72. execution/tools/datetime_tool.py +215 -0
  73. execution/tools/describe_image_tool.py +161 -0
  74. execution/tools/draw_tool.py +164 -0
  75. execution/tools/edit_image_tool.py +262 -0
  76. execution/tools/edit_tool.py +245 -0
  77. execution/tools/file_tool.py +90 -0
  78. execution/tools/find_anywhere_tool.py +255 -0
  79. execution/tools/forge_feature_tools.py +377 -0
  80. execution/tools/glob_tool.py +59 -0
  81. execution/tools/grep_tool.py +89 -0
  82. execution/tools/http_request_tool.py +224 -0
  83. execution/tools/load_skill_tool.py +104 -0
  84. execution/tools/longcat_avatar_tool.py +384 -0
  85. execution/tools/mcp_tool.py +100 -0
  86. execution/tools/notebook_tool.py +279 -0
  87. execution/tools/openapi_tool.py +440 -0
  88. execution/tools/plan_mode_tool.py +95 -0
  89. execution/tools/push_notification_tool.py +157 -0
  90. execution/tools/python_tool.py +61 -0
  91. execution/tools/respond_tool.py +40 -0
  92. execution/tools/sandbox_tool.py +378 -0
  93. execution/tools/search_tool.py +153 -0
  94. execution/tools/semantic_search_tool.py +106 -0
  95. execution/tools/shell_tool.py +283 -0
  96. execution/tools/speak_tool.py +134 -0
  97. execution/tools/storyboard_tool.py +727 -0
  98. execution/tools/system_info_tool.py +212 -0
  99. execution/tools/task_tool.py +323 -0
  100. execution/tools/think_tool.py +49 -0
  101. execution/tools/transcribe_audio_tool.py +86 -0
  102. execution/tools/update_memory_tool.py +92 -0
  103. execution/tools/web_fetch_tool.py +82 -0
  104. execution/tools/worktree_tool.py +174 -0
  105. llm/__init__.py +0 -0
  106. llm/fallback.py +116 -0
  107. llm/models.py +320 -0
  108. llm/provider.py +1356 -0
  109. llm/router.py +373 -0
  110. main.py +1889 -0
  111. memory/__init__.py +0 -0
  112. memory/episodic.py +99 -0
  113. memory/procedural.py +145 -0
  114. memory/semantic.py +71 -0
  115. memory/working.py +64 -0
  116. nn/__init__.py +43 -0
  117. nn/auto_evolve.py +245 -0
  118. nn/caudate.py +136 -0
  119. nn/config.py +141 -0
  120. nn/consolidator.py +81 -0
  121. nn/data.py +1635 -0
  122. nn/encoder.py +258 -0
  123. nn/forge_advisor.py +303 -0
  124. nn/format.py +235 -0
  125. nn/heads.py +432 -0
  126. nn/observer.py +994 -0
  127. nn/policy.py +214 -0
  128. nn/runtime.py +343 -0
  129. nn/scorer.py +175 -0
  130. nn/trainer.py +515 -0
  131. nn/vision.py +352 -0
  132. personality/__init__.py +23 -0
  133. personality/engine.py +129 -0
  134. personality/identity.py +144 -0
  135. personality/inner_voice.py +100 -0
  136. personality/mood.py +205 -0
  137. planning/__init__.py +0 -0
  138. planning/dev_server.py +221 -0
  139. planning/forge_models.py +718 -0
  140. planning/orchestrator.py +1363 -0
  141. planning/planner.py +451 -0
  142. planning/task_graph.py +61 -0
  143. reflection/__init__.py +0 -0
  144. reflection/meta_learner.py +156 -0
  145. reflection/reflector.py +127 -0
  146. ui/__init__.py +5 -0
  147. ui/display.py +88 -0
  148. voice/__init__.py +0 -0
  149. voice/conversation.py +125 -0
  150. voice/listener.py +111 -0
  151. voice/speaker.py +59 -0
  152. voice/stt.py +126 -0
  153. voice/tts.py +214 -0
@@ -0,0 +1,89 @@
1
+ """Grep tool — regex content search across files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from core.schemas import ToolResult
10
+ from execution.tools.base import BaseTool
11
+
12
+
13
+ class GrepTool(BaseTool):
14
+ name = "Grep"
15
+ description = (
16
+ "Search for a regex pattern across files. Returns matching lines "
17
+ "with file:line:text. Supports include-glob filtering."
18
+ )
19
+
20
+ @property
21
+ def input_schema(self) -> dict:
22
+ return {
23
+ "type": "object",
24
+ "properties": {
25
+ "pattern": {
26
+ "type": "string",
27
+ "description": "Regex pattern to search for",
28
+ },
29
+ "path": {
30
+ "type": "string",
31
+ "description": "Root directory to search. Defaults to cwd.",
32
+ },
33
+ "include": {
34
+ "type": "string",
35
+ "description": "Glob to restrict files, e.g. '**/*.py'",
36
+ "default": "**/*",
37
+ },
38
+ "case_insensitive": {
39
+ "type": "boolean",
40
+ "description": "If true, ignore case. Default false.",
41
+ "default": False,
42
+ },
43
+ "limit": {
44
+ "type": "integer",
45
+ "description": "Maximum matching lines to return (default 200)",
46
+ "default": 200,
47
+ },
48
+ },
49
+ "required": ["pattern"],
50
+ }
51
+
52
+ async def execute(self, **kwargs: Any) -> ToolResult:
53
+ pattern = kwargs.get("pattern", "")
54
+ if not pattern:
55
+ return self._error("No pattern provided")
56
+
57
+ root = Path(kwargs.get("path") or ".").resolve()
58
+ include = kwargs.get("include", "**/*")
59
+ flags = re.IGNORECASE if kwargs.get("case_insensitive") else 0
60
+ limit = int(kwargs.get("limit", 200))
61
+
62
+ try:
63
+ regex = re.compile(pattern, flags)
64
+ except re.error as e:
65
+ return self._error(f"Invalid regex: {e}")
66
+
67
+ matches: list[str] = []
68
+ try:
69
+ for path in root.glob(include):
70
+ if not path.is_file():
71
+ continue
72
+ try:
73
+ # Skip obviously binary files
74
+ text = path.read_text(errors="ignore")
75
+ except Exception:
76
+ continue
77
+ for i, line in enumerate(text.splitlines(), 1):
78
+ if regex.search(line):
79
+ matches.append(f"{path}:{i}: {line.rstrip()}")
80
+ if len(matches) >= limit:
81
+ break
82
+ if len(matches) >= limit:
83
+ break
84
+ except Exception as e:
85
+ return self._error(str(e))
86
+
87
+ if not matches:
88
+ return self._success(f"No matches for /{pattern}/ in {root}/{include}")
89
+ return self._success("\n".join(matches))
@@ -0,0 +1,224 @@
1
+ """HttpRequest — generic REST tool for any HTTP API.
2
+
3
+ Inspired by the Vercel AI SDK's universal-tool pattern: instead of
4
+ writing a custom tool per service, give the LLM one well-shaped HTTP
5
+ tool and let it figure out the right URL/headers/body for whatever
6
+ API the user names.
7
+
8
+ Safety:
9
+ - Defaults timeout to 30s
10
+ - Caps response size (so a 1GB response can't OOM the agent)
11
+ - No follow-redirects to private network ranges by default (basic
12
+ SSRF guard — overridable for advanced users)
13
+ - Body size limited
14
+ - Only HTTP/HTTPS schemes accepted
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import ipaddress
20
+ import json
21
+ import logging
22
+ import re
23
+ import socket
24
+ from typing import Any
25
+ from urllib.parse import urlparse
26
+
27
+ import httpx
28
+
29
+ from core.schemas import ToolResult
30
+ from execution.tools.base import BaseTool
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+
35
+ _ALLOWED_METHODS = frozenset(("GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"))
36
+ _MAX_RESPONSE_BYTES = 5 * 1024 * 1024 # 5 MB
37
+
38
+
39
+ def _is_private_host(host: str) -> bool:
40
+ """Return True if the hostname resolves into a private network
41
+ range (RFC1918, link-local, loopback, IPv6 ULA, etc). Used as a
42
+ basic SSRF guard so the LLM can't accidentally probe localhost
43
+ services or the user's LAN."""
44
+ try:
45
+ ip = ipaddress.ip_address(host)
46
+ except ValueError:
47
+ # Not a literal IP — try DNS lookup. Multiple A records may
48
+ # come back; conservatively flag as private if ANY of them is.
49
+ try:
50
+ infos = socket.getaddrinfo(host, None)
51
+ except OSError:
52
+ return False
53
+ for info in infos:
54
+ sockaddr = info[4]
55
+ try:
56
+ if ipaddress.ip_address(sockaddr[0]).is_private:
57
+ return True
58
+ if ipaddress.ip_address(sockaddr[0]).is_loopback:
59
+ return True
60
+ if ipaddress.ip_address(sockaddr[0]).is_link_local:
61
+ return True
62
+ except ValueError:
63
+ continue
64
+ return False
65
+ return ip.is_private or ip.is_loopback or ip.is_link_local
66
+
67
+
68
+ class HttpRequestTool(BaseTool):
69
+ mutates = True
70
+ name = "HttpRequest"
71
+ description = (
72
+ "Make a generic HTTP request to any public API. Use when the "
73
+ "user asks to interact with a service that doesn't have a "
74
+ "dedicated Cognos tool — e.g. 'fetch this Stripe customer', "
75
+ "'POST to my webhook', 'get the GitHub rate limit'. Specify "
76
+ "method, URL, headers (auth tokens go here), and optional "
77
+ "JSON body. Doesn't follow links into private networks by "
78
+ "default; set `allow_private=true` ONLY if the user explicitly "
79
+ "wants to hit localhost/LAN."
80
+ )
81
+
82
+ @property
83
+ def input_schema(self) -> dict[str, Any]:
84
+ return {
85
+ "type": "object",
86
+ "properties": {
87
+ "method": {
88
+ "type": "string",
89
+ "enum": list(_ALLOWED_METHODS),
90
+ "description": "HTTP method.",
91
+ },
92
+ "url": {
93
+ "type": "string",
94
+ "description": "Full URL (must be http:// or https://).",
95
+ },
96
+ "headers": {
97
+ "type": "object",
98
+ "description": "Optional headers as a JSON object. Auth tokens go here.",
99
+ "additionalProperties": {"type": "string"},
100
+ },
101
+ "body_json": {
102
+ "type": "object",
103
+ "description": "Optional JSON request body (sent as application/json).",
104
+ },
105
+ "body_text": {
106
+ "type": "string",
107
+ "description": "Optional raw text request body. Use body_json or body_text, not both.",
108
+ },
109
+ "timeout_s": {
110
+ "type": "number",
111
+ "description": "Timeout in seconds (default 30, max 120).",
112
+ },
113
+ "allow_private": {
114
+ "type": "boolean",
115
+ "description": "Set true only when explicitly hitting localhost/LAN endpoints.",
116
+ "default": False,
117
+ },
118
+ },
119
+ "required": ["method", "url"],
120
+ }
121
+
122
+ async def execute(self, **kwargs: Any) -> ToolResult:
123
+ method = (kwargs.get("method") or "").upper().strip()
124
+ url = (kwargs.get("url") or "").strip()
125
+ headers = kwargs.get("headers") or {}
126
+ body_json = kwargs.get("body_json")
127
+ body_text = kwargs.get("body_text")
128
+ timeout_s = float(kwargs.get("timeout_s") or 30.0)
129
+ allow_private = bool(kwargs.get("allow_private") or False)
130
+
131
+ # Validate
132
+ if method not in _ALLOWED_METHODS:
133
+ return ToolResult(
134
+ tool_name=self.name, status="error",
135
+ error=f"unsupported method {method!r}",
136
+ )
137
+ timeout_s = max(1.0, min(120.0, timeout_s))
138
+ parsed = urlparse(url)
139
+ if parsed.scheme not in ("http", "https"):
140
+ return ToolResult(
141
+ tool_name=self.name, status="error",
142
+ error=f"only http/https schemes allowed, got {parsed.scheme!r}",
143
+ )
144
+ if not parsed.hostname:
145
+ return ToolResult(
146
+ tool_name=self.name, status="error",
147
+ error="missing hostname in URL",
148
+ )
149
+ if not allow_private and _is_private_host(parsed.hostname):
150
+ return ToolResult(
151
+ tool_name=self.name, status="error",
152
+ error=(f"refusing to call private host {parsed.hostname!r}; "
153
+ f"set allow_private=true if you really mean it"),
154
+ )
155
+ if body_json is not None and body_text is not None:
156
+ return ToolResult(
157
+ tool_name=self.name, status="error",
158
+ error="provide body_json OR body_text, not both",
159
+ )
160
+
161
+ # Send
162
+ request_kwargs: dict[str, Any] = {
163
+ "method": method,
164
+ "url": url,
165
+ "headers": dict(headers) if headers else None,
166
+ "timeout": timeout_s,
167
+ }
168
+ if body_json is not None:
169
+ request_kwargs["json"] = body_json
170
+ elif body_text is not None:
171
+ request_kwargs["content"] = body_text
172
+
173
+ try:
174
+ async with httpx.AsyncClient(follow_redirects=True) as client:
175
+ resp = await client.request(**request_kwargs)
176
+ except httpx.TimeoutException:
177
+ return ToolResult(
178
+ tool_name=self.name, status="error",
179
+ error=f"timed out after {timeout_s}s",
180
+ )
181
+ except httpx.RequestError as e:
182
+ return ToolResult(
183
+ tool_name=self.name, status="error",
184
+ error=f"network error: {e}",
185
+ )
186
+
187
+ # Read body, capped
188
+ body_bytes = resp.content[:_MAX_RESPONSE_BYTES]
189
+ truncated = len(resp.content) > _MAX_RESPONSE_BYTES
190
+ ctype = resp.headers.get("content-type", "")
191
+
192
+ # Decode best-effort
193
+ body_repr: Any
194
+ if "json" in ctype:
195
+ try:
196
+ body_repr = json.loads(body_bytes.decode("utf-8", errors="replace"))
197
+ body_str = json.dumps(body_repr, indent=2)[:4000]
198
+ except Exception:
199
+ body_str = body_bytes.decode("utf-8", errors="replace")[:4000]
200
+ body_repr = body_str
201
+ elif ctype.startswith("text/") or "html" in ctype or "xml" in ctype:
202
+ body_str = body_bytes.decode("utf-8", errors="replace")[:4000]
203
+ body_repr = body_str
204
+ else:
205
+ body_str = f"<binary {ctype} {len(body_bytes)}B>"
206
+ body_repr = None
207
+
208
+ out = (
209
+ f"{method} {url}\n"
210
+ f" status: {resp.status_code} {resp.reason_phrase}\n"
211
+ f" type: {ctype or '?'}\n"
212
+ f" bytes: {len(resp.content)}{' (truncated)' if truncated else ''}\n"
213
+ f"\n--- body ---\n{body_str}"
214
+ )
215
+ return ToolResult(
216
+ tool_name=self.name, status="success",
217
+ output=out,
218
+ metadata={
219
+ "status_code": resp.status_code,
220
+ "headers": dict(resp.headers),
221
+ "body": body_repr,
222
+ "url": str(resp.url),
223
+ },
224
+ )
@@ -0,0 +1,104 @@
1
+ """LoadSkill — fetch the full body of an installed skill.
2
+
3
+ Skills are listed in the system prompt as a TOC (just name +
4
+ description) to keep prompt size bounded. When the LLM decides a
5
+ skill is relevant for the current turn, it calls this tool to get
6
+ the full markdown instructions for that skill.
7
+
8
+ Companion to `core/skills.py`. Reload via `RescanSkills` after the
9
+ user runs `npx skills add ...`.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ from typing import Any
16
+
17
+ from core.schemas import ToolResult
18
+ from core.skills import get_skills, reload_skills
19
+ from execution.tools.base import BaseTool
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ class LoadSkillTool(BaseTool):
25
+ name = "LoadSkill"
26
+ description = (
27
+ "Fetch the full instructions for an installed skill. The system "
28
+ "prompt lists skills as a table of contents (name + one-line "
29
+ "description); call this tool to read the actual markdown body "
30
+ "of a skill that looks relevant to the current turn. Use the "
31
+ "exact `name` from the TOC."
32
+ )
33
+
34
+ @property
35
+ def input_schema(self) -> dict[str, Any]:
36
+ return {
37
+ "type": "object",
38
+ "properties": {
39
+ "name": {
40
+ "type": "string",
41
+ "description": "Skill name (exact, from the TOC).",
42
+ },
43
+ },
44
+ "required": ["name"],
45
+ }
46
+
47
+ async def execute(self, **kwargs: Any) -> ToolResult:
48
+ name = (kwargs.get("name") or "").strip()
49
+ if not name:
50
+ return ToolResult(
51
+ tool_name=self.name, status="error",
52
+ error="`name` is required",
53
+ )
54
+ registry = get_skills()
55
+ skill = registry.get(name)
56
+ if skill is None:
57
+ close = [n for n in registry.list_names()
58
+ if name.lower() in n.lower() or n.lower() in name.lower()]
59
+ hint = (f" did you mean: {close[:5]}?" if close else
60
+ f" available: {registry.list_names()[:10]}")
61
+ return ToolResult(
62
+ tool_name=self.name, status="error",
63
+ error=f"skill {name!r} not found.{hint}",
64
+ )
65
+ return ToolResult(
66
+ tool_name=self.name, status="success",
67
+ output=(
68
+ f"# {skill.name}\n"
69
+ f"{skill.description}\n"
70
+ f"\n---\n\n{skill.body}"
71
+ ),
72
+ metadata={
73
+ "name": skill.name,
74
+ "description": skill.description,
75
+ "path": str(skill.path),
76
+ "source_dir": str(skill.source_dir),
77
+ },
78
+ )
79
+
80
+
81
+ class RescanSkillsTool(BaseTool):
82
+ name = "RescanSkills"
83
+ description = (
84
+ "Re-scan the skill directories after running `npx skills add` "
85
+ "or manually adding/removing a skill. Returns the new count + "
86
+ "list of names."
87
+ )
88
+
89
+ @property
90
+ def input_schema(self) -> dict[str, Any]:
91
+ return {"type": "object", "properties": {}, "required": []}
92
+
93
+ async def execute(self, **_: Any) -> ToolResult:
94
+ registry = reload_skills()
95
+ names = registry.list_names()
96
+ return ToolResult(
97
+ tool_name=self.name, status="success",
98
+ output=(
99
+ f"Rescanned. Found {len(names)} skill(s):\n"
100
+ + "\n".join(f" - {n}" for n in names)
101
+ if names else "Rescanned. No skills found."
102
+ ),
103
+ metadata={"count": len(names), "names": names},
104
+ )