claude-smart 0.2.48 → 0.2.50

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 (193) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/README.md +11 -40
  3. package/bin/claude-smart.js +393 -55
  4. package/package.json +3 -2
  5. package/plugin/.claude-plugin/plugin.json +1 -1
  6. package/plugin/.codex-plugin/plugin.json +1 -1
  7. package/plugin/.coverage +0 -0
  8. package/plugin/README.md +4 -3
  9. package/plugin/dashboard/app/api/reflexio/[...path]/route.ts +4 -2
  10. package/plugin/dashboard/app/dashboard/page.tsx +6 -1
  11. package/plugin/dashboard/app/preferences/[id]/page.tsx +18 -6
  12. package/plugin/dashboard/app/preferences/page.tsx +32 -35
  13. package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +16 -1
  14. package/plugin/dashboard/app/sessions/page.tsx +2 -0
  15. package/plugin/dashboard/app/skills/page.tsx +65 -50
  16. package/plugin/dashboard/app/skills/project/[id]/page.tsx +17 -8
  17. package/plugin/dashboard/app/skills/shared/[id]/page.tsx +1 -6
  18. package/plugin/dashboard/components/common/host-badge.tsx +118 -0
  19. package/plugin/dashboard/components/common/learning-application-badge.tsx +34 -0
  20. package/plugin/dashboard/components/common/learnings-badge.tsx +1 -1
  21. package/plugin/dashboard/components/common/page-header.tsx +3 -3
  22. package/plugin/dashboard/lib/config-file.ts +5 -1
  23. package/plugin/dashboard/lib/host-attribution.ts +62 -0
  24. package/plugin/dashboard/lib/session-reader.ts +40 -2
  25. package/plugin/dashboard/lib/types.ts +7 -1
  26. package/plugin/opencode/dist/server.mjs +60 -2
  27. package/plugin/opencode/server.mts +64 -2
  28. package/plugin/pyproject.toml +2 -2
  29. package/plugin/scripts/_lib.sh +255 -7
  30. package/plugin/scripts/backend-python-runner.py +46 -0
  31. package/plugin/scripts/backend-service.sh +766 -123
  32. package/plugin/scripts/codex-hook.js +79 -229
  33. package/plugin/scripts/dashboard-open.sh +6 -4
  34. package/plugin/scripts/dashboard-service.sh +118 -137
  35. package/plugin/scripts/ensure-plugin-root.sh +106 -8
  36. package/plugin/scripts/hook_entry.sh +3 -0
  37. package/plugin/scripts/opencode-claude-compat.js +8 -1
  38. package/plugin/scripts/smart-install.sh +116 -21
  39. package/plugin/src/README.md +1 -1
  40. package/plugin/src/claude_smart/cli.py +93 -29
  41. package/plugin/src/claude_smart/context_inject.py +3 -0
  42. package/plugin/src/claude_smart/env_config.py +56 -11
  43. package/plugin/src/claude_smart/events/post_tool.py +2 -1
  44. package/plugin/src/claude_smart/events/session_end.py +2 -1
  45. package/plugin/src/claude_smart/events/stop.py +3 -0
  46. package/plugin/src/claude_smart/events/user_prompt.py +2 -1
  47. package/plugin/src/claude_smart/internal_call.py +5 -2
  48. package/plugin/src/claude_smart/optimizer_assistant.py +59 -13
  49. package/plugin/src/claude_smart/publish.py +59 -7
  50. package/plugin/src/claude_smart/reflexio_adapter.py +137 -14
  51. package/plugin/src/claude_smart/runtime.py +15 -6
  52. package/plugin/src/claude_smart/state.py +211 -52
  53. package/plugin/uv.lock +5 -5
  54. package/plugin/vendor/reflexio/.env.example +13 -0
  55. package/plugin/vendor/reflexio/reflexio/README.md +7 -3
  56. package/plugin/vendor/reflexio/reflexio/__init__.py +12 -0
  57. package/plugin/vendor/reflexio/reflexio/cli/README.md +3 -0
  58. package/plugin/vendor/reflexio/reflexio/client/client.py +166 -9
  59. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py +10 -3
  60. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_state.py +28 -0
  61. package/plugin/vendor/reflexio/reflexio/lib/_search.py +41 -0
  62. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +195 -25
  63. package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +7 -1
  64. package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +2 -1
  65. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +63 -4
  66. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +1 -0
  67. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +8 -1
  68. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +1 -1
  69. package/plugin/vendor/reflexio/reflexio/server/README.md +22 -4
  70. package/plugin/vendor/reflexio/reflexio/server/__init__.py +18 -8
  71. package/plugin/vendor/reflexio/reflexio/server/__main__.py +6 -0
  72. package/plugin/vendor/reflexio/reflexio/server/api.py +79 -5
  73. package/plugin/vendor/reflexio/reflexio/server/billing_meter.py +263 -3
  74. package/plugin/vendor/reflexio/reflexio/server/callback_executor.py +164 -0
  75. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +81 -81
  76. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +28 -0
  77. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +63 -6
  78. package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +19 -52
  79. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +1 -0
  80. package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +28 -0
  81. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +9 -1
  82. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedder_warmup.py +329 -0
  83. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +85 -10
  84. package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +199 -2
  85. package/plugin/vendor/reflexio/reflexio/server/llm/providers/nomic_embedding_provider.py +77 -9
  86. package/plugin/vendor/reflexio/reflexio/server/org_fanout.py +184 -0
  87. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.0.0.prompt.md +1 -1
  88. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.1.0.prompt.md +32 -0
  89. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.3.prompt.md +1 -1
  90. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.4.0.prompt.md +63 -0
  91. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v1.0.0.prompt.md +1 -1
  92. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v2.0.0.prompt.md +30 -0
  93. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/retrieved_learning_impact/v1.0.0.prompt.md +51 -0
  94. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/retrieved_learning_relevance/v1.0.0.prompt.md +39 -0
  95. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.0.0.prompt.md +1 -1
  96. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.1.0.prompt.md +43 -0
  97. package/plugin/vendor/reflexio/reflexio/server/routes/config.py +3 -3
  98. package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +122 -28
  99. package/plugin/vendor/reflexio/reflexio/server/routes/interactions.py +63 -2
  100. package/plugin/vendor/reflexio/reflexio/server/routes/system.py +22 -3
  101. package/plugin/vendor/reflexio/reflexio/server/scheduling.py +64 -3
  102. package/plugin/vendor/reflexio/reflexio/server/services/README.md +6 -4
  103. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/README.md +3 -2
  104. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/_eval_health.py +41 -0
  105. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/components/retrieved_learning_evaluator.py +554 -0
  106. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +35 -1
  107. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/runner.py +253 -101
  108. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/scheduler.py +5 -7
  109. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/service.py +27 -0
  110. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +11 -5
  111. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +4 -4
  112. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +6 -2
  113. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +255 -74
  114. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +72 -0
  115. package/plugin/vendor/reflexio/reflexio/server/services/deferred_learning_plan.py +270 -0
  116. package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/__init__.py +13 -0
  117. package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/scheduler.py +142 -0
  118. package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/worker.py +262 -0
  119. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/hero_state.py +2 -11
  120. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/rule_attribution.py +6 -2
  121. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +17 -13
  122. package/plugin/vendor/reflexio/reflexio/server/services/extraction/agent_run_records.py +18 -3
  123. package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +12 -1
  124. package/plugin/vendor/reflexio/reflexio/server/services/extraction/prior_answer_search.py +1 -1
  125. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +2 -1
  126. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +1 -3
  127. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +66 -0
  128. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +640 -80
  129. package/plugin/vendor/reflexio/reflexio/server/services/governance/service.py +2 -0
  130. package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +179 -99
  131. package/plugin/vendor/reflexio/reflexio/server/services/lineage/vector_backfill_sweep.py +139 -0
  132. package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +66 -27
  133. package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_trigger.py +177 -0
  134. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +68 -2
  135. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +360 -49
  136. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/extractor.py +27 -30
  137. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_edit_apply.py +8 -1
  138. package/plugin/vendor/reflexio/reflexio/server/services/playbook/service.py +137 -69
  139. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +20 -1
  140. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scheduler.py +13 -6
  141. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_query_reformulator.py +40 -25
  142. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/consolidator.py +12 -39
  143. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/extractor.py +30 -35
  144. package/plugin/vendor/reflexio/reflexio/server/services/profile/service.py +166 -66
  145. package/plugin/vendor/reflexio/reflexio/server/services/reflection/service.py +457 -107
  146. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/session_dedup.py +127 -0
  147. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/temporal.py +104 -0
  148. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/dispatcher.py +139 -0
  149. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/judge.py +16 -6
  150. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/worker.py +137 -0
  151. package/plugin/vendor/reflexio/reflexio/server/services/storage/governance_validation.py +12 -0
  152. package/plugin/vendor/reflexio/reflexio/server/services/storage/lifecycle_filters.py +54 -0
  153. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +41 -3
  154. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention_mixin.py +40 -2
  155. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +2 -0
  156. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +190 -2
  157. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +11 -4
  158. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +28 -1
  159. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_learning_jobs.py +414 -0
  160. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +11 -5
  161. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +8 -3
  162. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +4 -0
  163. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +16 -5
  164. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_fts_vec.py +86 -2
  165. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +104 -42
  166. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +7 -1
  167. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +59 -7
  168. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_eval_results.py +430 -5
  169. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +8 -3
  170. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +57 -12
  171. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +197 -12
  172. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +70 -11
  173. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +17 -0
  174. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_commit_scope.py +9 -0
  175. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +9 -2
  176. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_learning_jobs.py +269 -0
  177. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +7 -0
  178. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_retrieval_log.py +3 -1
  179. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +4 -0
  180. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +12 -1
  181. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/evaluation_state_keys.py +78 -0
  182. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_agent.py +38 -0
  183. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_eval_results.py +171 -0
  184. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py +52 -1
  185. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +82 -1
  186. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_profile_store.py +74 -2
  187. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/retrieved_learning_state.py +226 -0
  188. package/plugin/vendor/reflexio/reflexio/server/services/tagging/tagging_scheduler.py +5 -6
  189. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +209 -29
  190. package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +3 -0
  191. package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +61 -0
  192. package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +33 -0
  193. package/plugin/vendor/reflexio/reflexio/server/services/search/__init__.py +0 -0
@@ -13,7 +13,6 @@ import {
13
13
  Check,
14
14
  BookMarked,
15
15
  Hash,
16
- FolderGit2,
17
16
  Clock,
18
17
  FileText,
19
18
  } from "lucide-react";
@@ -298,10 +297,6 @@ export default function SharedSkillDetailPage({
298
297
 
299
298
  {playbook && (
300
299
  <div className="flex items-center gap-2 flex-wrap">
301
- <Badge variant="outline" className="gap-1.5">
302
- <FolderGit2 className="h-3 w-3" />
303
- {playbook.agent_version || "default"}
304
- </Badge>
305
300
  {editing ? (
306
301
  <ReviewStatusBadge
307
302
  status={playbook.playbook_status}
@@ -472,7 +467,7 @@ export default function SharedSkillDetailPage({
472
467
  value={formatTimestamp(playbook.created_at)}
473
468
  />
474
469
  <Meta
475
- label="Project"
470
+ label="Agent version"
476
471
  value={playbook.agent_version || "default"}
477
472
  mono
478
473
  />
@@ -0,0 +1,118 @@
1
+ import { Terminal } from "lucide-react";
2
+ import { Badge } from "@/components/ui/badge";
3
+ import type { RequestHostAttribution } from "@/lib/host-attribution";
4
+ import { cn } from "@/lib/utils";
5
+ import type { Host } from "@/lib/types";
6
+
7
+ // Exact product colors from the current official app assets. Label colors meet
8
+ // WCAG AA for the badge's small text: Claude 5.65:1, Codex 5.39:1, OpenCode 18.93:1.
9
+ const HOST_META: Record<
10
+ Exclude<Host, "unknown">,
11
+ { label: string; badgeClass: string; dotClass: string }
12
+ > = {
13
+ "claude-code": {
14
+ label: "Claude Code",
15
+ badgeClass: "border-[#D97757] bg-[#D97757] text-[#2A120B]",
16
+ dotClass: "bg-[#D97757]",
17
+ },
18
+ codex: {
19
+ label: "Codex",
20
+ badgeClass: "border-[#0169CC] bg-[#0169CC] text-white",
21
+ dotClass: "bg-[#0169CC]",
22
+ },
23
+ opencode: {
24
+ label: "OpenCode",
25
+ badgeClass:
26
+ "border-[#131010] bg-[#131010] text-white dark:border-white/30",
27
+ dotClass: "bg-[#131010] dark:bg-white",
28
+ },
29
+ };
30
+
31
+ export function HostBadge({
32
+ host,
33
+ display = "badge",
34
+ size = "md",
35
+ className,
36
+ }: {
37
+ host: Host | null;
38
+ display?: "badge" | "provenance";
39
+ size?: "md" | "sm";
40
+ className?: string;
41
+ }) {
42
+ const knownHost = host && host !== "unknown" ? host : null;
43
+ const meta = knownHost ? HOST_META[knownHost] : null;
44
+ const label = meta?.label ?? "unknown host";
45
+
46
+ if (display === "provenance") {
47
+ return (
48
+ <span
49
+ className={cn(
50
+ "inline-flex items-center gap-1 text-[11px] text-muted-foreground",
51
+ className,
52
+ )}
53
+ title={
54
+ knownHost
55
+ ? `This learning was generated from a ${label} session; it is not limited to that host.`
56
+ : "The session host for this learning's generation request was not recorded."
57
+ }
58
+ >
59
+ <span>Generated via</span>
60
+ <span
61
+ aria-hidden="true"
62
+ className={cn(
63
+ "h-1.5 w-1.5 shrink-0 rounded-full",
64
+ meta
65
+ ? meta.dotClass
66
+ : "border border-dashed border-muted-foreground/70",
67
+ )}
68
+ />
69
+ <span className="font-medium text-foreground/75">{label}</span>
70
+ </span>
71
+ );
72
+ }
73
+
74
+ const compact = size === "sm";
75
+ const sizeClass = compact ? "h-4 px-1.5 text-[10px]" : "h-5";
76
+ const badgeLabel = knownHost ? label : "Host unknown";
77
+ return (
78
+ <Badge
79
+ variant="outline"
80
+ className={cn(
81
+ "gap-1",
82
+ meta
83
+ ? meta.badgeClass
84
+ : "border-dashed text-muted-foreground",
85
+ sizeClass,
86
+ className,
87
+ )}
88
+ title={knownHost ? `Session host: ${label}` : "Session host was not recorded"}
89
+ >
90
+ <Terminal
91
+ className={cn(
92
+ knownHost ? "text-current" : "text-muted-foreground",
93
+ compact ? "h-2.5 w-2.5" : "h-3 w-3",
94
+ )}
95
+ />
96
+ {badgeLabel}
97
+ </Badge>
98
+ );
99
+ }
100
+
101
+ export function LearningHostProvenance({
102
+ attribution,
103
+ requestId,
104
+ }: {
105
+ attribution: RequestHostAttribution;
106
+ requestId: string;
107
+ }) {
108
+ if (attribution.unavailable) {
109
+ return <span className="text-muted-foreground">Unavailable</span>;
110
+ }
111
+
112
+ return (
113
+ <HostBadge
114
+ host={attribution.hosts.get(requestId) ?? null}
115
+ display="provenance"
116
+ />
117
+ );
118
+ }
@@ -0,0 +1,34 @@
1
+ import { Sparkles } from "lucide-react";
2
+ import { Badge } from "@/components/ui/badge";
3
+ import { formatRelative } from "@/lib/format";
4
+ import type { PlaybookApplicationStat } from "@/lib/types";
5
+
6
+ export function LearningApplicationBadge({
7
+ stat,
8
+ }: {
9
+ stat: PlaybookApplicationStat | undefined;
10
+ }) {
11
+ if (!stat || stat.applied_count === 0) {
12
+ return (
13
+ <Badge
14
+ variant="outline"
15
+ className="h-5 text-[10px] text-muted-foreground"
16
+ title="No citations recorded yet for this learning. It will count once an assistant reply cites it."
17
+ >
18
+ Never applied
19
+ </Badge>
20
+ );
21
+ }
22
+
23
+ const last = formatRelative(stat.last_applied_at);
24
+ return (
25
+ <Badge
26
+ variant="outline"
27
+ className="h-5 gap-1 border-amber-500/45 bg-amber-500/10 text-[10px] text-amber-700 dark:text-amber-300"
28
+ title={`Last applied ${last}`}
29
+ >
30
+ <Sparkles className="h-2.5 w-2.5 text-amber-500" />
31
+ Applied {stat.applied_count}×{stat.last_applied_at ? ` · ${last}` : ""}
32
+ </Badge>
33
+ );
34
+ }
@@ -28,7 +28,7 @@ export function LearningsBadge({
28
28
  isSmall ? "h-2.5 w-2.5" : "h-3 w-3",
29
29
  )}
30
30
  />
31
- {count} learning applied
31
+ {count} learning{count === 1 ? "" : "s"} applied
32
32
  </Badge>
33
33
  );
34
34
  }
@@ -7,7 +7,7 @@ export function PageHeader({
7
7
  className,
8
8
  }: {
9
9
  title: string;
10
- description?: string;
10
+ description?: React.ReactNode;
11
11
  actions?: React.ReactNode;
12
12
  className?: string;
13
13
  }) {
@@ -21,9 +21,9 @@ export function PageHeader({
21
21
  <div className="min-w-[min(18rem,100%)] flex-1">
22
22
  <h1 className="text-2xl font-semibold">{title}</h1>
23
23
  {description && (
24
- <p className="text-sm text-muted-foreground mt-1 max-w-3xl">
24
+ <div className="text-sm text-muted-foreground mt-1 max-w-3xl">
25
25
  {description}
26
- </p>
26
+ </div>
27
27
  )}
28
28
  </div>
29
29
  {actions && (
@@ -27,6 +27,10 @@ const BOOL_KEYS = new Set([
27
27
  "CLAUDE_SMART_READ_ONLY",
28
28
  ]);
29
29
 
30
+ function defaultReflexioUrl(): string {
31
+ return `http://localhost:${process.env.BACKEND_PORT || "8071"}/`;
32
+ }
33
+
30
34
  function envPath(): string {
31
35
  return path.join(os.homedir(), ".reflexio", ".env");
32
36
  }
@@ -49,7 +53,7 @@ function parseLine(line: string): { key: string; value: string } | null {
49
53
 
50
54
  export async function readConfig(): Promise<ClaudeSmartConfig> {
51
55
  const defaults: ClaudeSmartConfig = {
52
- REFLEXIO_URL: "http://localhost:8071/",
56
+ REFLEXIO_URL: defaultReflexioUrl(),
53
57
  REFLEXIO_API_KEY: "",
54
58
  CLAUDE_SMART_USE_LOCAL_CLI: false,
55
59
  CLAUDE_SMART_USE_LOCAL_EMBEDDING: false,
@@ -0,0 +1,62 @@
1
+ import { useEffect, useState } from "react";
2
+ import type { Host, SessionSummary } from "./types";
3
+
4
+ export interface RequestHostAttribution {
5
+ hosts: ReadonlyMap<string, Host | null>;
6
+ unavailable: boolean;
7
+ }
8
+
9
+ function hostsByRequestId(
10
+ sessions: SessionSummary[],
11
+ ): Map<string, Host | null> {
12
+ const result = new Map<string, Host | null>();
13
+ for (const session of sessions) {
14
+ for (const requestId of session.request_ids) {
15
+ result.set(requestId, session.host);
16
+ }
17
+ }
18
+ return result;
19
+ }
20
+
21
+ export function useRequestHostAttribution(): RequestHostAttribution | null {
22
+ const [attribution, setAttribution] =
23
+ useState<RequestHostAttribution | null>(null);
24
+
25
+ useEffect(() => {
26
+ let cancelled = false;
27
+ const controller = new AbortController();
28
+ const timeout = setTimeout(() => controller.abort(), 5_000);
29
+
30
+ fetch("/api/sessions", {
31
+ cache: "no-store",
32
+ signal: controller.signal,
33
+ })
34
+ .then(async (response) => {
35
+ if (!response.ok) throw new Error(`sessions ${response.status}`);
36
+ const data = await response.json();
37
+ return (data.sessions ?? []) as SessionSummary[];
38
+ })
39
+ .then((sessions) => {
40
+ if (!cancelled) {
41
+ setAttribution({
42
+ hosts: hostsByRequestId(sessions),
43
+ unavailable: false,
44
+ });
45
+ }
46
+ })
47
+ .catch(() => {
48
+ if (!cancelled) {
49
+ setAttribution({ hosts: new Map(), unavailable: true });
50
+ }
51
+ })
52
+ .finally(() => clearTimeout(timeout));
53
+
54
+ return () => {
55
+ cancelled = true;
56
+ controller.abort();
57
+ clearTimeout(timeout);
58
+ };
59
+ }, []);
60
+
61
+ return attribution;
62
+ }
@@ -10,6 +10,7 @@ import path from "node:path";
10
10
  import os from "node:os";
11
11
  import type {
12
12
  CitedItem,
13
+ Host,
13
14
  PlaybookApplicationStat,
14
15
  SessionDetail,
15
16
  SessionSummary,
@@ -18,6 +19,18 @@ import type {
18
19
  UserActionType,
19
20
  } from "./types";
20
21
 
22
+ const VALID_HOSTS: ReadonlySet<Host> = new Set([
23
+ "claude-code",
24
+ "codex",
25
+ "opencode",
26
+ "unknown",
27
+ ]);
28
+
29
+ function parseRecordedHost(value: unknown): Host | null {
30
+ if (typeof value !== "string") return null;
31
+ return VALID_HOSTS.has(value as Host) ? (value as Host) : null;
32
+ }
33
+
21
34
  // Mirrors _TOOL_DATA_FIELD_MAX_LEN in plugin/src/claude_smart/state.py — we
22
35
  // truncate to the same length the publisher ships to reflexio so the
23
36
  // dashboard renders the exact bytes the extractor sees.
@@ -41,6 +54,8 @@ type RawRecord = {
41
54
  content?: string;
42
55
  ts?: number;
43
56
  user_id?: string;
57
+ host?: string;
58
+ request_id?: string;
44
59
  tool_name?: string;
45
60
  tool_input?: Record<string, unknown>;
46
61
  tool_output?: string;
@@ -280,6 +295,8 @@ function foldTurns(records: RawRecord[]): {
280
295
  lastTs: number | null;
281
296
  firstTs: number | null;
282
297
  preview: string | null;
298
+ host: Host | null;
299
+ requestIds: string[];
283
300
  } {
284
301
  let published = 0;
285
302
  let pendingTools: ToolUsed[] = [];
@@ -288,9 +305,17 @@ function foldTurns(records: RawRecord[]): {
288
305
  let lastTs: number | null = null;
289
306
  let firstTs: number | null = null;
290
307
  let preview: string | null = null;
308
+ let host: Host | null = null;
309
+ const requestIds = new Set<string>();
291
310
 
292
311
  for (let idx = 0; idx < records.length; idx++) {
293
312
  const rec = records[idx];
313
+ if (host === null && rec.host !== undefined) {
314
+ host = parseRecordedHost(rec.host);
315
+ }
316
+ if (typeof rec.request_id === "string" && rec.request_id) {
317
+ requestIds.add(rec.request_id);
318
+ }
294
319
  if (typeof rec.published_up_to === "number") {
295
320
  published = rec.published_up_to;
296
321
  pendingTools = [];
@@ -366,6 +391,8 @@ function foldTurns(records: RawRecord[]): {
366
391
  lastTs,
367
392
  firstTs,
368
393
  preview,
394
+ host,
395
+ requestIds: Array.from(requestIds),
369
396
  };
370
397
  }
371
398
 
@@ -391,6 +418,8 @@ export async function listSessions(): Promise<SessionSummary[]> {
391
418
  lastTs,
392
419
  firstTs,
393
420
  preview,
421
+ host,
422
+ requestIds,
394
423
  } = foldTurns(records);
395
424
  summaries.push({
396
425
  session_id: entry.replace(/\.jsonl$/, ""),
@@ -401,6 +430,8 @@ export async function listSessions(): Promise<SessionSummary[]> {
401
430
  published_up_to: publishedUpTo,
402
431
  preview,
403
432
  source: "local",
433
+ host,
434
+ request_ids: requestIds,
404
435
  });
405
436
  }
406
437
  summaries.sort((a, b) => (b.last_activity ?? 0) - (a.last_activity ?? 0));
@@ -456,6 +487,13 @@ export async function readSession(
456
487
  } catch {
457
488
  return null;
458
489
  }
459
- const { turns, publishedUpTo } = foldTurns(records);
460
- return { session_id: sessionId, turns, published_up_to: publishedUpTo };
490
+ const { turns, publishedUpTo, learningInteractionCount, host } =
491
+ foldTurns(records);
492
+ return {
493
+ session_id: sessionId,
494
+ turns,
495
+ learning_interaction_count: learningInteractionCount,
496
+ published_up_to: publishedUpTo,
497
+ host,
498
+ };
461
499
  }
@@ -41,6 +41,8 @@ export interface Interaction {
41
41
  tools_used: ToolUsed[];
42
42
  }
43
43
 
44
+ export type Host = "claude-code" | "codex" | "opencode" | "unknown";
45
+
44
46
  export interface UserPlaybook {
45
47
  user_playbook_id: number;
46
48
  user_id: string | null;
@@ -103,12 +105,16 @@ export interface SessionSummary {
103
105
  published_up_to: number;
104
106
  preview: string | null;
105
107
  source: "local";
108
+ host: Host | null;
109
+ request_ids: string[];
106
110
  }
107
111
 
108
112
  export interface SessionDetail {
109
113
  session_id: string;
110
114
  turns: SessionTurn[];
115
+ learning_interaction_count: number;
111
116
  published_up_to: number;
117
+ host: Host | null;
112
118
  }
113
119
 
114
120
  export interface ClaudeSmartConfig {
@@ -167,7 +173,7 @@ export interface ReflexioConfig {
167
173
  export interface PlaybookApplicationStat {
168
174
  real_id: string;
169
175
  citation_id?: string;
170
- kind: "playbook" | "profile";
176
+ kind: "playbook" | "profile" | "user_playbook" | "agent_playbook";
171
177
  source_kind?: "user_playbook" | "agent_playbook" | "profile";
172
178
  title: string;
173
179
  href?: string;
@@ -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.48"
3
+ version = "0.2.50"
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.