Add source field support and Fabric import script

- Add ToolSource dataclass for attribution metadata (type, license, url, author, original_tool)
- Add source and version fields to Tool dataclass
- Update Tool.from_dict() and to_dict() to handle source field
- Display source attribution in TUI info panel
- Show [imported]/[forked] markers in cmdforge list
- Add import_fabric.py script to import Fabric patterns as CmdForge tools

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
rob
2026-01-03 23:52:29 -04:00
co-authored by Claude Opus 4.5
parent 2c139b3982
commit 1caa3454f0
4 changed files with 463 additions and 2 deletions
+8 -1
View File
@@ -23,7 +23,14 @@ def cmd_list(args):
for name in tools:
tool = load_tool(name)
if tool:
print(f" {name}")
# Show source indicator for imported tools
source_marker = ""
if tool.source and tool.source.type == "imported":
source_marker = " [imported]"
elif tool.source and tool.source.type == "forked":
source_marker = " [forked]"
print(f" {name}{source_marker}")
print(f" {tool.description or 'No description'}")
# Show arguments
+47 -1
View File
@@ -134,6 +134,38 @@ class ToolStep:
Step = PromptStep | CodeStep | ToolStep
@dataclass
class ToolSource:
"""Attribution and source information for imported/external tools."""
type: str = "original" # "original", "imported", "forked"
license: Optional[str] = None
url: Optional[str] = None
author: Optional[str] = None
original_tool: Optional[str] = None # e.g., "fabric/patterns/extract_wisdom"
def to_dict(self) -> dict:
d = {"type": self.type}
if self.license:
d["license"] = self.license
if self.url:
d["url"] = self.url
if self.author:
d["author"] = self.author
if self.original_tool:
d["original_tool"] = self.original_tool
return d
@classmethod
def from_dict(cls, data: dict) -> "ToolSource":
return cls(
type=data.get("type", "original"),
license=data.get("license"),
url=data.get("url"),
author=data.get("author"),
original_tool=data.get("original_tool"),
)
# Default categories for organizing tools
DEFAULT_CATEGORIES = ["Text", "Developer", "Data", "Other"]
@@ -148,6 +180,8 @@ class Tool:
steps: List[Step] = field(default_factory=list)
output: str = "{input}" # Output template
dependencies: List[str] = field(default_factory=list) # Required tools for meta-tools
source: Optional[ToolSource] = None # Attribution for imported/external tools
version: str = "" # Tool version
@classmethod
def from_dict(cls, data: dict) -> "Tool":
@@ -164,6 +198,11 @@ class Tool:
elif step.get("type") == "tool":
steps.append(ToolStep.from_dict(step))
# Parse source attribution if present
source = None
if "source" in data:
source = ToolSource.from_dict(data["source"])
return cls(
name=data["name"],
description=data.get("description", ""),
@@ -171,7 +210,9 @@ class Tool:
arguments=arguments,
steps=steps,
output=data.get("output", "{input}"),
dependencies=data.get("dependencies", [])
dependencies=data.get("dependencies", []),
source=source,
version=data.get("version", ""),
)
def to_dict(self) -> dict:
@@ -179,9 +220,14 @@ class Tool:
"name": self.name,
"description": self.description,
}
if self.version:
d["version"] = self.version
# Only include category if it's not the default
if self.category and self.category != "Other":
d["category"] = self.category
# Include source attribution if present
if self.source:
d["source"] = self.source.to_dict()
if self.dependencies:
d["dependencies"] = self.dependencies
d["arguments"] = [arg.to_dict() for arg in self.arguments]
+20
View File
@@ -197,6 +197,7 @@ class CmdForgeUI:
self._info_args = urwid.Text("")
self._info_steps = urwid.Text("")
self._info_output = urwid.Text("")
self._info_source = urwid.Text("")
info_content = urwid.Pile([
self._info_name,
@@ -207,6 +208,8 @@ class CmdForgeUI:
self._info_steps,
urwid.Divider(),
self._info_output,
urwid.Divider(),
self._info_source,
])
info_filler = urwid.Filler(info_content, valign='top')
info_box = urwid.LineBox(info_filler, title='Tool Info')
@@ -279,12 +282,29 @@ class CmdForgeUI:
self._info_steps.set_text(steps_text.rstrip())
self._info_output.set_text(f"Output: {tool.output}")
# Display source attribution if present
if tool.source:
source_text = "Source:\n"
source_text += f" Type: {tool.source.type}\n"
if tool.source.author:
source_text += f" Author: {tool.source.author}\n"
if tool.source.license:
source_text += f" License: {tool.source.license}\n"
if tool.source.url:
source_text += f" URL: {tool.source.url}\n"
if tool.source.original_tool:
source_text += f" Original: {tool.source.original_tool}\n"
self._info_source.set_text(source_text.rstrip())
else:
self._info_source.set_text("")
else:
self._info_name.set_text("")
self._info_desc.set_text("")
self._info_args.set_text("")
self._info_steps.set_text("")
self._info_output.set_text("")
self._info_source.set_text("")
def _on_tool_select(self, name):
"""Called when a tool is selected (Enter/double-click)."""