claude-smart 0.2.47 → 0.2.49

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 (139) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +6 -2
  3. package/bin/claude-smart.js +289 -56
  4. package/package.json +1 -1
  5. package/plugin/.claude-plugin/plugin.json +1 -1
  6. package/plugin/.codex-plugin/plugin.json +1 -1
  7. package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +36 -2
  8. package/plugin/dashboard/package-lock.json +61 -390
  9. package/plugin/dashboard/package.json +2 -2
  10. package/plugin/opencode/dist/server.mjs +60 -2
  11. package/plugin/opencode/server.mts +64 -2
  12. package/plugin/pyproject.toml +2 -2
  13. package/plugin/scripts/_lib.sh +58 -6
  14. package/plugin/scripts/backend-service.sh +15 -10
  15. package/plugin/scripts/codex-hook.js +16 -4
  16. package/plugin/scripts/dashboard-service.sh +1 -1
  17. package/plugin/scripts/ensure-plugin-root.sh +106 -8
  18. package/plugin/scripts/opencode-claude-compat.js +8 -1
  19. package/plugin/scripts/smart-install.sh +101 -20
  20. package/plugin/src/README.md +1 -1
  21. package/plugin/src/claude_smart/cli.py +79 -29
  22. package/plugin/src/claude_smart/env_config.py +53 -11
  23. package/plugin/uv.lock +5 -5
  24. package/plugin/vendor/reflexio/.env.example +7 -0
  25. package/plugin/vendor/reflexio/pyproject.toml +2 -1
  26. package/plugin/vendor/reflexio/reflexio/README.md +8 -5
  27. package/plugin/vendor/reflexio/reflexio/cli/README.md +3 -0
  28. package/plugin/vendor/reflexio/reflexio/client/client.py +40 -6
  29. package/plugin/vendor/reflexio/reflexio/lib/_config.py +23 -18
  30. package/plugin/vendor/reflexio/reflexio/lib/_interactions.py +16 -1
  31. package/plugin/vendor/reflexio/reflexio/lib/_search.py +15 -11
  32. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +18 -0
  33. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +1 -0
  34. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +24 -3
  35. package/plugin/vendor/reflexio/reflexio/server/README.md +25 -8
  36. package/plugin/vendor/reflexio/reflexio/server/__init__.py +21 -2
  37. package/plugin/vendor/reflexio/reflexio/server/api.py +148 -3277
  38. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/README.md +4 -3
  39. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +16 -1
  40. package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +62 -36
  41. package/plugin/vendor/reflexio/reflexio/server/deployment_profile.py +69 -0
  42. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +424 -0
  43. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_json_extraction.py +249 -0
  44. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_structured_output.py +195 -0
  45. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +152 -0
  46. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +980 -0
  47. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_types.py +110 -0
  48. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +72 -1817
  49. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +56 -4
  50. package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +183 -1
  51. package/plugin/vendor/reflexio/reflexio/server/middleware.py +244 -0
  52. package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +9 -1
  53. package/plugin/vendor/reflexio/reflexio/server/rate_limit.py +79 -0
  54. package/plugin/vendor/reflexio/reflexio/server/routes/__init__.py +6 -0
  55. package/plugin/vendor/reflexio/reflexio/server/routes/_common.py +25 -0
  56. package/plugin/vendor/reflexio/reflexio/server/routes/_metering.py +98 -0
  57. package/plugin/vendor/reflexio/reflexio/server/routes/braintrust.py +129 -0
  58. package/plugin/vendor/reflexio/reflexio/server/routes/config.py +210 -0
  59. package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +549 -0
  60. package/plugin/vendor/reflexio/reflexio/server/routes/interactions.py +320 -0
  61. package/plugin/vendor/reflexio/reflexio/server/routes/playbooks.py +578 -0
  62. package/plugin/vendor/reflexio/reflexio/server/routes/profiles.py +423 -0
  63. package/plugin/vendor/reflexio/reflexio/server/routes/provenance.py +345 -0
  64. package/plugin/vendor/reflexio/reflexio/server/routes/search.py +349 -0
  65. package/plugin/vendor/reflexio/reflexio/server/routes/system.py +261 -0
  66. package/plugin/vendor/reflexio/reflexio/server/scheduling.py +132 -0
  67. package/plugin/vendor/reflexio/reflexio/server/services/README.md +3 -2
  68. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/__init__.py +38 -0
  69. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_batch_progress.py +298 -0
  70. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_config_filter.py +152 -0
  71. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +243 -0
  72. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +299 -0
  73. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_status_change.py +273 -0
  74. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +258 -0
  75. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +17 -1183
  76. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +8 -1
  77. package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/__init__.py +13 -0
  78. package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/scheduler.py +142 -0
  79. package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/worker.py +193 -0
  80. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +24 -41
  81. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +335 -113
  82. package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +362 -81
  83. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +68 -490
  84. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_clustering.py +184 -0
  85. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_postprocessing.py +130 -0
  86. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_prompt_formatting.py +212 -0
  87. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +258 -69
  88. package/plugin/vendor/reflexio/reflexio/server/services/profile/service.py +44 -22
  89. package/plugin/vendor/reflexio/reflexio/server/services/publish_learning_worker.py +288 -0
  90. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +31 -4
  91. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +10 -1167
  92. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +196 -353
  93. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +0 -1513
  94. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_learning_jobs.py +379 -0
  95. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +17 -6
  96. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +7 -1281
  97. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +8 -3
  98. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_share_links.py +30 -0
  99. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/__init__.py +9 -0
  100. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_agent_run_store.py +506 -0
  101. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_pending_tool_call_store.py +704 -0
  102. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_run_tool_dependency_store.py +123 -0
  103. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/__init__.py +6 -0
  104. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +263 -0
  105. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_fts_vec.py +216 -0
  106. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/__init__.py +13 -0
  107. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_audit.py +122 -0
  108. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +465 -0
  109. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_purge.py +387 -0
  110. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_rebuild_hide.py +332 -0
  111. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +511 -0
  112. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +22 -10
  113. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +8 -3
  114. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +25 -12
  115. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/__init__.py +9 -0
  116. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +308 -0
  117. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +920 -0
  118. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_search.py +270 -0
  119. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +50 -8
  120. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +64 -370
  121. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_commit_scope.py +9 -0
  122. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_learning_jobs.py +219 -0
  123. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_share_links.py +20 -0
  124. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/__init__.py +9 -0
  125. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_agent_run_store.py +86 -0
  126. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +195 -0
  127. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_pending_tool_call_store.py +148 -0
  128. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_run_tool_dependency_store.py +38 -0
  129. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/__init__.py +13 -0
  130. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_audit.py +29 -0
  131. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_erase_execution.py +30 -0
  132. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_purge.py +74 -0
  133. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_rebuild_hide.py +32 -0
  134. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_subject_barrier.py +49 -0
  135. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/__init__.py +9 -0
  136. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +95 -0
  137. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/{_profiles.py → profiles/_profile_store.py} +60 -80
  138. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_search.py +32 -0
  139. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_governance.py +0 -148
@@ -9,6 +9,7 @@
9
9
  "lint": "eslint"
10
10
  },
11
11
  "dependencies": {
12
+ "@tailwindcss/postcss": "^4",
12
13
  "@base-ui/react": "^1.3.0",
13
14
  "class-variance-authority": "^0.7.1",
14
15
  "clsx": "^2.1.1",
@@ -17,17 +18,16 @@
17
18
  "next-themes": "^0.4.6",
18
19
  "react": "19.2.4",
19
20
  "react-dom": "19.2.4",
21
+ "shadcn": "^4.0.8",
20
22
  "tailwind-merge": "^3.5.0",
21
23
  "tw-animate-css": "^1.4.0"
22
24
  },
23
25
  "devDependencies": {
24
- "@tailwindcss/postcss": "^4",
25
26
  "@types/node": "^20",
26
27
  "@types/react": "^19",
27
28
  "@types/react-dom": "^19",
28
29
  "eslint": "^9",
29
30
  "eslint-config-next": "16.2.6",
30
- "shadcn": "^4.0.8",
31
31
  "tailwindcss": "^4",
32
32
  "typescript": "^5"
33
33
  },
@@ -1,7 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { existsSync, readFileSync, realpathSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
- import { dirname, isAbsolute, join, relative, resolve } from "node:path";
4
+ import { delimiter, dirname, isAbsolute, join, relative, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { AssistantBuffer } from "./assistant-buffer.js";
7
7
  import { sessionIDFrom } from "./internal.js";
@@ -84,6 +84,64 @@ const SCRIPTS_DIR = resolve(PLUGIN_ROOT, "scripts");
84
84
  const HOOK_ENTRY = resolve(SCRIPTS_DIR, "hook_entry.sh");
85
85
  const BACKEND_SERVICE = resolve(SCRIPTS_DIR, "backend-service.sh");
86
86
  const DASHBOARD_SERVICE = resolve(SCRIPTS_DIR, "dashboard-service.sh");
87
+ function commandPath(names) {
88
+ const pathParts = (process.env.PATH || "").split(delimiter).filter(Boolean);
89
+ for (const dir of pathParts) {
90
+ for (const name of names) {
91
+ const candidate = join(dir, name);
92
+ if (existsSync(candidate))
93
+ return candidate;
94
+ }
95
+ }
96
+ return undefined;
97
+ }
98
+ const WINDOWS_SYSTEM_BASH_SUFFIXES = [
99
+ "\\windows\\system32\\bash.exe",
100
+ "\\windows\\sysnative\\bash.exe",
101
+ "\\windows\\syswow64\\bash.exe",
102
+ ];
103
+ function windowsPathText(path) {
104
+ return path.replace(/\//g, "\\").toLowerCase();
105
+ }
106
+ function isWindowsSystemBash(path) {
107
+ const normalized = windowsPathText(path);
108
+ return WINDOWS_SYSTEM_BASH_SUFFIXES.some((suffix) => normalized.endsWith(suffix));
109
+ }
110
+ function pathCommandCandidates(names) {
111
+ // Return every PATH match so Windows can skip System32 bash and still find Git Bash.
112
+ const pathParts = (process.env.PATH || "").split(delimiter).filter(Boolean);
113
+ const candidates = [];
114
+ for (const dir of pathParts) {
115
+ for (const name of names) {
116
+ const candidate = join(dir, name);
117
+ if (existsSync(candidate))
118
+ candidates.push(candidate);
119
+ }
120
+ }
121
+ return candidates;
122
+ }
123
+ function firstUsableBash(candidates) {
124
+ for (const candidate of candidates) {
125
+ const resolved = existsSync(candidate) ? candidate : commandPath([candidate]);
126
+ if (resolved && !isWindowsSystemBash(resolved))
127
+ return resolved;
128
+ }
129
+ return undefined;
130
+ }
131
+ function bashPath() {
132
+ if (process.platform !== "win32")
133
+ return commandPath(["bash"]);
134
+ const bashEnv = (process.env.BASH || "").trim();
135
+ return firstUsableBash([
136
+ ...(bashEnv ? [bashEnv] : []),
137
+ "C:\\Program Files\\Git\\bin\\bash.exe",
138
+ "C:\\Program Files (x86)\\Git\\bin\\bash.exe",
139
+ ...pathCommandCandidates(["bash.exe", "bash"]),
140
+ "bash.exe",
141
+ "bash",
142
+ ]);
143
+ }
144
+ const RESOLVED_BASH = bashPath();
87
145
  function contextFrom(result) {
88
146
  const hookOutput = result.hookSpecificOutput;
89
147
  if (!hookOutput || typeof hookOutput !== "object")
@@ -109,7 +167,7 @@ function parseFirstJsonObject(text) {
109
167
  }
110
168
  function runScript(script, args, payload) {
111
169
  return new Promise((resolvePromise) => {
112
- const child = spawn("bash", [script, ...args], {
170
+ const child = spawn(RESOLVED_BASH || "bash", [script, ...args], {
113
171
  cwd: PLUGIN_ROOT,
114
172
  env: {
115
173
  ...process.env,
@@ -1,7 +1,7 @@
1
1
  import { spawn } from "node:child_process"
2
2
  import { existsSync, readFileSync, realpathSync } from "node:fs"
3
3
  import { homedir } from "node:os"
4
- import { dirname, isAbsolute, join, relative, resolve } from "node:path"
4
+ import { delimiter, dirname, isAbsolute, join, relative, resolve } from "node:path"
5
5
  import { fileURLToPath } from "node:url"
6
6
 
7
7
  import { AssistantBuffer } from "./assistant-buffer.js"
@@ -103,6 +103,68 @@ const HOOK_ENTRY = resolve(SCRIPTS_DIR, "hook_entry.sh")
103
103
  const BACKEND_SERVICE = resolve(SCRIPTS_DIR, "backend-service.sh")
104
104
  const DASHBOARD_SERVICE = resolve(SCRIPTS_DIR, "dashboard-service.sh")
105
105
 
106
+ function commandPath(names: string[]): string | undefined {
107
+ const pathParts = (process.env.PATH || "").split(delimiter).filter(Boolean)
108
+ for (const dir of pathParts) {
109
+ for (const name of names) {
110
+ const candidate = join(dir, name)
111
+ if (existsSync(candidate)) return candidate
112
+ }
113
+ }
114
+ return undefined
115
+ }
116
+
117
+ const WINDOWS_SYSTEM_BASH_SUFFIXES = [
118
+ "\\windows\\system32\\bash.exe",
119
+ "\\windows\\sysnative\\bash.exe",
120
+ "\\windows\\syswow64\\bash.exe",
121
+ ]
122
+
123
+ function windowsPathText(path: string): string {
124
+ return path.replace(/\//g, "\\").toLowerCase()
125
+ }
126
+
127
+ function isWindowsSystemBash(path: string): boolean {
128
+ const normalized = windowsPathText(path)
129
+ return WINDOWS_SYSTEM_BASH_SUFFIXES.some((suffix) => normalized.endsWith(suffix))
130
+ }
131
+
132
+ function pathCommandCandidates(names: string[]): string[] {
133
+ // Return every PATH match so Windows can skip System32 bash and still find Git Bash.
134
+ const pathParts = (process.env.PATH || "").split(delimiter).filter(Boolean)
135
+ const candidates: string[] = []
136
+ for (const dir of pathParts) {
137
+ for (const name of names) {
138
+ const candidate = join(dir, name)
139
+ if (existsSync(candidate)) candidates.push(candidate)
140
+ }
141
+ }
142
+ return candidates
143
+ }
144
+
145
+ function firstUsableBash(candidates: string[]): string | undefined {
146
+ for (const candidate of candidates) {
147
+ const resolved = existsSync(candidate) ? candidate : commandPath([candidate])
148
+ if (resolved && !isWindowsSystemBash(resolved)) return resolved
149
+ }
150
+ return undefined
151
+ }
152
+
153
+ function bashPath(): string | undefined {
154
+ if (process.platform !== "win32") return commandPath(["bash"])
155
+ const bashEnv = (process.env.BASH || "").trim()
156
+ return firstUsableBash([
157
+ ...(bashEnv ? [bashEnv] : []),
158
+ "C:\\Program Files\\Git\\bin\\bash.exe",
159
+ "C:\\Program Files (x86)\\Git\\bin\\bash.exe",
160
+ ...pathCommandCandidates(["bash.exe", "bash"]),
161
+ "bash.exe",
162
+ "bash",
163
+ ])
164
+ }
165
+
166
+ const RESOLVED_BASH = bashPath()
167
+
106
168
  function contextFrom(result: HookResult): string {
107
169
  const hookOutput = result.hookSpecificOutput
108
170
  if (!hookOutput || typeof hookOutput !== "object") return ""
@@ -126,7 +188,7 @@ function parseFirstJsonObject(text: string): HookResult {
126
188
 
127
189
  function runScript(script: string, args: string[], payload?: Record<string, unknown>): Promise<HookResult> {
128
190
  return new Promise((resolvePromise) => {
129
- const child = spawn("bash", [script, ...args], {
191
+ const child = spawn(RESOLVED_BASH || "bash", [script, ...args], {
130
192
  cwd: PLUGIN_ROOT,
131
193
  env: {
132
194
  ...process.env,
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "claude-smart"
3
- version = "0.2.47"
3
+ version = "0.2.49"
4
4
  description = "Self-improving Claude Code, Codex, and OpenCode plugin that turns corrections into Preferences, Project-specific skills, and Shared skills"
5
5
  keywords = [
6
6
  "claude",
@@ -26,7 +26,7 @@ dependencies = [
26
26
  # normal installs; vendor releases may override it after uv sync by
27
27
  # installing plugin/vendor/reflexio. Keep in sync via
28
28
  # scripts/sync-reflexio-dep.py.
29
- "reflexio-ai>=0.2.27",
29
+ "reflexio-ai>=0.2.28",
30
30
  # Needed when Reflexio runs the local embedding provider (ONNXMiniLM_L6_V2).
31
31
  # Pulls in onnxruntime + tokenizers; the ~80 MB ONNX model itself is
32
32
  # downloaded on first use, not at install time.
@@ -71,7 +71,11 @@ claude_smart_prepend_astral_bins() {
71
71
  claude_smart_prepend_node_bins() {
72
72
  local _CS_NODE_ROOT
73
73
  _CS_NODE_ROOT="$HOME/.claude-smart/node/current"
74
- export PATH="$_CS_NODE_ROOT/bin:$_CS_NODE_ROOT:$PATH"
74
+ if claude_smart_is_windows; then
75
+ export PATH="$_CS_NODE_ROOT:$PATH"
76
+ else
77
+ export PATH="$_CS_NODE_ROOT/bin:$_CS_NODE_ROOT:$PATH"
78
+ fi
75
79
  }
76
80
 
77
81
  claude_smart_env_unquote() {
@@ -89,7 +93,7 @@ claude_smart_env_unquote() {
89
93
  claude_smart_source_reflexio_env() {
90
94
  # claude-smart's env lives at ~/.claude-smart/.env (kept separate from the OSS
91
95
  # reflexio default ~/.reflexio/.env). Stays in sync with
92
- # env_config.REFLEXIO_ENV_PATH and the REFLEXIO_ENV_FILE export in
96
+ # env_config.CLAUDE_SMART_ENV_PATH and the REFLEXIO_ENV_FILE export in
93
97
  # backend-service.sh.
94
98
  local env_file line key value
95
99
  env_file="$HOME/.claude-smart/.env"
@@ -117,7 +121,7 @@ claude_smart_source_reflexio_env() {
117
121
  ;;
118
122
  # CLAUDE_SMART_* flags: respect anything the caller already exported so
119
123
  # per-session overrides (e.g., a manual test) take precedence over the file.
120
- CLAUDE_SMART_USE_LOCAL_CLI|CLAUDE_SMART_USE_LOCAL_EMBEDDING|CLAUDE_SMART_BACKEND_AUTOSTART|CLAUDE_SMART_DASHBOARD_AUTOSTART|CLAUDE_SMART_CLI_PATH|CLAUDE_SMART_CLI_TIMEOUT|CLAUDE_SMART_STATE_DIR|CLAUDE_SMART_ENABLE_OPTIMIZER)
124
+ CLAUDE_SMART_USE_LOCAL_CLI|CLAUDE_SMART_USE_LOCAL_EMBEDDING|CLAUDE_SMART_HOST|CLAUDE_SMART_BACKEND_AUTOSTART|CLAUDE_SMART_DASHBOARD_AUTOSTART|CLAUDE_SMART_CLI_PATH|CLAUDE_SMART_OPENCODE_PATH|CLAUDE_SMART_CLI_TIMEOUT|CLAUDE_SMART_STATE_DIR|CLAUDE_SMART_ENABLE_OPTIMIZER)
121
125
  if [ -z "$(eval "printf '%s' \"\${$key:-}\"")" ]; then
122
126
  value="$(claude_smart_env_unquote "$value")"
123
127
  export "$key=$value"
@@ -210,16 +214,54 @@ claude_smart_is_windows() {
210
214
  esac
211
215
  }
212
216
 
217
+ claude_smart_to_windows_path() {
218
+ local path
219
+ path="$1"
220
+ if command -v cygpath >/dev/null 2>&1; then
221
+ cygpath -w "$path"
222
+ return $?
223
+ fi
224
+ # Convert MSYS/Cygwin/WSL-style paths to native Windows paths for cmd.exe.
225
+ printf '%s\n' "$path" | awk '
226
+ function slash_to_backslash(value) {
227
+ gsub(/\//, "\\", value)
228
+ return value
229
+ }
230
+ function drive_path(drive, rest) {
231
+ drive = toupper(drive)
232
+ sub(/^\//, "", rest)
233
+ if (rest == "") {
234
+ return drive ":\\"
235
+ }
236
+ return drive ":\\" slash_to_backslash(rest)
237
+ }
238
+ {
239
+ normalized = $0
240
+ gsub(/\\/, "/", normalized)
241
+ if (normalized ~ /^[A-Za-z]:($|\/)/) {
242
+ print drive_path(substr(normalized, 1, 1), substr(normalized, 3))
243
+ } else if (normalized ~ /^\/cygdrive\/[A-Za-z]($|\/)/) {
244
+ print drive_path(substr(normalized, 11, 1), substr(normalized, 12))
245
+ } else if (normalized ~ /^\/mnt\/[A-Za-z]($|\/)/) {
246
+ print drive_path(substr(normalized, 6, 1), substr(normalized, 7))
247
+ } else if (normalized ~ /^\/[A-Za-z]($|\/)/) {
248
+ print drive_path(substr(normalized, 2, 1), substr(normalized, 3))
249
+ } else {
250
+ print slash_to_backslash(normalized)
251
+ }
252
+ }'
253
+ }
254
+
213
255
  # Print the absolute path of a working python interpreter, or nothing
214
256
  # (and return non-zero) if none is usable. On Windows, `python3` is
215
257
  # usually the Microsoft Store "App Execution Alias" stub at
216
258
  # %LocalAppData%\Microsoft\WindowsApps\python3.exe — `command -v python3`
217
259
  # returns truthy but invoking it just prints a "Python was not found"
218
260
  # message and exits non-zero. We probe with `-V` to filter the stub out
219
- # and prefer `python` (the real interpreter when one is installed).
261
+ # and prefer the Windows `py` launcher before `python`/`python3`.
220
262
  claude_smart_resolve_python() {
221
263
  if claude_smart_is_windows; then
222
- for cand in python python3; do
264
+ for cand in py python python3; do
223
265
  if command -v "$cand" >/dev/null 2>&1 && "$cand" -V >/dev/null 2>&1; then
224
266
  command -v "$cand"
225
267
  return 0
@@ -508,7 +550,7 @@ claude_smart_node_satisfies() {
508
550
 
509
551
  claude_smart_resolve_npm() {
510
552
  local cand
511
- for cand in npm npm.cmd; do
553
+ for cand in npm npm.cmd npm.exe; do
512
554
  if command -v "$cand" >/dev/null 2>&1; then
513
555
  command -v "$cand"
514
556
  return 0
@@ -517,6 +559,16 @@ claude_smart_resolve_npm() {
517
559
  return 1
518
560
  }
519
561
 
562
+ claude_smart_opencode_compat_path() {
563
+ local plugin_root filename
564
+ plugin_root="$1"
565
+ filename="opencode-claude-compat"
566
+ if claude_smart_is_windows; then
567
+ filename="opencode-claude-compat.cmd"
568
+ fi
569
+ printf '%s\n' "$plugin_root/scripts/$filename"
570
+ }
571
+
520
572
  claude_smart_npm_available() {
521
573
  local npm_bin
522
574
  npm_bin=$(claude_smart_resolve_npm || true)
@@ -45,7 +45,7 @@ export REFLEXIO_DEFAULT_ORG_ID="claude-smart"
45
45
  # Load claude-smart's env from ~/.claude-smart/.env instead of the reflexio
46
46
  # default ~/.reflexio/.env, so an OSS reflexio backend's .env on this machine
47
47
  # can't leak in. The data dir is independent (LOCAL_STORAGE_PATH), so both still
48
- # share ~/.reflexio/data. Must stay in sync with env_config.REFLEXIO_ENV_PATH and
48
+ # share ~/.reflexio/data. Must stay in sync with env_config.CLAUDE_SMART_ENV_PATH and
49
49
  # the source path in _lib.sh (claude_smart_source_reflexio_env).
50
50
  export REFLEXIO_ENV_FILE="$HOME/.claude-smart/.env"
51
51
 
@@ -65,11 +65,13 @@ PLUGIN_ROOT="$(cd "$HERE/.." && pwd)"
65
65
  claude_smart_reexec_stable_plugin_root_if_needed "$PLUGIN_ROOT" "backend-service.sh" "$@"
66
66
 
67
67
  if [ -z "${CLAUDE_SMART_CLI_PATH:-}" ]; then
68
- if [ "${CLAUDE_SMART_HOST:-claude-code}" = "opencode" ] && command -v opencode >/dev/null 2>&1; then
68
+ if [ "${CLAUDE_SMART_HOST:-claude-code}" = "opencode" ]; then
69
69
  # Preserve Reflexio's Claude CLI provider contract while routing
70
- # generation through the user's authenticated OpenCode setup.
70
+ # generation through the user's authenticated OpenCode setup. The bridge
71
+ # resolves opencode from CLAUDE_SMART_OPENCODE_PATH or PATH; don't require
72
+ # Git Bash's PATH to match the installer shell's PATH here.
71
73
  claude_smart_prepend_node_bins
72
- export CLAUDE_SMART_CLI_PATH="$PLUGIN_ROOT/scripts/opencode-claude-compat"
74
+ export CLAUDE_SMART_CLI_PATH="$(claude_smart_opencode_compat_path "$PLUGIN_ROOT")"
73
75
  elif [ "${CLAUDE_SMART_HOST:-claude-code}" = "codex" ]; then
74
76
  # Reflexio's provider still calls CLAUDE_SMART_CLI_PATH with Claude CLI
75
77
  # flags. Use a small compatibility executable that translates that narrow
@@ -110,7 +112,8 @@ if reason:
110
112
  message += f">\n> Last startup error: `{reason}`\n"
111
113
  message += (
112
114
  ">\n> Make sure the local model provider is available: Claude Code needs "
113
- "`claude`, Codex needs `codex`. Then run `/claude-smart:restart`."
115
+ "`claude`, Codex needs `codex`, and OpenCode needs `opencode`. "
116
+ "Then run `/claude-smart:restart`."
114
117
  )
115
118
  print(json.dumps({
116
119
  "hookSpecificOutput": {
@@ -155,9 +158,13 @@ is_our_backend_running() {
155
158
  return 1
156
159
  }
157
160
 
158
- # True if *anything* is listening on the port (even non-HTTP). Used to
159
- # avoid stomping on a foreign listener with a failed-to-start uvicorn.
161
+ # Best-effort port probe. curl catches responsive HTTP listeners; /dev/tcp
162
+ # catches other listeners where bash supports it. Used to avoid stomping on
163
+ # a foreign listener with a failed-to-start uvicorn.
160
164
  port_occupied() {
165
+ if command -v curl >/dev/null 2>&1; then
166
+ curl -sf --max-time 2 -o /dev/null "http://127.0.0.1:$PORT" 2>/dev/null && return 0
167
+ fi
161
168
  (echo >"/dev/tcp/127.0.0.1/$PORT") 2>/dev/null
162
169
  }
163
170
 
@@ -326,9 +333,7 @@ case "$CMD" in
326
333
  # Native Windows Python expects ;-separated Windows-style paths in
327
334
  # PYTHONPATH; MSYS does not auto-convert arbitrary env vars.
328
335
  pythonpath_sep=";"
329
- if command -v cygpath >/dev/null 2>&1; then
330
- vendor_pythonpath="$(cygpath -w "$vendor_pythonpath")"
331
- fi
336
+ vendor_pythonpath="$(claude_smart_to_windows_path "$vendor_pythonpath")"
332
337
  fi
333
338
  backend_pythonpath="$vendor_pythonpath${backend_pythonpath:+$pythonpath_sep$backend_pythonpath}"
334
339
  fi
@@ -413,15 +413,27 @@ function writePid(file, pid) {
413
413
  function ensurePluginRoot(root) {
414
414
  ensureDir(REFLEXIO_DIR);
415
415
  const link = path.join(REFLEXIO_DIR, "plugin-root");
416
+ const metadata = path.join(REFLEXIO_DIR, "plugin-root.txt");
417
+ let blocked = false;
416
418
  try {
417
- fs.rmSync(link, { recursive: true, force: true });
418
- } catch {
419
- // Ignore and try to recreate below.
419
+ const existing = fs.lstatSync(link);
420
+ if (existing.isSymbolicLink() || existing.isFile()) {
421
+ fs.rmSync(link, { recursive: true, force: true });
422
+ } else {
423
+ blocked = true;
424
+ }
425
+ } catch (err) {
426
+ if (!err || err.code !== "ENOENT") blocked = true;
427
+ }
428
+ if (blocked) {
429
+ fs.writeFileSync(metadata, `${root}\n`);
430
+ return;
420
431
  }
421
432
  try {
422
433
  fs.symlinkSync(root, link, process.platform === "win32" ? "junction" : "dir");
434
+ fs.writeFileSync(metadata, `${root}\n`);
423
435
  } catch {
424
- fs.writeFileSync(path.join(REFLEXIO_DIR, "plugin-root.txt"), `${root}\n`);
436
+ fs.writeFileSync(metadata, `${root}\n`);
425
437
  }
426
438
  }
427
439
 
@@ -163,7 +163,7 @@ is_our_dashboard_running() {
163
163
  # True if *something* is listening on the port, regardless of marker.
164
164
  port_occupied() {
165
165
  if command -v curl >/dev/null 2>&1; then
166
- curl -sf -o /dev/null "http://127.0.0.1:$PORT" 2>/dev/null && return 0
166
+ curl -sf --max-time 2 -o /dev/null "http://127.0.0.1:$PORT" 2>/dev/null && return 0
167
167
  # curl with -sfI against a 404/405 still indicates "something answered".
168
168
  # Use a connect-only probe as a secondary signal.
169
169
  fi
@@ -8,6 +8,61 @@
8
8
  # invalid target
9
9
  set -eu
10
10
 
11
+ HERE="$(cd "$(dirname "$0")" && pwd)"
12
+ if [ -f "$HERE/_lib.sh" ]; then
13
+ # shellcheck source=_lib.sh
14
+ . "$HERE/_lib.sh"
15
+ fi
16
+
17
+ # This script can be invoked without plugin/scripts/_lib.sh; keep these fallbacks
18
+ # in sync with the shared helpers there.
19
+ if ! command -v claude_smart_is_windows >/dev/null 2>&1; then
20
+ claude_smart_is_windows() {
21
+ case "$(uname -s 2>/dev/null)" in
22
+ MINGW*|MSYS*|CYGWIN*) return 0 ;;
23
+ *) return 1 ;;
24
+ esac
25
+ }
26
+ fi
27
+
28
+ if ! command -v claude_smart_to_windows_path >/dev/null 2>&1; then
29
+ claude_smart_to_windows_path() {
30
+ path="$1"
31
+ if command -v cygpath >/dev/null 2>&1; then
32
+ cygpath -w "$path"
33
+ return $?
34
+ fi
35
+ printf '%s\n' "$path" | awk '
36
+ function slash_to_backslash(value) {
37
+ gsub(/\//, "\\", value)
38
+ return value
39
+ }
40
+ function drive_path(drive, rest) {
41
+ drive = toupper(drive)
42
+ sub(/^\//, "", rest)
43
+ if (rest == "") {
44
+ return drive ":\\"
45
+ }
46
+ return drive ":\\" slash_to_backslash(rest)
47
+ }
48
+ {
49
+ normalized = $0
50
+ gsub(/\\/, "/", normalized)
51
+ if (normalized ~ /^[A-Za-z]:($|\/)/) {
52
+ print drive_path(substr(normalized, 1, 1), substr(normalized, 3))
53
+ } else if (normalized ~ /^\/cygdrive\/[A-Za-z]($|\/)/) {
54
+ print drive_path(substr(normalized, 11, 1), substr(normalized, 12))
55
+ } else if (normalized ~ /^\/mnt\/[A-Za-z]($|\/)/) {
56
+ print drive_path(substr(normalized, 6, 1), substr(normalized, 7))
57
+ } else if (normalized ~ /^\/[A-Za-z]($|\/)/) {
58
+ print drive_path(substr(normalized, 2, 1), substr(normalized, 3))
59
+ } else {
60
+ print slash_to_backslash(normalized)
61
+ }
62
+ }'
63
+ }
64
+ fi
65
+
11
66
  TARGET="${1:-}"
12
67
  FORCE="${2:-}"
13
68
 
@@ -25,9 +80,46 @@ TARGET="$(cd "$TARGET" && pwd -P)"
25
80
  LINK="$HOME/.reflexio/plugin-root"
26
81
  mkdir -p "$(dirname "$LINK")"
27
82
 
28
- if [ "$FORCE" = "--force" ]; then
83
+ write_plugin_root_metadata() {
84
+ printf '%s\n' "$TARGET" >"$HOME/.reflexio/plugin-root.txt"
85
+ }
86
+
87
+ write_plugin_root_link() {
88
+ reason="$1"
89
+ if claude_smart_is_windows; then
90
+ link_win="$(claude_smart_to_windows_path "$LINK")"
91
+ target_win="$(claude_smart_to_windows_path "$TARGET")"
92
+ if [ -L "$LINK" ] || [ -f "$LINK" ]; then
93
+ rm -f "$LINK"
94
+ elif [ -e "$LINK" ]; then
95
+ # Intentionally avoid rmdir //S //Q here: if this is a real
96
+ # directory rather than a junction, preserve it and use metadata
97
+ # fallback instead of recursively deleting user-owned contents.
98
+ if ! cmd.exe //C rmdir "$link_win" >/dev/null 2>&1; then
99
+ echo "[claude-smart] ensure-plugin-root: could not remove existing Windows junction/path at $LINK" >&2
100
+ fi
101
+ fi
102
+ if [ ! -e "$LINK" ] && cmd.exe //C mklink //J "$link_win" "$target_win" >/dev/null 2>&1; then
103
+ write_plugin_root_metadata
104
+ echo "[claude-smart] plugin-root → $TARGET${reason:+ ($reason)}" >&2
105
+ return 0
106
+ fi
107
+ write_plugin_root_metadata
108
+ if [ -e "$LINK" ]; then
109
+ echo "[claude-smart] ensure-plugin-root: $LINK blocks Windows junction; preserving occupied path and wrote plugin-root.txt fallback" >&2
110
+ else
111
+ echo "[claude-smart] ensure-plugin-root: mklink //J failed; wrote plugin-root.txt fallback" >&2
112
+ fi
113
+ echo "[claude-smart] plugin-root.txt → $TARGET${reason:+ ($reason; junction unavailable)}" >&2
114
+ return 0
115
+ fi
116
+
29
117
  ln -sfn "$TARGET" "$LINK"
30
- echo "[claude-smart] plugin-root → $TARGET (forced)" >&2
118
+ echo "[claude-smart] plugin-root → $TARGET${reason:+ ($reason)}" >&2
119
+ }
120
+
121
+ if [ "$FORCE" = "--force" ]; then
122
+ write_plugin_root_link "forced"
31
123
  exit 0
32
124
  fi
33
125
 
@@ -43,8 +135,7 @@ if [ -z "$FOLLOW" ] && [ -f "$HOME/.claude-smart/.env" ]; then
43
135
  FOLLOW="${FOLLOW#\'}"; FOLLOW="${FOLLOW%\'}"
44
136
  fi
45
137
  if [ "$FOLLOW" = "1" ]; then
46
- ln -sfn "$TARGET" "$LINK"
47
- echo "[claude-smart] plugin-root → $TARGET (follow-session)" >&2
138
+ write_plugin_root_link "follow-session"
48
139
  exit 0
49
140
  fi
50
141
 
@@ -53,16 +144,24 @@ fi
53
144
  # always retarget it to $TARGET. Plugin updates leave old version
54
145
  # directories behind, so a valid pyproject.toml at the stale target is
55
146
  # not proof the link is fresh.
147
+ CURRENT=""
56
148
  if [ -L "$LINK" ]; then
57
149
  # Literal target string, not realpath: we compare against what was written by ln -s.
58
150
  CURRENT="$(readlink "$LINK" 2>/dev/null || true)"
151
+ elif claude_smart_is_windows && [ -e "$LINK" ] && [ -f "$HOME/.reflexio/plugin-root.txt" ]; then
152
+ # Windows junctions are not reported as symlinks by Git Bash, so use the
153
+ # metadata written alongside successful junction creation.
154
+ CURRENT="$(cat "$HOME/.reflexio/plugin-root.txt" 2>/dev/null || true)"
155
+ elif claude_smart_is_windows && [ -e "$LINK" ]; then
156
+ echo "[claude-smart] ensure-plugin-root: $LINK is not a symlink and plugin-root.txt is missing; cannot determine whether the Windows plugin-root tracks the active plugin. Run ensure-plugin-root.sh with --force or delete the occupied path to recreate it." >&2
157
+ fi
158
+ if [ -n "$CURRENT" ]; then
59
159
  case "$CURRENT" in
60
160
  "$HOME/.claude/plugins/cache/"*|"$HOME/.codex/plugins/cache/"*)
61
161
  CURRENT_NORM="${CURRENT%/}"
62
162
  TARGET_NORM="${TARGET%/}"
63
163
  if [ "$CURRENT_NORM" != "$TARGET_NORM" ]; then
64
- ln -sfn "$TARGET" "$LINK"
65
- echo "[claude-smart] plugin-root → $TARGET (cache-tracking, was $CURRENT)" >&2
164
+ write_plugin_root_link "cache-tracking, was $CURRENT"
66
165
  fi
67
166
  exit 0
68
167
  ;;
@@ -75,5 +174,4 @@ if [ -f "$LINK/pyproject.toml" ]; then
75
174
  exit 0
76
175
  fi
77
176
 
78
- ln -sfn "$TARGET" "$LINK"
79
- echo "[claude-smart] plugin-root → $TARGET" >&2
177
+ write_plugin_root_link ""
@@ -193,7 +193,14 @@ function parseOpenCodeJson(stdout) {
193
193
  chunks.push(event.result);
194
194
  }
195
195
  }
196
- return chunks.join("").trim();
196
+ return stripOuterMarkdownFence(chunks.join("").trim());
197
+ }
198
+
199
+ function stripOuterMarkdownFence(content) {
200
+ // OpenCode can wrap one-shot JSON in a whole-response markdown fence.
201
+ // Strip only that outer fence so streamed and one-shot output normalize alike.
202
+ const match = String(content || "").match(/^```[^\r\n]*\r?\n([\s\S]*?)\r?\n```[ \t]*$/);
203
+ return match ? match[1].trim() : content;
197
204
  }
198
205
 
199
206
  function timeoutMs() {