claude-smart 0.2.46 → 0.2.48

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 (170) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +19 -11
  3. package/bin/claude-smart.js +290 -68
  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/README.md +11 -10
  8. package/plugin/dashboard/app/layout.tsx +20 -0
  9. package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +36 -2
  10. package/plugin/dashboard/package-lock.json +61 -390
  11. package/plugin/dashboard/package.json +2 -2
  12. package/plugin/opencode/dist/server.mjs +76 -2
  13. package/plugin/opencode/server.mts +79 -2
  14. package/plugin/pyproject.toml +6 -2
  15. package/plugin/scripts/smart-install.sh +7 -1
  16. package/plugin/src/claude_smart/cli.py +210 -22
  17. package/plugin/src/claude_smart/context_format.py +9 -9
  18. package/plugin/src/claude_smart/cs_cite.py +66 -19
  19. package/plugin/uv.lock +5 -5
  20. package/plugin/vendor/reflexio/.env.example +7 -0
  21. package/plugin/vendor/reflexio/README.md +3 -3
  22. package/plugin/vendor/reflexio/pyproject.toml +2 -1
  23. package/plugin/vendor/reflexio/reflexio/README.md +11 -6
  24. package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +1 -1
  25. package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +2 -2
  26. package/plugin/vendor/reflexio/reflexio/cli/utils.py +44 -1
  27. package/plugin/vendor/reflexio/reflexio/client/client.py +97 -0
  28. package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +8 -0
  29. package/plugin/vendor/reflexio/reflexio/lib/_base.py +15 -0
  30. package/plugin/vendor/reflexio/reflexio/lib/_config.py +23 -18
  31. package/plugin/vendor/reflexio/reflexio/lib/_generation.py +9 -8
  32. package/plugin/vendor/reflexio/reflexio/lib/_interactions.py +16 -1
  33. package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +27 -16
  34. package/plugin/vendor/reflexio/reflexio/lib/_search.py +27 -5
  35. package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +9 -0
  36. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/__init__.py +1 -0
  37. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +1 -0
  38. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/governance.py +117 -0
  39. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +45 -3
  40. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +37 -0
  41. package/plugin/vendor/reflexio/reflexio/server/README.md +38 -9
  42. package/plugin/vendor/reflexio/reflexio/server/__init__.py +21 -2
  43. package/plugin/vendor/reflexio/reflexio/server/api.py +274 -3267
  44. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/README.md +4 -3
  45. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +16 -1
  46. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +1 -1
  47. package/plugin/vendor/reflexio/reflexio/server/{_auth.py → auth.py} +2 -0
  48. package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +62 -36
  49. package/plugin/vendor/reflexio/reflexio/server/deployment_profile.py +69 -0
  50. package/plugin/vendor/reflexio/reflexio/server/extensions.py +213 -0
  51. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +424 -0
  52. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_json_extraction.py +249 -0
  53. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_structured_output.py +195 -0
  54. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +152 -0
  55. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +980 -0
  56. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_types.py +110 -0
  57. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +73 -1819
  58. package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +4 -4
  59. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +57 -5
  60. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +12 -1
  61. package/plugin/vendor/reflexio/reflexio/server/middleware.py +244 -0
  62. package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +9 -1
  63. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.4.0.prompt.md +14 -2
  64. package/plugin/vendor/reflexio/reflexio/server/rate_limit.py +79 -0
  65. package/plugin/vendor/reflexio/reflexio/server/routes/__init__.py +6 -0
  66. package/plugin/vendor/reflexio/reflexio/server/routes/_common.py +25 -0
  67. package/plugin/vendor/reflexio/reflexio/server/routes/_metering.py +98 -0
  68. package/plugin/vendor/reflexio/reflexio/server/routes/braintrust.py +129 -0
  69. package/plugin/vendor/reflexio/reflexio/server/routes/config.py +210 -0
  70. package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +549 -0
  71. package/plugin/vendor/reflexio/reflexio/server/routes/interactions.py +259 -0
  72. package/plugin/vendor/reflexio/reflexio/server/routes/playbooks.py +578 -0
  73. package/plugin/vendor/reflexio/reflexio/server/routes/profiles.py +423 -0
  74. package/plugin/vendor/reflexio/reflexio/server/routes/provenance.py +345 -0
  75. package/plugin/vendor/reflexio/reflexio/server/routes/search.py +349 -0
  76. package/plugin/vendor/reflexio/reflexio/server/routes/system.py +261 -0
  77. package/plugin/vendor/reflexio/reflexio/server/scheduling.py +132 -0
  78. package/plugin/vendor/reflexio/reflexio/server/services/README.md +5 -2
  79. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/__init__.py +38 -0
  80. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_batch_progress.py +298 -0
  81. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_config_filter.py +152 -0
  82. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +243 -0
  83. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +299 -0
  84. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_status_change.py +273 -0
  85. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +258 -0
  86. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +17 -1183
  87. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +8 -1
  88. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +26 -41
  89. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +8 -0
  90. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +232 -123
  91. package/plugin/vendor/reflexio/reflexio/server/services/governance/config.py +52 -0
  92. package/plugin/vendor/reflexio/reflexio/server/services/governance/service.py +378 -0
  93. package/plugin/vendor/reflexio/reflexio/server/services/governance/subject_refs.py +34 -0
  94. package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +385 -78
  95. package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +9 -1
  96. package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_prompt_processing.py +100 -0
  97. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +121 -525
  98. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_clustering.py +184 -0
  99. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_postprocessing.py +130 -0
  100. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_prompt_formatting.py +212 -0
  101. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +258 -69
  102. package/plugin/vendor/reflexio/reflexio/server/services/playbook/service.py +9 -9
  103. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/extractor.py +3 -2
  104. package/plugin/vendor/reflexio/reflexio/server/services/publish_learning_worker.py +288 -0
  105. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/recency.py +211 -0
  106. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +29 -13
  107. package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +4 -0
  108. package/plugin/vendor/reflexio/reflexio/server/services/storage/governance_validation.py +681 -0
  109. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +43 -6
  110. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +10 -1167
  111. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +58 -351
  112. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +49 -19
  113. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +452 -0
  114. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +11 -4
  115. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +1 -2133
  116. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +7 -1126
  117. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +73 -33
  118. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_share_links.py +30 -0
  119. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/__init__.py +9 -0
  120. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_agent_run_store.py +506 -0
  121. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_pending_tool_call_store.py +704 -0
  122. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_run_tool_dependency_store.py +123 -0
  123. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/__init__.py +6 -0
  124. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +263 -0
  125. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_fts_vec.py +132 -0
  126. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/__init__.py +13 -0
  127. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_audit.py +122 -0
  128. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +465 -0
  129. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_purge.py +387 -0
  130. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_rebuild_hide.py +332 -0
  131. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +511 -0
  132. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/__init__.py +13 -0
  133. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +955 -0
  134. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_eval_results.py +189 -0
  135. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_optimization.py +247 -0
  136. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +145 -0
  137. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +844 -0
  138. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/__init__.py +9 -0
  139. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +263 -0
  140. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +896 -0
  141. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_search.py +270 -0
  142. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +50 -9
  143. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +64 -370
  144. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +4 -4
  145. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +0 -909
  146. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +2 -0
  147. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_share_links.py +20 -0
  148. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/__init__.py +9 -0
  149. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_agent_run_store.py +86 -0
  150. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +195 -0
  151. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_pending_tool_call_store.py +148 -0
  152. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_run_tool_dependency_store.py +38 -0
  153. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/__init__.py +13 -0
  154. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_audit.py +29 -0
  155. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_erase_execution.py +30 -0
  156. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_purge.py +74 -0
  157. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_rebuild_hide.py +32 -0
  158. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_subject_barrier.py +49 -0
  159. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/__init__.py +13 -0
  160. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_agent.py +365 -0
  161. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_eval_results.py +124 -0
  162. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_optimization.py +85 -0
  163. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_source_linkage.py +47 -0
  164. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py +333 -0
  165. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/__init__.py +9 -0
  166. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +73 -0
  167. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/{_profiles.py → profiles/_profile_store.py} +57 -86
  168. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_search.py +32 -0
  169. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +153 -12
  170. package/plugin/vendor/reflexio/reflexio/server/services/playbook/user_detail_stripping.py +0 -84
@@ -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,11 +1,85 @@
1
1
  import { spawn } from "node:child_process";
2
- import { dirname, resolve } from "node:path";
2
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
3
5
  import { fileURLToPath } from "node:url";
4
6
  import { AssistantBuffer } from "./assistant-buffer.js";
5
7
  import { sessionIDFrom } from "./internal.js";
6
8
  import { chatMessagePayload, eventPayload, stopPayload, toolAfterPayload } from "./payload.js";
7
9
  const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
8
- const PLUGIN_ROOT = resolve(MODULE_DIR, "../..");
10
+ function homeDir() {
11
+ return process.env.HOME || homedir();
12
+ }
13
+ function textFilePluginRoot() {
14
+ try {
15
+ return readFileSync(join(homeDir(), ".reflexio", "plugin-root.txt"), "utf8").trim() || undefined;
16
+ }
17
+ catch {
18
+ return undefined;
19
+ }
20
+ }
21
+ function isPluginRoot(candidate) {
22
+ return !!candidate
23
+ && existsSync(join(candidate, "pyproject.toml"))
24
+ && existsSync(join(candidate, "uv.lock"))
25
+ && existsSync(join(candidate, "scripts", "hook_entry.sh"));
26
+ }
27
+ function canonicalPluginRoot(candidate) {
28
+ if (!isPluginRoot(candidate))
29
+ return undefined;
30
+ try {
31
+ return realpathSync(candidate);
32
+ }
33
+ catch {
34
+ return undefined;
35
+ }
36
+ }
37
+ function pathParts(candidate) {
38
+ return candidate.split(/[\\/]+/).filter(Boolean);
39
+ }
40
+ function isDescendantOrSame(child, parent) {
41
+ const path = relative(parent, child);
42
+ return path === "" || (!path.startsWith("..") && !isAbsolute(path));
43
+ }
44
+ function isReflexioSessionCopy(candidate) {
45
+ try {
46
+ const reflexioRoot = realpathSync(join(homeDir(), ".reflexio"));
47
+ return isDescendantOrSame(candidate, reflexioRoot);
48
+ }
49
+ catch {
50
+ return false;
51
+ }
52
+ }
53
+ function isTransientPackageRoot(candidate) {
54
+ return pathParts(candidate).includes("_npx") || isReflexioSessionCopy(candidate);
55
+ }
56
+ function resolvePluginRoot() {
57
+ const current = resolve(MODULE_DIR, "../..");
58
+ const explicit = canonicalPluginRoot(process.env.CLAUDE_SMART_PLUGIN_ROOT);
59
+ if (explicit)
60
+ return explicit;
61
+ const currentRoot = canonicalPluginRoot(current);
62
+ if (currentRoot && !isTransientPackageRoot(currentRoot))
63
+ return currentRoot;
64
+ const candidates = [
65
+ join(homeDir(), ".reflexio", "plugin-root"),
66
+ textFilePluginRoot(),
67
+ ];
68
+ for (const candidate of candidates) {
69
+ const root = canonicalPluginRoot(candidate);
70
+ if (root)
71
+ return root;
72
+ }
73
+ if (currentRoot)
74
+ return currentRoot;
75
+ try {
76
+ return realpathSync(current);
77
+ }
78
+ catch {
79
+ return current;
80
+ }
81
+ }
82
+ const PLUGIN_ROOT = resolvePluginRoot();
9
83
  const SCRIPTS_DIR = resolve(PLUGIN_ROOT, "scripts");
10
84
  const HOOK_ENTRY = resolve(SCRIPTS_DIR, "hook_entry.sh");
11
85
  const BACKEND_SERVICE = resolve(SCRIPTS_DIR, "backend-service.sh");
@@ -1,5 +1,7 @@
1
1
  import { spawn } from "node:child_process"
2
- import { dirname, resolve } from "node:path"
2
+ import { existsSync, readFileSync, realpathSync } from "node:fs"
3
+ import { homedir } from "node:os"
4
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path"
3
5
  import { fileURLToPath } from "node:url"
4
6
 
5
7
  import { AssistantBuffer } from "./assistant-buffer.js"
@@ -20,7 +22,82 @@ type EventInput = {
20
22
  }
21
23
 
22
24
  const MODULE_DIR = dirname(fileURLToPath(import.meta.url))
23
- const PLUGIN_ROOT = resolve(MODULE_DIR, "../..")
25
+
26
+ function homeDir(): string {
27
+ return process.env.HOME || homedir()
28
+ }
29
+
30
+ function textFilePluginRoot(): string | undefined {
31
+ try {
32
+ return readFileSync(join(homeDir(), ".reflexio", "plugin-root.txt"), "utf8").trim() || undefined
33
+ } catch {
34
+ return undefined
35
+ }
36
+ }
37
+
38
+ function isPluginRoot(candidate: string | undefined): candidate is string {
39
+ return !!candidate
40
+ && existsSync(join(candidate, "pyproject.toml"))
41
+ && existsSync(join(candidate, "uv.lock"))
42
+ && existsSync(join(candidate, "scripts", "hook_entry.sh"))
43
+ }
44
+
45
+ function canonicalPluginRoot(candidate: string | undefined): string | undefined {
46
+ if (!isPluginRoot(candidate)) return undefined
47
+ try {
48
+ return realpathSync(candidate)
49
+ } catch {
50
+ return undefined
51
+ }
52
+ }
53
+
54
+ function pathParts(candidate: string): string[] {
55
+ return candidate.split(/[\\/]+/).filter(Boolean)
56
+ }
57
+
58
+ function isDescendantOrSame(child: string, parent: string): boolean {
59
+ const path = relative(parent, child)
60
+ return path === "" || (!path.startsWith("..") && !isAbsolute(path))
61
+ }
62
+
63
+ function isReflexioSessionCopy(candidate: string): boolean {
64
+ try {
65
+ const reflexioRoot = realpathSync(join(homeDir(), ".reflexio"))
66
+ return isDescendantOrSame(candidate, reflexioRoot)
67
+ } catch {
68
+ return false
69
+ }
70
+ }
71
+
72
+ function isTransientPackageRoot(candidate: string): boolean {
73
+ return pathParts(candidate).includes("_npx") || isReflexioSessionCopy(candidate)
74
+ }
75
+
76
+ function resolvePluginRoot(): string {
77
+ const current = resolve(MODULE_DIR, "../..")
78
+ const explicit = canonicalPluginRoot(process.env.CLAUDE_SMART_PLUGIN_ROOT)
79
+ if (explicit) return explicit
80
+
81
+ const currentRoot = canonicalPluginRoot(current)
82
+ if (currentRoot && !isTransientPackageRoot(currentRoot)) return currentRoot
83
+
84
+ const candidates = [
85
+ join(homeDir(), ".reflexio", "plugin-root"),
86
+ textFilePluginRoot(),
87
+ ]
88
+ for (const candidate of candidates) {
89
+ const root = canonicalPluginRoot(candidate)
90
+ if (root) return root
91
+ }
92
+ if (currentRoot) return currentRoot
93
+ try {
94
+ return realpathSync(current)
95
+ } catch {
96
+ return current
97
+ }
98
+ }
99
+
100
+ const PLUGIN_ROOT = resolvePluginRoot()
24
101
  const SCRIPTS_DIR = resolve(PLUGIN_ROOT, "scripts")
25
102
  const HOOK_ENTRY = resolve(SCRIPTS_DIR, "hook_entry.sh")
26
103
  const BACKEND_SERVICE = resolve(SCRIPTS_DIR, "backend-service.sh")
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "claude-smart"
3
- version = "0.2.46"
3
+ version = "0.2.48"
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",
@@ -38,9 +38,13 @@ dependencies = [
38
38
  "sqlite-vec>=0.1.6",
39
39
  ]
40
40
 
41
+ # claude-smart is distributed via npm only (see bin/claude-smart.js); it is not
42
+ # published to PyPI, so there is no `claude-smart` console script / uvx entry
43
+ # point. These two scripts are resolved by the runtime from the plugin venv
44
+ # (session_start.py locates claude-smart-optimizer-assistant; the hook is invoked
45
+ # via `python -m claude_smart.hook`), so they stay.
41
46
  [project.scripts]
42
47
  claude-smart-hook = "claude_smart.hook:main"
43
- claude-smart = "claude_smart.cli:main"
44
48
  claude-smart-optimizer-assistant = "claude_smart.optimizer_assistant:main"
45
49
 
46
50
  [build-system]
@@ -465,7 +465,13 @@ if [ -d "$PLUGIN_ROOT/.venv" ]; then
465
465
  fi
466
466
  echo "[claude-smart] running uv sync..." >&2
467
467
  if ! uv sync --locked --python 3.12 --quiet >&2; then
468
- write_failure "uv sync failed in $PLUGIN_ROOT — run 'uv sync --locked --python 3.12' there to diagnose"
468
+ if uv lock --check --python 3.12 >/dev/null 2>&1; then
469
+ write_failure "uv sync failed in $PLUGIN_ROOT — run 'uv sync --locked --python 3.12' there to diagnose"
470
+ fi
471
+ echo "[claude-smart] plugin/uv.lock is out of sync; refreshing local lockfile and retrying uv sync" >&2
472
+ if ! uv lock --python 3.12 >&2 || ! uv sync --python 3.12 --quiet >&2; then
473
+ write_failure "uv sync failed in $PLUGIN_ROOT after refreshing plugin/uv.lock — run 'uv lock --python 3.12 && uv sync --python 3.12' there to diagnose"
474
+ fi
469
475
  fi
470
476
  # Defend against a stale lockfile silently dropping the project install.
471
477
  # When plugin/uv.lock pins a different claude-smart version than pyproject.toml,
@@ -21,6 +21,10 @@ Exposes the following subcommands:
21
21
  from __future__ import annotations
22
22
 
23
23
  import argparse
24
+ from collections.abc import Iterable, Iterator
25
+ from contextlib import contextmanager
26
+ from dataclasses import dataclass
27
+ from hashlib import sha256
24
28
  import json
25
29
  import os
26
30
  import re
@@ -29,11 +33,11 @@ import shutil
29
33
  import subprocess
30
34
  import sys
31
35
  import time
32
- from collections.abc import Iterable
33
- from dataclasses import dataclass
34
36
  from pathlib import Path
37
+ from urllib.parse import urlparse
38
+ from urllib.request import url2pathname
35
39
 
36
- from claude_smart import context_format, env_config, ids, publish, state
40
+ from claude_smart import context_format, cs_cite, env_config, ids, publish, state
37
41
  from claude_smart.reflexio_adapter import Adapter
38
42
 
39
43
  _REFLEXIO_ENV_PATH = env_config.REFLEXIO_ENV_PATH
@@ -56,7 +60,7 @@ _CODEX_PLUGIN_CACHE_DIR = (
56
60
  / "claude-smart"
57
61
  )
58
62
  _CODEX_CLI_TIMEOUT_SECONDS = 30
59
- _OPENCODE_PLUGIN_SPEC = "claude-smart"
63
+ _OPENCODE_BARE_PLUGIN_SPEC = "claude-smart"
60
64
  _OPENCODE_CONFIG_NAMES = ("opencode.json", "opencode.jsonc")
61
65
  _REFLEXIO_UNREACHABLE_MSG = (
62
66
  "Failed to reach reflexio. Check ~/.claude-smart/backend.log "
@@ -72,6 +76,9 @@ _BACKEND_SCRIPT = _SCRIPTS_DIR / "backend-service.sh"
72
76
  _DASHBOARD_SCRIPT = _SCRIPTS_DIR / "dashboard-service.sh"
73
77
  _REFLEXIO_DIR = Path.home() / ".reflexio"
74
78
  _STATE_DIR = Path.home() / ".claude-smart"
79
+ _OPENCODE_LOCAL_PACKAGE_DIR = _STATE_DIR / "opencode" / "claude-smart"
80
+ _OPENCODE_PACKAGE_LOCK_TIMEOUT_SECONDS = 120.0
81
+ _OPENCODE_PACKAGE_LOCK_STALE_SECONDS = 10 * 60.0
75
82
  _LOCAL_DATA_NOTICE = (
76
83
  "Local data was kept so reinstalling claude-smart can reuse your learned "
77
84
  "rules, sessions, logs, and local Reflexio data.\n"
@@ -113,6 +120,7 @@ _COPYTREE_IGNORE = shutil.ignore_patterns(
113
120
  "*.pyo",
114
121
  ".pytest_cache",
115
122
  ".ruff_cache",
123
+ ".git",
116
124
  ".next",
117
125
  "node_modules",
118
126
  )
@@ -1080,9 +1088,50 @@ def _opencode_plugin_spec(entry: object) -> str | None:
1080
1088
  return None
1081
1089
 
1082
1090
 
1091
+ def _opencode_local_plugin_spec(
1092
+ package_root: Path = _OPENCODE_LOCAL_PACKAGE_DIR,
1093
+ ) -> str:
1094
+ return package_root.resolve(strict=False).as_uri()
1095
+
1096
+
1097
+ def _is_default_opencode_package_path(package_path: Path) -> bool:
1098
+ if _same_real_path(package_path, _OPENCODE_LOCAL_PACKAGE_DIR):
1099
+ return True
1100
+ try:
1101
+ return package_path.resolve(strict=False) == _OPENCODE_LOCAL_PACKAGE_DIR.resolve(
1102
+ strict=False
1103
+ )
1104
+ except OSError:
1105
+ return False
1106
+
1107
+
1108
+ def _is_opencode_claude_smart_spec(spec: str | None) -> bool:
1109
+ if not spec:
1110
+ return False
1111
+ if spec == _OPENCODE_BARE_PLUGIN_SPEC or spec.startswith(
1112
+ f"{_OPENCODE_BARE_PLUGIN_SPEC}@"
1113
+ ):
1114
+ return True
1115
+ parsed = urlparse(spec)
1116
+ if parsed.scheme != "file":
1117
+ return False
1118
+ package_path = Path(url2pathname(parsed.path))
1119
+ if _is_default_opencode_package_path(package_path):
1120
+ return True
1121
+ try:
1122
+ manifest = json.loads((package_path / "package.json").read_text())
1123
+ if manifest.get("name") == _OPENCODE_BARE_PLUGIN_SPEC:
1124
+ return True
1125
+ except (OSError, json.JSONDecodeError):
1126
+ pass
1127
+ return False
1128
+
1129
+
1083
1130
  def _patch_opencode_plugin_config(
1084
- config_path: Path, *, install: bool
1131
+ config_path: Path, *, install: bool, plugin_spec: str | None = None
1085
1132
  ) -> tuple[bool, Path]:
1133
+ if install and plugin_spec is None:
1134
+ plugin_spec = _opencode_local_plugin_spec()
1086
1135
  data = _read_jsonc_object(config_path)
1087
1136
  for field in ("plugins", "plugin"):
1088
1137
  value = data.get(field)
@@ -1100,9 +1149,13 @@ def _patch_opencode_plugin_config(
1100
1149
  kept = [
1101
1150
  item
1102
1151
  for item in plugins
1103
- if _opencode_plugin_spec(item) != _OPENCODE_PLUGIN_SPEC
1152
+ if not _is_opencode_claude_smart_spec(_opencode_plugin_spec(item))
1104
1153
  ]
1105
- next_plugins = [*kept, _OPENCODE_PLUGIN_SPEC] if install else kept
1154
+ if install:
1155
+ assert plugin_spec is not None
1156
+ next_plugins = [*kept, plugin_spec]
1157
+ else:
1158
+ next_plugins = kept
1106
1159
  if isinstance(current_plugin, list):
1107
1160
  changed = ("plugins" in data) or next_plugins != current_plugin
1108
1161
  else:
@@ -1144,6 +1197,102 @@ def _opencode_install_supported_from_this_package() -> bool:
1144
1197
  return (_SCRIPTS_DIR / "smart-install.sh").is_file()
1145
1198
 
1146
1199
 
1200
+ def _file_sha256(path: Path) -> str:
1201
+ return sha256(path.read_bytes()).hexdigest()
1202
+
1203
+
1204
+ def _same_real_path(left: Path, right: Path) -> bool:
1205
+ try:
1206
+ return left.resolve(strict=True) == right.resolve(strict=True)
1207
+ except OSError:
1208
+ return False
1209
+
1210
+
1211
+ def _verify_opencode_plugin_package(package_root: Path) -> None:
1212
+ source_script = _PLUGIN_ROOT / "scripts" / "smart-install.sh"
1213
+ copied_script = package_root / "plugin" / "scripts" / "smart-install.sh"
1214
+ for path in (package_root / "package.json", copied_script):
1215
+ if not path.exists():
1216
+ raise OSError(f"OpenCode local plugin package is missing {path}")
1217
+ if _file_sha256(source_script) != _file_sha256(copied_script):
1218
+ raise OSError(
1219
+ "OpenCode local plugin package does not match the installed claude-smart package"
1220
+ )
1221
+
1222
+
1223
+ @contextmanager
1224
+ def _opencode_package_install_lock(package_root: Path) -> Iterator[None]:
1225
+ lock_dir = package_root.parent / ".install.lock"
1226
+ package_root.parent.mkdir(parents=True, exist_ok=True)
1227
+ deadline = time.monotonic() + _OPENCODE_PACKAGE_LOCK_TIMEOUT_SECONDS
1228
+ while True:
1229
+ try:
1230
+ lock_dir.mkdir()
1231
+ break
1232
+ except FileExistsError:
1233
+ try:
1234
+ stale = (
1235
+ time.time() - lock_dir.stat().st_mtime
1236
+ > _OPENCODE_PACKAGE_LOCK_STALE_SECONDS
1237
+ )
1238
+ except OSError:
1239
+ stale = False
1240
+ if stale:
1241
+ shutil.rmtree(lock_dir, ignore_errors=True)
1242
+ continue
1243
+ if time.monotonic() >= deadline:
1244
+ raise OSError(
1245
+ f"timed out waiting for OpenCode package install lock at {lock_dir}"
1246
+ )
1247
+ time.sleep(0.1)
1248
+ try:
1249
+ yield
1250
+ finally:
1251
+ shutil.rmtree(lock_dir, ignore_errors=True)
1252
+
1253
+
1254
+ def _unique_opencode_package_path(parent: Path, prefix: str) -> Path:
1255
+ return parent / f"{prefix}-{os.getpid()}-{time.monotonic_ns()}"
1256
+
1257
+
1258
+ def _replace_opencode_plugin_package(staged_package: Path, package_root: Path) -> None:
1259
+ backup_package = _unique_opencode_package_path(
1260
+ package_root.parent, ".claude-smart-previous"
1261
+ )
1262
+ backup_created = False
1263
+ try:
1264
+ if package_root.exists() or package_root.is_symlink():
1265
+ package_root.rename(backup_package)
1266
+ backup_created = True
1267
+ staged_package.rename(package_root)
1268
+ if backup_created:
1269
+ shutil.rmtree(backup_package, ignore_errors=True)
1270
+ except OSError:
1271
+ if backup_created and not package_root.exists() and backup_package.exists():
1272
+ backup_package.rename(package_root)
1273
+ raise
1274
+
1275
+
1276
+ def _install_opencode_plugin_package() -> Path:
1277
+ package_root = _OPENCODE_LOCAL_PACKAGE_DIR
1278
+ if _same_real_path(_REPO_ROOT, package_root):
1279
+ _verify_opencode_plugin_package(package_root)
1280
+ return package_root
1281
+ with _opencode_package_install_lock(package_root):
1282
+ staged_package = _unique_opencode_package_path(
1283
+ package_root.parent, ".claude-smart-copy"
1284
+ )
1285
+ shutil.rmtree(staged_package, ignore_errors=True)
1286
+ try:
1287
+ shutil.copytree(_REPO_ROOT, staged_package, ignore=_COPYTREE_IGNORE)
1288
+ _verify_opencode_plugin_package(staged_package)
1289
+ _replace_opencode_plugin_package(staged_package, package_root)
1290
+ _verify_opencode_plugin_package(package_root)
1291
+ finally:
1292
+ shutil.rmtree(staged_package, ignore_errors=True)
1293
+ return package_root
1294
+
1295
+
1147
1296
  def _bootstrap_opencode_install(read_only: bool) -> tuple[bool, str]:
1148
1297
  if not _opencode_install_supported_from_this_package():
1149
1298
  return (
@@ -1152,22 +1301,28 @@ def _bootstrap_opencode_install(read_only: bool) -> tuple[bool, str]:
1152
1301
  "Run `npx claude-smart install --host opencode`.",
1153
1302
  )
1154
1303
  try:
1155
- _force_plugin_root(_PLUGIN_ROOT)
1304
+ package_root = _install_opencode_plugin_package()
1305
+ except OSError as exc:
1306
+ return False, str(exc)
1307
+ plugin_root = package_root / "plugin"
1308
+ try:
1309
+ _force_plugin_root(plugin_root)
1156
1310
  except OSError as exc:
1157
1311
  return False, str(exc)
1158
1312
  bash = _resolve_bash()
1159
1313
  if not bash:
1160
1314
  return False, "bash is required to bootstrap claude-smart dependencies"
1161
- result = subprocess.run([bash, str(_SCRIPTS_DIR / "smart-install.sh")], cwd=_PLUGIN_ROOT)
1315
+ scripts_dir = plugin_root / "scripts"
1316
+ result = subprocess.run([bash, str(scripts_dir / "smart-install.sh")], cwd=plugin_root)
1162
1317
  if result.returncode != 0:
1163
- return False, f"smart-install.sh failed in {_PLUGIN_ROOT}"
1318
+ return False, f"smart-install.sh failed in {plugin_root}"
1164
1319
  if _INSTALL_FAILURE_MARKER.is_file():
1165
1320
  first_line = _INSTALL_FAILURE_MARKER.read_text().splitlines()
1166
1321
  reason = (first_line[0].strip() if first_line else "") or "unknown error"
1167
1322
  return False, reason
1168
1323
  if read_only:
1169
- _prune_publish_hooks_for_read_only(_PLUGIN_ROOT)
1170
- return True, str(_PLUGIN_ROOT)
1324
+ _prune_publish_hooks_for_read_only(plugin_root)
1325
+ return True, str(package_root)
1171
1326
 
1172
1327
 
1173
1328
  def cmd_install_opencode(args: argparse.Namespace) -> int:
@@ -1181,24 +1336,31 @@ def cmd_install_opencode(args: argparse.Namespace) -> int:
1181
1336
  if not _has_extraction_provider():
1182
1337
  sys.stderr.write(_extraction_provider_error())
1183
1338
  return 1
1184
- bootstrapped, message = _bootstrap_opencode_install(read_only)
1339
+ bootstrapped, package_root_text = _bootstrap_opencode_install(read_only)
1185
1340
  if not bootstrapped:
1186
1341
  sys.stderr.write(
1187
- f"error: claude-smart OpenCode setup failed during dependency bootstrap: {message}\n"
1342
+ f"error: claude-smart OpenCode setup failed during dependency bootstrap: {package_root_text}\n"
1188
1343
  )
1189
1344
  return 1
1345
+ package_root = Path(package_root_text)
1346
+ plugin_root = package_root / "plugin"
1347
+ plugin_spec = _opencode_local_plugin_spec(package_root)
1190
1348
  config_path = _opencode_config_path(
1191
1349
  global_config=bool(getattr(args, "global_config", False))
1192
1350
  )
1193
1351
  try:
1194
- changed, config_path = _patch_opencode_plugin_config(config_path, install=True)
1352
+ changed, config_path = _patch_opencode_plugin_config(
1353
+ config_path,
1354
+ install=True,
1355
+ plugin_spec=plugin_spec,
1356
+ )
1195
1357
  except ValueError as exc:
1196
1358
  sys.stderr.write(f"error: {exc}\n")
1197
1359
  return 1
1198
1360
  action = "Updated" if changed else "OpenCode config already includes"
1199
1361
  sys.stdout.write(
1200
- f"{action} `{_OPENCODE_PLUGIN_SPEC}` in {config_path}.\n"
1201
- f"Prepared claude-smart runtime at {message}.\n"
1362
+ f"{action} `{plugin_spec}` in {config_path}.\n"
1363
+ f"Prepared claude-smart runtime at {plugin_root}.\n"
1202
1364
  "Restart OpenCode in your project so it loads the plugin.\n"
1203
1365
  )
1204
1366
  return 0
@@ -1517,6 +1679,13 @@ def cmd_uninstall_codex(_args: argparse.Namespace) -> int:
1517
1679
  def cmd_uninstall_opencode(args: argparse.Namespace) -> int:
1518
1680
  _run_service(_DASHBOARD_SCRIPT, "stop")
1519
1681
  _run_service(_BACKEND_SCRIPT, "stop")
1682
+ local_scripts = _OPENCODE_LOCAL_PACKAGE_DIR / "plugin" / "scripts"
1683
+ for script in (
1684
+ local_scripts / "dashboard-service.sh",
1685
+ local_scripts / "backend-service.sh",
1686
+ ):
1687
+ if script.exists():
1688
+ _run_service(script, "stop")
1520
1689
  config_path = _opencode_config_path(
1521
1690
  global_config=bool(getattr(args, "global_config", False))
1522
1691
  )
@@ -1525,10 +1694,15 @@ def cmd_uninstall_opencode(args: argparse.Namespace) -> int:
1525
1694
  except ValueError as exc:
1526
1695
  sys.stderr.write(f"error: {exc}\n")
1527
1696
  return 1
1697
+ shutil.rmtree(_OPENCODE_LOCAL_PACKAGE_DIR, ignore_errors=True)
1698
+ try:
1699
+ _OPENCODE_LOCAL_PACKAGE_DIR.parent.rmdir()
1700
+ except OSError:
1701
+ pass
1528
1702
  if changed:
1529
- sys.stdout.write(f"Removed `{_OPENCODE_PLUGIN_SPEC}` from {config_path}.\n")
1703
+ sys.stdout.write(f"Removed claude-smart OpenCode plugin entries from {config_path}.\n")
1530
1704
  else:
1531
- sys.stdout.write(f"OpenCode config did not include `{_OPENCODE_PLUGIN_SPEC}`.\n")
1705
+ sys.stdout.write("OpenCode config did not include claude-smart.\n")
1532
1706
  sys.stdout.write(
1533
1707
  "Restart OpenCode to apply. "
1534
1708
  f"{_LOCAL_DATA_NOTICE}"
@@ -1572,12 +1746,26 @@ def cmd_show(args: argparse.Namespace) -> int:
1572
1746
  f"First error: {adapter.read_errors[0]}\n"
1573
1747
  )
1574
1748
  return 1
1575
- sys.stdout.write(
1576
- md or f"_No skills or preferences yet for project `{project_id}`._\n"
1577
- )
1749
+ body = md or f"_No skills or preferences yet for project `{project_id}`._\n"
1750
+ sys.stdout.write(body + _reflexio_show_footer())
1578
1751
  return 0
1579
1752
 
1580
1753
 
1754
+ def _reflexio_show_footer() -> str:
1755
+ """Return the markdown attribution footer appended to ``show`` output.
1756
+
1757
+ Reinforces that claude-smart is powered by the open-source reflexio engine
1758
+ and nudges users to star the repo.
1759
+
1760
+ Returns:
1761
+ str: A leading-separator markdown footer ending in a newline.
1762
+ """
1763
+ return (
1764
+ f"\n---\n⭐ claude-smart is powered by [reflexio]({cs_cite.REFLEXIO_REPO_URL})"
1765
+ f" — if it helps you, [star it on GitHub]({cs_cite.REFLEXIO_REPO_URL}).\n"
1766
+ )
1767
+
1768
+
1581
1769
  def cmd_learn(args: argparse.Namespace) -> int:
1582
1770
  """Force reflexio extraction over the active session's interactions.
1583
1771
 
@@ -269,7 +269,7 @@ def _compact_citation_instruction(marker_parts: list[str] | None = None) -> str:
269
269
  gate = cs_cite.WHEN_TO_CITE_COMPACT
270
270
  link_style = os.environ.get(_CITATION_LINK_STYLE_ENV, "markdown")
271
271
  if link_style == "osc8" and marker_parts:
272
- marker = f" claude-smart rule applied: {' | '.join(marker_parts)}"
272
+ marker = cs_cite.build_marker(" | ".join(marker_parts), "osc8")
273
273
  separator_instruction = (
274
274
  " Separate multiple linked memories with the visible ` | ` separator."
275
275
  if len(marker_parts) > 1
@@ -281,12 +281,13 @@ def _compact_citation_instruction(marker_parts: list[str] | None = None) -> str:
281
281
  )
282
282
  if link_style == "osc8":
283
283
  return _remoteize_citation_instruction(
284
- f"{gate} If you do, end with `✨ claude-smart rule applied:` "
285
- "followed by the same linked memory text; keep the link, but do "
286
- "not show the URL."
284
+ f"{gate} If you do, end with `{cs_cite.MARKER_PREFIX}` "
285
+ "followed by the same linked memory text, then "
286
+ f"`{cs_cite.marker_attribution('osc8')}` linking to the reflexio "
287
+ "repo; keep the links, but do not show the URL."
287
288
  )
288
289
  if marker_parts:
289
- marker = f" claude-smart rule applied: {' | '.join(marker_parts)}"
290
+ marker = cs_cite.build_marker(" | ".join(marker_parts), "markdown")
290
291
  separator_instruction = (
291
292
  " Separate multiple linked memories with the visible ` | ` separator."
292
293
  if len(marker_parts) > 1
@@ -297,9 +298,8 @@ def _compact_citation_instruction(marker_parts: list[str] | None = None) -> str:
297
298
  f"links: `{marker}`.{separator_instruction}"
298
299
  )
299
300
  return _remoteize_citation_instruction(
300
- f"{gate} If you do, end with one final marker like `✨ claude-smart "
301
- "rule applied: [verify process state](http://localhost:3001/rules/"
302
- "s1-123)` using the shown rule URL."
301
+ f"{gate} If you do, end with one final marker like "
302
+ f"`{cs_cite.MARKDOWN_EXAMPLE_ONE}` using the shown rule URL."
303
303
  )
304
304
 
305
305
 
@@ -337,7 +337,7 @@ def _exact_osc8_marker_instruction(entries: list[dict[str, Any]]) -> str:
337
337
  if not marker_parts:
338
338
  return ""
339
339
 
340
- marker = f" claude-smart rule applied: {' | '.join(marker_parts)}"
340
+ marker = cs_cite.build_marker(" | ".join(marker_parts), "osc8")
341
341
  return (
342
342
  "If any listed memory above was used, copy this exact final marker, "
343
343
  f"preserving its hidden OSC 8 terminal links: `{marker}`. "