rafaygen-cli 1.3.1 → 1.3.3

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.
package/src/state.js CHANGED
@@ -1,27 +1,271 @@
1
- // Global Session State for rgcli
2
- export const SessionState = {
3
- sandboxMode: "danger-full-access", // read-only, workspace-write, danger-full-access
4
- approvalMode: "auto-edit", // suggest, auto-edit, full-auto, never
1
+ import crypto from 'crypto';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import os from 'os';
5
+
6
+ const RGCLI_DIR = path.join(os.homedir(), '.rgcli');
7
+ const SESSIONS_DIR = path.join(RGCLI_DIR, 'sessions');
8
+
9
+ function ensureDir(dir) {
10
+ if (!fs.existsSync(dir)) {
11
+ fs.mkdirSync(dir, { recursive: true });
12
+ }
13
+ }
14
+
15
+ const sessionState = {
16
+ sandboxMode: 'workspace-write',
17
+ approvalMode: 'suggest',
5
18
  attachedFiles: new Set(),
6
19
  mcpServers: [],
7
- reasoningEffort: "medium", // low, medium, high
20
+ reasoningEffort: 'medium',
8
21
  verbose: false,
9
22
  compactMode: false,
10
- cwd: process.cwd()
23
+ cwd: process.cwd(),
24
+ jsonOutput: false,
25
+ colorEnabled: true,
26
+ skipGitCheck: false,
27
+ autoEdit: false,
28
+ searchEnabled: false,
29
+ imageAttached: null,
30
+ activeSkill: null,
31
+ conversationHistory: [],
32
+ sessionId: crypto.randomUUID(),
11
33
  };
12
34
 
35
+ /**
36
+ * Returns a shallow copy of the current session state.
37
+ * The attachedFiles Set is copied into a new Set so external
38
+ * mutations don't leak back into the canonical state.
39
+ */
13
40
  export function getSessionState() {
14
- return SessionState;
41
+ return {
42
+ ...sessionState,
43
+ attachedFiles: new Set(sessionState.attachedFiles),
44
+ mcpServers: [...sessionState.mcpServers],
45
+ conversationHistory: [...sessionState.conversationHistory],
46
+ };
15
47
  }
16
48
 
49
+ /**
50
+ * Merge partial updates into the session state.
51
+ * Supports both plain objects and Sets / Arrays for the
52
+ * collection fields.
53
+ */
17
54
  export function updateSessionState(newState) {
18
- Object.assign(SessionState, newState);
55
+ if (!newState || typeof newState !== 'object') {
56
+ return getSessionState();
57
+ }
58
+
59
+ for (const [key, value] of Object.entries(newState)) {
60
+ if (!(key in sessionState)) {
61
+ continue;
62
+ }
63
+
64
+ if (key === 'attachedFiles') {
65
+ if (value instanceof Set) {
66
+ sessionState.attachedFiles = new Set(value);
67
+ } else if (Array.isArray(value)) {
68
+ sessionState.attachedFiles = new Set(value);
69
+ } else {
70
+ sessionState.attachedFiles = new Set();
71
+ }
72
+ } else if (key === 'mcpServers') {
73
+ sessionState.mcpServers = Array.isArray(value) ? [...value] : [];
74
+ } else if (key === 'conversationHistory') {
75
+ sessionState.conversationHistory = Array.isArray(value) ? [...value] : [];
76
+ } else {
77
+ sessionState[key] = value;
78
+ }
79
+ }
80
+
81
+ return getSessionState();
19
82
  }
20
83
 
84
+ /**
85
+ * Attach a file to the current session context.
86
+ * Resolves the path against the current working directory stored
87
+ * in state so relative paths work as expected.
88
+ */
21
89
  export function attachFileContext(filePath) {
22
- SessionState.attachedFiles.add(filePath);
90
+ if (!filePath || typeof filePath !== 'string') {
91
+ return false;
92
+ }
93
+
94
+ const resolved = path.isAbsolute(filePath)
95
+ ? filePath
96
+ : path.resolve(sessionState.cwd, filePath);
97
+
98
+ if (!fs.existsSync(resolved)) {
99
+ return false;
100
+ }
101
+
102
+ sessionState.attachedFiles.add(resolved);
103
+ return true;
23
104
  }
24
105
 
106
+ /**
107
+ * Remove all attached files from the session.
108
+ */
25
109
  export function clearAttachedFiles() {
26
- SessionState.attachedFiles.clear();
110
+ sessionState.attachedFiles.clear();
111
+ }
112
+
113
+ /**
114
+ * Append a message to the conversation history.
115
+ * @param {'user'|'assistant'|'system'} role
116
+ * @param {string} content
117
+ */
118
+ export function addToHistory(role, content) {
119
+ const validRoles = ['user', 'assistant', 'system'];
120
+ const normalizedRole = validRoles.includes(role) ? role : 'user';
121
+
122
+ sessionState.conversationHistory.push({
123
+ role: normalizedRole,
124
+ content: typeof content === 'string' ? content : String(content),
125
+ timestamp: new Date().toISOString(),
126
+ });
127
+ }
128
+
129
+ /**
130
+ * Return the full conversation history array (copy).
131
+ */
132
+ export function getHistory() {
133
+ return [...sessionState.conversationHistory];
134
+ }
135
+
136
+ /**
137
+ * Clear the in-memory conversation history.
138
+ */
139
+ export function clearHistory() {
140
+ sessionState.conversationHistory = [];
141
+ }
142
+
143
+ /**
144
+ * Persist the current session to disk.
145
+ * File: ~/.rgcli/sessions/<sessionId>.json
146
+ */
147
+ export function saveSession() {
148
+ ensureDir(SESSIONS_DIR);
149
+
150
+ const filePath = path.join(SESSIONS_DIR, `${sessionState.sessionId}.json`);
151
+
152
+ const serializable = {
153
+ sessionId: sessionState.sessionId,
154
+ sandboxMode: sessionState.sandboxMode,
155
+ approvalMode: sessionState.approvalMode,
156
+ attachedFiles: [...sessionState.attachedFiles],
157
+ mcpServers: [...sessionState.mcpServers],
158
+ reasoningEffort: sessionState.reasoningEffort,
159
+ verbose: sessionState.verbose,
160
+ compactMode: sessionState.compactMode,
161
+ cwd: sessionState.cwd,
162
+ jsonOutput: sessionState.jsonOutput,
163
+ colorEnabled: sessionState.colorEnabled,
164
+ skipGitCheck: sessionState.skipGitCheck,
165
+ autoEdit: sessionState.autoEdit,
166
+ searchEnabled: sessionState.searchEnabled,
167
+ imageAttached: sessionState.imageAttached,
168
+ activeSkill: sessionState.activeSkill,
169
+ conversationHistory: sessionState.conversationHistory,
170
+ savedAt: new Date().toISOString(),
171
+ };
172
+
173
+ fs.writeFileSync(filePath, JSON.stringify(serializable, null, 2), 'utf-8');
174
+ return filePath;
175
+ }
176
+
177
+ /**
178
+ * Load a previously-saved session from disk and hydrate state.
179
+ * @param {string} id Session UUID
180
+ * @returns {boolean} true if loaded successfully
181
+ */
182
+ export function loadSession(id) {
183
+ if (!id || typeof id !== 'string') {
184
+ return false;
185
+ }
186
+
187
+ const filePath = path.join(SESSIONS_DIR, `${id}.json`);
188
+
189
+ if (!fs.existsSync(filePath)) {
190
+ return false;
191
+ }
192
+
193
+ try {
194
+ const raw = fs.readFileSync(filePath, 'utf-8');
195
+ const data = JSON.parse(raw);
196
+
197
+ if (data.sessionId) sessionState.sessionId = data.sessionId;
198
+ if (data.sandboxMode) sessionState.sandboxMode = data.sandboxMode;
199
+ if (data.approvalMode) sessionState.approvalMode = data.approvalMode;
200
+ if (Array.isArray(data.attachedFiles)) {
201
+ sessionState.attachedFiles = new Set(data.attachedFiles);
202
+ }
203
+ if (Array.isArray(data.mcpServers)) {
204
+ sessionState.mcpServers = [...data.mcpServers];
205
+ }
206
+ if (data.reasoningEffort) sessionState.reasoningEffort = data.reasoningEffort;
207
+ if (typeof data.verbose === 'boolean') sessionState.verbose = data.verbose;
208
+ if (typeof data.compactMode === 'boolean') sessionState.compactMode = data.compactMode;
209
+ if (data.cwd) sessionState.cwd = data.cwd;
210
+ if (typeof data.jsonOutput === 'boolean') sessionState.jsonOutput = data.jsonOutput;
211
+ if (typeof data.colorEnabled === 'boolean') sessionState.colorEnabled = data.colorEnabled;
212
+ if (typeof data.skipGitCheck === 'boolean') sessionState.skipGitCheck = data.skipGitCheck;
213
+ if (typeof data.autoEdit === 'boolean') sessionState.autoEdit = data.autoEdit;
214
+ if (typeof data.searchEnabled === 'boolean') sessionState.searchEnabled = data.searchEnabled;
215
+ sessionState.imageAttached = data.imageAttached ?? null;
216
+ sessionState.activeSkill = data.activeSkill ?? null;
217
+ if (Array.isArray(data.conversationHistory)) {
218
+ sessionState.conversationHistory = [...data.conversationHistory];
219
+ }
220
+
221
+ return true;
222
+ } catch {
223
+ return false;
224
+ }
225
+ }
226
+
227
+ /**
228
+ * List every saved session with basic metadata.
229
+ * Returns an array of { sessionId, savedAt, messageCount, filePath }.
230
+ */
231
+ export function listSessions() {
232
+ ensureDir(SESSIONS_DIR);
233
+
234
+ const files = fs.readdirSync(SESSIONS_DIR).filter((f) => f.endsWith('.json'));
235
+
236
+ const sessions = [];
237
+
238
+ for (const file of files) {
239
+ const filePath = path.join(SESSIONS_DIR, file);
240
+
241
+ try {
242
+ const raw = fs.readFileSync(filePath, 'utf-8');
243
+ const data = JSON.parse(raw);
244
+
245
+ sessions.push({
246
+ sessionId: data.sessionId || path.basename(file, '.json'),
247
+ savedAt: data.savedAt || null,
248
+ messageCount: Array.isArray(data.conversationHistory)
249
+ ? data.conversationHistory.length
250
+ : 0,
251
+ filePath,
252
+ });
253
+ } catch {
254
+ sessions.push({
255
+ sessionId: path.basename(file, '.json'),
256
+ savedAt: null,
257
+ messageCount: 0,
258
+ filePath,
259
+ });
260
+ }
261
+ }
262
+
263
+ sessions.sort((a, b) => {
264
+ if (!a.savedAt && !b.savedAt) return 0;
265
+ if (!a.savedAt) return 1;
266
+ if (!b.savedAt) return -1;
267
+ return new Date(b.savedAt).getTime() - new Date(a.savedAt).getTime();
268
+ });
269
+
270
+ return sessions;
27
271
  }