jac-coder 0.2.2__tar.gz → 0.2.3__tar.gz

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 (115) hide show
  1. {jac_coder-0.2.2 → jac_coder-0.2.3}/PKG-INFO +1 -1
  2. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/api.impl.jac +22 -0
  3. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/api.jac +3 -0
  4. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/core/nodes.impl.jac +13 -5
  5. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/infra/config.impl.jac +1 -0
  6. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/infra/config.jac +1 -0
  7. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/infra/mcp_manager.impl.jac +7 -1
  8. jac_coder-0.2.3/jac_coder/runtime/file_logger.jac +235 -0
  9. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/server.jac +14 -1
  10. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/skills/ROADMAP.md +1 -0
  11. jac_coder-0.2.3/jac_coder/skills/jac-cl-auth/SKILL.md +134 -0
  12. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/skills/jac-cl-components/SKILL.md +34 -11
  13. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/skills/jac-cl-organization/SKILL.md +26 -1
  14. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/skills/jac-cl-routing/SKILL.md +2 -5
  15. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/skills/jac-cl-styling/SKILL.md +2 -0
  16. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/skills/jac-core-cheatsheet/SKILL.md +0 -1
  17. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/skills/jac-fullstack-patterns/SKILL.md +1 -0
  18. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/skills/jac-node-edge-patterns/SKILL.md +24 -3
  19. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/skills/jac-npm-packages/SKILL.md +2 -0
  20. jac_coder-0.2.3/jac_coder/skills/jac-shadcn-components/SKILL.md +340 -0
  21. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/skills/jac-sv-auth/SKILL.md +9 -9
  22. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/skills/jac-sv-endpoints/SKILL.md +8 -8
  23. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/skills/jac-sv-persistence/SKILL.md +15 -15
  24. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/skills/jac-types/SKILL.md +1 -0
  25. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/skills/jac-walker-patterns/SKILL.md +5 -5
  26. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder.egg-info/PKG-INFO +1 -1
  27. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder.egg-info/SOURCES.txt +2 -0
  28. {jac_coder-0.2.2 → jac_coder-0.2.3}/pyproject.toml +1 -1
  29. jac_coder-0.2.2/jac_coder/skills/jac-cl-auth/SKILL.md +0 -93
  30. {jac_coder-0.2.2 → jac_coder-0.2.3}/README.md +0 -0
  31. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/__init__.jac +0 -0
  32. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/__init__.py +0 -0
  33. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/cli_entry.py +0 -0
  34. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/core/__init__.jac +0 -0
  35. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/core/nodes.jac +0 -0
  36. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/core/walkers.impl.jac +0 -0
  37. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/core/walkers.jac +0 -0
  38. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/infra/__init__.jac +0 -0
  39. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/infra/kv.jac +0 -0
  40. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/infra/mcp_manager.jac +0 -0
  41. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/lib/__init__.jac +0 -0
  42. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/lib/coder.impl.jac +0 -0
  43. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/lib/coder.jac +0 -0
  44. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/runtime/__init__.jac +0 -0
  45. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/runtime/context.impl.jac +0 -0
  46. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/runtime/context.jac +0 -0
  47. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/runtime/cost_tracker.impl.jac +0 -0
  48. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/runtime/cost_tracker.jac +0 -0
  49. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/runtime/events.jac +0 -0
  50. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/runtime/memory.impl.jac +0 -0
  51. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/runtime/memory.jac +0 -0
  52. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/runtime/permission.impl.jac +0 -0
  53. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/runtime/permission.jac +0 -0
  54. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/runtime/prompt.impl.jac +0 -0
  55. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/runtime/prompt.jac +0 -0
  56. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/runtime/skills.impl.jac +0 -0
  57. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/runtime/skills.jac +0 -0
  58. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/serve_entry.jac +0 -0
  59. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/skills/jac-by-llm/SKILL.md +0 -0
  60. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/skills/jac-has-fields/SKILL.md +0 -0
  61. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/skills/jac-impl-files/SKILL.md +0 -0
  62. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/skills/jac-scaffold/SKILL.md +0 -0
  63. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/tool/__init__.jac +0 -0
  64. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/tool/git.impl.jac +0 -0
  65. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/tool/git.jac +0 -0
  66. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/tool/mcp.impl.jac +0 -0
  67. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/tool/mcp.jac +0 -0
  68. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/tool/meta/__init__.jac +0 -0
  69. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/tool/meta/delegation.impl.jac +0 -0
  70. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/tool/meta/delegation.jac +0 -0
  71. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/tool/meta/question.impl.jac +0 -0
  72. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/tool/meta/question.jac +0 -0
  73. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/tool/meta/task.impl.jac +0 -0
  74. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/tool/meta/task.jac +0 -0
  75. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/tool/meta/think.impl.jac +0 -0
  76. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/tool/meta/think.jac +0 -0
  77. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/tool/meta/todo.impl.jac +0 -0
  78. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/tool/meta/todo.jac +0 -0
  79. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/tool/meta/validate.impl.jac +0 -0
  80. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/tool/meta/validate.jac +0 -0
  81. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/tool/net/__init__.jac +0 -0
  82. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/tool/net/preview.impl.jac +0 -0
  83. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/tool/net/preview.jac +0 -0
  84. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/tool/net/web.impl.jac +0 -0
  85. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/tool/net/web.jac +0 -0
  86. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/tool/read/__init__.jac +0 -0
  87. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/tool/read/filesystem.impl.jac +0 -0
  88. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/tool/read/filesystem.jac +0 -0
  89. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/tool/read/jac_analyzer.impl.jac +0 -0
  90. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/tool/read/jac_analyzer.jac +0 -0
  91. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/tool/read/load_jac_skill.impl.jac +0 -0
  92. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/tool/read/load_jac_skill.jac +0 -0
  93. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/tool/read/search.impl.jac +0 -0
  94. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/tool/read/search.jac +0 -0
  95. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/tool/run/__init__.jac +0 -0
  96. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/tool/run/guarded.impl.jac +0 -0
  97. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/tool/run/guarded.jac +0 -0
  98. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/tool/run/jac_tools.impl.jac +0 -0
  99. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/tool/run/jac_tools.jac +0 -0
  100. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/tool/run/shell.impl.jac +0 -0
  101. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/tool/run/shell.jac +0 -0
  102. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/tool/write/__init__.jac +0 -0
  103. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/tool/write/checked.impl.jac +0 -0
  104. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/tool/write/checked.jac +0 -0
  105. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/util/__init__.jac +0 -0
  106. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/util/colors.jac +0 -0
  107. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/util/sandbox.impl.jac +0 -0
  108. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/util/sandbox.jac +0 -0
  109. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/util/tool_output.impl.jac +0 -0
  110. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder/util/tool_output.jac +0 -0
  111. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder.egg-info/dependency_links.txt +0 -0
  112. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder.egg-info/entry_points.txt +0 -0
  113. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder.egg-info/requires.txt +0 -0
  114. {jac_coder-0.2.2 → jac_coder-0.2.3}/jac_coder.egg-info/top_level.txt +0 -0
  115. {jac_coder-0.2.2 → jac_coder-0.2.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jac-coder
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: AI coding agent backend for Jac, powered by jac-byllm
5
5
  Requires-Python: >=3.12
6
6
  Requires-Dist: python-dotenv>=1.0.0
@@ -524,6 +524,28 @@ impl api_get_model() -> dict {
524
524
  return _get_model();
525
525
  }
526
526
 
527
+ impl api_check_local_setup(alias: str) -> dict {
528
+ runtime: bool = False;
529
+ try {
530
+ import llama_cpp;
531
+ runtime = True;
532
+ } except ImportError { }
533
+
534
+ model: bool = False;
535
+ valid_alias: bool = False;
536
+ try {
537
+ import from byllm.local_runtime { LOCAL_MODELS }
538
+ import from byllm.model_cache { is_downloaded }
539
+ spec = LOCAL_MODELS.get(alias);
540
+ if spec is not None {
541
+ valid_alias = True;
542
+ model = is_downloaded(alias, spec);
543
+ }
544
+ } except Exception { }
545
+
546
+ return {"runtime": runtime, "model": model, "valid_alias": valid_alias};
547
+ }
548
+
527
549
 
528
550
  # ---------------------------------------------------------------------------
529
551
  # MCP management API
@@ -78,6 +78,9 @@ def api_set_model(model: str) -> dict;
78
78
  """Get current model info."""
79
79
  def api_get_model() -> dict;
80
80
 
81
+ """Check if local model runtime and model weights are ready for a given alias."""
82
+ def api_check_local_setup(alias: str) -> dict;
83
+
81
84
  # --- MCP management API ---
82
85
  """Add or update an MCP server."""
83
86
  def api_mcp_add(name: str, config: dict) -> dict;
@@ -140,11 +140,19 @@ Build breadth-first — get all files written and app running before polishing.
140
140
  ## Workflow
141
141
  1. Read .jaccoder/progress.md if it exists, update it as you go.
142
142
  2. analyze_project(dir) for existing projects.
143
- 3. jac.toml needs `[plugins.client.vite]` with tailwindcss. Run `jac install` after toml changes.
144
- 4. Build bottom-up: services styles/global.css hooks components → layout.
145
- 5. styles/global.css: `@import "tailwindcss"`, @theme tokens. In main.jac: `cl import ".styles.global.css";`
146
- 6. After all files: jac start --dev main.jac (background=True).
147
- 7. browser_validate(url) follow FAIL action instructions.
143
+ 3. Build bottom-up: services hooks components layout.
144
+ 4. **If `components/ui/` exists AND jac.toml has `[jac-shadcn]` (jac-shadcn project):**
145
+ - Load `jac-shadcn-components` skill BEFORE writing any UI.
146
+ - Load `jac-cl-styling` for conditional class and cn() patterns.
147
+ - `styles/global.css` and `jac.toml` are pre-configured do NOT recreate or modify them.
148
+ - Import UI from `components/ui/` instead of writing raw primitive components. No inline styles.
149
+ **Otherwise (standard project):**
150
+ - jac.toml needs `[plugins.client.vite]` with tailwindcss. Run `jac install` after toml changes.
151
+ - styles/global.css: `@import "tailwindcss"`, @theme tokens. In main.jac: `cl import ".styles.global.css";`
152
+ - Load `jac-cl-styling` when writing Tailwind classes or dynamic styles.
153
+ - Load `jac-npm-packages` when adding third-party npm packages.
154
+ 5. After all files: jac start --dev main.jac (background=True).
155
+ 6. browser_validate(url) → follow FAIL action instructions.
148
156
 
149
157
  Load the relevant `load_jac_skill(name)` from `<jac-skills-available>` BEFORE writing Jac. Tools catch obvious anti-patterns — trust BLOCKED messages.
150
158
  """;
@@ -99,6 +99,7 @@ impl set_model(model: str) -> dict {
99
99
  }
100
100
  model_name = model;
101
101
  llm.model_name = model;
102
+ llm.postinit();
102
103
  _config.default_model = model;
103
104
  return {"status": "ok", "model": model};
104
105
  }
@@ -87,4 +87,5 @@ with entry {
87
87
  # with the hardcoded default before load_config() runs.
88
88
  model_name = _config.default_model;
89
89
  llm.model_name = model_name;
90
+ llm.postinit();
90
91
  }
@@ -73,7 +73,13 @@ def _get_registry() -> Any {
73
73
  target_anchor = edge_anchor.target;
74
74
  target_anchor.populate();
75
75
  if isinstance(target_anchor.archetype, McpRegistry) {
76
- return target_anchor.archetype;
76
+ reg = target_anchor.archetype;
77
+ # Migration guard: nodes persisted before disconnected_servers was
78
+ # added to the schema won't have this attribute after deserialization.
79
+ if not hasattr(reg, "disconnected_servers") {
80
+ reg.disconnected_servers = [];
81
+ }
82
+ return reg;
77
83
  }
78
84
  } except Exception { }
79
85
  }
@@ -0,0 +1,235 @@
1
+ """Shared file-based structured logger for JacCoder.
2
+
3
+ Enabled via JACCODER_LOG=1. Writes JSON lines to
4
+ .jaccoder/logs/<branch>_<timestamp>.log in the working directory.
5
+
6
+ Used by both cli.jac (single session, session_id="") and server.jac
7
+ (multiple concurrent sessions, keyed by session_id). Thread-safe.
8
+ """
9
+
10
+ import os;
11
+ import sys;
12
+ import json;
13
+ import threading;
14
+ import from datetime { datetime }
15
+
16
+
17
+ glob _log_file_path: str = "";
18
+ glob _log_file_handle: object = None;
19
+ glob _log_iteration: dict = {}; # int counter per session_id
20
+ glob _log_turn_cost: dict = {}; # float accumulator per session_id
21
+ glob _log_lock: threading.Lock = threading.Lock();
22
+
23
+
24
+ """True when JACCODER_LOG=1 is set and logger is initialized."""
25
+ def is_logging_enabled() -> bool {
26
+ return bool(_log_file_handle);
27
+ }
28
+
29
+
30
+ """Initialize the file logger. No-op if JACCODER_LOG is not set.
31
+
32
+ Call once at startup. Safe to call multiple times — only initializes once.
33
+ model_name is written to the session_start record.
34
+ """
35
+ def init_file_logger(model_name: str = "") -> None {
36
+ global _log_file_path, _log_file_handle;
37
+
38
+ if _log_file_handle {
39
+ return;
40
+ }
41
+ if not os.environ.get("JACCODER_LOG", "") {
42
+ return;
43
+ }
44
+
45
+ ts = datetime.now().strftime("%Y-%m-%d_%H-%M-%S");
46
+ branch = "";
47
+ try {
48
+ import subprocess;
49
+ r = subprocess.run(["git", "branch", "--show-current"], capture_output=True, text=True);
50
+ branch = r.stdout.strip().replace("/", "-") if r.returncode == 0 else "";
51
+ } except Exception {
52
+ branch = "";
53
+ }
54
+
55
+ log_dir = os.path.join(os.getcwd(), ".jaccoder", "logs");
56
+ os.makedirs(log_dir, exist_ok=True);
57
+
58
+ name = f"{branch}_{ts}.log" if branch else f"{ts}.log";
59
+ _log_file_path = os.path.join(log_dir, name);
60
+
61
+ try {
62
+ _log_file_handle = open(_log_file_path, "w", buffering=1);
63
+ } except OSError as e {
64
+ sys.stderr.write(f"[JacCoder] Could not open log file: {e}\n");
65
+ return;
66
+ }
67
+
68
+ import from jac_coder.runtime.cost_tracker { enable_cost_tracking }
69
+ enable_cost_tracking();
70
+
71
+ _log_file_handle.write(json.dumps({
72
+ "event": "session_start",
73
+ "branch": branch,
74
+ "timestamp": ts,
75
+ "model": model_name
76
+ }) + "\n");
77
+ _log_file_handle.flush();
78
+ sys.stderr.write(f"[JacCoder] Log: {_log_file_path}\n");
79
+ }
80
+
81
+
82
+ """Write one structured event. Thread-safe. No-op if logger not initialized.
83
+
84
+ session_id: use "" for CLI (single session), or actual session UUID for server.
85
+ """
86
+ def file_log_event(session_id: str, event_type: str, data: dict) -> None {
87
+ global _log_file_handle, _log_iteration, _log_turn_cost, _log_lock;
88
+
89
+ if not _log_file_handle {
90
+ return;
91
+ }
92
+
93
+ with _log_lock {
94
+ try {
95
+ ts = data.get("timestamp", 0.0);
96
+ record: dict = {"event": event_type, "ts": ts};
97
+ if session_id {
98
+ record["session"] = session_id;
99
+ }
100
+
101
+ it: int = _log_iteration.get(session_id, 0);
102
+ tc: float = _log_turn_cost.get(session_id, 0.0);
103
+ try {
104
+ import from jac_coder.infra.config { llm as _llm }
105
+ _current_model: str = str(_llm.model_name);
106
+ } except Exception {
107
+ _current_model: str = "";
108
+ }
109
+
110
+ if event_type == "llm_tool_call" {
111
+ it += 1;
112
+ _log_iteration[session_id] = it;
113
+ record["iteration"] = it;
114
+ record["tool"] = data.get("tool", "");
115
+ record["args"] = data.get("args", {});
116
+
117
+ } elif event_type == "llm_tool_result" {
118
+ record["iteration"] = it;
119
+ record["tool"] = data.get("tool", "");
120
+ result_str = str(data.get("result", ""));
121
+ record["ok"] = (
122
+ not result_str.startswith("Error")
123
+ and "BLOCKED" not in result_str
124
+ and "LOOP DETECTED" not in result_str
125
+ );
126
+ record["error"] = not record["ok"];
127
+ record["result_preview"] = result_str[:1000];
128
+ if "LOOP DETECTED" in result_str {
129
+ record["loop_detected"] = True;
130
+ }
131
+
132
+ } elif event_type == "llm_usage" {
133
+ total = data.get("total") or {};
134
+ inp = int(total.get("prompt_tokens") or total.get("input_tokens") or 0);
135
+ out = int(total.get("completion_tokens") or total.get("output_tokens") or 0);
136
+ cached = int(
137
+ total.get("cache_read_input_tokens")
138
+ or (total.get("prompt_tokens_details") or {}).get("cached_tokens")
139
+ or 0
140
+ );
141
+ req_cost = float(data.get("request_cost") or 0.0);
142
+ tc += req_cost;
143
+ _log_turn_cost[session_id] = tc;
144
+ record["iteration"] = it;
145
+ record["model"] = _current_model;
146
+ record["input_tokens"] = inp;
147
+ record["output_tokens"] = out;
148
+ record["cached_tokens"] = cached;
149
+ record["effective_input_tokens"] = inp - cached;
150
+ record["cost_usd"] = round(req_cost, 6);
151
+
152
+ } elif event_type == "llm_steps_done" {
153
+ record["iterations"] = data.get("iterations", 0);
154
+ record["reason"] = data.get("reason", "");
155
+
156
+ } elif event_type == "turn_summary" {
157
+ record["tools_count"] = data.get("tools_count", 0);
158
+ record["files_modified"] = data.get("files_modified", []);
159
+ record["errors"] = data.get("errors", 0);
160
+ record["duration_s"] = data.get("duration_s", 0.0);
161
+ record["turn_cost_usd"] = round(tc, 6);
162
+ record["model"] = _current_model;
163
+ _log_turn_cost[session_id] = 0.0;
164
+ _log_iteration[session_id] = 0;
165
+
166
+ } elif event_type == "llm_thought" {
167
+ record["iteration"] = it;
168
+ record["thought_preview"] = str(data.get("content", ""))[:500];
169
+
170
+ } elif event_type == "tool_end" {
171
+ record["iteration"] = it;
172
+ record["tool"] = data.get("tool", "");
173
+ record["step"] = data.get("step", 0);
174
+ record["status"] = data.get("status", "done");
175
+ record["duration_ms"] = data.get("duration_ms", 0);
176
+ record["result_preview"] = str(data.get("result_preview", ""));
177
+
178
+ } elif event_type == "file_written" {
179
+ record["iteration"] = it;
180
+ record["path"] = data.get("path", "");
181
+ record["lines"] = data.get("lines", 0);
182
+
183
+ } elif event_type == "file_edited" {
184
+ record["iteration"] = it;
185
+ record["path"] = data.get("path", "");
186
+ record["replacements"] = data.get("replacements", 0);
187
+
188
+ } elif event_type == "context_compaction" {
189
+ record["status"] = data.get("status", "");
190
+ if data.get("turns_to_summarize") is not None {
191
+ record["turns_to_summarize"] = data.get("turns_to_summarize");
192
+ }
193
+ if data.get("summary_length") is not None {
194
+ record["summary_length"] = data.get("summary_length");
195
+ }
196
+
197
+ } else {
198
+ return;
199
+ }
200
+
201
+ _log_file_handle.write(json.dumps(record) + "\n");
202
+
203
+ } except Exception as e {
204
+ sys.stderr.write(f"[JacCoder] log write error: {e}\n");
205
+ }
206
+ }
207
+ }
208
+
209
+
210
+ """Write session_end summary and close the log file. Safe to call multiple times."""
211
+ def close_file_logger() -> None {
212
+ global _log_file_handle, _log_file_path, _log_lock;
213
+
214
+ with _log_lock {
215
+ if not _log_file_handle {
216
+ return;
217
+ }
218
+
219
+ try {
220
+ import from jac_coder.runtime.cost_tracker { get_summary, get_per_call_data }
221
+ _log_file_handle.write(json.dumps({
222
+ "event": "session_end",
223
+ "summary": get_summary(),
224
+ "per_call": get_per_call_data()
225
+ }) + "\n");
226
+ _log_file_handle.flush();
227
+ _log_file_handle.close();
228
+ sys.stderr.write(f"[JacCoder] Log saved: {_log_file_path}\n");
229
+ } except Exception as e {
230
+ sys.stderr.write(f"[JacCoder] log close error: {e}\n");
231
+ } finally {
232
+ _log_file_handle = None;
233
+ }
234
+ }
235
+ }
@@ -24,6 +24,7 @@ Message format (JSON-RPC 2.0):
24
24
  api.validate → validate the configured API key
25
25
  model.get → get the currently active model
26
26
  model.set → switch the active model
27
+ model.local.check → check if local runtime + model weights are ready
27
28
 
28
29
  Inbound notification (extension → server, no "id", no reply):
29
30
  chat.cancel → abort the running agent turn
@@ -46,6 +47,7 @@ import logging;
46
47
  import threading;
47
48
  import from typing { Any, IO }
48
49
  import from importlib.metadata { version as _pkg_version, PackageNotFoundError }
50
+ import from jac_coder.runtime.file_logger { init_file_logger, file_log_event, close_file_logger }
49
51
 
50
52
  import from jac_coder.tool.run.guarded { _permission_events, _permission_decisions }
51
53
 
@@ -63,7 +65,8 @@ import from jac_coder.api {
63
65
  api_mcp_delete as _api_mcp_delete,
64
66
  api_mcp_list as _api_mcp_list,
65
67
  api_set_model as _api_set_model,
66
- api_get_model as _api_get_model
68
+ api_get_model as _api_get_model,
69
+ api_check_local_setup as _api_check_local_setup
67
70
  }
68
71
  import from jac_coder.runtime.events { request_abort_thread }
69
72
  import from jac_coder.runtime.cost_tracker { get_summary as _get_usage_summary, is_cost_tracking_enabled, reset as _reset_usage }
@@ -92,6 +95,8 @@ glob _reply_flags: dict = {};
92
95
  glob _reply_lock: threading.Lock = threading.Lock();
93
96
 
94
97
 
98
+
99
+
95
100
  """Return installed jac-coder version, or 'unknown' if not installed as a package."""
96
101
  def _get_server_version() -> str {
97
102
  try {
@@ -196,6 +201,7 @@ def _handle_chat(req_id: Any, params: dict) -> None {
196
201
  raise KeyboardInterrupt("cancelled"); # unwinds the agent immediately
197
202
  }
198
203
  _write(_notify("chat.event", {"type": event_type, "session_id": session_id, **data}));
204
+ file_log_event(session_id, event_type, data);
199
205
  }
200
206
 
201
207
  try {
@@ -369,6 +375,9 @@ async def _dispatch(req_id: Any, method: str, params: dict) -> None {
369
375
  _write(_ok(req_id, _api_set_model(model=model)));
370
376
  }
371
377
 
378
+ } elif method == "model.local.check" {
379
+ _write(_ok(req_id, _api_check_local_setup(alias=params.get("alias", ""))));
380
+
372
381
  } else {
373
382
  _write(_err(req_id, -32601, f"Method not found: {method}"));
374
383
  }
@@ -395,6 +404,9 @@ async def _dispatch(req_id: Any, method: str, params: dict) -> None {
395
404
  async def _main() -> None {
396
405
  loop: asyncio.AbstractEventLoop = asyncio.get_event_loop();
397
406
 
407
+ # Initialize file logger if JACCODER_LOG=1 is set.
408
+ init_file_logger(model_name=str(llm.model_name));
409
+
398
410
  # Tell the extension we are ready to receive requests.
399
411
  _write(_notify("server.ready", {"service": "jac-coder", "version": _get_server_version()}));
400
412
 
@@ -410,6 +422,7 @@ async def _main() -> None {
410
422
 
411
423
  if not line {
412
424
  # Empty string = EOF = extension disconnected. Exit cleanly.
425
+ close_file_logger();
413
426
  break;
414
427
  }
415
428
 
@@ -27,6 +27,7 @@ Check off with `✅` when shipped, `🟡` when in-progress, `⬜` when planned.
27
27
  - ✅ `jac-cl-routing` — Router, Routes, Route, Navigate, useNavigate, Link
28
28
  - ✅ `jac-cl-auth` — jacLogin, jacSignup, jacLogout, jacIsLoggedIn, protected routes
29
29
  - ✅ `jac-cl-organization` — file layout, component reuse discipline, hook pattern, when to extract
30
+ - ✅ `jac-shadcn-components` — using pre-installed jac-shadcn primitives (components/ui/), import patterns, composition rules, styling, icons
30
31
  - ⬜ `jac-cl-forms` (maybe) — controlled inputs, submit handlers, validation patterns
31
32
 
32
33
  ### Server (`.sv.jac`)
@@ -0,0 +1,134 @@
1
+ ---
2
+ name: jac-cl-auth
3
+ description: Client-side authentication — signing up, logging in, logging out, and protecting pages behind login. Load when adding any auth UI or guarding pages from unauthenticated users. Pair with `jac-sv-auth` (server side of the auth loop), `jac-cl-routing` (post-login navigation).
4
+ ---
5
+
6
+ Client auth uses four helpers from `@jac/runtime`. **Return types differ — get them wrong and the file fails `jac check` with E1001:**
7
+
8
+ | Helper | Async? | Returns | Pre-declare as |
9
+ |---|---|---|---|
10
+ | `jacSignup(email, password)` | yes | `dict` (account info) | `result: dict \| None = None` |
11
+ | `jacLogin(email, password)` | yes | `bool` | `ok: bool = False` |
12
+ | `jacLogout()` | no | `None` | — (call it, no assign) |
13
+ | `jacIsLoggedIn()` | no | `bool` | — (use inline) |
14
+
15
+ Both async helpers' return values are truthy on success / falsy on failure, so `if not result { ... }` works for both. But the **types** differ — typing a `jacSignup` result as `bool` fails `jac check` with `E1001: Cannot assign dict to bool`.
16
+
17
+ ## ⚠ Read first — signup + first `def:priv` call must be 3 awaited steps
18
+
19
+ When the same form that signs a user up also calls a `def:priv` endpoint (saving the new user's profile, preferences, default workspace), the call order is **fixed and each step MUST be awaited**:
20
+
21
+ 1. `await jacSignup(email, password)` — creates the account; **no session yet**
22
+ 2. `await jacLogin(email, password)` — establishes the session cookie
23
+ 3. `await save_profile(...)` — only NOW does the `def:priv` call have an authenticated session
24
+
25
+ ```jac
26
+ # `save_profile` here is YOUR server function (def:priv) — imported from a .sv.jac module.
27
+ async def handle_register(name: str, email: str, password: str) -> str {
28
+ # Pre-declare every var that holds an `await` result. `let` scoping in the
29
+ # generated JS can otherwise leave them undefined at the if-check.
30
+ # Note the per-helper types: jacSignup -> dict, jacLogin -> bool.
31
+ signup_result: dict | None = None;
32
+ login_ok: bool = False;
33
+ profile_result: Any = None;
34
+
35
+ signup_result = await jacSignup(email, password);
36
+ if not signup_result { return "registration failed"; }
37
+
38
+ login_ok = await jacLogin(email, password);
39
+ if not login_ok { return "login after signup failed"; }
40
+
41
+ profile_result = await save_profile(name, email);
42
+ if profile_result is None { return "save failed"; }
43
+ if not profile_result.success { return profile_result.message; }
44
+
45
+ return "success";
46
+ }
47
+ ```
48
+
49
+ **Why every `await` matters:** all three are async (return Promises). Skipping any `await` fires that call as a background Promise and lets the next line execute immediately. If `save_profile` fires before `jacLogin` completes, the session cookie isn't set yet → server gets the request without auth → `401 Unauthorized`. **Silent at compile time — only surfaces at runtime on first registration.**
50
+
51
+ **Why pre-declare:** in `.cl.jac`, `var = await fn()` can compile to a JS `let var = ...` that is scoped tighter than the surrounding function (especially around `try`/`except` and certain async patterns), so `if not var { ... }` on the next line throws `ReferenceError: var is not defined`. Declaring `var: T = default;` at the top forces a function-scope `let` that the if-check can see.
52
+
53
+ ---
54
+
55
+ ```jac
56
+ import from "@jac/runtime" { jacLogin, jacSignup, jacLogout, jacIsLoggedIn, Navigate }
57
+
58
+ # Login attempt — call from a submit handler or effect. Returns a status string.
59
+ # Pre-declare `ok` at the top — see "Why pre-declare" above.
60
+ async def try_login(email: str, password: str) -> str {
61
+ ok: bool = False;
62
+ try {
63
+ ok = await jacLogin(email, password);
64
+ if ok {
65
+ return "success";
66
+ }
67
+ return "invalid credentials";
68
+ } except Exception as e {
69
+ return "error";
70
+ }
71
+ }
72
+
73
+ # Signup is usually followed by a login to establish the session.
74
+ async def try_signup(email: str, password: str) -> str {
75
+ signup_result: dict | None = None; # jacSignup returns dict
76
+ login_ok: bool = False; # jacLogin returns bool
77
+ try {
78
+ signup_result = await jacSignup(email, password);
79
+ if not signup_result {
80
+ return "signup failed (email may be in use)";
81
+ }
82
+ login_ok = await jacLogin(email, password);
83
+ if login_ok {
84
+ return "success";
85
+ }
86
+ return "login after signup failed";
87
+ } except Exception as e {
88
+ return "error";
89
+ }
90
+ }
91
+
92
+ # Logout is synchronous — no await.
93
+ def perform_logout() -> None {
94
+ jacLogout();
95
+ }
96
+
97
+ # Typical protected page — inline guard at the top.
98
+ def:pub Dashboard() -> JsxElement {
99
+ if not jacIsLoggedIn() {
100
+ return <Navigate to="/login" replace={True} />;
101
+ }
102
+ return <div className="p-4">Welcome to the dashboard</div>;
103
+ }
104
+ ```
105
+
106
+ ## Auth-relevant `@jac/runtime` exports
107
+
108
+ `jacLogin`, `jacSignup`, `jacLogout`, `jacIsLoggedIn`, plus `Navigate` / `useNavigate` for post-auth redirects. For the full client export list and the "compile-passes-build-fails" rule, see `jac-cl-components`.
109
+
110
+ ## Pitfalls
111
+
112
+ - Import auth helpers from `@jac/runtime` — see `jac-core-cheatsheet` for import form rules.
113
+ - Post-logout pattern: `jacLogout(); nav("/login");` — synchronous call, then navigate.
114
+ - Post-login navigation uses `useNavigate()` from `jac-cl-routing` — `nav = useNavigate(); ... nav("/dashboard");` after a successful login.
115
+ - **`jacSignup` does NOT establish a session.** ALWAYS follow with a `jacLogin` call using the same credentials — signup alone leaves the user unauthenticated.
116
+ - **`jacLogin` and `jacSignup` are `async` — always `await`. `jacLogout` and `jacIsLoggedIn` are sync — NEVER `await` them.** `await jacLogout()` type-errors; missing `await` on `jacLogin` silently returns a coroutine instead of the result.
117
+ - **Pre-declare any var that holds an `await` result before the assignment.** In `.cl.jac`, `var = await fn()` can compile to a JS `let var = ...` whose scope is tighter than the surrounding function, so a later `if not var { ... }` throws `ReferenceError: var is not defined` at runtime. Compile passes; the page just blanks. Fix: declare the var with a default at the top, then assign.
118
+
119
+ ```jac
120
+ # FRAGILE — runtime ReferenceError on the if-check
121
+ async def handle_login(email: str, password: str) -> str {
122
+ ok = await jacLogin(email, password);
123
+ if not ok { return "failed"; } # ReferenceError: ok is not defined
124
+ return "success";
125
+ }
126
+
127
+ # CORRECT — function-scope let, visible to the if-check
128
+ async def handle_login(email: str, password: str) -> str {
129
+ ok: bool = False;
130
+ ok = await jacLogin(email, password);
131
+ if not ok { return "failed"; }
132
+ return "success";
133
+ }
134
+ ```
@@ -3,7 +3,7 @@ name: jac-cl-components
3
3
  description: Writing a client-side UI component — shape, reactive state, mount effects, rendering, event handlers. Load when creating or editing any `.cl.jac` file. Pair with `jac-cl-routing` (multi-page apps), `jac-cl-organization` (file layout & hooks), `jac-cl-auth` (protected pages).
4
4
  ---
5
5
 
6
- `.cl.jac` files are client-side Jac. A component is a `def:pub` function returning `JsxElement`. State = `has` fields (assign directly, UI re-renders; NO `useState`/`setX`). Mount effects = `async can with entry`. Event handlers = `def` methods typed with ambient DOM events (`MouseEvent`, `ChangeEvent`, `FormEvent`, `KeyboardEvent`). No `to cl:` header — the extension sets client context.
6
+ `.cl.jac` files are client-side Jac. A component is a `def:pub` function returning `JsxElement`. State = `has` fields, which compile 1:1 to React `useState` — assign directly (`x = x + 1` re-renders; no `setX(...)` call) but all `useState` semantics apply: writes are async, the closure stays stale until the next render. Mount effects = `async can with entry` (compiles to `useEffect`). Event handlers = `def` methods typed with ambient DOM events (`MouseEvent`, `ChangeEvent`, `FormEvent`, `KeyboardEvent`). No `to cl:` header — the extension sets client context.
7
7
 
8
8
  ```jac
9
9
  def:pub Counter() -> JsxElement {
@@ -72,15 +72,39 @@ Fall back to `Any` (capital, built-in) only when you don't read `e`.
72
72
  - **Validation / error boundary:** `JacSchema`, `JacClientErrorBoundary`
73
73
  - **DO NOT use:** `useState`, `useEffect` (aliases exist but `has` + `async can with entry` are idiomatic)
74
74
 
75
+ ## ⚠ Critical gotchas (silent runtime bugs — read every time)
76
+
77
+ - **Hooks + `has` BEFORE any conditional return.** React hooks must fire in the same order every render. `if not jacIsLoggedIn() { return <Navigate />; } has x: int = 0;` → white screen, no compile error.
78
+ - **Mount effects (`async can with entry`) fire even when the component returns `<Navigate>`.** Guard the EFFECT body with `if jacIsLoggedIn() { ... }`, not just the render — otherwise `def:priv` calls fire and return 401 silently.
79
+
75
80
  ## Rules
76
81
 
77
82
  - **`has` fields are reactive state — assign directly.** `count = count + 1` re-renders. No `setCount`. Non-default fields come before defaulted ones (E2004 — see `jac-has-fields`).
78
- - **Hooks + `has` BEFORE any conditional return.** React hooks must fire in the same order every render. `if not jacIsLoggedIn() { return <Navigate />; } has x: int = 0;` white screen, no compile error.
79
- - **Mount effects (`async can with entry`) fire even when the component returns `<Navigate>`.** Guard the EFFECT body with `if jacIsLoggedIn() { ... }`, not just the render — otherwise `def:priv` calls fire and return 401.
80
- - **Server RPC import uses `sv import from ...services.X { fn, Types }`** (prefix required). Plain `import from` to a `.sv.jac` breaks the Vite build. Include obj/node types too. See `jac-fullstack-patterns`.
83
+ - **Derived values are locals, not `has` fields.** Anything computable from props/params/hook results gets recomputed every render so it's a local. Putting it in `has` forces a top-level write to keep it in sync, which can cascade to React error #301. Rule: `has` is only for event-driven or async values (user input, fetch results, server data).
84
+
85
+ ```jac
86
+ # FRAGILE
87
+ has has_filter: bool = False;
88
+ if useParams()["category"] is not None { has_filter = True; }
89
+
90
+ # CORRECT
91
+ has_filter: bool = useParams()["category"] is not None;
92
+ ```
93
+ - **Server RPC import uses `sv import from ...services.X { fn, Types }`** (prefix required). Plain `import from` to a `.sv.jac` breaks the Vite build. Include obj/node types too — they're needed to type your `has` state (next rule). See `jac-fullstack-patterns`.
94
+ - **Type `has` state with the imported `sv` types — `list[Any]` loses the element type.** Store data from `sv import` calls in fields typed with the actual node/obj. Without it, attribute access in loops fails `E1032: Type is Unknown`.
95
+
96
+ ```jac
97
+ sv import from .services.linkedin { Post };
98
+
99
+ # FRAGILE
100
+ has posts: list[Any] = []; # E1032 on p.title in any loop
101
+
102
+ # CORRECT
103
+ has posts: list[Post] = []; # `p` in `for p in posts` is typed Post
104
+ ```
81
105
  - **Call server endpoints POSITIONAL, not kwargs.** `save(a, b)` works; `save(a=a, b=b)` sends empty body → 422. Also: the caller's variable names become the JSON keys - they must match the server parameter names exactly. See `jac-fullstack-patterns`.
82
106
  - **JSX ternary is Python-style:** `{X if cond else Y}`. NOT `{cond ? X : Y}` (parse error even inside JSX). Short-circuit also works: `{cond and <X />}`.
83
- - **Iterate with comprehensions, NOT `.map()`.** `items.map(...)` on a Jac list fails E1030. Use `[<li>...</li> for i in items]` inside JSX.
107
+ - **Iterate with comprehensions, NOT `.map()`.** `items.map(...)` on a Jac list fails E1030. Use `[<li>...</li> for i in items]` inside JSX. `for i, x in enumerate(items)` parse-fails E0001 — use a single loop var, or `range(len(items))` with index access: `[<li key={str(i)}>{items[i]}</li> for i in range(len(items))]`.
84
108
  - **Dict access uses `[key]`, NOT `.get()`.** `params.get("id")` runtime-fails in browser (`useParams` returns plain JS object). Use `val = params["id"]; if val is None { ... }`.
85
109
  - **Guard None/null/undefined when iterating or dotting into server data.** Runtime-only failure (`Cannot read properties of null/undefined`), nothing at compile. Four hot spots — single-level access is the most common:
86
110
 
@@ -91,10 +115,10 @@ status = result.recipe.status; # result.recipe is None
91
115
  rows = [<Card title={s["title"]} /> for s in songs]; # any s is None in list → crash
92
116
  first = songs[0]["title"]; # empty list → crash
93
117
 
94
- # SAFE — narrow, filter, or length-check first (use `!= None`, NOT `is not None` in .cl.jac)
95
- if result != None { total = result.total_posts; }
96
- if result.recipe != None { status = result.recipe.status; }
97
- rows = [<Card ... /> for s in songs if s != None];
118
+ # SAFE — narrow, filter, or length-check first
119
+ if result is not None { total = result.total_posts; }
120
+ if result.recipe is not None { status = result.recipe.status; }
121
+ rows = [<Card ... /> for s in songs if s is not None];
98
122
  if len(songs) > 0 { first = songs[0]["title"]; }
99
123
  ```
100
124
 
@@ -102,11 +126,10 @@ Also works: short-circuit in JSX — `{result and <X total={result.total_posts}
102
126
 
103
127
  **For server response objects (dicts/lists from `sv import` calls), prefer truthy checks (`if result {`) over `!= None`.** The `!=` operator uses deep equality which calls `Object.keys()` — crashes with `"Cannot convert undefined or null to object"` if the value is `null`/`undefined`. `!= None` is safe for primitives (strings, ints, bools) but not for complex objects returned from server calls.
104
128
  - **Event params are typed — `MouseEvent`/`ChangeEvent`/etc.** `e: any` (lowercase) is the Python `any()` builtin, fails E1103. Use capital `Any` (no import) for untyped.
129
+ - **`style` prop takes a `dict[str, object]`, not a CSS string.** `<div style="color: red">` fails E1103. Use inline dict `<div style={{"color": "red"}}>` or move styling to `className` + a CSS file.
105
130
  - **JSX uses `className`, curly-brace interpolation `{expr}`, camelCase events** (`onClick`, `onChange`).
106
131
  - **No `to cl:` / `cl def:pub` / `cl { }` in `.cl.jac` files.** Extension already sets context. Braced blocks are deprecated (W0064).
107
132
  - **Top-level component name is `def:pub app()`** — lowercase. Runtime mounts the literal name.
108
- - **No JSX comments — `{/* ... */}` is INVALID Jac syntax.** Never write JSX-style comments. Use `# comment` on lines outside JSX blocks, or just delete the comment.
109
- - **`is not None` is a `.cl.jac` compiler bug — use `!= None`.** Both `x is None` and `x is not None` compile to `x === null` in the generated JS — the `not` is silently dropped. Your guard runs the OPPOSITE of what you wrote (a `for ... if x is not None` comprehension keeps only the nulls). Use `!= None` / `== None` in client code. (Server `.sv.jac` compiles to Python normally and is unaffected.)
110
133
 
111
134
  ## See also
112
135