claude-smart 0.2.31 → 0.2.32

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 (59) hide show
  1. package/README.md +2 -2
  2. package/bin/claude-smart.js +172 -18
  3. package/package.json +1 -1
  4. package/plugin/.claude-plugin/plugin.json +1 -1
  5. package/plugin/.codex-plugin/plugin.json +1 -1
  6. package/plugin/dashboard/app/api/config/route.ts +11 -2
  7. package/plugin/dashboard/app/api/health/route.ts +45 -6
  8. package/plugin/dashboard/app/api/reflexio/[...path]/route.ts +15 -7
  9. package/plugin/dashboard/app/api/rules/applied/route.ts +27 -0
  10. package/plugin/dashboard/app/configure/env/page.tsx +36 -31
  11. package/plugin/dashboard/app/configure/layout.tsx +1 -1
  12. package/plugin/dashboard/app/configure/server/page.tsx +8 -14
  13. package/plugin/dashboard/app/dashboard/page.tsx +311 -115
  14. package/plugin/dashboard/app/globals.css +80 -66
  15. package/plugin/dashboard/app/layout.tsx +13 -10
  16. package/plugin/dashboard/app/preferences/[id]/page.tsx +21 -32
  17. package/plugin/dashboard/app/preferences/page.tsx +154 -54
  18. package/plugin/dashboard/app/preferences/project/[id]/page.tsx +1 -0
  19. package/plugin/dashboard/app/rules/[id]/page.tsx +51 -0
  20. package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +4 -4
  21. package/plugin/dashboard/app/sessions/page.tsx +14 -10
  22. package/plugin/dashboard/app/skills/page.tsx +175 -56
  23. package/plugin/dashboard/app/skills/project/[id]/page.tsx +22 -38
  24. package/plugin/dashboard/app/skills/shared/[id]/page.tsx +20 -37
  25. package/plugin/dashboard/components/common/delete-learning-danger-zone.tsx +166 -0
  26. package/plugin/dashboard/components/common/empty-state.tsx +4 -2
  27. package/plugin/dashboard/components/common/learnings-badge.tsx +1 -1
  28. package/plugin/dashboard/components/common/page-header.tsx +5 -3
  29. package/plugin/dashboard/components/common/page-tabs.tsx +4 -4
  30. package/plugin/dashboard/components/common/stat-card.tsx +9 -5
  31. package/plugin/dashboard/components/layout/sidebar.tsx +24 -9
  32. package/plugin/dashboard/components/layout/top-bar.tsx +37 -25
  33. package/plugin/dashboard/components/ui/input.tsx +1 -0
  34. package/plugin/dashboard/hooks/use-settings.tsx +30 -61
  35. package/plugin/dashboard/hooks/use-stall-state.ts +5 -9
  36. package/plugin/dashboard/lib/config-file.ts +2 -0
  37. package/plugin/dashboard/lib/reflexio-client.ts +23 -48
  38. package/plugin/dashboard/lib/session-reader.ts +222 -6
  39. package/plugin/dashboard/lib/types.ts +20 -1
  40. package/plugin/dashboard/package-lock.json +70 -95
  41. package/plugin/dashboard/package.json +5 -2
  42. package/plugin/pyproject.toml +1 -1
  43. package/plugin/scripts/_lib.sh +61 -0
  44. package/plugin/scripts/backend-service.sh +13 -2
  45. package/plugin/scripts/cli.sh +1 -0
  46. package/plugin/scripts/codex-hook.js +100 -3
  47. package/plugin/scripts/dashboard-service.sh +95 -19
  48. package/plugin/scripts/hook_entry.sh +13 -7
  49. package/plugin/scripts/smart-install.sh +18 -13
  50. package/plugin/src/claude_smart/cli.py +86 -20
  51. package/plugin/src/claude_smart/context_format.py +244 -6
  52. package/plugin/src/claude_smart/context_inject.py +8 -1
  53. package/plugin/src/claude_smart/cs_cite.py +186 -34
  54. package/plugin/src/claude_smart/env_config.py +102 -0
  55. package/plugin/src/claude_smart/events/stop.py +32 -6
  56. package/plugin/src/claude_smart/internal_call.py +30 -0
  57. package/plugin/src/claude_smart/publish.py +5 -0
  58. package/plugin/src/claude_smart/reflexio_adapter.py +102 -26
  59. package/plugin/uv.lock +1 -1
package/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
  <img src="https://img.shields.io/badge/License-Apache%202.0-blue.svg" alt="License">
14
14
  </a>
15
15
  <a href="plugin/pyproject.toml">
16
- <img src="https://img.shields.io/badge/version-0.2.31-green.svg" alt="Version">
16
+ <img src="https://img.shields.io/badge/version-0.2.32-green.svg" alt="Version">
17
17
  </a>
18
18
  <a href="plugin/pyproject.toml">
19
19
  <img src="https://img.shields.io/badge/python-%3E%3D3.12-brightgreen.svg" alt="Python">
@@ -120,7 +120,7 @@ To uninstall:
120
120
  npx claude-smart uninstall --host codex
121
121
  ```
122
122
 
123
- Restart Codex after uninstalling. Learned data under `~/.reflexio/` and `~/.claude-smart/` is preserved and shared with Claude Code, so you can switch between hosts without losing skills or preferences.
123
+ Restart Codex after uninstalling. The uninstaller stops local claude-smart services and removes plugin/cache/config state; learned data under `~/.reflexio/` and `~/.claude-smart/` is preserved and shared with Claude Code, so you can switch between hosts without losing skills or preferences.
124
124
 
125
125
  Developing the plugin itself? See [DEVELOPER.md](./DEVELOPER.md#developing-locally) for what the installer does, manual toggles via `/plugins`, and clone-based development.
126
126
 
@@ -11,10 +11,11 @@
11
11
  */
12
12
  "use strict";
13
13
 
14
- const { execSync, spawn } = require("child_process");
14
+ const { execSync, spawn, spawnSync } = require("child_process");
15
15
  const crypto = require("crypto");
16
16
  const {
17
17
  appendFileSync,
18
+ chmodSync,
18
19
  cpSync,
19
20
  existsSync,
20
21
  lstatSync,
@@ -37,6 +38,7 @@ const CODEX_MARKETPLACE_NAME = "reflexioai";
37
38
  const CODEX_MARKETPLACE_DISPLAY_NAME = "ReflexioAI";
38
39
  const CODEX_PLUGIN_ID = `claude-smart@${CODEX_MARKETPLACE_NAME}`;
39
40
  const REFLEXIO_ENV_PATH = join(homedir(), ".reflexio", ".env");
41
+ const MANAGED_REFLEXIO_URL = "https://www.reflexio.ai/";
40
42
  const REFLEXIO_DIR = join(homedir(), ".reflexio");
41
43
  const CLAUDE_SMART_STATE_DIR = join(homedir(), ".claude-smart");
42
44
  const CODEX_CONFIG_PATH = join(homedir(), ".codex", "config.toml");
@@ -48,7 +50,7 @@ const CODEX_MARKETPLACE_DIR = join(
48
50
  "marketplaces",
49
51
  CODEX_MARKETPLACE_NAME,
50
52
  );
51
- const CODEX_MARKETPLACE_PLUGIN_PATH = join("plugins", "claude-smart");
53
+ const CODEX_MARKETPLACE_PLUGIN_PATH = "plugin";
52
54
  const CODEX_PLUGIN_CACHE_DIR = join(
53
55
  homedir(),
54
56
  ".codex",
@@ -57,6 +59,14 @@ const CODEX_PLUGIN_CACHE_DIR = join(
57
59
  CODEX_MARKETPLACE_NAME,
58
60
  "claude-smart",
59
61
  );
62
+ const LOCAL_DATA_NOTICE = [
63
+ "Local data was kept so reinstalling claude-smart can reuse your learned rules, sessions, logs, and local Reflexio data.",
64
+ "Kept folders:",
65
+ " ~/.claude-smart",
66
+ " ~/.reflexio",
67
+ "Delete them only if you want a full reset or need to remove local claude-smart data from this machine:",
68
+ " rm -rf ~/.claude-smart ~/.reflexio",
69
+ ];
60
70
  const CODEX_REQUIRED_FILES = [
61
71
  ".agents/plugins/marketplace.json",
62
72
  "plugin/.codex-plugin/plugin.json",
@@ -68,6 +78,7 @@ const CODEX_REQUIRED_FILES = [
68
78
  "plugin/scripts/_codex_env.sh",
69
79
  ];
70
80
  const CODEX_CLI_TIMEOUT_MS = 30_000;
81
+ const PLUGIN_SERVICE_TIMEOUT_MS = 15_000;
71
82
  const COPYTREE_IGNORE_NAMES = new Set([
72
83
  "__pycache__",
73
84
  ".venv",
@@ -197,9 +208,82 @@ function seedReflexioEnv() {
197
208
  const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
198
209
  const body = missing.map((f) => `${f}=1`).join("\n") + "\n";
199
210
  appendFileSync(REFLEXIO_ENV_PATH, prefix + body);
211
+ chmodSync(REFLEXIO_ENV_PATH, 0o600);
200
212
  return missing;
201
213
  }
202
214
 
215
+ function escapeEnvValue(value) {
216
+ return String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
217
+ }
218
+
219
+ function parseEnvLine(line) {
220
+ let trimmed = String(line || "").trim();
221
+ if (!trimmed || trimmed.startsWith("#")) return null;
222
+ if (trimmed.startsWith("export ")) trimmed = trimmed.slice("export ".length).trimStart();
223
+ const eq = trimmed.indexOf("=");
224
+ if (eq < 0) return null;
225
+ const key = trimmed.slice(0, eq).trim();
226
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) return null;
227
+ return { key };
228
+ }
229
+
230
+ function setReflexioEnvVars(values) {
231
+ mkdirSync(dirname(REFLEXIO_ENV_PATH), { recursive: true });
232
+ const existing = existsSync(REFLEXIO_ENV_PATH)
233
+ ? readFileSync(REFLEXIO_ENV_PATH, "utf8")
234
+ : "";
235
+ const lines = existing ? existing.split(/\r?\n/) : [];
236
+ const seen = new Set();
237
+ const out = [];
238
+ for (const line of lines) {
239
+ const parsed = parseEnvLine(line);
240
+ if (parsed && Object.prototype.hasOwnProperty.call(values, parsed.key)) {
241
+ out.push(`${parsed.key}="${escapeEnvValue(values[parsed.key])}"`);
242
+ seen.add(parsed.key);
243
+ } else {
244
+ out.push(line);
245
+ }
246
+ }
247
+ const added = [];
248
+ for (const [key, value] of Object.entries(values)) {
249
+ if (seen.has(key)) continue;
250
+ out.push(`${key}="${escapeEnvValue(value)}"`);
251
+ added.push(key);
252
+ }
253
+ const content = out.join("\n").replace(/\n*$/, "\n");
254
+ writeFileSync(REFLEXIO_ENV_PATH, content);
255
+ chmodSync(REFLEXIO_ENV_PATH, 0o600);
256
+ return added;
257
+ }
258
+
259
+ function maskSecret(value) {
260
+ if (!value) return "";
261
+ if (value.length <= 8) return "*".repeat(value.length);
262
+ const prefix = value.slice(0, 8).includes("-") ? value.slice(0, 5) : value.slice(0, 4);
263
+ return `${prefix}****${value.slice(-4)}`;
264
+ }
265
+
266
+ function configureReflexioSetup(args) {
267
+ const apiKey = parseOptionalArg(args, "--api-key").trim();
268
+ if (apiKey) {
269
+ const reflexioUrl = parseOptionalArg(args, "--reflexio-url") || MANAGED_REFLEXIO_URL;
270
+ const added = setReflexioEnvVars({
271
+ REFLEXIO_URL: reflexioUrl,
272
+ REFLEXIO_API_KEY: apiKey,
273
+ });
274
+ const changed = added.length > 0 ? added.join(", ") : "managed Reflexio settings";
275
+ process.stdout.write(
276
+ `Configured ${REFLEXIO_ENV_PATH} for managed Reflexio (${changed}; API key ${maskSecret(apiKey)}).\n`,
277
+ );
278
+ return;
279
+ }
280
+
281
+ const added = seedReflexioEnv();
282
+ if (added.length > 0) {
283
+ process.stdout.write(`Seeded ${REFLEXIO_ENV_PATH} with ${added.join(", ")}.\n`);
284
+ }
285
+ }
286
+
203
287
  function findClaudeCodePluginRoot() {
204
288
  const cacheRoot = join(homedir(), ".claude", "plugins", "cache", CODEX_MARKETPLACE_NAME, "claude-smart");
205
289
  const candidates = [];
@@ -342,6 +426,45 @@ function runChecked(command, args, options = {}) {
342
426
  });
343
427
  }
344
428
 
429
+ function runPluginService(pluginRoot, scriptName, subcommand) {
430
+ const script = join(pluginRoot, "scripts", scriptName);
431
+ if (!existsSync(script)) return false;
432
+ const bash = resolveCommand(isWindows() ? ["bash.exe", "bash"] : ["bash"]);
433
+ if (!bash) return false;
434
+ const result = spawnSync(bash, [script, subcommand], {
435
+ cwd: pluginRoot,
436
+ env: runtimeEnv(),
437
+ stdio: "ignore",
438
+ windowsHide: true,
439
+ timeout: PLUGIN_SERVICE_TIMEOUT_MS,
440
+ killSignal: "SIGTERM",
441
+ });
442
+ if (result.error || result.signal) {
443
+ const reason = result.error && result.error.code === "ETIMEDOUT"
444
+ ? `timed out after ${PLUGIN_SERVICE_TIMEOUT_MS / 1000}s`
445
+ : result.error
446
+ ? result.error.message
447
+ : `terminated by ${result.signal}`;
448
+ process.stderr.write(
449
+ `warning: ${scriptName} ${subcommand} ${reason}; continuing.\n`,
450
+ );
451
+ return false;
452
+ }
453
+ return result.status === 0;
454
+ }
455
+
456
+ function refreshDashboardService(pluginRoot) {
457
+ // dashboard-service.sh is marker-gated: stop only reaps a listener that
458
+ // identifies as claude-smart, so foreign apps on 3001 are left alone.
459
+ runPluginService(pluginRoot, "dashboard-service.sh", "stop");
460
+ return runPluginService(pluginRoot, "dashboard-service.sh", "start");
461
+ }
462
+
463
+ function stopClaudeSmartServices(pluginRoot) {
464
+ runPluginService(pluginRoot, "dashboard-service.sh", "stop");
465
+ runPluginService(pluginRoot, "backend-service.sh", "stop");
466
+ }
467
+
345
468
  function downloadFile(url, dest) {
346
469
  return new Promise((resolve, reject) => {
347
470
  const request = https.get(url, (response) => {
@@ -676,6 +799,7 @@ function printHelp() {
676
799
  " npx claude-smart install Install the plugin into Claude Code",
677
800
  " npx claude-smart install --host codex Register the plugin marketplace for Codex",
678
801
  " npx claude-smart install --source <owner/repo> Override the marketplace source",
802
+ " npx claude-smart install --api-key <key> Use managed Reflexio service",
679
803
  " npx claude-smart uninstall --host codex Remove the Codex marketplace registration",
680
804
  " npx claude-smart --help Show this help",
681
805
  "",
@@ -684,6 +808,7 @@ function printHelp() {
684
808
  ` 2. claude plugin install ${PLUGIN_SPEC}`,
685
809
  " 3. Appends CLAUDE_SMART_USE_LOCAL_CLI=1 and CLAUDE_SMART_USE_LOCAL_EMBEDDING=1",
686
810
  " to ~/.reflexio/.env (idempotent).",
811
+ " Passing --api-key writes REFLEXIO_URL and REFLEXIO_API_KEY instead.",
687
812
  "",
688
813
  "Codex install:",
689
814
  ` 1. Copies the bundled marketplace to ${CODEX_MARKETPLACE_DIR}`,
@@ -715,6 +840,17 @@ function parseSource(args) {
715
840
  return value;
716
841
  }
717
842
 
843
+ function parseOptionalArg(args, flag) {
844
+ const idx = args.indexOf(flag);
845
+ if (idx === -1) return "";
846
+ const value = args[idx + 1];
847
+ if (!value) {
848
+ process.stderr.write(`error: ${flag} requires a value\n`);
849
+ process.exit(1);
850
+ }
851
+ return value;
852
+ }
853
+
718
854
  function parseHost(args) {
719
855
  const idx = args.indexOf("--host");
720
856
  if (idx === -1) return "claude-code";
@@ -791,6 +927,21 @@ function copyCodexMarketplace() {
791
927
  return CODEX_MARKETPLACE_DIR;
792
928
  }
793
929
 
930
+ function codexMarketplacePluginRoot(marketplaceRoot) {
931
+ const manifestPath = join(marketplaceRoot, ".agents", "plugins", "marketplace.json");
932
+ const fallback = join(marketplaceRoot, CODEX_MARKETPLACE_PLUGIN_PATH);
933
+ try {
934
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
935
+ const entry = (manifest.plugins || []).find((plugin) => plugin.name === "claude-smart");
936
+ const rawPath = entry && entry.source && entry.source.path;
937
+ if (typeof rawPath !== "string" || !rawPath) return fallback;
938
+ const relPath = rawPath.replace(/^\.\//, "");
939
+ return join(marketplaceRoot, relPath);
940
+ } catch {
941
+ return fallback;
942
+ }
943
+ }
944
+
794
945
  function removeTomlSections(path, { exact, prefixes = [] }) {
795
946
  if (!existsSync(path)) return true;
796
947
  const text = readFileSync(path, "utf8");
@@ -1138,12 +1289,13 @@ async function runUninstall(args) {
1138
1289
  );
1139
1290
  process.exit(code);
1140
1291
  }
1292
+ stopClaudeSmartServices(join(PACKAGE_ROOT, "plugin"));
1141
1293
 
1142
1294
  process.stdout.write(
1143
1295
  [
1144
1296
  "",
1145
1297
  "claude-smart uninstalled. Restart Claude Code to apply.",
1146
- "Local data in ~/.reflexio/ and ~/.claude-smart/ was left in place — remove manually if desired.",
1298
+ ...LOCAL_DATA_NOTICE,
1147
1299
  "",
1148
1300
  ].join("\n"),
1149
1301
  );
@@ -1151,7 +1303,7 @@ async function runUninstall(args) {
1151
1303
 
1152
1304
  async function runInstall(args) {
1153
1305
  if (parseHost(args) === "codex") {
1154
- await runInstallCodex();
1306
+ await runInstallCodex(args);
1155
1307
  return;
1156
1308
  }
1157
1309
 
@@ -1179,15 +1331,13 @@ async function runInstall(args) {
1179
1331
  }
1180
1332
  }
1181
1333
 
1182
- const added = seedReflexioEnv();
1183
- if (added.length > 0) {
1184
- process.stdout.write(
1185
- `Seeded ${REFLEXIO_ENV_PATH} with ${added.join(", ")}.\n`,
1186
- );
1187
- }
1334
+ configureReflexioSetup(args);
1188
1335
  try {
1189
1336
  const pluginRoot = await bootstrapClaudeCodeInstall();
1190
1337
  process.stdout.write(`Prepared claude-smart runtime at ${pluginRoot}.\n`);
1338
+ if (refreshDashboardService(pluginRoot)) {
1339
+ process.stdout.write("Refreshed claude-smart dashboard service.\n");
1340
+ }
1191
1341
  } catch (err) {
1192
1342
  process.stderr.write(
1193
1343
  `error: claude-smart installed, but dependency bootstrap failed: ${err && err.message ? err.message : err}\n`,
@@ -1209,7 +1359,7 @@ async function runInstall(args) {
1209
1359
  );
1210
1360
  }
1211
1361
 
1212
- async function runInstallCodex() {
1362
+ async function runInstallCodex(args) {
1213
1363
  if (!hasCli("codex")) {
1214
1364
  process.stderr.write("error: 'codex' CLI not found on PATH. Install Codex first.\n");
1215
1365
  process.exit(1);
@@ -1254,9 +1404,13 @@ async function runInstallCodex() {
1254
1404
  let trustedHookCount = 0;
1255
1405
  let trustError = null;
1256
1406
  try {
1257
- cacheDir = installCodexPluginCache(join(marketplaceRoot, CODEX_MARKETPLACE_PLUGIN_PATH));
1407
+ cacheDir = installCodexPluginCache(codexMarketplacePluginRoot(marketplaceRoot));
1258
1408
  process.stdout.write(`Installed Codex plugin cache at ${cacheDir}.\n`);
1409
+ configureReflexioSetup(args);
1259
1410
  await bootstrapPluginRuntime(cacheDir);
1411
+ if (refreshDashboardService(cacheDir)) {
1412
+ process.stdout.write("Refreshed claude-smart dashboard service.\n");
1413
+ }
1260
1414
  } catch (err) {
1261
1415
  process.stderr.write(
1262
1416
  `error: automatic Codex plugin install failed: ${err && err.message ? err.message : err}\n`,
@@ -1289,11 +1443,6 @@ async function runInstallCodex() {
1289
1443
  process.stdout.write(`Trusted and enabled ${trustedHookCount} claude-smart Codex hooks.\n`);
1290
1444
  }
1291
1445
 
1292
- const added = seedReflexioEnv();
1293
- if (added.length > 0) {
1294
- process.stdout.write(`Seeded ${REFLEXIO_ENV_PATH} with ${added.join(", ")}.\n`);
1295
- }
1296
-
1297
1446
  process.stdout.write(
1298
1447
  [
1299
1448
  "",
@@ -1306,6 +1455,7 @@ async function runInstallCodex() {
1306
1455
  }
1307
1456
 
1308
1457
  async function runUninstallCodex() {
1458
+ stopClaudeSmartServices(join(PACKAGE_ROOT, "plugin"));
1309
1459
  if (!hasCli("codex")) {
1310
1460
  process.stdout.write("Codex CLI not found; skipping marketplace removal.\n");
1311
1461
  cleanupCodexInstallState();
@@ -1324,7 +1474,8 @@ async function runUninstallCodex() {
1324
1474
  [
1325
1475
  "",
1326
1476
  "claude-smart Codex plugin and marketplace state removed. Restart Codex to apply.",
1327
- "Codex's global hook feature flags and local data under ~/.reflexio/ and ~/.claude-smart/ were left in place.",
1477
+ "Codex's global hook feature flags were left in place.",
1478
+ ...LOCAL_DATA_NOTICE,
1328
1479
  "",
1329
1480
  ].join("\n"),
1330
1481
  );
@@ -1370,8 +1521,11 @@ if (require.main === module) {
1370
1521
  module.exports = {
1371
1522
  assertSupportedRuntimePlatform,
1372
1523
  bootstrapPluginRuntime,
1524
+ codexMarketplacePluginRoot,
1525
+ copyCodexMarketplace,
1373
1526
  ensurePrivateNode,
1374
1527
  ensureUv,
1528
+ configureReflexioSetup,
1375
1529
  patchCodexHooksForNode,
1376
1530
  platformSupportError,
1377
1531
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-smart",
3
- "version": "0.2.31",
3
+ "version": "0.2.32",
4
4
  "description": "Self-improving Claude Code plugin — learns from corrections via reflexio",
5
5
  "keywords": [
6
6
  "claude",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-smart",
3
- "version": "0.2.31",
3
+ "version": "0.2.32",
4
4
  "description": "Self-improving Claude Code plugin — learns from corrections across sessions via reflexio",
5
5
  "author": {
6
6
  "name": "Yi Lu"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-smart",
3
- "version": "0.2.31",
3
+ "version": "0.2.32",
4
4
  "description": "Self-improving coding assistant plugin — learns from corrections across sessions via reflexio",
5
5
  "author": {
6
6
  "name": "Yi Lu"
@@ -1,16 +1,25 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import { readConfig, writeConfig } from "@/lib/config-file";
3
+ import type { ClaudeSmartConfig } from "@/lib/types";
3
4
 
4
5
  export const dynamic = "force-dynamic";
5
6
 
7
+ function publicConfig(config: ClaudeSmartConfig): ClaudeSmartConfig {
8
+ return {
9
+ ...config,
10
+ REFLEXIO_API_KEY: "",
11
+ REFLEXIO_API_KEY_SET: Boolean(config.REFLEXIO_API_KEY),
12
+ };
13
+ }
14
+
6
15
  export async function GET() {
7
16
  const config = await readConfig();
8
- return NextResponse.json(config);
17
+ return NextResponse.json(publicConfig(config));
9
18
  }
10
19
 
11
20
  export async function PUT(req: Request) {
12
21
  const body = await req.json();
13
22
  await writeConfig(body);
14
23
  const config = await readConfig();
15
- return NextResponse.json(config);
24
+ return NextResponse.json(publicConfig(config));
16
25
  }
@@ -1,10 +1,49 @@
1
- export const dynamic = "force-static";
1
+ import { readFileSync, realpathSync } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ export const dynamic = "force-dynamic";
5
+
6
+ function realpathOrSelf(value: string): string {
7
+ try {
8
+ return realpathSync(value);
9
+ } catch {
10
+ return value;
11
+ }
12
+ }
13
+
14
+ function pluginVersion(pluginRoot: string): string {
15
+ try {
16
+ const manifest = JSON.parse(
17
+ readFileSync(path.join(pluginRoot, ".codex-plugin", "plugin.json"), "utf8"),
18
+ );
19
+ return typeof manifest.version === "string" && manifest.version
20
+ ? manifest.version
21
+ : "unknown";
22
+ } catch {
23
+ return "unknown";
24
+ }
25
+ }
2
26
 
3
27
  export function GET() {
4
- return new Response(JSON.stringify({ service: "claude-smart-dashboard" }), {
5
- headers: {
6
- "content-type": "application/json",
7
- "x-claude-smart-dashboard": "1",
28
+ const dashboardDir = realpathOrSelf(process.cwd());
29
+ const pluginRoot = realpathOrSelf(path.dirname(dashboardDir));
30
+ const version = pluginVersion(pluginRoot);
31
+
32
+ return new Response(
33
+ JSON.stringify({
34
+ service: "claude-smart-dashboard",
35
+ pluginRoot,
36
+ dashboardDir,
37
+ version,
38
+ }),
39
+ {
40
+ headers: {
41
+ "content-type": "application/json",
42
+ "x-claude-smart-dashboard": "1",
43
+ "x-claude-smart-plugin-root": pluginRoot,
44
+ "x-claude-smart-dashboard-dir": dashboardDir,
45
+ "x-claude-smart-version": version,
46
+ },
8
47
  },
9
- });
48
+ );
10
49
  }
@@ -1,16 +1,21 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import { originOnly } from "@/lib/reflexio-url";
3
+ import { readConfig } from "@/lib/config-file";
3
4
 
4
5
  export const dynamic = "force-dynamic";
5
6
 
6
7
  const DEFAULT_URL = "http://localhost:8071";
7
8
 
8
- function reflexioBase(req: Request): string {
9
- const header = req.headers.get("x-reflexio-url");
10
- const fromHeader = header ? originOnly(header) : null;
11
- if (fromHeader) return fromHeader;
9
+ async function reflexioConfig(): Promise<{ base: string; apiKey: string }> {
10
+ const config = await readConfig();
11
+ const apiKey = config.REFLEXIO_API_KEY || process.env.REFLEXIO_API_KEY || "";
12
12
  const fromEnv = originOnly(process.env.REFLEXIO_URL ?? "");
13
- return fromEnv ?? DEFAULT_URL;
13
+ const fromConfig = originOnly(config.REFLEXIO_URL ?? "");
14
+ const configuredBase = fromEnv ?? fromConfig;
15
+ return {
16
+ base: configuredBase ?? DEFAULT_URL,
17
+ apiKey: configuredBase ? apiKey : "",
18
+ };
14
19
  }
15
20
 
16
21
  async function proxy(
@@ -20,12 +25,15 @@ async function proxy(
20
25
  const { path } = await context.params;
21
26
  const targetPath = path.join("/");
22
27
  const url = new URL(req.url);
23
- const target = `${reflexioBase(req)}/${targetPath}${url.search}`;
28
+ const { base, apiKey } = await reflexioConfig();
29
+ const target = `${base}/${targetPath}${url.search}`;
24
30
 
25
31
  const headers = new Headers(req.headers);
26
32
  headers.delete("host");
27
- headers.delete("x-reflexio-url");
28
33
  headers.delete("connection");
34
+ headers.delete("authorization");
35
+ headers.set("user-agent", "claude-smart");
36
+ if (apiKey) headers.set("authorization", `Bearer ${apiKey}`);
29
37
 
30
38
  const init: RequestInit = {
31
39
  method: req.method,
@@ -0,0 +1,27 @@
1
+ import { NextResponse } from "next/server";
2
+ import { listAppliedRules } from "@/lib/session-reader";
3
+
4
+ export const dynamic = "force-dynamic";
5
+
6
+ const MAX_DAYS_BACK = 365;
7
+ const MAX_LIMIT = 500;
8
+
9
+ function positiveInt(value: string | null, fallback: number): number {
10
+ if (!value) return fallback;
11
+ const parsed = Number.parseInt(value, 10);
12
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
13
+ }
14
+
15
+ export async function GET(req: Request) {
16
+ const url = new URL(req.url);
17
+ const daysBack = Math.min(
18
+ positiveInt(url.searchParams.get("daysBack"), 30),
19
+ MAX_DAYS_BACK,
20
+ );
21
+ const limit = Math.min(
22
+ positiveInt(url.searchParams.get("limit"), 20),
23
+ MAX_LIMIT,
24
+ );
25
+ const stats = await listAppliedRules({ daysBack, limit });
26
+ return NextResponse.json({ success: true, stats });
27
+ }