gekto 0.0.1

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.
@@ -0,0 +1,223 @@
1
+ import { spawn } from 'child_process';
2
+ // === Tool Definitions ===
3
+ const TOOLS_DESCRIPTION = `
4
+ Available tools:
5
+ 1. chat - For greetings, questions, conversations (not coding tasks)
6
+ 2. build - For coding tasks: features, bug fixes, refactoring, file changes
7
+ 3. remove - For removing/cleaning up worker agents
8
+ `;
9
+ const GEKTO_SYSTEM_PROMPT = `You are Gekto, a task orchestration assistant. You MUST respond with ONLY valid JSON - no other text.
10
+
11
+ ${TOOLS_DESCRIPTION}
12
+
13
+ Analyze the user message and respond with the appropriate tool as JSON.
14
+
15
+ JSON FORMAT (respond with ONLY this, no markdown, no explanation):
16
+ {"tool":"chat"|"build"|"remove","params":{...}}
17
+
18
+ Tool params:
19
+ - chat: {"message":"your response"}
20
+ - build: {"tasks":[{"id":"task_1","description":"Brief desc","prompt":"Detailed prompt for worker agent","files":["path/file.ts"],"dependencies":[]}]}
21
+ - remove: {"target":"all"|"workers"|"completed"|["id1","id2"]}
22
+
23
+ Examples (respond EXACTLY like this):
24
+ "hey" -> {"tool":"chat","params":{"message":"Hey! How can I help?"}}
25
+ "add dark mode" -> {"tool":"build","params":{"tasks":[{"id":"task_1","description":"Add dark mode toggle","prompt":"Implement dark mode toggle in the settings","files":[],"dependencies":[]}]}}
26
+ "remove all agents" -> {"tool":"remove","params":{"target":"all"}}
27
+ "spawn 3 agents" -> {"tool":"build","params":{"tasks":[{"id":"task_1","description":"Agent 1","prompt":"Task 1","files":[],"dependencies":[]},{"id":"task_2","description":"Agent 2","prompt":"Task 2","files":[],"dependencies":[]},{"id":"task_3","description":"Agent 3","prompt":"Task 3","files":[],"dependencies":[]}]}}
28
+
29
+ CRITICAL: Output ONLY the JSON object. No markdown code blocks. No explanation. Just the raw JSON.`;
30
+ // === Main Processing Function ===
31
+ export async function processWithTools(prompt, planId, workingDir, activeAgents = [], callbacks) {
32
+ // Add context about active agents for remove decisions
33
+ const contextPrompt = activeAgents.length > 0
34
+ ? `${prompt}\n\n[Context: Active agents: ${activeAgents.map(a => a.lizardId).join(', ')}]`
35
+ : prompt;
36
+ const result = await runClaudeOnce(contextPrompt, GEKTO_SYSTEM_PROMPT, workingDir, callbacks);
37
+ // Parse the JSON response
38
+ try {
39
+ // Try to find JSON - handle markdown code blocks too
40
+ let jsonStr = result.trim();
41
+ // Strip markdown code blocks if present
42
+ if (jsonStr.startsWith('```json')) {
43
+ jsonStr = jsonStr.replace(/^```json\s*/, '').replace(/\s*```$/, '');
44
+ }
45
+ else if (jsonStr.startsWith('```')) {
46
+ jsonStr = jsonStr.replace(/^```\s*/, '').replace(/\s*```$/, '');
47
+ }
48
+ // Find JSON object
49
+ const jsonMatch = jsonStr.match(/\{[\s\S]*\}/);
50
+ if (!jsonMatch) {
51
+ return { type: 'chat', message: result.trim() || "I'm here to help! What would you like me to work on?" };
52
+ }
53
+ const parsed = JSON.parse(jsonMatch[0]);
54
+ const tool = parsed.tool;
55
+ const params = parsed.params || {};
56
+ switch (tool) {
57
+ case 'chat':
58
+ return {
59
+ type: 'chat',
60
+ message: params.message || 'Hello!',
61
+ };
62
+ case 'build':
63
+ const plan = createPlanFromTasks(params.tasks || [], planId, prompt);
64
+ return {
65
+ type: 'build',
66
+ plan,
67
+ };
68
+ case 'remove':
69
+ return {
70
+ type: 'remove',
71
+ removedAgents: resolveRemoveTarget(params.target, activeAgents),
72
+ };
73
+ default:
74
+ return { type: 'chat', message: "I'm not sure how to help with that." };
75
+ }
76
+ }
77
+ catch {
78
+ // Use the raw response as a chat message instead of showing an error
79
+ return { type: 'chat', message: result.trim() || "I'm here to help! What would you like me to work on?" };
80
+ }
81
+ }
82
+ // === Helper Functions ===
83
+ function createPlanFromTasks(tasks, planId, originalPrompt) {
84
+ // Extract taskId from planId (planId format: "plan_test_123456")
85
+ // taskId should be "test_123456" for task IDs like "test_123456_1"
86
+ const taskId = planId.replace(/^plan_/, '');
87
+ // Use same format as hardcoded Test button: test_X_1, test_X_2, etc.
88
+ const parsedTasks = tasks.map((t, i) => ({
89
+ id: `${taskId}_${i + 1}`,
90
+ description: t.description || 'Task',
91
+ prompt: t.prompt || originalPrompt,
92
+ files: t.files || [],
93
+ status: 'pending',
94
+ dependencies: t.dependencies || [],
95
+ }));
96
+ // Fallback to single task if empty
97
+ if (parsedTasks.length === 0) {
98
+ parsedTasks.push({
99
+ id: `${taskId}_1`,
100
+ description: 'Execute task',
101
+ prompt: originalPrompt,
102
+ files: [],
103
+ status: 'pending',
104
+ dependencies: [],
105
+ });
106
+ }
107
+ return {
108
+ id: planId,
109
+ status: 'ready',
110
+ originalPrompt,
111
+ tasks: parsedTasks,
112
+ createdAt: new Date().toISOString(),
113
+ };
114
+ }
115
+ function resolveRemoveTarget(target, activeAgents) {
116
+ if (Array.isArray(target)) {
117
+ // Specific agent IDs
118
+ return target;
119
+ }
120
+ switch (target) {
121
+ case 'all':
122
+ // All agents (including regular lizards, but not master)
123
+ return activeAgents.filter(a => a.lizardId !== 'master').map(a => a.lizardId);
124
+ case 'workers':
125
+ // Only worker agents (by flag or by ID prefix)
126
+ return activeAgents.filter(a => a.isWorker || a.lizardId.startsWith('worker_')).map(a => a.lizardId);
127
+ case 'completed':
128
+ // This would need status info - for now return workers
129
+ return activeAgents.filter(a => a.isWorker || a.lizardId.startsWith('worker_')).map(a => a.lizardId);
130
+ default:
131
+ return [];
132
+ }
133
+ }
134
+ // === Claude Helper ===
135
+ function runClaudeOnce(prompt, systemPrompt, workingDir, callbacks) {
136
+ return new Promise((resolve, reject) => {
137
+ const args = [
138
+ '-p', prompt,
139
+ '--output-format', 'stream-json',
140
+ '--verbose',
141
+ '--model', 'claude-opus-4-5-20251101',
142
+ '--system-prompt', systemPrompt,
143
+ '--dangerously-skip-permissions',
144
+ ];
145
+ const proc = spawn('claude', args, {
146
+ cwd: workingDir,
147
+ env: process.env,
148
+ stdio: ['pipe', 'pipe', 'pipe'],
149
+ });
150
+ proc.stdin?.end();
151
+ let buffer = '';
152
+ let resultText = '';
153
+ let currentTool = null;
154
+ proc.stdout.on('data', (data) => {
155
+ buffer += data.toString();
156
+ const lines = buffer.split('\n');
157
+ buffer = lines.pop() || '';
158
+ for (const line of lines) {
159
+ if (!line.trim())
160
+ continue;
161
+ try {
162
+ const event = JSON.parse(line);
163
+ // Stream tool events
164
+ if (event.type === 'assistant' && event.message?.content) {
165
+ for (const block of event.message.content) {
166
+ if (block.type === 'tool_use' && block.name) {
167
+ currentTool = block.name;
168
+ callbacks?.onToolStart?.(block.name, block.input);
169
+ }
170
+ }
171
+ }
172
+ // Tool completed
173
+ if (event.type === 'user' && event.message?.content) {
174
+ for (const block of event.message.content) {
175
+ if (block.type === 'tool_result' && currentTool) {
176
+ callbacks?.onToolEnd?.(currentTool);
177
+ currentTool = null;
178
+ }
179
+ }
180
+ }
181
+ // Text streaming
182
+ if (event.type === 'content_block_delta') {
183
+ const delta = event.delta;
184
+ if (delta?.type === 'text_delta' && delta.text) {
185
+ callbacks?.onText?.(delta.text);
186
+ }
187
+ }
188
+ if (event.type === 'result' && event.result) {
189
+ resultText = event.result;
190
+ }
191
+ }
192
+ catch {
193
+ // Ignore parse errors
194
+ }
195
+ }
196
+ });
197
+ let stderrOutput = '';
198
+ proc.stderr.on('data', (data) => {
199
+ stderrOutput += data.toString();
200
+ });
201
+ proc.on('close', (code) => {
202
+ if (buffer.trim()) {
203
+ try {
204
+ const event = JSON.parse(buffer);
205
+ if (event.type === 'result' && event.result) {
206
+ resultText = event.result;
207
+ }
208
+ }
209
+ catch {
210
+ // Ignore
211
+ }
212
+ }
213
+ if (resultText) {
214
+ resolve(resultText);
215
+ }
216
+ else {
217
+ const errorMsg = stderrOutput || `Process exited with code ${code}`;
218
+ reject(new Error(`No result from Gekto: ${errorMsg}`));
219
+ }
220
+ });
221
+ proc.on('error', reject);
222
+ });
223
+ }
package/dist/proxy.js ADDED
@@ -0,0 +1,318 @@
1
+ #!/usr/bin/env node
2
+ import http from 'http';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import { parseArgs } from 'util';
7
+ import { setupTerminalWebSocket } from './terminal.js';
8
+ import { setupAgentWebSocket } from './agents/agentWebSocket.js';
9
+ import { initStore, getData, setData } from './store.js';
10
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
+ // Parse CLI arguments
12
+ const { values: args } = parseArgs({
13
+ options: {
14
+ port: { type: 'string', short: 'p' },
15
+ target: { type: 'string', short: 't' },
16
+ help: { type: 'boolean', short: 'h' },
17
+ },
18
+ strict: false,
19
+ });
20
+ if (args.help) {
21
+ console.log(`
22
+ Gekto Proxy - Inject widget into any web app
23
+
24
+ Usage:
25
+ bun gekto.ts --target 3000
26
+ bun gekto.ts -t 3000 -p 8080
27
+
28
+ Options:
29
+ -t, --target Target app port (required)
30
+ -p, --port Proxy port (default: 3200)
31
+ -h, --help Show this help
32
+ `);
33
+ process.exit(0);
34
+ }
35
+ // Only require --target for bundled version (not dev mode)
36
+ if (!args.target && !process.env.TARGET_PORT && !process.env.GEKTO_DEV) {
37
+ console.error('Error: --target port is required\n');
38
+ console.error('Usage: bun gekto.ts --target 3000');
39
+ process.exit(1);
40
+ }
41
+ // Configuration
42
+ const PROXY_PORT = parseInt(String(args.port ?? process.env.PORT ?? '3200'), 10);
43
+ const TARGET_PORT = parseInt(String(args.target ?? process.env.TARGET_PORT ?? '5173'), 10);
44
+ const WIDGET_PORT = parseInt(process.env.WIDGET_PORT ?? '5174', 10);
45
+ const DEV_MODE = process.env.GEKTO_DEV === '1';
46
+ // Initialize store
47
+ initStore();
48
+ // Widget paths - in dev mode use source, in production use bundled widget folder
49
+ const WIDGET_DIST_PATH = DEV_MODE
50
+ ? path.resolve(__dirname, '../../widget/dist')
51
+ : path.resolve(__dirname, './widget');
52
+ const WIDGET_JS_PATH = path.join(WIDGET_DIST_PATH, 'gekto-widget.iife.js');
53
+ const WIDGET_CSS_PATH = path.join(WIDGET_DIST_PATH, 'style.css');
54
+ // Load widget bundle
55
+ function loadWidgetBundle() {
56
+ try {
57
+ const js = fs.readFileSync(WIDGET_JS_PATH, 'utf8');
58
+ const css = fs.existsSync(WIDGET_CSS_PATH)
59
+ ? fs.readFileSync(WIDGET_CSS_PATH, 'utf8')
60
+ : '';
61
+ return { js, css };
62
+ }
63
+ catch (err) {
64
+ console.error('❌ Could not load widget bundle:', err);
65
+ return { js: '// Widget bundle not found', css: '' };
66
+ }
67
+ }
68
+ // Generate injection script
69
+ function getInjectionScript() {
70
+ if (DEV_MODE) {
71
+ // In dev mode, load as ES module from widget dev server
72
+ return `
73
+ <!-- Gekto Widget (dev) -->
74
+ <script type="module" id="gekto-widget" src="http://localhost:${WIDGET_PORT}/src/main.tsx"></script>
75
+ `;
76
+ }
77
+ // In production, load IIFE bundle
78
+ return `
79
+ <!-- Gekto Widget -->
80
+ <script id="gekto-widget" src="/__gekto/widget.js"></script>
81
+ `;
82
+ }
83
+ const server = http.createServer((req, res) => {
84
+ const url = req.url || '/';
85
+ // API: Get lizards (must be before widget handling)
86
+ if (url === '/__gekto/api/lizards' && req.method === 'GET') {
87
+ const lizards = getData('lizards') || [];
88
+ res.writeHead(200, { 'Content-Type': 'application/json' });
89
+ res.end(JSON.stringify(lizards));
90
+ return;
91
+ }
92
+ // API: Save lizards (must be before widget handling)
93
+ if (url === '/__gekto/api/lizards' && req.method === 'POST') {
94
+ let body = '';
95
+ req.on('data', chunk => { body += chunk; });
96
+ req.on('end', () => {
97
+ try {
98
+ const lizards = JSON.parse(body);
99
+ setData('lizards', lizards);
100
+ res.writeHead(200, { 'Content-Type': 'application/json' });
101
+ res.end(JSON.stringify({ success: true }));
102
+ }
103
+ catch (err) {
104
+ console.error('[Store] Failed to save lizards:', err);
105
+ res.writeHead(400, { 'Content-Type': 'application/json' });
106
+ res.end(JSON.stringify({ error: 'Invalid JSON' }));
107
+ }
108
+ });
109
+ return;
110
+ }
111
+ // API: Get chat history for a lizard
112
+ const chatGetMatch = url.match(/^\/__gekto\/api\/chats\/([^/]+)$/);
113
+ if (chatGetMatch && req.method === 'GET') {
114
+ const lizardId = chatGetMatch[1];
115
+ const allChats = getData('chats') || {};
116
+ const messages = allChats[lizardId] || [];
117
+ res.writeHead(200, { 'Content-Type': 'application/json' });
118
+ res.end(JSON.stringify(messages));
119
+ return;
120
+ }
121
+ // API: Save chat history for a lizard
122
+ const chatPostMatch = url.match(/^\/__gekto\/api\/chats\/([^/]+)$/);
123
+ if (chatPostMatch && req.method === 'POST') {
124
+ const lizardId = chatPostMatch[1];
125
+ let body = '';
126
+ req.on('data', chunk => { body += chunk; });
127
+ req.on('end', () => {
128
+ try {
129
+ const messages = JSON.parse(body);
130
+ const allChats = getData('chats') || {};
131
+ allChats[lizardId] = messages;
132
+ setData('chats', allChats);
133
+ res.writeHead(200, { 'Content-Type': 'application/json' });
134
+ res.end(JSON.stringify({ success: true }));
135
+ }
136
+ catch (err) {
137
+ console.error('[Store] Failed to save chat:', err);
138
+ res.writeHead(400, { 'Content-Type': 'application/json' });
139
+ res.end(JSON.stringify({ error: 'Invalid JSON' }));
140
+ }
141
+ });
142
+ return;
143
+ }
144
+ // Serve widget assets - proxy to widget dev server or serve from dist
145
+ if (url.startsWith('/__gekto/')) {
146
+ if (DEV_MODE) {
147
+ // In dev mode, if widget.js is requested, return a module loader script
148
+ // This handles cases where the non-module script tag is cached
149
+ if (url === '/__gekto/widget.js') {
150
+ res.writeHead(200, { 'Content-Type': 'application/javascript', 'Cache-Control': 'no-cache' });
151
+ res.end(`
152
+ // Dev mode: dynamically load as ES module
153
+ const script = document.createElement('script');
154
+ script.type = 'module';
155
+ script.src = 'http://localhost:${WIDGET_PORT}/src/main.tsx';
156
+ document.head.appendChild(script);
157
+ `);
158
+ return;
159
+ }
160
+ // Proxy other widget assets to widget dev server for HMR
161
+ const widgetPath = url.replace('/__gekto/', '/@fs' + path.resolve(__dirname, '../../widget/') + '/');
162
+ const widgetReq = http.request({
163
+ hostname: 'localhost',
164
+ port: WIDGET_PORT,
165
+ path: widgetPath,
166
+ method: 'GET',
167
+ headers: { host: `localhost:${WIDGET_PORT}` }
168
+ }, (widgetRes) => {
169
+ const headers = {};
170
+ for (const [key, value] of Object.entries(widgetRes.headers)) {
171
+ headers[key] = value;
172
+ }
173
+ res.writeHead(widgetRes.statusCode || 200, headers);
174
+ widgetRes.pipe(res);
175
+ });
176
+ widgetReq.on('error', () => {
177
+ // Fallback to dist if widget dev server not available
178
+ res.writeHead(200, { 'Content-Type': 'application/javascript', 'Cache-Control': 'no-cache' });
179
+ const { js } = loadWidgetBundle();
180
+ res.end(js);
181
+ });
182
+ widgetReq.end();
183
+ return;
184
+ }
185
+ else {
186
+ // Production: serve from dist
187
+ if (url === '/__gekto/widget.js') {
188
+ res.writeHead(200, { 'Content-Type': 'application/javascript', 'Cache-Control': 'no-cache' });
189
+ const { js } = loadWidgetBundle();
190
+ res.end(js);
191
+ return;
192
+ }
193
+ if (url === '/__gekto/widget.css') {
194
+ res.writeHead(200, { 'Content-Type': 'text/css', 'Cache-Control': 'no-cache' });
195
+ const { css } = loadWidgetBundle();
196
+ res.end(css);
197
+ return;
198
+ }
199
+ }
200
+ }
201
+ // Proxy request to target
202
+ // Strip headers that prevent HTML injection from working
203
+ const forwardHeaders = {
204
+ ...req.headers,
205
+ host: `localhost:${TARGET_PORT}`
206
+ };
207
+ delete forwardHeaders['accept-encoding']; // Disable compression so we can read HTML
208
+ delete forwardHeaders['if-none-match']; // Prevent 304 responses
209
+ delete forwardHeaders['if-modified-since'];
210
+ const proxyReq = http.request({
211
+ hostname: 'localhost',
212
+ port: TARGET_PORT,
213
+ path: url,
214
+ method: req.method,
215
+ headers: forwardHeaders
216
+ }, (proxyRes) => {
217
+ const contentType = proxyRes.headers['content-type'] || '';
218
+ const isHtml = contentType.includes('text/html');
219
+ if (isHtml) {
220
+ // Buffer HTML response and inject widget
221
+ const chunks = [];
222
+ proxyRes.on('data', (chunk) => chunks.push(chunk));
223
+ proxyRes.on('end', () => {
224
+ let html = Buffer.concat(chunks).toString('utf8');
225
+ const injection = getInjectionScript();
226
+ if (html.includes('</body>')) {
227
+ html = html.replace('</body>', `${injection}</body>`);
228
+ }
229
+ else if (html.includes('</html>')) {
230
+ html = html.replace('</html>', `${injection}</html>`);
231
+ }
232
+ else {
233
+ html += injection;
234
+ }
235
+ // Copy headers but remove ones that break injection
236
+ const headers = {};
237
+ const skipHeaders = ['content-length', 'transfer-encoding', 'content-encoding', 'content-security-policy', 'content-security-policy-report-only'];
238
+ for (const [key, value] of Object.entries(proxyRes.headers)) {
239
+ if (!skipHeaders.includes(key.toLowerCase())) {
240
+ headers[key] = value;
241
+ }
242
+ }
243
+ res.writeHead(proxyRes.statusCode || 200, headers);
244
+ res.end(html);
245
+ });
246
+ }
247
+ else {
248
+ // Stream non-HTML responses directly
249
+ const headers = {};
250
+ for (const [key, value] of Object.entries(proxyRes.headers)) {
251
+ headers[key] = value;
252
+ }
253
+ res.writeHead(proxyRes.statusCode || 200, headers);
254
+ proxyRes.pipe(res);
255
+ }
256
+ });
257
+ proxyReq.on('error', (err) => {
258
+ res.writeHead(502, { 'Content-Type': 'text/html' });
259
+ res.end(`
260
+ <html>
261
+ <body style="font-family: system-ui; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; background: linear-gradient(135deg, #ff6b6b, #ff8e53);">
262
+ <div style="text-align: center; color: white;">
263
+ <h1>🔥 Proxy Error</h1>
264
+ <p>Could not connect to localhost:${TARGET_PORT}</p>
265
+ <pre style="background: rgba(0,0,0,0.2); padding: 10px; border-radius: 5px;">${err.message}</pre>
266
+ </div>
267
+ </body>
268
+ </html>
269
+ `);
270
+ });
271
+ // Forward request body
272
+ req.pipe(proxyReq);
273
+ });
274
+ // Setup terminal WebSocket (handles /__gekto/terminal)
275
+ setupTerminalWebSocket(server);
276
+ // Setup agent WebSocket (handles /__gekto/agent)
277
+ setupAgentWebSocket(server);
278
+ // Handle WebSocket upgrades for Vite HMR (skip gekto paths)
279
+ server.on('upgrade', (req, socket, _head) => {
280
+ const url = req.url || '';
281
+ // Terminal and Agent WebSockets are handled separately
282
+ if (url.startsWith('/__gekto/terminal') || url.startsWith('/__gekto/agent')) {
283
+ return;
284
+ }
285
+ const proxyReq = http.request({
286
+ hostname: 'localhost',
287
+ port: TARGET_PORT,
288
+ path: req.url,
289
+ method: req.method,
290
+ headers: {
291
+ ...req.headers,
292
+ host: `localhost:${TARGET_PORT}`
293
+ }
294
+ });
295
+ proxyReq.on('upgrade', (proxyRes, proxySocket, _proxyHead) => {
296
+ socket.write('HTTP/1.1 101 Switching Protocols\r\n' +
297
+ Object.entries(proxyRes.headers)
298
+ .map(([k, v]) => `${k}: ${v}`)
299
+ .join('\r\n') +
300
+ '\r\n\r\n');
301
+ proxySocket.pipe(socket);
302
+ socket.pipe(proxySocket);
303
+ });
304
+ proxyReq.on('error', () => {
305
+ socket.end();
306
+ });
307
+ proxyReq.end();
308
+ });
309
+ server.listen(PROXY_PORT, () => {
310
+ console.log(`
311
+ Gekto Proxy Server
312
+
313
+ Proxy: http://localhost:${PROXY_PORT}
314
+ Target: http://localhost:${TARGET_PORT}
315
+ Mode: ${DEV_MODE ? 'development' : 'production'}${DEV_MODE ? `
316
+ Widget: http://localhost:${WIDGET_PORT}` : ''}
317
+ `);
318
+ });
package/dist/store.js ADDED
@@ -0,0 +1,53 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ const STORE_FILENAME = 'gekto-store.json';
4
+ function getStorePath() {
5
+ return path.join(process.cwd(), STORE_FILENAME);
6
+ }
7
+ function createEmptyStore() {
8
+ const now = new Date().toISOString();
9
+ return {
10
+ version: 1,
11
+ createdAt: now,
12
+ updatedAt: now,
13
+ data: {},
14
+ };
15
+ }
16
+ export function initStore() {
17
+ const storePath = getStorePath();
18
+ if (!fs.existsSync(storePath)) {
19
+ const emptyStore = createEmptyStore();
20
+ fs.writeFileSync(storePath, JSON.stringify(emptyStore, null, 2), 'utf8');
21
+ }
22
+ }
23
+ export function readStore() {
24
+ const storePath = getStorePath();
25
+ if (!fs.existsSync(storePath)) {
26
+ initStore();
27
+ }
28
+ const content = fs.readFileSync(storePath, 'utf8');
29
+ return JSON.parse(content);
30
+ }
31
+ export function writeStore(data) {
32
+ const storePath = getStorePath();
33
+ data.updatedAt = new Date().toISOString();
34
+ fs.writeFileSync(storePath, JSON.stringify(data, null, 2), 'utf8');
35
+ }
36
+ export function getData(key) {
37
+ const store = readStore();
38
+ return store.data[key];
39
+ }
40
+ export function setData(key, value) {
41
+ const store = readStore();
42
+ store.data[key] = value;
43
+ writeStore(store);
44
+ }
45
+ export function deleteData(key) {
46
+ const store = readStore();
47
+ if (key in store.data) {
48
+ delete store.data[key];
49
+ writeStore(store);
50
+ return true;
51
+ }
52
+ return false;
53
+ }