jac-coder 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.
- jac_coder/__init__.jac +0 -0
- jac_coder/api.jac +82 -0
- jac_coder/cli_entry.py +25 -0
- jac_coder/config.jac +36 -0
- jac_coder/context.jac +17 -0
- jac_coder/data/examples/ai_agent.md +90 -0
- jac_coder/data/examples/blog_app.md +386 -0
- jac_coder/data/examples/core_patterns.md +321 -0
- jac_coder/data/examples/todo_app.md +321 -0
- jac_coder/data/reference/ai.md +131 -0
- jac_coder/data/reference/backend.md +215 -0
- jac_coder/data/reference/frontend.md +271 -0
- jac_coder/data/reference/osp.md +229 -0
- jac_coder/data/reference/pitfalls.md +141 -0
- jac_coder/data/reference/syntax.md +159 -0
- jac_coder/data/rules/core_jac.md +559 -0
- jac_coder/data/rules/fullstack.md +362 -0
- jac_coder/data/rules/workflow.md +88 -0
- jac_coder/events.jac +110 -0
- jac_coder/impl/api.impl.jac +399 -0
- jac_coder/impl/config.impl.jac +163 -0
- jac_coder/impl/context.impl.jac +117 -0
- jac_coder/impl/mcp_manager.impl.jac +380 -0
- jac_coder/impl/memory.impl.jac +247 -0
- jac_coder/impl/nodes.impl.jac +259 -0
- jac_coder/impl/permission.impl.jac +62 -0
- jac_coder/impl/walkers.impl.jac +298 -0
- jac_coder/mcp_manager.jac +35 -0
- jac_coder/memory.jac +15 -0
- jac_coder/nodes.jac +306 -0
- jac_coder/permission.jac +19 -0
- jac_coder/serve_entry.jac +30 -0
- jac_coder/server.jac +324 -0
- jac_coder/tool/__init__.jac +17 -0
- jac_coder/tool/checked.jac +10 -0
- jac_coder/tool/delegation.jac +23 -0
- jac_coder/tool/filesystem.jac +25 -0
- jac_coder/tool/git.jac +18 -0
- jac_coder/tool/guarded.jac +23 -0
- jac_coder/tool/impl/checked.impl.jac +38 -0
- jac_coder/tool/impl/delegation.impl.jac +157 -0
- jac_coder/tool/impl/filesystem.impl.jac +781 -0
- jac_coder/tool/impl/git.impl.jac +115 -0
- jac_coder/tool/impl/guarded.impl.jac +72 -0
- jac_coder/tool/impl/jac_analyzer.impl.jac +593 -0
- jac_coder/tool/impl/jac_docs.impl.jac +136 -0
- jac_coder/tool/impl/jac_tools.impl.jac +79 -0
- jac_coder/tool/impl/mcp.impl.jac +32 -0
- jac_coder/tool/impl/preview.impl.jac +233 -0
- jac_coder/tool/impl/question.impl.jac +29 -0
- jac_coder/tool/impl/scaffold.impl.jac +231 -0
- jac_coder/tool/impl/search.impl.jac +85 -0
- jac_coder/tool/impl/shell.impl.jac +89 -0
- jac_coder/tool/impl/task.impl.jac +12 -0
- jac_coder/tool/impl/think.impl.jac +4 -0
- jac_coder/tool/impl/todo.impl.jac +58 -0
- jac_coder/tool/impl/validate.impl.jac +236 -0
- jac_coder/tool/impl/web.impl.jac +91 -0
- jac_coder/tool/jac_analyzer.jac +21 -0
- jac_coder/tool/jac_docs.jac +9 -0
- jac_coder/tool/jac_tools.jac +11 -0
- jac_coder/tool/mcp.jac +17 -0
- jac_coder/tool/preview.jac +31 -0
- jac_coder/tool/question.jac +7 -0
- jac_coder/tool/scaffold.jac +10 -0
- jac_coder/tool/search.jac +14 -0
- jac_coder/tool/shell.jac +12 -0
- jac_coder/tool/task.jac +9 -0
- jac_coder/tool/think.jac +5 -0
- jac_coder/tool/todo.jac +12 -0
- jac_coder/tool/validate.jac +11 -0
- jac_coder/tool/vision.jac +17 -0
- jac_coder/tool/web.jac +10 -0
- jac_coder/util/__init__.jac +18 -0
- jac_coder/util/colors.jac +20 -0
- jac_coder/util/impl/sandbox.impl.jac +38 -0
- jac_coder/util/impl/tool_output.impl.jac +208 -0
- jac_coder/util/sandbox.jac +8 -0
- jac_coder/util/tool_output.jac +29 -0
- jac_coder/walkers.jac +67 -0
- jac_coder-0.1.0.dist-info/METADATA +9 -0
- jac_coder-0.1.0.dist-info/RECORD +85 -0
- jac_coder-0.1.0.dist-info/WHEEL +5 -0
- jac_coder-0.1.0.dist-info/entry_points.txt +3 -0
- jac_coder-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
def _write(base_dir: str, relative_path: str, content: str) -> None {
|
|
2
|
+
full_path = os.path.join(base_dir, relative_path);
|
|
3
|
+
Path(full_path).parent.mkdir(parents=True, exist_ok=True);
|
|
4
|
+
Path(full_path).write_text(content);
|
|
5
|
+
emit_event(
|
|
6
|
+
"file_written",
|
|
7
|
+
{"path": full_path, "lines": len(content.splitlines()), "source": "scaffold"}
|
|
8
|
+
);
|
|
9
|
+
if os.environ.get("JACCODER_WEB_MODE", "") {
|
|
10
|
+
import time;
|
|
11
|
+
time.sleep(0.3);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
impl scaffold_project(project_type: str, name: str, directory: str) -> str {
|
|
17
|
+
(directory, err) = sandbox_path(directory);
|
|
18
|
+
if err {
|
|
19
|
+
return err;
|
|
20
|
+
}
|
|
21
|
+
tool_status("scaffold", f"{project_type} project '{name}' in {directory}");
|
|
22
|
+
|
|
23
|
+
if not directory {
|
|
24
|
+
return "Error: directory path is required";
|
|
25
|
+
}
|
|
26
|
+
if project_type not in ["frontend", "backend", "fullstack", "ai_agent"] {
|
|
27
|
+
return f"Error: project_type must be one of: frontend, backend, fullstack, ai_agent. Got: {project_type}";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
proj_name: str = name if name else directory.rstrip("/").split("/")[-1];
|
|
31
|
+
if name {
|
|
32
|
+
project_dir = os.path.join(directory, name);
|
|
33
|
+
} else {
|
|
34
|
+
project_dir = directory;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
(project_dir, err) = sandbox_path(project_dir);
|
|
38
|
+
if err {
|
|
39
|
+
return err;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
os.makedirs(project_dir, exist_ok=True);
|
|
44
|
+
files_created: list = [];
|
|
45
|
+
|
|
46
|
+
if project_type == "frontend" {
|
|
47
|
+
files_created = _scaffold_frontend(project_dir, proj_name);
|
|
48
|
+
} elif project_type == "backend" {
|
|
49
|
+
files_created = _scaffold_backend(project_dir, proj_name);
|
|
50
|
+
} elif project_type == "fullstack" {
|
|
51
|
+
files_created = _scaffold_fullstack(project_dir, proj_name);
|
|
52
|
+
} elif project_type == "ai_agent" {
|
|
53
|
+
files_created = _scaffold_ai_agent(project_dir, proj_name);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
result_lines: list = [
|
|
57
|
+
f"Scaffolded {project_type} project '{name}' at {project_dir}",
|
|
58
|
+
""
|
|
59
|
+
];
|
|
60
|
+
result_lines.append("Files created:");
|
|
61
|
+
for f in files_created {
|
|
62
|
+
result_lines.append(f" {f}");
|
|
63
|
+
}
|
|
64
|
+
result_lines.append("");
|
|
65
|
+
result_lines.append("NEXT STEPS:");
|
|
66
|
+
result_lines.append(
|
|
67
|
+
"1. Read the scaffolded .cl.jac and .jac files to learn the correct syntax patterns."
|
|
68
|
+
);
|
|
69
|
+
result_lines.append(
|
|
70
|
+
"2. When creating new components, copy the EXACT syntax from the scaffolded files."
|
|
71
|
+
);
|
|
72
|
+
result_lines.append("3. Build incrementally — add one component at a time.");
|
|
73
|
+
|
|
74
|
+
(output, _) = truncate_output("\n".join(result_lines));
|
|
75
|
+
tool_end(f"-> {len(files_created)} files");
|
|
76
|
+
return output;
|
|
77
|
+
} except Exception as e {
|
|
78
|
+
tool_end("-> error", error=True);
|
|
79
|
+
return f"Error creating project: {str(e)}";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# ===========================================================================
|
|
85
|
+
# jac.toml templates
|
|
86
|
+
# ===========================================================================
|
|
87
|
+
|
|
88
|
+
glob _TOML_FRONTEND = '[project]\nname = "__NAME__"\nversion = "1.0.0"\ndescription = "Jac client application"\nentry-point = "main.jac"\n\n[dependencies.npm]\njac-client-node = "1.0.5"\nclsx = "^2.1.1"\ntailwind-merge = "^3.4.0"\ntailwindcss = "^4.1.17"\n\n[dependencies.npm.dev]\n"@jac-client/dev-deps" = "2.0.0"\n\n[serve]\nbase_route_app = "app"\n\n[plugins.client]\n\n[plugins.client.vite]\nplugins = ["tailwindcss()"]\nlib_imports = ["import tailwindcss from \'@tailwindcss/vite\'"]\n\n[plugins.client.app_meta_data]\ntitle = "__NAME__"\n';
|
|
89
|
+
|
|
90
|
+
glob _TOML_FULLSTACK = '[project]\nname = "__NAME__"\nversion = "1.0.0"\ndescription = "Jac full-stack application"\nentry-point = "main.jac"\n\n[dependencies]\npython-dotenv = ">=1.0.0"\n\n[dependencies.npm]\njac-client-node = "1.0.5"\nclsx = "^2.1.1"\ntailwind-merge = "^3.4.0"\ntailwindcss = "^4.1.17"\n\n[dependencies.npm.dev]\n"@jac-client/dev-deps" = "2.0.0"\n\n[serve]\nbase_route_app = "app"\n\n[plugins.client]\n\n[plugins.client.vite]\nplugins = ["tailwindcss()"]\nlib_imports = ["import tailwindcss from \'@tailwindcss/vite\'"]\n\n[plugins.client.app_meta_data]\ntitle = "__NAME__"\n';
|
|
91
|
+
|
|
92
|
+
glob _TOML_BACKEND = '[project]\nname = "__NAME__"\nversion = "1.0.0"\nentry-point = "main.jac"\n\n[dependencies]\npython-dotenv = ">=1.0.0"\n';
|
|
93
|
+
|
|
94
|
+
glob _TOML_AGENT = '[project]\nname = "__NAME__"\nversion = "1.0.0"\nentry-point = "main.jac"\n\n[dependencies]\npython-dotenv = ">=1.0.0"\nbyllm = ">=0.1.0"\n';
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ===========================================================================
|
|
98
|
+
# CSS template
|
|
99
|
+
# ===========================================================================
|
|
100
|
+
|
|
101
|
+
glob _GLOBAL_CSS = '@import "tailwindcss";\n\n@theme {\n --color-primary: #3b82f6;\n --color-primary-foreground: #ffffff;\n --color-secondary: #64748b;\n --color-secondary-foreground: #ffffff;\n --color-background: #f8fafc;\n --color-surface: #ffffff;\n --color-surface-hover: #f1f5f9;\n --color-text-primary: #0f172a;\n --color-text-secondary: #64748b;\n --color-border: #e2e8f0;\n --color-success: #22c55e;\n --color-warning: #f59e0b;\n --color-error: #ef4444;\n}\n\n.dark {\n --color-primary: #60a5fa;\n --color-primary-foreground: #ffffff;\n --color-secondary: #94a3b8;\n --color-secondary-foreground: #ffffff;\n --color-background: #0f172a;\n --color-surface: #1e293b;\n --color-surface-hover: #334155;\n --color-text-primary: #f1f5f9;\n --color-text-secondary: #94a3b8;\n --color-border: #334155;\n --color-success: #4ade80;\n --color-warning: #fbbf24;\n --color-error: #f87171;\n}\n';
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ===========================================================================
|
|
105
|
+
# Utility template
|
|
106
|
+
# ===========================================================================
|
|
107
|
+
|
|
108
|
+
glob _UTILS_CL = 'import from "clsx" { clsx }\nimport from "tailwind-merge" { twMerge }\n\ndef:pub cn(input: any) -> str {\n inputs = [].slice.call(arguments);\n return twMerge(clsx(inputs));\n}\n';
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ===========================================================================
|
|
112
|
+
# Frontend-only scaffold
|
|
113
|
+
# ===========================================================================
|
|
114
|
+
|
|
115
|
+
glob _FRONTEND_MAIN = 'cl import from .components.Layout { Layout }\ncl {\n def:pub app() -> JsxElement {\n return <Layout />;\n }\n}\n';
|
|
116
|
+
|
|
117
|
+
glob _FRONTEND_HEADER = 'import from "..lib.utils" { cn }\n\ndef:pub Header(title: str, theme: str, on_toggle_theme: any) -> JsxElement {\n return (\n <header className="border-b border-border px-6 py-4 flex items-center justify-between">\n <h1 className="text-xl font-bold">{title}</h1>\n <button onClick={on_toggle_theme}\n className="px-3 py-1 bg-secondary text-secondary-foreground rounded-lg text-sm">\n {(theme == "dark") and "Light" or "Dark"}\n </button>\n </header>\n );\n}\n';
|
|
118
|
+
|
|
119
|
+
glob _FRONTEND_COUNTER = 'import from "..lib.utils" { cn }\n\ndef:pub Counter() -> JsxElement {\n # State\n has count: int = 0;\n\n # Handlers\n def handle_increment() -> None {\n count = count + 1;\n }\n\n # Render\n return (\n <button onClick={handle_increment}\n className="px-4 py-2 bg-primary text-primary-foreground rounded-lg">\n Count: {str(count)}\n </button>\n );\n}\n';
|
|
120
|
+
|
|
121
|
+
glob _FRONTEND_APP = 'import "..styles.global.css";\nimport from ".Header" { Header }\nimport from ".Counter" { Counter }\n\ndef:pub Layout() -> JsxElement {\n # State\n has theme: str = "light";\n\n # Effects\n can with (theme) entry {\n if theme == "dark" {\n document.documentElement.classList.add("dark");\n } else {\n document.documentElement.classList.remove("dark");\n }\n }\n\n # Handlers\n def handle_toggle_theme() -> None {\n theme = (theme == "dark") and "light" or "dark";\n }\n\n # Render\n return (\n <div className="min-h-screen bg-background text-text-primary">\n <Header title="__NAME__" theme={theme} on_toggle_theme={handle_toggle_theme} />\n <main className="max-w-2xl mx-auto px-4 py-8">\n <Counter />\n </main>\n </div>\n );\n}\n';
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# ===========================================================================
|
|
125
|
+
# Fullstack scaffold
|
|
126
|
+
# ===========================================================================
|
|
127
|
+
|
|
128
|
+
glob _FULLSTACK_MAIN = 'import from uuid { uuid4 }\nimport from datetime { datetime }\n\n\nnode Item {\n has id: str = "";\n has name: str;\n has description: str = "";\n has created_at: str = "";\n\n def postinit {\n if not self.id { self.id = str(uuid4()); }\n if not self.created_at { self.created_at = datetime.now().isoformat(); }\n }\n}\n\n\ndef:pub get_items() -> list {\n return [{"id": i.id, "name": i.name, "description": i.description}\n for i in [root()-->][?:Item];\n}\n\n\ndef:pub add_item(name: str, description: str = "") -> dict {\n item = (root() ++> Item(name=name, description=description))[0];\n return {"id": item.id, "name": item.name, "description": item.description};\n}\n\n\ndef:pub delete_item(id: str) -> dict {\n for item in [root()-->][?:Item] {\n if item.id == id {\n root() del--> item;\n return {"deleted": id};\n }\n }\n return {"error": "not found"};\n}\n\n\ncl import from .components.Layout { Layout }\ncl {\n def:pub app() -> JsxElement {\n return <Layout />;\n }\n}\n';
|
|
129
|
+
|
|
130
|
+
glob _FULLSTACK_HOOK = 'sv import from ..main { get_items, add_item, delete_item }\n\ndef:pub useItems() -> dict {\n # State\n has items: list = [];\n has loading: bool = True;\n has error: str = "";\n\n # Fetch data on mount\n async can with entry {\n try {\n result = await get_items();\n items = result or [];\n } except Exception as e {\n error = str(e);\n }\n loading = False;\n }\n\n # Handlers\n async def handle_add(name: str, description: str = "") -> None {\n if not name { return; }\n result = await add_item(name, description); # positional args only — kwargs broken in .cl.jac\n if result and not result.error {\n items = items + [result];\n }\n }\n\n async def handle_delete(id: str) -> None {\n result = await delete_item(id); # positional args only — kwargs broken in .cl.jac\n if result and not result.error {\n items = [i for i in items if i["id"] != id];\n }\n }\n\n return {\n "items": items,\n "loading": loading,\n "error": error,\n "handleAdd": handle_add,\n "handleDelete": handle_delete\n };\n}\n';
|
|
131
|
+
|
|
132
|
+
glob _FULLSTACK_HEADER = 'import from "..lib.utils" { cn }\n\ndef:pub Header(title: str, theme: str, on_toggle_theme: any) -> JsxElement {\n return (\n <header className="border-b border-border px-6 py-4 flex items-center justify-between">\n <h1 className="text-xl font-bold">{title}</h1>\n <button onClick={on_toggle_theme}\n className="px-3 py-1 bg-secondary text-secondary-foreground rounded-lg text-sm">\n {(theme == "dark") and "Light" or "Dark"}\n </button>\n </header>\n );\n}\n';
|
|
133
|
+
|
|
134
|
+
glob _FULLSTACK_ITEMLIST = 'import from "..lib.utils" { cn }\nimport from "..hooks.useItems" { useItems }\nimport from ".ItemCard" { ItemCard }\n\ndef:pub ItemList() -> JsxElement {\n # Fetch data\n data = useItems();\n items = data["items"] or [];\n\n # State\n has input_value: str = "";\n\n # Handlers\n def handle_input_change(e: any) -> None {\n input_value = e.target.value;\n }\n\n def handle_add() -> None {\n if input_value {\n data["handleAdd"](input_value);\n input_value = "";\n }\n }\n\n def handle_key_down(e: any) -> None {\n if e.key == "Enter" {\n e.preventDefault();\n handle_add();\n }\n }\n\n # Guard\n if data["loading"] {\n return <p className="text-text-secondary text-center py-8">Loading...</p>;\n }\n\n # Render\n return (\n <div>\n {data["error"] and (\n <p className="text-error mb-4">{data["error"]}</p>\n )}\n <div className="flex gap-2 mb-6">\n <input\n value={input_value}\n onChange={handle_input_change}\n onKeyDown={handle_key_down}\n placeholder="Add an item..."\n className="flex-1 px-4 py-2 border border-border rounded-lg bg-surface"\n />\n <button onClick={handle_add}\n className="px-4 py-2 bg-primary text-primary-foreground rounded-lg flex items-center gap-1">\n + Add\n </button>\n </div>\n {len(items) == 0 and (\n <p className="text-text-secondary text-center py-8">No items yet. Add one above!</p>\n )}\n <div className="space-y-2">\n {[\n <ItemCard key={item["id"]} item={item} on_delete={data["handleDelete"]} />\n for item in items\n ]}\n </div>\n </div>\n );\n}\n';
|
|
135
|
+
|
|
136
|
+
glob _FULLSTACK_APP = 'import "..styles.global.css";\nimport from ".Header" { Header }\nimport from ".ItemList" { ItemList }\n\ndef:pub Layout() -> JsxElement {\n # State\n has theme: str = "light";\n\n # Effects\n can with (theme) entry {\n if theme == "dark" {\n document.documentElement.classList.add("dark");\n } else {\n document.documentElement.classList.remove("dark");\n }\n }\n\n # Handlers\n def handle_toggle_theme() -> None {\n theme = (theme == "dark") and "light" or "dark";\n }\n\n # Render\n return (\n <div className="min-h-screen bg-background text-text-primary">\n <Header title="__NAME__" theme={theme} on_toggle_theme={handle_toggle_theme} />\n <main className="max-w-2xl mx-auto px-4 py-8">\n <ItemList />\n </main>\n </div>\n );\n}\n';
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
glob _FULLSTACK_ITEMCARD = 'def:pub ItemCard(item: dict, on_delete: any) -> JsxElement {\n # Guard props\n itemId = item["id"] or "";\n name = item["name"] or "";\n description = item["description"] or "";\n\n # Handlers\n def handle_delete() -> None {\n on_delete(itemId);\n }\n\n # Render\n return (\n <div className="flex items-center justify-between p-3 bg-surface border border-border rounded-lg">\n <div>\n <span className="font-medium">{name}</span>\n {description and (\n <p className="text-sm text-text-secondary mt-1">{description}</p>\n )}\n </div>\n <button onClick={handle_delete}\n className="text-error hover:opacity-75">\n ×\n </button>\n </div>\n );\n}\n';
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ===========================================================================
|
|
143
|
+
# Backend-only scaffold
|
|
144
|
+
# ===========================================================================
|
|
145
|
+
|
|
146
|
+
glob _BACKEND_MAIN = 'import from uuid { uuid4 }\nimport from datetime { datetime }\n\n\nnode Item {\n has id: str = "";\n has name: str;\n has description: str = "";\n has created_at: str = "";\n\n def postinit {\n if not self.id { self.id = str(uuid4()); }\n if not self.created_at { self.created_at = datetime.now().isoformat(); }\n }\n}\n\n\ndef:pub get_items() -> list {\n return [{"id": i.id, "name": i.name, "description": i.description}\n for i in [root()-->][?:Item];\n}\n\n\ndef:pub add_item(name: str, description: str = "") -> dict {\n item = (root() ++> Item(name=name, description=description))[0];\n return {"id": item.id, "name": item.name, "description": item.description};\n}\n\n\ndef:pub delete_item(id: str) -> dict {\n for item in [root()-->][?:Item] {\n if item.id == id {\n root() del--> item;\n return {"deleted": id};\n }\n }\n return {"error": "not found"};\n}\n';
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# ===========================================================================
|
|
150
|
+
# AI Agent scaffold
|
|
151
|
+
# ===========================================================================
|
|
152
|
+
|
|
153
|
+
glob _AGENT_MAIN = 'import os;\nimport from dotenv { load_dotenv }\nimport from byllm.lib { Model }\n\nwith entry {\n load_dotenv(override=True);\n}\n\nglob model_name: str = os.environ.get("MODEL", "gpt-4o");\nglob llm = Model(model_name=model_name);\n';
|
|
154
|
+
|
|
155
|
+
glob _AGENT_NODES = 'node Router {\n def classify(message: str, chat_history: list[dict]) -> str\n by llm(method="Reason", temperature=0.1);\n}\n\n\nnode Handler {\n def respond(message: str, chat_history: list[dict]) -> str\n by llm(tools=[], messages=chat_history,\n max_react_iterations=10, temperature=0.2);\n}\n\n\nwalker:pub interact {\n has message: str;\n has session_id: str = "";\n has chat_history: list[dict] = [];\n\n obj __specs__ { static has methods: list = ["post"]; }\n\n can init with Root entry;\n can route with Router entry;\n can handle with Handler entry;\n}\n';
|
|
156
|
+
|
|
157
|
+
glob _AGENT_IMPL = 'sem Router.classify = """Return exactly one label: HANDLE or DONE.\nIf the user needs a response, return HANDLE.\nIf the conversation is complete, return DONE.""";\n\n\nsem Handler.respond = """You are a helpful AI assistant.\nAnswer the user\'s questions clearly and concisely.""";\n\n\nimpl interact.init with Root entry {\n visit [-->][?:Router] else {\n router = (here ++> Router())[0];\n router ++> Handler();\n visit router;\n };\n}\n\n\nimpl interact.route with Router entry {\n label = here.classify(\n message=self.message,\n chat_history=self.chat_history\n ).strip().upper();\n\n if "DONE" in label {\n disengage;\n } else {\n visit [-->][?:Handler];\n }\n}\n\n\nimpl interact.handle with Handler entry {\n response = here.respond(\n message=self.message,\n chat_history=self.chat_history\n );\n report {"response": response};\n}\n';
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# ===========================================================================
|
|
161
|
+
# Scaffold functions
|
|
162
|
+
# ===========================================================================
|
|
163
|
+
|
|
164
|
+
def _scaffold_frontend(project_dir: str, name: str) -> list {
|
|
165
|
+
files: list = [];
|
|
166
|
+
_write(project_dir, "jac.toml", _TOML_FRONTEND.replace("__NAME__", name));
|
|
167
|
+
files.append("jac.toml");
|
|
168
|
+
_write(project_dir, "main.jac", _FRONTEND_MAIN);
|
|
169
|
+
files.append("main.jac");
|
|
170
|
+
_write(project_dir, "components/Header.cl.jac", _FRONTEND_HEADER);
|
|
171
|
+
files.append("components/Header.cl.jac");
|
|
172
|
+
_write(project_dir, "components/Counter.cl.jac", _FRONTEND_COUNTER);
|
|
173
|
+
files.append("components/Counter.cl.jac");
|
|
174
|
+
_write(project_dir, "components/Layout.cl.jac", _FRONTEND_APP.replace("__NAME__", name));
|
|
175
|
+
files.append("components/Layout.cl.jac");
|
|
176
|
+
_write(project_dir, "lib/utils.cl.jac", _UTILS_CL);
|
|
177
|
+
files.append("lib/utils.cl.jac");
|
|
178
|
+
_write(project_dir, "styles/global.css", _GLOBAL_CSS);
|
|
179
|
+
files.append("styles/global.css");
|
|
180
|
+
return files;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _scaffold_backend(project_dir: str, name: str) -> list {
|
|
185
|
+
files: list = [];
|
|
186
|
+
_write(project_dir, "jac.toml", _TOML_BACKEND.replace("__NAME__", name));
|
|
187
|
+
files.append("jac.toml");
|
|
188
|
+
_write(project_dir, "main.jac", _BACKEND_MAIN);
|
|
189
|
+
files.append("main.jac");
|
|
190
|
+
return files;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _scaffold_fullstack(project_dir: str, name: str) -> list {
|
|
195
|
+
files: list = [];
|
|
196
|
+
_write(project_dir, "jac.toml", _TOML_FULLSTACK.replace("__NAME__", name));
|
|
197
|
+
files.append("jac.toml");
|
|
198
|
+
_write(project_dir, "main.jac", _FULLSTACK_MAIN);
|
|
199
|
+
files.append("main.jac");
|
|
200
|
+
_write(project_dir, "hooks/useItems.cl.jac", _FULLSTACK_HOOK);
|
|
201
|
+
files.append("hooks/useItems.cl.jac");
|
|
202
|
+
_write(project_dir, "components/Header.cl.jac", _FULLSTACK_HEADER);
|
|
203
|
+
files.append("components/Header.cl.jac");
|
|
204
|
+
_write(project_dir, "components/ItemList.cl.jac", _FULLSTACK_ITEMLIST);
|
|
205
|
+
files.append("components/ItemList.cl.jac");
|
|
206
|
+
_write(project_dir, "components/ItemCard.cl.jac", _FULLSTACK_ITEMCARD);
|
|
207
|
+
files.append("components/ItemCard.cl.jac");
|
|
208
|
+
_write(project_dir, "components/Layout.cl.jac", _FULLSTACK_APP.replace("__NAME__", name));
|
|
209
|
+
files.append("components/Layout.cl.jac");
|
|
210
|
+
_write(project_dir, "lib/utils.cl.jac", _UTILS_CL);
|
|
211
|
+
files.append("lib/utils.cl.jac");
|
|
212
|
+
_write(project_dir, "styles/global.css", _GLOBAL_CSS);
|
|
213
|
+
files.append("styles/global.css");
|
|
214
|
+
return files;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _scaffold_ai_agent(project_dir: str, name: str) -> list {
|
|
219
|
+
files: list = [];
|
|
220
|
+
_write(project_dir, "jac.toml", _TOML_AGENT.replace("__NAME__", name));
|
|
221
|
+
files.append("jac.toml");
|
|
222
|
+
_write(project_dir, "main.jac", _AGENT_MAIN);
|
|
223
|
+
files.append("main.jac");
|
|
224
|
+
_write(project_dir, "nodes.jac", _AGENT_NODES);
|
|
225
|
+
files.append("nodes.jac");
|
|
226
|
+
_write(project_dir, "impl/nodes.impl.jac", _AGENT_IMPL);
|
|
227
|
+
files.append("impl/nodes.impl.jac");
|
|
228
|
+
_write(project_dir, ".env", 'MODEL=gpt-4o\nOPENAI_API_KEY=your-api-key-here\n');
|
|
229
|
+
files.append(".env");
|
|
230
|
+
return files;
|
|
231
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
impl grep_search(
|
|
2
|
+
pattern: str, path: str = ".", file_pattern: str = "", max_results: int = 50
|
|
3
|
+
) -> str {
|
|
4
|
+
(path, err) = sandbox_path(path);
|
|
5
|
+
if err {
|
|
6
|
+
return err;
|
|
7
|
+
}
|
|
8
|
+
tool_status("grep", f"'{pattern}' in {path}");
|
|
9
|
+
if not pattern {
|
|
10
|
+
return "Error: pattern is required";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
cmd = ["grep", "-rn", "-E", "--color=never"];
|
|
14
|
+
cmd += [
|
|
15
|
+
"--exclude-dir=.git",
|
|
16
|
+
"--exclude-dir=node_modules",
|
|
17
|
+
"--exclude-dir=__pycache__",
|
|
18
|
+
"--exclude-dir=.jac"
|
|
19
|
+
];
|
|
20
|
+
if file_pattern {
|
|
21
|
+
cmd += ["--include", file_pattern];
|
|
22
|
+
}
|
|
23
|
+
cmd += ["-m", str(max_results), pattern, path];
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=15);
|
|
27
|
+
output = result.stdout.strip();
|
|
28
|
+
if not output {
|
|
29
|
+
tool_end("-> 0 matches");
|
|
30
|
+
return f"No matches for '{pattern}' in {path}";
|
|
31
|
+
}
|
|
32
|
+
match_count = len(output.splitlines());
|
|
33
|
+
tool_end(f"-> {match_count} matches");
|
|
34
|
+
(output, _) = truncate_output(output);
|
|
35
|
+
return output;
|
|
36
|
+
} except subprocess.TimeoutExpired {
|
|
37
|
+
tool_end("-> timeout", error=True);
|
|
38
|
+
return "Error: Search timed out after 15 seconds";
|
|
39
|
+
} except Exception as e {
|
|
40
|
+
tool_end("-> error", error=True);
|
|
41
|
+
return f"Error: {str(e)}";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
impl find_files(pattern: str, directory: str = ".") -> str {
|
|
47
|
+
(directory, err) = sandbox_path(directory);
|
|
48
|
+
if err {
|
|
49
|
+
return err;
|
|
50
|
+
}
|
|
51
|
+
tool_status("glob", f"'{pattern}' in {directory}");
|
|
52
|
+
if not pattern {
|
|
53
|
+
return "Error: pattern is required";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
p = Path(directory);
|
|
57
|
+
if not p.exists() {
|
|
58
|
+
return f"Error: Directory not found: {directory}";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
files = sorted(
|
|
63
|
+
[
|
|
64
|
+
str(f)
|
|
65
|
+
for f in p.glob(pattern)
|
|
66
|
+
if f.is_file() and ".git" not in f.parts
|
|
67
|
+
]
|
|
68
|
+
);
|
|
69
|
+
} except Exception as e {
|
|
70
|
+
tool_end("-> error", error=True);
|
|
71
|
+
return f"Error: {str(e)}";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if not files {
|
|
75
|
+
tool_end("-> 0 files");
|
|
76
|
+
return f"No files matching '{pattern}'";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
total = len(files);
|
|
80
|
+
tool_end(f"-> {total} files");
|
|
81
|
+
if total > 200 {
|
|
82
|
+
return "\n".join(files[:200]) + f"\n\n... and {total - 200} more ({total} total)";
|
|
83
|
+
}
|
|
84
|
+
return "\n".join(files);
|
|
85
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
impl bash_exec(
|
|
2
|
+
command: str, workdir: str = ".", timeout: int = 120, background: bool = False
|
|
3
|
+
) -> str {
|
|
4
|
+
blocked = ["rm -rf /", "rm -rf ~", "mkfs", "dd if=", "> /dev/sd"];
|
|
5
|
+
for pattern in blocked {
|
|
6
|
+
if pattern in command {
|
|
7
|
+
return "Error: Blocked dangerous command";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
timeout = min(timeout, 600);
|
|
12
|
+
tool_status("bash", command);
|
|
13
|
+
|
|
14
|
+
sroot = get_sandbox_root();
|
|
15
|
+
if sroot {
|
|
16
|
+
(workdir, err) = sandbox_path(workdir);
|
|
17
|
+
if err {
|
|
18
|
+
return err;
|
|
19
|
+
}
|
|
20
|
+
if not workdir or not os.path.isdir(workdir) {
|
|
21
|
+
workdir = sroot;
|
|
22
|
+
}
|
|
23
|
+
} else {
|
|
24
|
+
if not workdir or not os.path.isdir(workdir) {
|
|
25
|
+
workdir = os.getcwd();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if background {
|
|
30
|
+
try {
|
|
31
|
+
proc = subprocess.Popen(
|
|
32
|
+
["bash", "-c", command],
|
|
33
|
+
stdout=subprocess.PIPE,
|
|
34
|
+
stderr=subprocess.PIPE,
|
|
35
|
+
cwd=workdir,
|
|
36
|
+
start_new_session=True
|
|
37
|
+
);
|
|
38
|
+
return f"Background process started (PID: {proc.pid}). Use `kill {proc.pid}` to stop it.";
|
|
39
|
+
} except Exception as e {
|
|
40
|
+
return f"Error starting background process: {str(e)}";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# Append pwd to capture final CWD after command
|
|
45
|
+
wrapped = command + '\necho "\n__JACCODER_CWD__:$(pwd)"';
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
result = subprocess.run(
|
|
49
|
+
["bash", "-c", wrapped],
|
|
50
|
+
capture_output=True,
|
|
51
|
+
text=True,
|
|
52
|
+
timeout=timeout,
|
|
53
|
+
cwd=workdir
|
|
54
|
+
);
|
|
55
|
+
} except subprocess.TimeoutExpired {
|
|
56
|
+
return f"Error: Command timed out after {timeout} seconds. If this is a long-running process (server, watcher), use background=True instead.";
|
|
57
|
+
} except Exception as e {
|
|
58
|
+
return f"Error: {str(e)}";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
stdout = result.stdout or "";
|
|
62
|
+
cwd_marker = "__JACCODER_CWD__:";
|
|
63
|
+
if cwd_marker in stdout {
|
|
64
|
+
parts = stdout.rsplit(cwd_marker, 1);
|
|
65
|
+
stdout = parts[0].rstrip("\n");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
output_parts = [];
|
|
69
|
+
if stdout {
|
|
70
|
+
output_parts.append(stdout);
|
|
71
|
+
}
|
|
72
|
+
if result.stderr {
|
|
73
|
+
output_parts.append(f"STDERR:\n{result.stderr}");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
output = "\n".join(output_parts) if output_parts else "(no output)";
|
|
77
|
+
|
|
78
|
+
if result.returncode != 0 {
|
|
79
|
+
output = f"Exit code: {result.returncode}\n{output}";
|
|
80
|
+
tool_end(f"-> exit {result.returncode}", error=True);
|
|
81
|
+
} else {
|
|
82
|
+
out_lines = len(output.splitlines());
|
|
83
|
+
tool_end(f"-> {out_lines} lines");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
(output, _) = truncate_output(output);
|
|
87
|
+
|
|
88
|
+
return output;
|
|
89
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
impl spawn_task(description: str, prompt: str, agent_type: str = "explore") -> str {
|
|
2
|
+
tool_status("spawn_task", f"[{agent_type}] {description}");
|
|
3
|
+
if not prompt {
|
|
4
|
+
return "Error: prompt is required";
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
# Delegate to the new spawn_agent tool
|
|
8
|
+
import from jac_coder.tool.delegation { spawn_agent }
|
|
9
|
+
|
|
10
|
+
mode = "explorer" if agent_type == "explore" else "worker";
|
|
11
|
+
return spawn_agent(task=prompt, mode=mode);
|
|
12
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
impl update_todos(todos_json: str) -> str {
|
|
2
|
+
tool_status("update_todos");
|
|
3
|
+
global _todos;
|
|
4
|
+
|
|
5
|
+
if not todos_json {
|
|
6
|
+
return "Error: todos_json is required (JSON array of {content, status})";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
todos = json.loads(todos_json);
|
|
11
|
+
} except json.JSONDecodeError as e {
|
|
12
|
+
return f"Error: invalid JSON — {e}";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if not isinstance(todos, list) {
|
|
16
|
+
return "Error: todos_json must be a JSON array";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
items = [];
|
|
20
|
+
for item in todos {
|
|
21
|
+
content = item.get("content", "") if isinstance(item, dict) else str(item);
|
|
22
|
+
status = item.get("status", "pending") if isinstance(item, dict) else "pending";
|
|
23
|
+
if status not in ["pending", "in_progress", "completed"] {
|
|
24
|
+
status = "pending";
|
|
25
|
+
}
|
|
26
|
+
items.append({"content": content, "status": status});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
_todos["default"] = items;
|
|
30
|
+
done = len(
|
|
31
|
+
[
|
|
32
|
+
i
|
|
33
|
+
for i in items
|
|
34
|
+
if i["status"] == "completed"
|
|
35
|
+
]
|
|
36
|
+
);
|
|
37
|
+
tool_end(f"-> {done}/{len(items)} done");
|
|
38
|
+
|
|
39
|
+
lines = [];
|
|
40
|
+
for item in items {
|
|
41
|
+
if item["status"] == "completed" {
|
|
42
|
+
icon = "[x]";
|
|
43
|
+
} elif item["status"] == "in_progress" {
|
|
44
|
+
icon = "[>]";
|
|
45
|
+
} else {
|
|
46
|
+
icon = "[ ]";
|
|
47
|
+
}
|
|
48
|
+
lines.append(f"{icon} {item['content']}");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return "\n".join(lines) if lines else "Todo list cleared.";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
impl get_todos(session_id: str = "default") -> list {
|
|
56
|
+
global _todos;
|
|
57
|
+
return _todos.get(session_id, []);
|
|
58
|
+
}
|