claude-smart 0.2.23 → 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,245 @@
1
+ /**
2
+ * Server-side reader for claude-smart JSONL session buffers.
3
+ * Mirrors the format documented in src/claude_smart/state.py:
4
+ * - {role: "User" | "Assistant" | "Assistant_tool", ...}
5
+ * - {published_up_to: N} watermark
6
+ */
7
+
8
+ import fs from "node:fs/promises";
9
+ import path from "node:path";
10
+ import os from "node:os";
11
+ import type {
12
+ CitedItem,
13
+ SessionDetail,
14
+ SessionSummary,
15
+ SessionTurn,
16
+ ToolUsed,
17
+ UserActionType,
18
+ } from "./types";
19
+
20
+ // Mirrors _TOOL_DATA_FIELD_MAX_LEN in plugin/src/claude_smart/state.py — we
21
+ // truncate to the same length the publisher ships to reflexio so the
22
+ // dashboard renders the exact bytes the extractor sees.
23
+ const TOOL_DATA_FIELD_MAX_LEN = 256;
24
+
25
+ function truncateToolField<T>(value: T): T {
26
+ if (typeof value === "string" && value.length > TOOL_DATA_FIELD_MAX_LEN) {
27
+ return value.slice(0, TOOL_DATA_FIELD_MAX_LEN) as T;
28
+ }
29
+ return value;
30
+ }
31
+
32
+ export function stateDir(): string {
33
+ const override = process.env.CLAUDE_SMART_STATE_DIR;
34
+ if (override) return override;
35
+ return path.join(os.homedir(), ".claude-smart", "sessions");
36
+ }
37
+
38
+ type RawRecord = {
39
+ role?: "User" | "Assistant" | "Assistant_tool";
40
+ content?: string;
41
+ ts?: number;
42
+ user_id?: string;
43
+ tool_name?: string;
44
+ tool_input?: Record<string, unknown>;
45
+ tool_output?: string;
46
+ status?: string;
47
+ user_action?: UserActionType;
48
+ user_action_description?: string;
49
+ cited_items?: CitedItem[];
50
+ published_up_to?: number;
51
+ };
52
+
53
+ async function readJsonl(filePath: string): Promise<RawRecord[]> {
54
+ const text = await fs.readFile(filePath, "utf-8");
55
+ const out: RawRecord[] = [];
56
+ for (const line of text.split("\n")) {
57
+ const trimmed = line.trim();
58
+ if (!trimmed) continue;
59
+ try {
60
+ out.push(JSON.parse(trimmed));
61
+ } catch {
62
+ // skip malformed line, matches state.py behaviour
63
+ }
64
+ }
65
+ return out;
66
+ }
67
+
68
+ function foldTurns(records: RawRecord[]): {
69
+ turns: SessionTurn[];
70
+ publishedUpTo: number;
71
+ learningInteractionCount: number;
72
+ lastTs: number | null;
73
+ firstTs: number | null;
74
+ preview: string | null;
75
+ } {
76
+ let published = 0;
77
+ let pendingTools: ToolUsed[] = [];
78
+ const turns: SessionTurn[] = [];
79
+ let learningInteractionCount = 0;
80
+ let lastTs: number | null = null;
81
+ let firstTs: number | null = null;
82
+ let preview: string | null = null;
83
+
84
+ for (let idx = 0; idx < records.length; idx++) {
85
+ const rec = records[idx];
86
+ if (typeof rec.published_up_to === "number") {
87
+ published = rec.published_up_to;
88
+ pendingTools = [];
89
+ continue;
90
+ }
91
+ const role = rec.role;
92
+ if (role === "Assistant_tool") {
93
+ const entry: ToolUsed = {
94
+ tool_name: rec.tool_name ?? "",
95
+ status: rec.status ?? "success",
96
+ };
97
+ const toolData: { input?: Record<string, unknown>; output?: string } = {};
98
+ if (rec.tool_input && Object.keys(rec.tool_input).length > 0) {
99
+ const input: Record<string, unknown> = {};
100
+ for (const [k, v] of Object.entries(rec.tool_input)) {
101
+ input[k] = truncateToolField(v);
102
+ }
103
+ toolData.input = input;
104
+ }
105
+ if (typeof rec.tool_output === "string" && rec.tool_output.length > 0) {
106
+ toolData.output = truncateToolField(rec.tool_output);
107
+ }
108
+ if (toolData.input || toolData.output) {
109
+ entry.tool_data = toolData;
110
+ }
111
+ pendingTools.push(entry);
112
+ continue;
113
+ }
114
+ if (role !== "User" && role !== "Assistant") continue;
115
+
116
+ if (
117
+ role === "Assistant" &&
118
+ rec.cited_items &&
119
+ rec.cited_items.length > 0
120
+ ) {
121
+ learningInteractionCount += 1;
122
+ }
123
+ if (typeof rec.ts === "number") {
124
+ lastTs = rec.ts;
125
+ if (firstTs === null) firstTs = rec.ts;
126
+ }
127
+
128
+ const turn: SessionTurn = {
129
+ role,
130
+ content: rec.content ?? "",
131
+ ts: rec.ts,
132
+ user_id: rec.user_id,
133
+ user_action: rec.user_action,
134
+ user_action_description: rec.user_action_description,
135
+ };
136
+ if (role === "Assistant" && pendingTools.length) {
137
+ turn.tools_used = pendingTools;
138
+ pendingTools = [];
139
+ }
140
+ if (role === "Assistant" && rec.cited_items && rec.cited_items.length) {
141
+ turn.cited_items = rec.cited_items;
142
+ }
143
+ if (
144
+ preview === null &&
145
+ role === "User" &&
146
+ typeof turn.content === "string" &&
147
+ turn.content.trim()
148
+ ) {
149
+ preview = turn.content.trim().slice(0, 240);
150
+ }
151
+ turns.push(turn);
152
+ }
153
+
154
+ return {
155
+ turns,
156
+ publishedUpTo: published,
157
+ learningInteractionCount,
158
+ lastTs,
159
+ firstTs,
160
+ preview,
161
+ };
162
+ }
163
+
164
+ export async function listSessions(): Promise<SessionSummary[]> {
165
+ const dir = stateDir();
166
+ let entries: string[];
167
+ try {
168
+ entries = await fs.readdir(dir);
169
+ } catch {
170
+ return [];
171
+ }
172
+
173
+ const summaries: SessionSummary[] = [];
174
+ for (const entry of entries) {
175
+ if (!entry.endsWith(".jsonl")) continue;
176
+ if (entry.endsWith(".injected.jsonl")) continue;
177
+ const fullPath = path.join(dir, entry);
178
+ const records = await readJsonl(fullPath).catch(() => []);
179
+ const {
180
+ turns,
181
+ publishedUpTo,
182
+ learningInteractionCount,
183
+ lastTs,
184
+ firstTs,
185
+ preview,
186
+ } = foldTurns(records);
187
+ summaries.push({
188
+ session_id: entry.replace(/\.jsonl$/, ""),
189
+ turn_count: turns.length,
190
+ learning_interaction_count: learningInteractionCount,
191
+ last_activity: lastTs,
192
+ first_activity: firstTs,
193
+ published_up_to: publishedUpTo,
194
+ preview,
195
+ source: "local",
196
+ });
197
+ }
198
+ summaries.sort((a, b) => (b.last_activity ?? 0) - (a.last_activity ?? 0));
199
+ return summaries;
200
+ }
201
+
202
+ export async function deleteSession(sessionId: string): Promise<boolean> {
203
+ const file = path.join(stateDir(), `${sessionId}.jsonl`);
204
+ try {
205
+ await fs.unlink(file);
206
+ return true;
207
+ } catch {
208
+ return false;
209
+ }
210
+ }
211
+
212
+ export async function deleteAllSessions(): Promise<number> {
213
+ const dir = stateDir();
214
+ let entries: string[];
215
+ try {
216
+ entries = await fs.readdir(dir);
217
+ } catch {
218
+ return 0;
219
+ }
220
+ let count = 0;
221
+ for (const entry of entries) {
222
+ if (!entry.endsWith(".jsonl")) continue;
223
+ try {
224
+ await fs.unlink(path.join(dir, entry));
225
+ count += 1;
226
+ } catch {
227
+ // ignore
228
+ }
229
+ }
230
+ return count;
231
+ }
232
+
233
+ export async function readSession(
234
+ sessionId: string,
235
+ ): Promise<SessionDetail | null> {
236
+ const file = path.join(stateDir(), `${sessionId}.jsonl`);
237
+ let records: RawRecord[];
238
+ try {
239
+ records = await readJsonl(file);
240
+ } catch {
241
+ return null;
242
+ }
243
+ const { turns, publishedUpTo } = foldTurns(records);
244
+ return { session_id: sessionId, turns, published_up_to: publishedUpTo };
245
+ }
@@ -0,0 +1,24 @@
1
+ // Wire status comes from Python's Status StrEnum: lowercase strings
2
+ // ("archived", "pending"). CURRENT rows are omitted from JSON entirely
3
+ // (response_model_exclude_none) and arrive as `null`/undefined.
4
+ // Normalize to the uppercase label the dashboard renders.
5
+
6
+ export type StatusLabel = "CURRENT" | "ARCHIVED" | "PENDING";
7
+ export type AgentPlaybookStatusLabel = "PENDING" | "APPROVED" | "REJECTED";
8
+
9
+ export function statusLabel(p: { status?: string | null }): StatusLabel {
10
+ if (!p.status) return "CURRENT";
11
+ const s = String(p.status).toLowerCase();
12
+ if (s === "archived") return "ARCHIVED";
13
+ if (s === "pending") return "PENDING";
14
+ return "CURRENT";
15
+ }
16
+
17
+ export function agentPlaybookStatusLabel(p: {
18
+ playbook_status?: string | null;
19
+ }): AgentPlaybookStatusLabel {
20
+ const s = String(p.playbook_status ?? "pending").toLowerCase();
21
+ if (s === "approved") return "APPROVED";
22
+ if (s === "rejected") return "REJECTED";
23
+ return "PENDING";
24
+ }
@@ -0,0 +1,145 @@
1
+ export type UserActionType =
2
+ | "NONE"
3
+ | "CORRECTIVE_PHRASE"
4
+ | "CORRECTION"
5
+ | "PRAISE"
6
+ | "STOP";
7
+
8
+ // Wire format from the reflexio API. CURRENT rows arrive as `status: null`
9
+ // (response_model_exclude_none strips it from the JSON entirely), so the type
10
+ // only enumerates the non-null values. The values are lowercase because they
11
+ // come from Python's Status StrEnum.
12
+ export type LifecycleStatus = "pending" | "archived";
13
+
14
+ export type AgentPlaybookStatus = "pending" | "approved" | "rejected";
15
+
16
+ export type ProfileStatus = "pending" | "archived";
17
+
18
+ export interface ToolUsed {
19
+ tool_name: string;
20
+ status: string;
21
+ tool_data?: { input?: Record<string, unknown>; output?: string };
22
+ }
23
+
24
+ export interface CitedItem {
25
+ id: string;
26
+ kind: "playbook" | "profile";
27
+ title: string;
28
+ real_id?: string;
29
+ source_kind?: "user_playbook" | "agent_playbook" | "profile";
30
+ }
31
+
32
+ export interface Interaction {
33
+ interaction_id: number;
34
+ user_id: string;
35
+ request_id: string;
36
+ created_at: number;
37
+ role: string;
38
+ content: string;
39
+ user_action: UserActionType;
40
+ user_action_description?: string;
41
+ tools_used: ToolUsed[];
42
+ }
43
+
44
+ export interface UserPlaybook {
45
+ user_playbook_id: number;
46
+ user_id: string | null;
47
+ agent_version: string;
48
+ request_id: string;
49
+ playbook_name: string;
50
+ created_at: number;
51
+ content: string;
52
+ trigger: string | null;
53
+ rationale: string | null;
54
+ status: LifecycleStatus | null;
55
+ source: string | null;
56
+ source_interaction_ids: number[];
57
+ }
58
+
59
+ export interface AgentPlaybook {
60
+ agent_playbook_id: number;
61
+ playbook_name: string;
62
+ agent_version: string;
63
+ created_at: number;
64
+ content: string;
65
+ trigger: string | null;
66
+ rationale: string | null;
67
+ playbook_status: AgentPlaybookStatus;
68
+ playbook_metadata: string;
69
+ status: LifecycleStatus | null;
70
+ }
71
+
72
+ export interface UserProfile {
73
+ profile_id: string;
74
+ user_id: string;
75
+ content: string;
76
+ last_modified_timestamp: number;
77
+ generated_from_request_id: string;
78
+ profile_time_to_live?: string;
79
+ expiration_timestamp?: number;
80
+ custom_features?: Record<string, unknown> | null;
81
+ extractor_names?: string[] | null;
82
+ status: ProfileStatus | null;
83
+ source: string | null;
84
+ }
85
+
86
+ export interface SessionTurn {
87
+ role: "User" | "Assistant";
88
+ content: string;
89
+ ts?: number;
90
+ user_id?: string;
91
+ tools_used?: ToolUsed[];
92
+ cited_items?: CitedItem[];
93
+ user_action?: UserActionType;
94
+ user_action_description?: string;
95
+ }
96
+
97
+ export interface SessionSummary {
98
+ session_id: string;
99
+ turn_count: number;
100
+ learning_interaction_count: number;
101
+ last_activity: number | null;
102
+ first_activity: number | null;
103
+ published_up_to: number;
104
+ preview: string | null;
105
+ source: "local";
106
+ }
107
+
108
+ export interface SessionDetail {
109
+ session_id: string;
110
+ turns: SessionTurn[];
111
+ published_up_to: number;
112
+ }
113
+
114
+ export interface ClaudeSmartConfig {
115
+ REFLEXIO_URL: string;
116
+ CLAUDE_SMART_USE_LOCAL_CLI: boolean;
117
+ CLAUDE_SMART_USE_LOCAL_EMBEDDING: boolean;
118
+ CLAUDE_SMART_CLI_PATH: string;
119
+ CLAUDE_SMART_CLI_TIMEOUT: string;
120
+ CLAUDE_SMART_STATE_DIR: string;
121
+ [extra: string]: string | boolean;
122
+ }
123
+
124
+ export interface ClaudeCodeHookConfig {
125
+ CLAUDE_SMART_ENABLE_OPTIMIZER: boolean;
126
+ effectiveValue: boolean;
127
+ localValue: boolean | null;
128
+ userValue: boolean | null;
129
+ settingsPath: string;
130
+ userSettingsPath: string;
131
+ }
132
+
133
+ export interface ReflexioExtractorConfig {
134
+ extraction_definition_prompt?: string;
135
+ [k: string]: unknown;
136
+ }
137
+
138
+ export interface ReflexioConfig {
139
+ agent_context_prompt?: string | null;
140
+ window_size?: number;
141
+ stride_size?: number;
142
+ profile_extractor_configs?: ReflexioExtractorConfig[] | null;
143
+ user_playbook_extractor_configs?: ReflexioExtractorConfig[] | null;
144
+ [k: string]: unknown;
145
+ }
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
@@ -0,0 +1,7 @@
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {
4
+ /* config options here */
5
+ };
6
+
7
+ export default nextConfig;