claude-smart 0.2.22 → 0.2.24

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 (113) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/README.md +69 -27
  3. package/bin/claude-smart.js +296 -11
  4. package/package.json +11 -1
  5. package/plugin/.claude-plugin/plugin.json +17 -0
  6. package/plugin/.codex-plugin/plugin.json +35 -0
  7. package/plugin/LICENSE +202 -0
  8. package/plugin/README.md +37 -0
  9. package/plugin/bin/cs-cite +77 -0
  10. package/plugin/commands/clear-all.md +8 -0
  11. package/plugin/commands/dashboard.md +8 -0
  12. package/plugin/commands/learn.md +12 -0
  13. package/plugin/commands/restart.md +8 -0
  14. package/plugin/commands/show.md +8 -0
  15. package/plugin/dashboard/AGENTS.md +6 -0
  16. package/plugin/dashboard/app/api/claude-settings/route.ts +19 -0
  17. package/plugin/dashboard/app/api/config/route.ts +16 -0
  18. package/plugin/dashboard/app/api/health/route.ts +10 -0
  19. package/plugin/dashboard/app/api/reflexio/[...path]/route.ts +63 -0
  20. package/plugin/dashboard/app/api/sessions/[id]/route.ts +28 -0
  21. package/plugin/dashboard/app/api/sessions/route.ts +14 -0
  22. package/plugin/dashboard/app/configure/env/page.tsx +318 -0
  23. package/plugin/dashboard/app/configure/layout.tsx +47 -0
  24. package/plugin/dashboard/app/configure/page.tsx +5 -0
  25. package/plugin/dashboard/app/configure/server/page.tsx +258 -0
  26. package/plugin/dashboard/app/dashboard/page.tsx +227 -0
  27. package/plugin/dashboard/app/globals.css +129 -0
  28. package/plugin/dashboard/app/icon.png +0 -0
  29. package/plugin/dashboard/app/layout.tsx +40 -0
  30. package/plugin/dashboard/app/page.tsx +5 -0
  31. package/plugin/dashboard/app/preferences/[id]/page.tsx +531 -0
  32. package/plugin/dashboard/app/preferences/page.tsx +126 -0
  33. package/plugin/dashboard/app/providers.tsx +12 -0
  34. package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +321 -0
  35. package/plugin/dashboard/app/sessions/page.tsx +186 -0
  36. package/plugin/dashboard/app/skills/page.tsx +362 -0
  37. package/plugin/dashboard/app/skills/project/[id]/page.tsx +597 -0
  38. package/plugin/dashboard/app/skills/shared/[id]/page.tsx +830 -0
  39. package/plugin/dashboard/components/common/delete-all-button.tsx +45 -0
  40. package/plugin/dashboard/components/common/empty-state.tsx +34 -0
  41. package/plugin/dashboard/components/common/learnings-badge.tsx +34 -0
  42. package/plugin/dashboard/components/common/page-header.tsx +34 -0
  43. package/plugin/dashboard/components/common/page-tabs.tsx +115 -0
  44. package/plugin/dashboard/components/common/stat-card.tsx +38 -0
  45. package/plugin/dashboard/components/layout/nav-items.ts +22 -0
  46. package/plugin/dashboard/components/layout/sidebar.tsx +45 -0
  47. package/plugin/dashboard/components/layout/top-bar.tsx +64 -0
  48. package/plugin/dashboard/components/stall-banner.tsx +53 -0
  49. package/plugin/dashboard/components/ui/badge.tsx +52 -0
  50. package/plugin/dashboard/components/ui/button.tsx +60 -0
  51. package/plugin/dashboard/components/ui/collapsible.tsx +21 -0
  52. package/plugin/dashboard/components/ui/input.tsx +20 -0
  53. package/plugin/dashboard/components/ui/label.tsx +20 -0
  54. package/plugin/dashboard/components/ui/scroll-area.tsx +55 -0
  55. package/plugin/dashboard/components/ui/select.tsx +201 -0
  56. package/plugin/dashboard/components/ui/separator.tsx +25 -0
  57. package/plugin/dashboard/components/ui/sheet.tsx +135 -0
  58. package/plugin/dashboard/components/ui/switch.tsx +32 -0
  59. package/plugin/dashboard/components.json +25 -0
  60. package/plugin/dashboard/eslint.config.mjs +16 -0
  61. package/plugin/dashboard/hooks/use-settings.tsx +88 -0
  62. package/plugin/dashboard/hooks/use-stall-state.ts +59 -0
  63. package/plugin/dashboard/lib/claude-settings-file.ts +114 -0
  64. package/plugin/dashboard/lib/config-file.ts +131 -0
  65. package/plugin/dashboard/lib/format.ts +58 -0
  66. package/plugin/dashboard/lib/reflexio-client.ts +238 -0
  67. package/plugin/dashboard/lib/reflexio-url.ts +17 -0
  68. package/plugin/dashboard/lib/session-reader.ts +245 -0
  69. package/plugin/dashboard/lib/status.ts +24 -0
  70. package/plugin/dashboard/lib/types.ts +145 -0
  71. package/plugin/dashboard/lib/utils.ts +6 -0
  72. package/plugin/dashboard/next.config.ts +7 -0
  73. package/plugin/dashboard/package-lock.json +10275 -0
  74. package/plugin/dashboard/package.json +37 -0
  75. package/plugin/dashboard/postcss.config.mjs +7 -0
  76. package/plugin/dashboard/public/claude-smart-icon.png +0 -0
  77. package/plugin/dashboard/tsconfig.json +34 -0
  78. package/plugin/hooks/codex-hooks.json +67 -0
  79. package/plugin/hooks/hooks.json +111 -0
  80. package/plugin/pyproject.toml +49 -0
  81. package/plugin/scripts/_codex_env.sh +27 -0
  82. package/plugin/scripts/_lib.sh +325 -0
  83. package/plugin/scripts/backend-service.sh +208 -0
  84. package/plugin/scripts/cli.sh +40 -0
  85. package/plugin/scripts/dashboard-build.sh +139 -0
  86. package/plugin/scripts/dashboard-open.sh +107 -0
  87. package/plugin/scripts/dashboard-service.sh +195 -0
  88. package/plugin/scripts/ensure-plugin-root.sh +84 -0
  89. package/plugin/scripts/hook_entry.sh +70 -0
  90. package/plugin/scripts/smart-install.sh +411 -0
  91. package/plugin/src/claude_smart/__init__.py +3 -0
  92. package/plugin/src/claude_smart/cli.py +1273 -0
  93. package/plugin/src/claude_smart/context_format.py +277 -0
  94. package/plugin/src/claude_smart/context_inject.py +92 -0
  95. package/plugin/src/claude_smart/cs_cite.py +236 -0
  96. package/plugin/src/claude_smart/events/__init__.py +1 -0
  97. package/plugin/src/claude_smart/events/post_tool.py +148 -0
  98. package/plugin/src/claude_smart/events/pre_tool.py +52 -0
  99. package/plugin/src/claude_smart/events/session_end.py +20 -0
  100. package/plugin/src/claude_smart/events/session_start.py +119 -0
  101. package/plugin/src/claude_smart/events/stop.py +393 -0
  102. package/plugin/src/claude_smart/events/user_prompt.py +73 -0
  103. package/plugin/src/claude_smart/hook.py +114 -0
  104. package/plugin/src/claude_smart/ids.py +56 -0
  105. package/plugin/src/claude_smart/internal_call.py +89 -0
  106. package/plugin/src/claude_smart/optimizer_assistant.py +203 -0
  107. package/plugin/src/claude_smart/publish.py +71 -0
  108. package/plugin/src/claude_smart/query_compose.py +51 -0
  109. package/plugin/src/claude_smart/reflexio_adapter.py +403 -0
  110. package/plugin/src/claude_smart/runtime.py +52 -0
  111. package/plugin/src/claude_smart/stall_banner.py +61 -0
  112. package/plugin/src/claude_smart/state.py +276 -0
  113. package/plugin/uv.lock +3720 -0
@@ -0,0 +1,318 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { Save, CheckCircle2 } from "lucide-react";
5
+ import { PageHeader } from "@/components/common/page-header";
6
+ import { Button } from "@/components/ui/button";
7
+ import { Input } from "@/components/ui/input";
8
+ import { Label } from "@/components/ui/label";
9
+ import { Switch } from "@/components/ui/switch";
10
+ import { Separator } from "@/components/ui/separator";
11
+ import { useSettings } from "@/hooks/use-settings";
12
+ import type { ClaudeCodeHookConfig, ClaudeSmartConfig } from "@/lib/types";
13
+
14
+ export default function ConfigureEnvPage() {
15
+ const { reflexioUrl, setReflexioUrl } = useSettings();
16
+ const [config, setConfig] = useState<ClaudeSmartConfig | null>(null);
17
+ const [hookConfig, setHookConfig] = useState<ClaudeCodeHookConfig | null>(null);
18
+ const [hookDirty, setHookDirty] = useState(false);
19
+ const [error, setError] = useState<string | null>(null);
20
+ const [saved, setSaved] = useState(false);
21
+ const [saving, setSaving] = useState(false);
22
+
23
+ useEffect(() => {
24
+ Promise.all([
25
+ fetch("/api/config", { cache: "no-store" }).then((r) => r.json()),
26
+ fetch("/api/claude-settings", { cache: "no-store" }).then((r) =>
27
+ r.json(),
28
+ ),
29
+ ])
30
+ .then(([envConfig, claudeConfig]) => {
31
+ setConfig(envConfig);
32
+ setHookConfig(claudeConfig);
33
+ setHookDirty(false);
34
+ })
35
+ .catch((e) => setError(e instanceof Error ? e.message : String(e)));
36
+ }, []);
37
+
38
+ const update = <K extends keyof ClaudeSmartConfig>(
39
+ key: K,
40
+ value: ClaudeSmartConfig[K],
41
+ ) => {
42
+ setConfig((prev) => (prev ? { ...prev, [key]: value } : prev));
43
+ setSaved(false);
44
+ };
45
+
46
+ const updateOptimizer = (enabled: boolean) => {
47
+ setHookConfig((prev) =>
48
+ prev
49
+ ? {
50
+ ...prev,
51
+ CLAUDE_SMART_ENABLE_OPTIMIZER: enabled,
52
+ effectiveValue: enabled,
53
+ localValue: enabled,
54
+ }
55
+ : prev,
56
+ );
57
+ setHookDirty(true);
58
+ setSaved(false);
59
+ };
60
+
61
+ const save = async () => {
62
+ if (!config || !hookConfig) return;
63
+ setSaving(true);
64
+ setError(null);
65
+ try {
66
+ const [envRes, hookRes] = await Promise.all([
67
+ fetch("/api/config", {
68
+ method: "PUT",
69
+ headers: { "content-type": "application/json" },
70
+ body: JSON.stringify(config),
71
+ }),
72
+ fetch("/api/claude-settings", {
73
+ method: "PUT",
74
+ headers: { "content-type": "application/json" },
75
+ body: JSON.stringify(
76
+ hookDirty
77
+ ? {
78
+ CLAUDE_SMART_ENABLE_OPTIMIZER:
79
+ hookConfig.CLAUDE_SMART_ENABLE_OPTIMIZER,
80
+ }
81
+ : {},
82
+ ),
83
+ }),
84
+ ]);
85
+ if (!envRes.ok) throw new Error(`environment save failed: ${envRes.status}`);
86
+ if (!hookRes.ok)
87
+ throw new Error(`Claude Code settings save failed: ${hookRes.status}`);
88
+ const updated: ClaudeSmartConfig = await envRes.json();
89
+ const updatedHook: ClaudeCodeHookConfig = await hookRes.json();
90
+ setConfig(updated);
91
+ setHookConfig(updatedHook);
92
+ setHookDirty(false);
93
+ setSaved(true);
94
+ } catch (e) {
95
+ setError(e instanceof Error ? e.message : String(e));
96
+ } finally {
97
+ setSaving(false);
98
+ }
99
+ };
100
+
101
+ return (
102
+ <>
103
+ <PageHeader
104
+ title="Environment"
105
+ description="Dashboard settings, Reflexio backend environment, and Claude Code hook environment."
106
+ actions={
107
+ <Button onClick={save} disabled={!config || !hookConfig || saving} size="sm">
108
+ <Save className="h-3.5 w-3.5" />
109
+ {saving ? "Saving…" : "Save"}
110
+ </Button>
111
+ }
112
+ />
113
+
114
+ <div className="p-6 max-w-2xl mx-auto space-y-6">
115
+ {error && (
116
+ <div className="rounded-lg border border-destructive/30 bg-destructive/5 text-destructive px-4 py-3 text-sm">
117
+ {error}
118
+ </div>
119
+ )}
120
+ {saved && (
121
+ <div className="rounded-lg border border-border bg-accent/40 px-4 py-2.5 text-sm flex items-center gap-2">
122
+ <CheckCircle2 className="h-4 w-4 text-muted-foreground" />
123
+ Saved to ~/.reflexio/.env and Claude Code settings
124
+ </div>
125
+ )}
126
+
127
+ <section className="space-y-4">
128
+ <div>
129
+ <h2 className="text-sm font-semibold">Dashboard</h2>
130
+ <p className="text-xs text-muted-foreground">
131
+ Stored in browser localStorage — only affects this UI.
132
+ </p>
133
+ </div>
134
+ <div className="space-y-2">
135
+ <Label>Reflexio endpoint (dashboard)</Label>
136
+ <Input
137
+ value={reflexioUrl}
138
+ onChange={(e) => setReflexioUrl(e.target.value)}
139
+ className="font-mono text-xs"
140
+ placeholder="http://localhost:8071"
141
+ />
142
+ </div>
143
+ </section>
144
+
145
+ <Separator />
146
+
147
+ <section className="space-y-4">
148
+ <div>
149
+ <h2 className="text-sm font-semibold">claude-smart environment</h2>
150
+ <p className="text-xs text-muted-foreground">
151
+ Writes to <code className="font-mono">~/.reflexio/.env</code>. Unknown
152
+ keys are preserved. These values are read by the Reflexio backend.
153
+ </p>
154
+ </div>
155
+
156
+ {config === null && !error ? (
157
+ <div className="text-sm text-muted-foreground">Loading…</div>
158
+ ) : config ? (
159
+ <div className="space-y-5">
160
+ <div className="space-y-2">
161
+ <Label>REFLEXIO_URL</Label>
162
+ <Input
163
+ value={config.REFLEXIO_URL}
164
+ onChange={(e) => update("REFLEXIO_URL", e.target.value)}
165
+ className="font-mono text-xs"
166
+ placeholder="http://localhost:8071/"
167
+ />
168
+ </div>
169
+
170
+ <div className="flex items-start justify-between gap-4">
171
+ <div className="min-w-0">
172
+ <Label htmlFor="use-local-cli">CLAUDE_SMART_USE_LOCAL_CLI</Label>
173
+ <p className="text-xs text-muted-foreground mt-0.5">
174
+ Route generation through the local <code>claude</code> CLI.
175
+ </p>
176
+ </div>
177
+ <Switch
178
+ id="use-local-cli"
179
+ checked={!!config.CLAUDE_SMART_USE_LOCAL_CLI}
180
+ onCheckedChange={(v) =>
181
+ update("CLAUDE_SMART_USE_LOCAL_CLI", v)
182
+ }
183
+ />
184
+ </div>
185
+
186
+ <div className="flex items-start justify-between gap-4">
187
+ <div className="min-w-0">
188
+ <Label htmlFor="use-local-embed">
189
+ CLAUDE_SMART_USE_LOCAL_EMBEDDING
190
+ </Label>
191
+ <p className="text-xs text-muted-foreground mt-0.5">
192
+ Use in-process ONNX embedder (offline-friendly).
193
+ </p>
194
+ </div>
195
+ <Switch
196
+ id="use-local-embed"
197
+ checked={!!config.CLAUDE_SMART_USE_LOCAL_EMBEDDING}
198
+ onCheckedChange={(v) =>
199
+ update("CLAUDE_SMART_USE_LOCAL_EMBEDDING", v)
200
+ }
201
+ />
202
+ </div>
203
+
204
+ <div className="space-y-2">
205
+ <Label>CLAUDE_SMART_CLI_PATH</Label>
206
+ <Input
207
+ value={String(config.CLAUDE_SMART_CLI_PATH ?? "")}
208
+ onChange={(e) => update("CLAUDE_SMART_CLI_PATH", e.target.value)}
209
+ className="font-mono text-xs"
210
+ placeholder="(empty — auto-detect via $PATH)"
211
+ />
212
+ </div>
213
+
214
+ <div className="space-y-2">
215
+ <Label>CLAUDE_SMART_CLI_TIMEOUT</Label>
216
+ <Input
217
+ value={String(config.CLAUDE_SMART_CLI_TIMEOUT ?? "")}
218
+ onChange={(e) =>
219
+ update("CLAUDE_SMART_CLI_TIMEOUT", e.target.value)
220
+ }
221
+ className="font-mono text-xs"
222
+ placeholder="120"
223
+ />
224
+ </div>
225
+
226
+ <div className="space-y-2">
227
+ <Label>CLAUDE_SMART_STATE_DIR</Label>
228
+ <Input
229
+ value={String(config.CLAUDE_SMART_STATE_DIR ?? "")}
230
+ onChange={(e) =>
231
+ update("CLAUDE_SMART_STATE_DIR", e.target.value)
232
+ }
233
+ className="font-mono text-xs"
234
+ placeholder="(empty — default ~/.claude-smart/sessions)"
235
+ />
236
+ </div>
237
+ </div>
238
+ ) : null}
239
+ </section>
240
+
241
+ <Separator />
242
+
243
+ <section className="space-y-4">
244
+ <div>
245
+ <h2 className="text-sm font-semibold">Claude Code hook environment</h2>
246
+ <p className="text-xs text-muted-foreground">
247
+ Writes to{" "}
248
+ <code className="font-mono">.claude/settings.local.json</code>.
249
+ Hook-side env changes apply to new Claude Code sessions.
250
+ </p>
251
+ </div>
252
+
253
+ {hookConfig === null && !error ? (
254
+ <div className="text-sm text-muted-foreground">Loading…</div>
255
+ ) : hookConfig ? (
256
+ <div className="space-y-5">
257
+ <div className="flex items-start justify-between gap-4">
258
+ <div className="min-w-0">
259
+ <Label htmlFor="enable-optimizer">
260
+ CLAUDE_SMART_ENABLE_OPTIMIZER
261
+ </Label>
262
+ <p className="text-xs text-muted-foreground mt-0.5">
263
+ Enable shared skill optimization and rollups during
264
+ SessionStart.
265
+ </p>
266
+ </div>
267
+ <Switch
268
+ id="enable-optimizer"
269
+ checked={!!hookConfig.CLAUDE_SMART_ENABLE_OPTIMIZER}
270
+ onCheckedChange={updateOptimizer}
271
+ />
272
+ </div>
273
+ <div className="rounded-md border border-border bg-muted/20 p-3 text-xs text-muted-foreground space-y-2">
274
+ <div className="flex items-center justify-between gap-4">
275
+ <span>Effective value</span>
276
+ <span className="font-medium text-foreground">
277
+ {formatSettingValue(hookConfig.effectiveValue)}
278
+ </span>
279
+ </div>
280
+ <div className="flex items-center justify-between gap-4">
281
+ <span>Project override</span>
282
+ <span className="font-medium text-foreground">
283
+ {formatSettingValue(hookConfig.localValue)}
284
+ </span>
285
+ </div>
286
+ <div className="flex items-center justify-between gap-4">
287
+ <span>User setting</span>
288
+ <span className="font-medium text-foreground">
289
+ {formatSettingValue(hookConfig.userValue)}
290
+ </span>
291
+ </div>
292
+ <div className="pt-1 space-y-1">
293
+ <p>
294
+ Project file:{" "}
295
+ <code className="font-mono break-all">
296
+ {hookConfig.settingsPath}
297
+ </code>
298
+ </p>
299
+ <p>
300
+ User file:{" "}
301
+ <code className="font-mono break-all">
302
+ {hookConfig.userSettingsPath}
303
+ </code>
304
+ </p>
305
+ </div>
306
+ </div>
307
+ </div>
308
+ ) : null}
309
+ </section>
310
+ </div>
311
+ </>
312
+ );
313
+ }
314
+
315
+ function formatSettingValue(value: boolean | null): string {
316
+ if (value === null) return "Not set";
317
+ return value ? "Enabled" : "Disabled";
318
+ }
@@ -0,0 +1,47 @@
1
+ "use client";
2
+
3
+ import { usePathname } from "next/navigation";
4
+ import { Server, SlidersHorizontal } from "lucide-react";
5
+ import { PageTabs } from "@/components/common/page-tabs";
6
+
7
+ const tabs = [
8
+ {
9
+ id: "env",
10
+ href: "/configure/env",
11
+ label: "Environment",
12
+ description: "Local plugin paths and runtime flags",
13
+ icon: SlidersHorizontal,
14
+ },
15
+ {
16
+ id: "server",
17
+ href: "/configure/server",
18
+ label: "Reflexio server",
19
+ description: "Extraction settings from the backend",
20
+ icon: Server,
21
+ },
22
+ ];
23
+
24
+ export default function ConfigureLayout({
25
+ children,
26
+ }: {
27
+ children: React.ReactNode;
28
+ }) {
29
+ const pathname = usePathname();
30
+
31
+ return (
32
+ <div className="flex-1 flex flex-col overflow-hidden">
33
+ <div className="border-b border-border bg-muted/20 px-6 py-4">
34
+ <PageTabs
35
+ items={tabs}
36
+ activeId={
37
+ tabs.find(
38
+ (tab) =>
39
+ pathname === tab.href || pathname.startsWith(`${tab.href}/`),
40
+ )?.id ?? "env"
41
+ }
42
+ />
43
+ </div>
44
+ <div className="flex-1 overflow-auto">{children}</div>
45
+ </div>
46
+ );
47
+ }
@@ -0,0 +1,5 @@
1
+ import { redirect } from "next/navigation";
2
+
3
+ export default function ConfigurePage() {
4
+ redirect("/configure/env");
5
+ }
@@ -0,0 +1,258 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useState } from "react";
4
+ import { Save, CheckCircle2 } from "lucide-react";
5
+ import { PageHeader } from "@/components/common/page-header";
6
+ import { Button } from "@/components/ui/button";
7
+ import { Input } from "@/components/ui/input";
8
+ import { Label } from "@/components/ui/label";
9
+ import { useSettings } from "@/hooks/use-settings";
10
+ import type { ReflexioConfig } from "@/lib/types";
11
+
12
+ type ExtractorField =
13
+ | "profile_extractor_configs"
14
+ | "user_playbook_extractor_configs";
15
+
16
+ export default function ConfigureServerPage() {
17
+ const { reflexioUrl } = useSettings();
18
+ const [srvConfig, setSrvConfig] = useState<ReflexioConfig | null>(null);
19
+ const [srvError, setSrvError] = useState<string | null>(null);
20
+ const [srvSaved, setSrvSaved] = useState(false);
21
+ const [srvSaving, setSrvSaving] = useState(false);
22
+ const [srvLoading, setSrvLoading] = useState(true);
23
+
24
+ const fetchSrvConfig = useCallback(async (): Promise<ReflexioConfig> => {
25
+ const headers: HeadersInit = {};
26
+ if (reflexioUrl) headers["x-reflexio-url"] = reflexioUrl;
27
+ const res = await fetch("/api/reflexio/api/get_config", {
28
+ cache: "no-store",
29
+ headers,
30
+ });
31
+ if (!res.ok) {
32
+ const body = await res.json().catch(() => null);
33
+ const detail = body?.error
34
+ ? `${body.error}${body.detail ? `: ${body.detail}` : ""}`
35
+ : `HTTP ${res.status}`;
36
+ throw new Error(detail);
37
+ }
38
+ return (await res.json()) as ReflexioConfig;
39
+ }, [reflexioUrl]);
40
+
41
+ useEffect(() => {
42
+ let alive = true;
43
+ async function load() {
44
+ setSrvLoading(true);
45
+ setSrvError(null);
46
+ try {
47
+ const data = await fetchSrvConfig();
48
+ if (alive) setSrvConfig(data);
49
+ } catch (e) {
50
+ if (alive) setSrvError(e instanceof Error ? e.message : String(e));
51
+ } finally {
52
+ if (alive) setSrvLoading(false);
53
+ }
54
+ }
55
+ void load();
56
+ return () => {
57
+ alive = false;
58
+ };
59
+ }, [fetchSrvConfig]);
60
+
61
+ const updateSrv = <K extends keyof ReflexioConfig>(
62
+ key: K,
63
+ value: ReflexioConfig[K],
64
+ ) => {
65
+ setSrvConfig((prev) => (prev ? { ...prev, [key]: value } : prev));
66
+ setSrvSaved(false);
67
+ };
68
+
69
+ const updateExtractorPrompt = (field: ExtractorField, v: string) => {
70
+ setSrvConfig((prev) => {
71
+ if (!prev) return prev;
72
+ const list = prev[field];
73
+ if (!list || list.length === 0) return prev;
74
+ const next = [...list];
75
+ next[0] = { ...next[0], extraction_definition_prompt: v };
76
+ return { ...prev, [field]: next };
77
+ });
78
+ setSrvSaved(false);
79
+ };
80
+
81
+ const saveSrv = async () => {
82
+ if (!srvConfig) return;
83
+ setSrvSaving(true);
84
+ setSrvError(null);
85
+ try {
86
+ const headers: HeadersInit = { "content-type": "application/json" };
87
+ if (reflexioUrl) headers["x-reflexio-url"] = reflexioUrl;
88
+ const res = await fetch("/api/reflexio/api/set_config", {
89
+ method: "POST",
90
+ headers,
91
+ body: JSON.stringify(srvConfig),
92
+ });
93
+ if (!res.ok) {
94
+ const body = await res.json().catch(() => null);
95
+ const detail = body?.error
96
+ ? `${body.error}${body.detail ? `: ${body.detail}` : ""}`
97
+ : `HTTP ${res.status}`;
98
+ throw new Error(detail);
99
+ }
100
+ const fresh = await fetchSrvConfig();
101
+ setSrvConfig(fresh);
102
+ setSrvSaved(true);
103
+ } catch (e) {
104
+ setSrvError(e instanceof Error ? e.message : String(e));
105
+ } finally {
106
+ setSrvSaving(false);
107
+ }
108
+ };
109
+
110
+ return (
111
+ <>
112
+ <PageHeader
113
+ title="Reflexio server"
114
+ description="Fetched live from the reflexio backend via /api/get_config. Requires reflexio to be running."
115
+ actions={
116
+ <Button onClick={saveSrv} disabled={!srvConfig || srvSaving} size="sm">
117
+ <Save className="h-3.5 w-3.5" />
118
+ {srvSaving ? "Saving…" : "Save"}
119
+ </Button>
120
+ }
121
+ />
122
+
123
+ <div className="p-6 max-w-2xl mx-auto space-y-6">
124
+ {srvError && (
125
+ <div className="rounded-lg border border-destructive/30 bg-destructive/5 text-destructive px-4 py-3 text-sm">
126
+ {srvError}
127
+ </div>
128
+ )}
129
+ {srvSaved && (
130
+ <div className="rounded-lg border border-border bg-accent/40 px-4 py-2.5 text-sm flex items-center gap-2">
131
+ <CheckCircle2 className="h-4 w-4 text-muted-foreground" />
132
+ Saved to reflexio
133
+ </div>
134
+ )}
135
+
136
+ {srvLoading ? (
137
+ <div className="text-sm text-muted-foreground">Loading…</div>
138
+ ) : srvConfig ? (
139
+ <section className="space-y-5">
140
+ <div className="space-y-2">
141
+ <Label>Additional agent description</Label>
142
+ <p className="text-xs text-muted-foreground">
143
+ Free-form context about the agent&apos;s working environment.
144
+ Prepended to extractor prompts.
145
+ </p>
146
+ <textarea
147
+ value={srvConfig.agent_context_prompt ?? ""}
148
+ onChange={(e) =>
149
+ updateSrv("agent_context_prompt", e.target.value || null)
150
+ }
151
+ className="w-full font-mono text-xs min-h-[80px] rounded-md border border-input bg-transparent px-3 py-2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
152
+ placeholder="(empty)"
153
+ />
154
+ </div>
155
+
156
+ <div className="space-y-2">
157
+ <Label>Extraction window size</Label>
158
+ <p className="text-xs text-muted-foreground">
159
+ How many recent interactions are fed into each extraction run
160
+ (past K interactions).
161
+ </p>
162
+ <Input
163
+ type="number"
164
+ min={1}
165
+ value={srvConfig.window_size ?? ""}
166
+ onChange={(e) => {
167
+ const n = Number(e.target.value);
168
+ updateSrv(
169
+ "window_size",
170
+ Number.isFinite(n) && n > 0 ? n : undefined,
171
+ );
172
+ }}
173
+ className="font-mono text-xs"
174
+ placeholder="10"
175
+ />
176
+ </div>
177
+
178
+ <div className="space-y-2">
179
+ <Label>Extraction stride</Label>
180
+ <p className="text-xs text-muted-foreground">
181
+ How many new interactions must accumulate before the next
182
+ extraction run.
183
+ </p>
184
+ <Input
185
+ type="number"
186
+ min={1}
187
+ value={srvConfig.stride_size ?? ""}
188
+ onChange={(e) => {
189
+ const n = Number(e.target.value);
190
+ updateSrv(
191
+ "stride_size",
192
+ Number.isFinite(n) && n > 0 ? n : undefined,
193
+ );
194
+ }}
195
+ className="font-mono text-xs"
196
+ placeholder="5"
197
+ />
198
+ </div>
199
+
200
+ <div className="space-y-2">
201
+ <Label>Preference focus area</Label>
202
+ <p className="text-xs text-muted-foreground">
203
+ What the preference extractor should focus on.
204
+ </p>
205
+ {srvConfig.profile_extractor_configs &&
206
+ srvConfig.profile_extractor_configs.length > 0 ? (
207
+ <textarea
208
+ value={
209
+ srvConfig.profile_extractor_configs[0]
210
+ .extraction_definition_prompt ?? ""
211
+ }
212
+ onChange={(e) =>
213
+ updateExtractorPrompt(
214
+ "profile_extractor_configs",
215
+ e.target.value,
216
+ )
217
+ }
218
+ className="w-full font-mono text-xs min-h-[120px] rounded-md border border-input bg-transparent px-3 py-2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
219
+ />
220
+ ) : (
221
+ <div className="text-xs text-muted-foreground italic">
222
+ No preference extractor configured on the server.
223
+ </div>
224
+ )}
225
+ </div>
226
+
227
+ <div className="space-y-2">
228
+ <Label>Project-specific skill focus area</Label>
229
+ <p className="text-xs text-muted-foreground">
230
+ What the project-specific skill extractor should focus on.
231
+ </p>
232
+ {srvConfig.user_playbook_extractor_configs &&
233
+ srvConfig.user_playbook_extractor_configs.length > 0 ? (
234
+ <textarea
235
+ value={
236
+ srvConfig.user_playbook_extractor_configs[0]
237
+ .extraction_definition_prompt ?? ""
238
+ }
239
+ onChange={(e) =>
240
+ updateExtractorPrompt(
241
+ "user_playbook_extractor_configs",
242
+ e.target.value,
243
+ )
244
+ }
245
+ className="w-full font-mono text-xs min-h-[120px] rounded-md border border-input bg-transparent px-3 py-2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
246
+ />
247
+ ) : (
248
+ <div className="text-xs text-muted-foreground italic">
249
+ No project-specific skill extractor configured on the server.
250
+ </div>
251
+ )}
252
+ </div>
253
+ </section>
254
+ ) : null}
255
+ </div>
256
+ </>
257
+ );
258
+ }