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,279 @@
1
+ """NotebookEdit — surgical edits to Jupyter `.ipynb` notebook cells.
2
+
3
+ `.ipynb` files are JSON; you *can* edit them with `Edit` if you carefully
4
+ match the surrounding JSON, but cell `source` is stored as a list of
5
+ strings with embedded escapes — it's brittle and easy to corrupt. This
6
+ tool parses the notebook properly, performs the requested cell-level
7
+ operation, and writes valid notebook JSON back out.
8
+
9
+ Modes (`mode` argument):
10
+ - `replace` (default) — replace the entire source of one cell.
11
+ - `insert` — insert a new cell at `cell_index` (existing
12
+ cells shift down). Existing default if no
13
+ cell_index → append at the end.
14
+ - `delete` — delete the cell at `cell_index`.
15
+
16
+ Cell types: `code` (default), `markdown`, `raw`. New cells default to
17
+ `code` unless `cell_type` is set.
18
+
19
+ Identifies the cell by either:
20
+ - `cell_index` (0-based integer), or
21
+ - `cell_id` (notebook v4.5+ stable id like `abc123`).
22
+
23
+ Outputs (cell `outputs` and `execution_count`) are reset on edits to
24
+ `code` cells so the file looks "fresh" after editing — same convention
25
+ Claude Code's NotebookEdit uses.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import json
31
+ import logging
32
+ from pathlib import Path
33
+ from typing import Any
34
+
35
+ from core.schemas import ToolResult
36
+ from execution.tools.base import BaseTool
37
+
38
+ logger = logging.getLogger(__name__)
39
+
40
+
41
+ _VALID_MODES = {"replace", "insert", "delete"}
42
+ _VALID_CELL_TYPES = {"code", "markdown", "raw"}
43
+
44
+
45
+ def _normalize_source(text: str) -> list[str]:
46
+ """Notebook spec wants `source` as a list of lines (each line ends
47
+ in `\\n` except possibly the last). Most notebooks store it that
48
+ way; some store a single string. We always emit the list form."""
49
+ if not text:
50
+ return []
51
+ # Preserve final newline behavior of the original text.
52
+ lines = text.splitlines(keepends=True)
53
+ return lines
54
+
55
+
56
+ class NotebookEditTool(BaseTool):
57
+ mutates = True
58
+ name = "NotebookEdit"
59
+ description = (
60
+ "Edit Jupyter `.ipynb` notebook cells in place. Modes: "
61
+ "`replace` an existing cell's source, `insert` a new cell, or "
62
+ "`delete` a cell. Identify the target by `cell_index` (0-based) "
63
+ "or `cell_id` (v4.5+). New cells default to `code`; pass "
64
+ "`cell_type` for `markdown` / `raw`. Code-cell outputs are "
65
+ "reset on edit. Use this instead of the generic `Edit` tool — "
66
+ "the JSON-with-escaped-strings shape is too brittle for "
67
+ "string replacement."
68
+ )
69
+
70
+ @property
71
+ def input_schema(self) -> dict[str, Any]:
72
+ return {
73
+ "type": "object",
74
+ "properties": {
75
+ "path": {
76
+ "type": "string",
77
+ "description": "Absolute or relative path to the .ipynb file.",
78
+ },
79
+ "mode": {
80
+ "type": "string",
81
+ "enum": list(_VALID_MODES),
82
+ "description": "replace (default) | insert | delete",
83
+ "default": "replace",
84
+ },
85
+ "cell_index": {
86
+ "type": "integer",
87
+ "description": (
88
+ "0-based cell index. For `insert`, this is "
89
+ "where the new cell goes; omit to append at the "
90
+ "end. For `replace`/`delete`, this identifies "
91
+ "the target cell (or use `cell_id`)."
92
+ ),
93
+ },
94
+ "cell_id": {
95
+ "type": "string",
96
+ "description": (
97
+ "Stable cell ID (notebook v4.5+). Alternative "
98
+ "to `cell_index` for `replace`/`delete`."
99
+ ),
100
+ },
101
+ "source": {
102
+ "type": "string",
103
+ "description": (
104
+ "New cell content (required for `replace` and "
105
+ "`insert`). Pass the raw text — newlines are "
106
+ "handled automatically."
107
+ ),
108
+ },
109
+ "cell_type": {
110
+ "type": "string",
111
+ "enum": list(_VALID_CELL_TYPES),
112
+ "description": "code (default) | markdown | raw. Used for `insert`.",
113
+ "default": "code",
114
+ },
115
+ },
116
+ "required": ["path"],
117
+ }
118
+
119
+ async def execute(self, **kwargs: Any) -> ToolResult:
120
+ path_str = (kwargs.get("path") or "").strip()
121
+ mode = (kwargs.get("mode") or "replace").lower()
122
+ cell_index = kwargs.get("cell_index")
123
+ cell_id = kwargs.get("cell_id")
124
+ source = kwargs.get("source")
125
+ cell_type = (kwargs.get("cell_type") or "code").lower()
126
+
127
+ # ---- validation ----
128
+ if not path_str:
129
+ return ToolResult(
130
+ tool_name=self.name, status="error",
131
+ error="`path` is required",
132
+ )
133
+ if mode not in _VALID_MODES:
134
+ return ToolResult(
135
+ tool_name=self.name, status="error",
136
+ error=f"unknown mode {mode!r}; use one of {sorted(_VALID_MODES)}",
137
+ )
138
+ if cell_type not in _VALID_CELL_TYPES:
139
+ return ToolResult(
140
+ tool_name=self.name, status="error",
141
+ error=f"unknown cell_type {cell_type!r}; use one of {sorted(_VALID_CELL_TYPES)}",
142
+ )
143
+ if mode in ("replace", "insert") and source is None:
144
+ return ToolResult(
145
+ tool_name=self.name, status="error",
146
+ error=f"`source` is required for mode={mode!r}",
147
+ )
148
+
149
+ path = Path(path_str).expanduser().resolve()
150
+ if not path.exists() or not path.is_file():
151
+ return ToolResult(
152
+ tool_name=self.name, status="error",
153
+ error=f"notebook not found: {path}",
154
+ )
155
+ if path.suffix != ".ipynb":
156
+ return ToolResult(
157
+ tool_name=self.name, status="error",
158
+ error=f"not a .ipynb file: {path.suffix}",
159
+ )
160
+
161
+ # ---- load ----
162
+ try:
163
+ nb = json.loads(path.read_text(encoding="utf-8"))
164
+ except (OSError, json.JSONDecodeError) as e:
165
+ return ToolResult(
166
+ tool_name=self.name, status="error",
167
+ error=f"failed to parse notebook: {e}",
168
+ )
169
+
170
+ cells = nb.get("cells")
171
+ if not isinstance(cells, list):
172
+ return ToolResult(
173
+ tool_name=self.name, status="error",
174
+ error="notebook has no `cells` array — corrupted file?",
175
+ )
176
+
177
+ # ---- resolve target index ----
178
+ target_idx: int | None = None
179
+ if cell_id is not None and cell_id != "":
180
+ for i, c in enumerate(cells):
181
+ if isinstance(c, dict) and c.get("id") == cell_id:
182
+ target_idx = i
183
+ break
184
+ if target_idx is None and mode != "insert":
185
+ return ToolResult(
186
+ tool_name=self.name, status="error",
187
+ error=f"no cell with id {cell_id!r} (notebook has {len(cells)} cells)",
188
+ )
189
+ elif cell_index is not None:
190
+ try:
191
+ target_idx = int(cell_index)
192
+ except (TypeError, ValueError):
193
+ return ToolResult(
194
+ tool_name=self.name, status="error",
195
+ error=f"cell_index must be an integer, got {cell_index!r}",
196
+ )
197
+ if mode != "insert":
198
+ if not (0 <= target_idx < len(cells)):
199
+ return ToolResult(
200
+ tool_name=self.name, status="error",
201
+ error=(f"cell_index {target_idx} out of range "
202
+ f"(notebook has {len(cells)} cells)"),
203
+ )
204
+
205
+ # ---- apply ----
206
+ action_summary = ""
207
+ if mode == "replace":
208
+ if target_idx is None:
209
+ return ToolResult(
210
+ tool_name=self.name, status="error",
211
+ error="replace mode needs `cell_index` or `cell_id`",
212
+ )
213
+ cell = cells[target_idx]
214
+ if not isinstance(cell, dict):
215
+ return ToolResult(
216
+ tool_name=self.name, status="error",
217
+ error=f"cell at index {target_idx} is not an object",
218
+ )
219
+ cell["source"] = _normalize_source(source)
220
+ # Reset transient state on code cells so the file looks
221
+ # fresh after edit. Mirrors Claude Code's NotebookEdit.
222
+ if cell.get("cell_type") == "code":
223
+ cell["outputs"] = []
224
+ cell["execution_count"] = None
225
+ action_summary = (
226
+ f"replaced cell {target_idx} "
227
+ f"({cell.get('cell_type', '?')}, {len(source)} chars)"
228
+ )
229
+
230
+ elif mode == "delete":
231
+ if target_idx is None:
232
+ return ToolResult(
233
+ tool_name=self.name, status="error",
234
+ error="delete mode needs `cell_index` or `cell_id`",
235
+ )
236
+ removed = cells.pop(target_idx)
237
+ action_summary = (
238
+ f"deleted cell {target_idx} "
239
+ f"({removed.get('cell_type', '?')})"
240
+ )
241
+
242
+ elif mode == "insert":
243
+ new_cell: dict[str, Any] = {
244
+ "cell_type": cell_type,
245
+ "metadata": {},
246
+ "source": _normalize_source(source),
247
+ }
248
+ if cell_type == "code":
249
+ new_cell["outputs"] = []
250
+ new_cell["execution_count"] = None
251
+ insert_at = target_idx if target_idx is not None else len(cells)
252
+ insert_at = max(0, min(insert_at, len(cells)))
253
+ cells.insert(insert_at, new_cell)
254
+ action_summary = (
255
+ f"inserted {cell_type} cell at index {insert_at} "
256
+ f"({len(source)} chars)"
257
+ )
258
+
259
+ # ---- write back ----
260
+ try:
261
+ path.write_text(
262
+ json.dumps(nb, indent=1, ensure_ascii=False) + "\n",
263
+ encoding="utf-8",
264
+ )
265
+ except OSError as e:
266
+ return ToolResult(
267
+ tool_name=self.name, status="error",
268
+ error=f"failed to write notebook: {e}",
269
+ )
270
+
271
+ return ToolResult(
272
+ tool_name=self.name, status="success",
273
+ output=f"{path}: {action_summary} (notebook now has {len(cells)} cells)",
274
+ metadata={
275
+ "path": str(path),
276
+ "mode": mode,
277
+ "cell_count": len(cells),
278
+ },
279
+ )