tsugite-cli 0.3.3__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 (101) hide show
  1. tsugite/__init__.py +6 -0
  2. tsugite/agent_composition.py +163 -0
  3. tsugite/agent_inheritance.py +479 -0
  4. tsugite/agent_preparation.py +236 -0
  5. tsugite/agent_runner/__init__.py +45 -0
  6. tsugite/agent_runner/helpers.py +106 -0
  7. tsugite/agent_runner/history_integration.py +248 -0
  8. tsugite/agent_runner/metrics.py +100 -0
  9. tsugite/agent_runner/runner.py +1879 -0
  10. tsugite/agent_runner/validation.py +70 -0
  11. tsugite/agent_utils.py +167 -0
  12. tsugite/attachments/__init__.py +65 -0
  13. tsugite/attachments/auto_context.py +199 -0
  14. tsugite/attachments/base.py +34 -0
  15. tsugite/attachments/file.py +51 -0
  16. tsugite/attachments/inline.py +31 -0
  17. tsugite/attachments/storage.py +178 -0
  18. tsugite/attachments/url.py +59 -0
  19. tsugite/attachments/youtube.py +101 -0
  20. tsugite/benchmark/__init__.py +62 -0
  21. tsugite/benchmark/config.py +183 -0
  22. tsugite/benchmark/core.py +292 -0
  23. tsugite/benchmark/discovery.py +377 -0
  24. tsugite/benchmark/evaluators.py +671 -0
  25. tsugite/benchmark/execution.py +657 -0
  26. tsugite/benchmark/metrics.py +204 -0
  27. tsugite/benchmark/reports.py +420 -0
  28. tsugite/benchmark/utils.py +288 -0
  29. tsugite/builtin_agents/chat-assistant.md +53 -0
  30. tsugite/builtin_agents/default.md +140 -0
  31. tsugite/builtin_agents.py +5 -0
  32. tsugite/cache.py +195 -0
  33. tsugite/cli/__init__.py +1042 -0
  34. tsugite/cli/agents.py +148 -0
  35. tsugite/cli/attachments.py +193 -0
  36. tsugite/cli/benchmark.py +663 -0
  37. tsugite/cli/cache.py +113 -0
  38. tsugite/cli/config.py +272 -0
  39. tsugite/cli/helpers.py +534 -0
  40. tsugite/cli/history.py +193 -0
  41. tsugite/cli/init.py +387 -0
  42. tsugite/cli/mcp.py +193 -0
  43. tsugite/cli/tools.py +419 -0
  44. tsugite/config.py +204 -0
  45. tsugite/console.py +48 -0
  46. tsugite/constants.py +21 -0
  47. tsugite/core/__init__.py +19 -0
  48. tsugite/core/agent.py +774 -0
  49. tsugite/core/executor.py +300 -0
  50. tsugite/core/memory.py +67 -0
  51. tsugite/core/tools.py +271 -0
  52. tsugite/docker_cli.py +270 -0
  53. tsugite/events/__init__.py +55 -0
  54. tsugite/events/base.py +46 -0
  55. tsugite/events/bus.py +62 -0
  56. tsugite/events/events.py +224 -0
  57. tsugite/exceptions.py +40 -0
  58. tsugite/history/__init__.py +29 -0
  59. tsugite/history/index.py +210 -0
  60. tsugite/history/models.py +106 -0
  61. tsugite/history/storage.py +157 -0
  62. tsugite/mcp_client.py +219 -0
  63. tsugite/mcp_config.py +174 -0
  64. tsugite/md_agents.py +751 -0
  65. tsugite/models.py +257 -0
  66. tsugite/renderer.py +151 -0
  67. tsugite/shell_tool_config.py +265 -0
  68. tsugite/templates/assistant.md +14 -0
  69. tsugite/tools/__init__.py +265 -0
  70. tsugite/tools/agents.py +312 -0
  71. tsugite/tools/edit_strategies.py +393 -0
  72. tsugite/tools/fs.py +329 -0
  73. tsugite/tools/http.py +239 -0
  74. tsugite/tools/interactive.py +430 -0
  75. tsugite/tools/shell.py +129 -0
  76. tsugite/tools/shell_tools.py +214 -0
  77. tsugite/tools/tasks.py +339 -0
  78. tsugite/tsugite.py +7 -0
  79. tsugite/ui/__init__.py +46 -0
  80. tsugite/ui/base.py +638 -0
  81. tsugite/ui/chat.py +265 -0
  82. tsugite/ui/chat.tcss +92 -0
  83. tsugite/ui/chat_history.py +286 -0
  84. tsugite/ui/helpers.py +102 -0
  85. tsugite/ui/jsonl.py +125 -0
  86. tsugite/ui/live_template.py +529 -0
  87. tsugite/ui/plain.py +419 -0
  88. tsugite/ui/textual_chat.py +642 -0
  89. tsugite/ui/textual_handler.py +225 -0
  90. tsugite/ui/widgets/__init__.py +6 -0
  91. tsugite/ui/widgets/base_scroll_log.py +27 -0
  92. tsugite/ui/widgets/message_list.py +121 -0
  93. tsugite/ui/widgets/thought_log.py +80 -0
  94. tsugite/ui_context.py +90 -0
  95. tsugite/utils.py +367 -0
  96. tsugite/xdg.py +104 -0
  97. tsugite_cli-0.3.3.dist-info/METADATA +325 -0
  98. tsugite_cli-0.3.3.dist-info/RECORD +101 -0
  99. tsugite_cli-0.3.3.dist-info/WHEEL +4 -0
  100. tsugite_cli-0.3.3.dist-info/entry_points.txt +5 -0
  101. tsugite_cli-0.3.3.dist-info/licenses/LICENSE +235 -0
@@ -0,0 +1,70 @@
1
+ """Agent validation and information utilities."""
2
+
3
+ from pathlib import Path
4
+ from typing import Any, Dict
5
+
6
+ from tsugite.md_agents import parse_agent_file, validate_agent_execution
7
+
8
+
9
+ def validate_agent_file(agent_path: Path) -> tuple[bool, str]:
10
+ """Validate that an agent file can be executed.
11
+
12
+ Args:
13
+ agent_path: Path to agent markdown file (or builtin agent path like <builtin-default>)
14
+
15
+ Returns:
16
+ Tuple of (is_valid, error_message)
17
+ """
18
+ try:
19
+ # Parse agent with inheritance resolution
20
+ agent = parse_agent_file(agent_path)
21
+
22
+ # Use centralized validation
23
+ return validate_agent_execution(agent)
24
+
25
+ except Exception as e:
26
+ return False, f"Agent file validation failed: {e}"
27
+
28
+
29
+ def get_agent_info(agent_path: Path) -> Dict[str, Any]:
30
+ """Get information about an agent without executing it.
31
+
32
+ Args:
33
+ agent_path: Path to agent markdown file (or builtin agent path like <builtin-default>)
34
+
35
+ Returns:
36
+ Dictionary with agent information
37
+ """
38
+ try:
39
+ # Parse agent with inheritance resolution
40
+ agent = parse_agent_file(agent_path)
41
+ agent_config = agent.config
42
+
43
+ model_display = agent_config.model
44
+ if not model_display:
45
+ from tsugite.config import load_config
46
+
47
+ config = load_config()
48
+ if config.default_model:
49
+ model_display = f"{config.default_model} (default)"
50
+ else:
51
+ model_display = "not set"
52
+
53
+ return {
54
+ "name": agent_config.name,
55
+ "description": getattr(agent_config, "description", "No description"),
56
+ "model": model_display,
57
+ "max_turns": agent_config.max_turns,
58
+ "tools": agent_config.tools,
59
+ "prefetch_count": (len(agent_config.prefetch) if agent_config.prefetch else 0),
60
+ "attachments": agent_config.attachments,
61
+ "auto_context": getattr(agent_config, "auto_context", None),
62
+ "permissions_profile": getattr(agent_config, "permissions_profile", None),
63
+ "valid": validate_agent_file(agent_path)[0],
64
+ "instructions": getattr(agent_config, "instructions", ""),
65
+ }
66
+ except Exception as e:
67
+ return {
68
+ "error": str(e),
69
+ "valid": False,
70
+ }
tsugite/agent_utils.py ADDED
@@ -0,0 +1,167 @@
1
+ """Utility functions for agent management."""
2
+
3
+ from pathlib import Path
4
+ from typing import List, Tuple
5
+
6
+
7
+ def _parse_agent_from_path(path: Path):
8
+ """Parse an agent from a file path.
9
+
10
+ Args:
11
+ path: Path to agent file
12
+
13
+ Returns:
14
+ Parsed Agent object
15
+ """
16
+ from tsugite.md_agents import parse_agent
17
+
18
+ # All agents are now file-based (including built-ins)
19
+ content = path.read_text(encoding="utf-8")
20
+ return parse_agent(content, path)
21
+
22
+
23
+ def _is_valid_agent_file(path: Path) -> bool:
24
+ """Check if a file is a valid agent file.
25
+
26
+ Args:
27
+ path: Path to check
28
+
29
+ Returns:
30
+ True if file is a valid agent with a name field
31
+ """
32
+ try:
33
+ agent = _parse_agent_from_path(path)
34
+ return bool(agent.config.name)
35
+ except Exception:
36
+ return False
37
+
38
+
39
+ def build_inheritance_chain(agent_path: Path) -> List[Tuple[str, Path]]:
40
+ """Build the inheritance chain for an agent.
41
+
42
+ Args:
43
+ agent_path: Path to the agent file
44
+
45
+ Returns:
46
+ List of (agent_name, agent_path) tuples in inheritance order (parent to child)
47
+ """
48
+ from tsugite.agent_inheritance import _get_default_base_agent_name, find_agent_file
49
+
50
+ chain = []
51
+ visited = set()
52
+
53
+ current_path = agent_path.resolve()
54
+ visited.add(current_path)
55
+
56
+ current_agent = _parse_agent_from_path(current_path)
57
+
58
+ if current_agent.config.extends and current_agent.config.extends != "none":
59
+ extends_chain = _get_parent_chain(current_agent.config.extends, current_path, visited.copy())
60
+ chain.extend(extends_chain)
61
+ elif current_agent.config.extends != "none":
62
+ # Auto-inherit from default base agent
63
+ # Priority: 1) Local .tsugite/default.md, 2) Config's default_base_agent
64
+ user_default_path = current_path.parent / ".tsugite" / "default.md"
65
+ default_path = None
66
+ default_name = None
67
+
68
+ if user_default_path.exists() and user_default_path.resolve() != current_path:
69
+ # Use local default.md if it exists
70
+ default_path = user_default_path
71
+ default_name = "default"
72
+ else:
73
+ # Fall back to config's default_base_agent
74
+ default_base_name = _get_default_base_agent_name()
75
+ if default_base_name:
76
+ default_path = find_agent_file(default_base_name, current_path.parent)
77
+ default_name = default_base_name
78
+
79
+ if default_path and default_path.resolve() != current_path:
80
+ chain.append((default_name, default_path))
81
+ visited.add(default_path.resolve())
82
+
83
+ default_agent = _parse_agent_from_path(default_path)
84
+ if default_agent.config.extends and default_agent.config.extends != "none":
85
+ parent_chain = _get_parent_chain(default_agent.config.extends, default_path, visited.copy())
86
+ # Insert at beginning (parents come before children)
87
+ chain = parent_chain + chain
88
+
89
+ chain.append((current_agent.config.name, current_path))
90
+
91
+ return chain
92
+
93
+
94
+ def _get_parent_chain(extends_ref: str, current_path: Path, visited: set) -> List[Tuple[str, Path]]:
95
+ """Recursively get parent chain.
96
+
97
+ Args:
98
+ extends_ref: Reference to parent agent
99
+ current_path: Current agent path
100
+ visited: Set of already visited paths (for cycle detection)
101
+
102
+ Returns:
103
+ List of (agent_name, agent_path) tuples
104
+ """
105
+ from tsugite.agent_inheritance import find_agent_file
106
+
107
+ chain = []
108
+
109
+ parent_path = find_agent_file(extends_ref, current_path.parent)
110
+ if not parent_path:
111
+ return chain
112
+
113
+ parent_resolved = parent_path.resolve()
114
+ if parent_resolved in visited:
115
+ return chain
116
+
117
+ visited.add(parent_resolved)
118
+
119
+ parent_agent = _parse_agent_from_path(parent_path)
120
+
121
+ if parent_agent.config.extends and parent_agent.config.extends != "none":
122
+ grandparent_chain = _get_parent_chain(parent_agent.config.extends, parent_path, visited.copy())
123
+ chain.extend(grandparent_chain)
124
+
125
+ chain.append((parent_agent.config.name, parent_path))
126
+
127
+ return chain
128
+
129
+
130
+ def list_local_agents(base_path: Path = None) -> dict[str, List[Path]]:
131
+ """List agents in local directories.
132
+
133
+ Args:
134
+ base_path: Base directory to search from (defaults to cwd)
135
+
136
+ Returns:
137
+ Dictionary mapping location names to list of agent paths
138
+ """
139
+ from .agent_inheritance import get_builtin_agents_path
140
+
141
+ if base_path is None:
142
+ base_path = Path.cwd()
143
+
144
+ results = {}
145
+
146
+ # Add built-in agents first
147
+ builtin_path = get_builtin_agents_path()
148
+ if builtin_path.exists() and builtin_path.is_dir():
149
+ builtin_agents = sorted(builtin_path.glob("*.md"))
150
+ if builtin_agents:
151
+ results["Built-in"] = builtin_agents
152
+
153
+ locations = [
154
+ ("Current directory", base_path),
155
+ (".tsugite/", base_path / ".tsugite"),
156
+ ("agents/", base_path / "agents"),
157
+ ]
158
+
159
+ for location_name, location_path in locations:
160
+ if location_path.exists() and location_path.is_dir():
161
+ all_md_files = sorted(location_path.glob("*.md"))
162
+ agent_files = [f for f in all_md_files if _is_valid_agent_file(f)]
163
+
164
+ if agent_files:
165
+ results[location_name] = agent_files
166
+
167
+ return results
@@ -0,0 +1,65 @@
1
+ """Attachment handler system for different content sources."""
2
+
3
+ from typing import List
4
+
5
+ from tsugite.attachments.auto_context import AutoContextHandler
6
+ from tsugite.attachments.base import AttachmentHandler
7
+ from tsugite.attachments.file import FileHandler
8
+ from tsugite.attachments.inline import InlineHandler
9
+ from tsugite.attachments.storage import (
10
+ add_attachment,
11
+ get_attachment,
12
+ get_attachments_path,
13
+ list_attachments,
14
+ remove_attachment,
15
+ search_attachments,
16
+ )
17
+ from tsugite.attachments.url import GenericURLHandler
18
+ from tsugite.attachments.youtube import YouTubeHandler
19
+
20
+ __all__ = [
21
+ # Handlers
22
+ "AttachmentHandler",
23
+ "InlineHandler",
24
+ "FileHandler",
25
+ "YouTubeHandler",
26
+ "GenericURLHandler",
27
+ "AutoContextHandler",
28
+ "HANDLERS",
29
+ "get_handler",
30
+ # Storage functions
31
+ "add_attachment",
32
+ "get_attachment",
33
+ "get_attachments_path",
34
+ "list_attachments",
35
+ "remove_attachment",
36
+ "search_attachments",
37
+ ]
38
+
39
+ # Handler registry - order matters! More specific handlers first
40
+ HANDLERS: List[AttachmentHandler] = [
41
+ InlineHandler(),
42
+ AutoContextHandler(), # Before FileHandler (might match file paths)
43
+ YouTubeHandler(), # Before GenericURLHandler
44
+ FileHandler(),
45
+ GenericURLHandler(), # Catch-all for URLs
46
+ ]
47
+
48
+
49
+ def get_handler(source: str) -> AttachmentHandler:
50
+ """Get appropriate handler for a source.
51
+
52
+ Args:
53
+ source: Source string
54
+
55
+ Returns:
56
+ Handler that can process this source
57
+
58
+ Raises:
59
+ ValueError: If no handler found
60
+ """
61
+ for handler in HANDLERS:
62
+ if handler.can_handle(source):
63
+ return handler
64
+
65
+ raise ValueError(f"No handler found for source: {source}")
@@ -0,0 +1,199 @@
1
+ """Auto-context handler for discovering project context files."""
2
+
3
+ import subprocess
4
+ from pathlib import Path
5
+ from typing import List, Optional
6
+
7
+ from tsugite.attachments.base import AttachmentHandler
8
+
9
+
10
+ class AutoContextHandler(AttachmentHandler):
11
+ """Handler for auto-discovering context files.
12
+
13
+ Searches from current directory up to git root for specific context files
14
+ like CONTEXT.md, AGENTS.md, CLAUDE.md and concatenates them.
15
+ """
16
+
17
+ def __init__(self, context_files: Optional[List[str]] = None):
18
+ """Initialize handler.
19
+
20
+ Args:
21
+ context_files: List of filenames to search for.
22
+ If None, will use config at fetch time.
23
+ """
24
+ self.context_files = context_files
25
+
26
+ def can_handle(self, source: str) -> bool:
27
+ """Check if source is auto-context marker.
28
+
29
+ Args:
30
+ source: Source string
31
+
32
+ Returns:
33
+ True if source is "auto-context" or starts with "auto:"
34
+ """
35
+ return source in ("auto-context", "auto") or source.startswith("auto:")
36
+
37
+ def fetch(self, source: str) -> str:
38
+ """Not supported. Use fetch_multiple() instead.
39
+
40
+ Args:
41
+ source: Source string
42
+
43
+ Raises:
44
+ NotImplementedError: This method is not supported
45
+ """
46
+ raise NotImplementedError("AutoContextHandler.fetch() is not supported. Use fetch_multiple() instead.")
47
+
48
+ def fetch_multiple(self, source: str) -> List[tuple[str, str]]:
49
+ """Discover context files and return as separate attachments.
50
+
51
+ Args:
52
+ source: Auto-context marker
53
+
54
+ Returns:
55
+ List of (attachment_name, content) tuples, one per discovered file
56
+
57
+ Raises:
58
+ ValueError: If discovery fails
59
+ """
60
+ try:
61
+ found_files = self._discover_context_files()
62
+
63
+ result = []
64
+ for file_path, relative_name in found_files:
65
+ try:
66
+ content = file_path.read_text(encoding="utf-8")
67
+ # Use the relative name as the attachment name
68
+ result.append((relative_name, content))
69
+ except Exception as e:
70
+ # Add error as attachment content for visibility
71
+ error_content = f"# Error reading {relative_name}\n\n{str(e)}"
72
+ result.append((relative_name, error_content))
73
+
74
+ return result
75
+ except Exception as e:
76
+ raise ValueError(f"Failed to fetch auto-context: {e}")
77
+
78
+ def _discover_context_files(self) -> List[tuple[Path, str]]:
79
+ """Discover all context files from project and global locations.
80
+
81
+ Returns:
82
+ List of (file_path, display_name) tuples for found files
83
+ """
84
+ # Load context files from config if not set at init
85
+ context_files = self.context_files
86
+ include_global = True
87
+ if context_files is None:
88
+ from tsugite.config import load_config
89
+
90
+ config = load_config()
91
+ context_files = config.auto_context_files
92
+ include_global = config.auto_context_include_global
93
+
94
+ cwd = Path.cwd()
95
+ git_root = self._find_git_root(cwd)
96
+
97
+ # Search from cwd up to git root (or just cwd if not in git repo)
98
+ search_dirs = self._get_search_directories(cwd, git_root)
99
+
100
+ # Find all project context files
101
+ found_files = self._discover_files(search_dirs, context_files)
102
+
103
+ # Add global context file if enabled
104
+ if include_global:
105
+ global_file = self._get_global_context_file()
106
+ if global_file:
107
+ found_files.append(global_file)
108
+
109
+ return found_files
110
+
111
+ def _find_git_root(self, start_dir: Path) -> Optional[Path]:
112
+ """Find git repository root.
113
+
114
+ Args:
115
+ start_dir: Directory to start searching from
116
+
117
+ Returns:
118
+ Path to git root, or None if not in a git repository
119
+ """
120
+ try:
121
+ result = subprocess.run(
122
+ ["git", "rev-parse", "--show-toplevel"], cwd=start_dir, capture_output=True, text=True, check=False
123
+ )
124
+ if result.returncode == 0:
125
+ return Path(result.stdout.strip())
126
+ return None
127
+ except (subprocess.SubprocessError, FileNotFoundError):
128
+ return None
129
+
130
+ def _get_search_directories(self, cwd: Path, git_root: Optional[Path]) -> List[Path]:
131
+ """Get list of directories to search for context files.
132
+
133
+ Walks from cwd up to git root (inclusive).
134
+
135
+ Args:
136
+ cwd: Current working directory
137
+ git_root: Git repository root, or None
138
+
139
+ Returns:
140
+ List of directories to search, from most specific to most general
141
+ """
142
+ dirs = []
143
+ current = cwd
144
+
145
+ while True:
146
+ dirs.append(current)
147
+
148
+ # Stop at git root if we have one
149
+ if git_root and current == git_root:
150
+ break
151
+
152
+ # Stop at filesystem root
153
+ parent = current.parent
154
+ if parent == current:
155
+ break
156
+
157
+ current = parent
158
+
159
+ return dirs
160
+
161
+ def _discover_files(self, search_dirs: List[Path], context_files: List[str]) -> List[tuple[Path, str]]:
162
+ """Discover context files in search directories.
163
+
164
+ Args:
165
+ search_dirs: Directories to search
166
+ context_files: List of filenames to search for
167
+
168
+ Returns:
169
+ List of (file_path, relative_name) tuples for found files
170
+ """
171
+ found = []
172
+ seen_names = set()
173
+
174
+ # Search in order (most specific directory first)
175
+ for directory in search_dirs:
176
+ for filename in context_files:
177
+ # Skip if we've already found a file with this name
178
+ if filename in seen_names:
179
+ continue
180
+
181
+ file_path = directory / filename
182
+ if file_path.exists() and file_path.is_file():
183
+ found.append((file_path, filename))
184
+ seen_names.add(filename)
185
+
186
+ return found
187
+
188
+ def _get_global_context_file(self) -> Optional[tuple[Path, str]]:
189
+ """Get global context file from user's config directory.
190
+
191
+ Returns:
192
+ Tuple of (file_path, "Global Context") if file exists, None otherwise
193
+ """
194
+ from tsugite.xdg import get_xdg_config_path
195
+
196
+ global_context_path = get_xdg_config_path("CONTEXT.md")
197
+ if global_context_path.exists() and global_context_path.is_file():
198
+ return (global_context_path, "Global Context (~/.config/tsugite/CONTEXT.md)")
199
+ return None
@@ -0,0 +1,34 @@
1
+ """Base class for attachment handlers."""
2
+
3
+ from abc import ABC, abstractmethod
4
+
5
+
6
+ class AttachmentHandler(ABC):
7
+ """Base class for attachment handlers."""
8
+
9
+ @abstractmethod
10
+ def can_handle(self, source: str) -> bool:
11
+ """Check if this handler can process the source.
12
+
13
+ Args:
14
+ source: Source string (URL, file path, etc.)
15
+
16
+ Returns:
17
+ True if this handler can process the source
18
+ """
19
+ pass
20
+
21
+ @abstractmethod
22
+ def fetch(self, source: str) -> str:
23
+ """Fetch and return content for this source.
24
+
25
+ Args:
26
+ source: Source string to fetch
27
+
28
+ Returns:
29
+ Content as string
30
+
31
+ Raises:
32
+ ValueError: If fetch fails
33
+ """
34
+ pass
@@ -0,0 +1,51 @@
1
+ """File handler for local file attachments."""
2
+
3
+ from pathlib import Path
4
+
5
+ from tsugite.attachments.base import AttachmentHandler
6
+
7
+
8
+ class FileHandler(AttachmentHandler):
9
+ """Handler for local file references."""
10
+
11
+ def can_handle(self, source: str) -> bool:
12
+ """Check if source is a file path.
13
+
14
+ Args:
15
+ source: Source string
16
+
17
+ Returns:
18
+ True if source looks like a file path and exists
19
+ """
20
+ # Don't handle URLs or inline markers
21
+ if source.lower() in ("inline", "text"):
22
+ return False
23
+ if source.startswith("http://") or source.startswith("https://"):
24
+ return False
25
+ if source.startswith("youtube:"):
26
+ return False
27
+
28
+ # Check if it's a valid file path
29
+ try:
30
+ path = Path(source).expanduser()
31
+ return path.exists() and path.is_file()
32
+ except (OSError, RuntimeError):
33
+ return False
34
+
35
+ def fetch(self, source: str) -> str:
36
+ """Read file content.
37
+
38
+ Args:
39
+ source: File path
40
+
41
+ Returns:
42
+ File content as string
43
+
44
+ Raises:
45
+ ValueError: If file cannot be read
46
+ """
47
+ try:
48
+ path = Path(source).expanduser()
49
+ return path.read_text(encoding="utf-8")
50
+ except Exception as e:
51
+ raise ValueError(f"Failed to read file '{source}': {e}")
@@ -0,0 +1,31 @@
1
+ """Inline text handler for attachments."""
2
+
3
+ from tsugite.attachments.base import AttachmentHandler
4
+
5
+
6
+ class InlineHandler(AttachmentHandler):
7
+ """Handler for inline text content."""
8
+
9
+ def can_handle(self, source: str) -> bool:
10
+ """Check if source is inline text.
11
+
12
+ Args:
13
+ source: Source string
14
+
15
+ Returns:
16
+ True if source is "inline" or "text"
17
+ """
18
+ return source.lower() in ("inline", "text")
19
+
20
+ def fetch(self, source: str) -> str:
21
+ """Inline content is stored in attachments JSON, not fetched.
22
+
23
+ Args:
24
+ source: Source string (ignored)
25
+
26
+ Returns:
27
+ Empty string (inline content comes from attachments.json)
28
+ """
29
+ # Inline content is handled specially in resolve_attachments
30
+ # This method should not be called for inline content
31
+ return ""