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.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +6 -2
- package/bin/claude-smart.js +289 -56
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +36 -2
- package/plugin/dashboard/package-lock.json +61 -390
- package/plugin/dashboard/package.json +2 -2
- package/plugin/opencode/dist/server.mjs +60 -2
- package/plugin/opencode/server.mts +64 -2
- package/plugin/pyproject.toml +2 -2
- package/plugin/scripts/_lib.sh +58 -6
- package/plugin/scripts/backend-service.sh +15 -10
- package/plugin/scripts/codex-hook.js +16 -4
- package/plugin/scripts/dashboard-service.sh +1 -1
- package/plugin/scripts/ensure-plugin-root.sh +106 -8
- package/plugin/scripts/opencode-claude-compat.js +8 -1
- package/plugin/scripts/smart-install.sh +101 -20
- package/plugin/src/README.md +1 -1
- package/plugin/src/claude_smart/cli.py +79 -29
- package/plugin/src/claude_smart/env_config.py +53 -11
- package/plugin/uv.lock +5 -5
- package/plugin/vendor/reflexio/.env.example +7 -0
- package/plugin/vendor/reflexio/pyproject.toml +2 -1
- package/plugin/vendor/reflexio/reflexio/README.md +8 -5
- package/plugin/vendor/reflexio/reflexio/cli/README.md +3 -0
- package/plugin/vendor/reflexio/reflexio/client/client.py +40 -6
- package/plugin/vendor/reflexio/reflexio/lib/_config.py +23 -18
- package/plugin/vendor/reflexio/reflexio/lib/_interactions.py +16 -1
- package/plugin/vendor/reflexio/reflexio/lib/_search.py +15 -11
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +18 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +1 -0
- package/plugin/vendor/reflexio/reflexio/models/config_schema.py +24 -3
- package/plugin/vendor/reflexio/reflexio/server/README.md +25 -8
- package/plugin/vendor/reflexio/reflexio/server/__init__.py +21 -2
- package/plugin/vendor/reflexio/reflexio/server/api.py +148 -3277
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/README.md +4 -3
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +16 -1
- package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +62 -36
- package/plugin/vendor/reflexio/reflexio/server/deployment_profile.py +69 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +424 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_json_extraction.py +249 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_structured_output.py +195 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +152 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +980 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_types.py +110 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +72 -1817
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +56 -4
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +183 -1
- package/plugin/vendor/reflexio/reflexio/server/middleware.py +244 -0
- package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +9 -1
- package/plugin/vendor/reflexio/reflexio/server/rate_limit.py +79 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/__init__.py +6 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/_common.py +25 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/_metering.py +98 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/braintrust.py +129 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/config.py +210 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +549 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/interactions.py +320 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/playbooks.py +578 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/profiles.py +423 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/provenance.py +345 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/search.py +349 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/system.py +261 -0
- package/plugin/vendor/reflexio/reflexio/server/scheduling.py +132 -0
- package/plugin/vendor/reflexio/reflexio/server/services/README.md +3 -2
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/__init__.py +38 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_batch_progress.py +298 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_config_filter.py +152 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +243 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +299 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_status_change.py +273 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +258 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +17 -1183
- package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +8 -1
- package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/scheduler.py +142 -0
- package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/worker.py +193 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +24 -41
- package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +335 -113
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +362 -81
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +68 -490
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_clustering.py +184 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_postprocessing.py +130 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_prompt_formatting.py +212 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +258 -69
- package/plugin/vendor/reflexio/reflexio/server/services/profile/service.py +44 -22
- package/plugin/vendor/reflexio/reflexio/server/services/publish_learning_worker.py +288 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +31 -4
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +10 -1167
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +196 -353
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +0 -1513
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_learning_jobs.py +379 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +17 -6
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +7 -1281
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +8 -3
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_share_links.py +30 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/__init__.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_agent_run_store.py +506 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_pending_tool_call_store.py +704 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_run_tool_dependency_store.py +123 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/__init__.py +6 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +263 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_fts_vec.py +216 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_audit.py +122 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +465 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_purge.py +387 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_rebuild_hide.py +332 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +511 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +22 -10
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +8 -3
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +25 -12
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/__init__.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +308 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +920 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_search.py +270 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +50 -8
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +64 -370
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_commit_scope.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_learning_jobs.py +219 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_share_links.py +20 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/__init__.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_agent_run_store.py +86 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +195 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_pending_tool_call_store.py +148 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_run_tool_dependency_store.py +38 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_audit.py +29 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_erase_execution.py +30 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_purge.py +74 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_rebuild_hide.py +32 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_subject_barrier.py +49 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/__init__.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +95 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/{_profiles.py → profiles/_profile_store.py} +60 -80
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_search.py +32 -0
- 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,
|
package/plugin/pyproject.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "claude-smart"
|
|
3
|
-
version = "0.2.
|
|
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.
|
|
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.
|
package/plugin/scripts/_lib.sh
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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" ]
|
|
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
|
|
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
|
|
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
|
-
#
|
|
159
|
-
#
|
|
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
|
-
|
|
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.
|
|
418
|
-
|
|
419
|
-
|
|
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(
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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() {
|